(一)Java Instrumentation介绍
Java Instrumentation是从JavaSE 5开始提供的新特性,用于构建独立于java应用的agent程序,主要目的是对JVM上的应用进行监控,比如性能优化监控等等。
通过这个特性,我们可以实现在不修改JVM源码的基础上操控字节码,这也就可以实现一种虚拟机级别的AOP机制,这和Spring中的基于动态代理实现的AOP机制是有所不同的,前者更加轻量化,与项目的耦合性更低。
JavaSE 6中,Instrumentation的功能更加强大,甚至可以对原生代码(Native code)进行修改。
(二)Java Agent示例代码
在Instrumentation的基础上我们可以实现一个Java agent程序,实现Hook机制。
首先来说一下我们的需求,看下面的这段示例代码:
我们要做的是对这里的say方法进行hook。
(1)首先,新建一个maven工程,并添加resources/META-INF/MANIFEST.MF文件:右键项目-> Properties-> Java Build path -> Add Folder
(2)编写Agent类,实现premain方法:
(2)编写Agent类,实现premain方法:
Premain方法只做一件事情,就是设置一个ClassFileTransform,用来获取和操作字节码。
(3)TestTransform的实现如下:
首先在pom.xml中引入asm依赖,这个库用来插入字节码:
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-all</artifactId>
<version>5.1</version>
</dependency>
代码:
package agenttest.AgentTest;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
public class TestTransform implements ClassFileTransformer{
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
ClassReader classReader = new ClassReader(classfileBuffer) ;
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES) ;
classReader.accept(new MyClassVisitor(classWriter), 0);
byte[] bytes = classWriter.toByteArray();
return bytes;
}
}
我们在MyClassVisitor里来实现修改字节码的操作。
(4)实现MyClassVisitor,继承ClassVisitor类。
package agenttest.AgentTest;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MyClassVisitor extends ClassVisitor{
public MyClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if ("say".equals(name)) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions) ;
methodVisitor.visitCode();
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("CALL " + name);
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitEnd();
return methodVisitor ;
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
在上面的代码中,重写visitMethod方法,然后判断方法名为say的情况。这里的具体操作可以阅读asm5的相关文档。
(5)编写resources/META-INF/MANIFEST.MF文件,指定premain类:
Manifest-Version: 1.0
Premain-Class: agenttest.AgentTest.AgentMain
Built-By: chongrui
Build-Jdk: 1.8.0_111
Created-By: Maven Integration for Eclipse
(6)在pom中添加打包所需项
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>${basedir}/resources</directory>
</resource>
<resource>
<directory>${basedir}/src/main/java</directory>
</resource>
</resources>
</build>
(7)eclipse下编译为jar包
(8)实验
单独执行Test.java.输出say Hello。
设置了agent.jar之后,执行java应用的命令用-javaagent:path选项来指定agent。
可以看到会把CALL say输出出来,而且发生在调用say方法之前输出:
(三)Java Rasp技术
Java Rasp技术原理大致如下图:
PS. 这个图片来自于引用的参考文献[1]。
我们可以看到,RASP进程是直接嵌入到APP执行流程中去,这一点和WAF有本质的不同。正是由于这一点,RASP可以避免WAF规则被各种奇异的编码绕过的痛点,因为Agent进程最终获取的参数正是各个层面编码转换完成后真正执行的参数。并且RASP不像WAF那样需要拦截每个请求去check是否命中了攻击的规则,而是当HOOK住的危险函数被调用之后,才会触发检测逻辑。
大致说一下Rasp Agent的实现方法,首先,我们可以在Transform类中注册很多CodeFileVisitor,比如每个漏洞类型编写一个Visitor,其实这里asm提供的这个Visitor是个基于事件模型的处理类,编写起代码来和XML的事件解析差不多。在这些Visitor中我们就可以只监控我们关心的一些危险函数,比如JDBC的execSQL函数,利用asm编程获取到调用这个函数时的具体参数,然后编写我们的规则来判定是否存在漏洞,如果存在漏洞就根据配置进行上报、阻断动作就行了。
在部署时,Agent程序可以通过嵌入到Java中间件启动脚本中与其进行整合。举个例子,在tomcat启动脚本catalina.sh中,我们可以修改JAVA_OPTS变量来设置为javaagent方式进行启动。
(四)总结
Rasp技术的本质就是如何实现这样一个HOOK动作的agent。各个语言的实现都不同,比如PHP和Java的机制就不同,PHP是通过编写PHP扩展的形式进行HOOK与漏洞判断、上报。但是漏洞判定规则是通用的,因此这部分完全可以做成通用的服务运行。
Rasp技术虽然好用,但是令人诟病的是对源程序本身的性能影响,当并发一旦增大,漏洞判定逻辑势必会拖慢程序本身的运行速度。因此,除非有很强大的魄力与上层的支持,否则一般大公司业务线都不会为了安全需求来轻易接受这种性能的影响,毕竟性能、稳定性考虑是算在KPI里的。最好的应用场景是在中小型企业部署,效果应该会很明显。