0x00 RASP实现思路
目前 RASP 的主方向还是 Java RASP, 它的实现方式是通过Instrumentation编写一个agent,在 agent 中加入 hook 点,当程序运行流程到了 hook 点时,将检测流程插入到字节码文件中,统一进入JVM中执行。——摘自JRASP
0x01 Agent的实现
1. 什么是Agent
一个Agent的体现就是一个Jar包,但是与常规的Jar包不同的是,他不能独立运行,必须依附在其他程序上。
2. Agent的两种加载方式
一个agent有两种main方法:
- premain:程序运行前加载,通过JVM参数- javaagetn:**.jar
- agentmain: 程序运行后加载,通过vitualMachine的attach api
写起来的代码大概如下所示:
public class MyAgent{
public static void premain(String args, Instrumentation ins){
System.out.println("===Use Premain To Begin MyAgent");
ins.addTransformer(new Agent());
}
}
可以看到他最后要借助Instrumentation来实现
3. Instrumentation
Instrumentation是JVM提供的一个修改已加载类的类库,用于插桩服务,能够实现改变字节码、新增jar包等操作。**addTransformer
方法允许我们在类加载之前,重新定义Class,其参数需要为ClassFileTransformer类型。**
定义一个类,继承ClassFileTransformer接口,并重写其中的transform方法(这个方法在类加载时候就会调用),在其中使用ASM或者javassist即可修改字节码。其中transform中的参数,className为传入的类名,classfileBuffer为类原来的字节码。
public class Agent implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
}
}
除了addTransformer
之外,Instrumentation中重要的方法还有retransformClasses
,其用于类加载完成后的字节码转换,因此被用在agentmain中。要使用他需要在addTransformer时,设定boolean canRetransform
为True,表示之后还可被转换:
ins.addTransformer(new Agent(),true);
ins.retransformClasses(Test.class);
当指定的类(可以是一个可变长数组)加载完成后,就会重新触发addTransformer中设置的transform方法。
4. META-INF.MF
因为Agent不能自己运行,需要依附其他程序加载,所以我们需要在META-INF标明我们Agent在哪:
Manifest-Version: 1.0
premain-class: com.example.MyAgent
agent-class: com.example.MyAgent
还有一些其他可选的参数:
Boot-Class-Path
Can-Redefine-Classes: 默认为fasle,为true时允许重新定义Class
Can-Retransform-Classes:默认为false,为true时能够重新重新转换Class,实现字节码替换
Can-Set-Native-Method-Prefix: 默认为false,为true时能够设置native方法的前缀
在Maven中,还可以如下配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.example.MyAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
5. Premain-Agent实例
实验目标:利用Agent修改测试类的字节码,改变输出。
5.1 测试类
首先编写一个测试类如下:
public class Test {
public static void main(String[] args){
Test test = new Test();
String name = test.echoSomething();
System.out.println(name);
}
public String echoSomething(){
return "chenlvtang";
}
}
5.2 premain的实现
首先创建一个MyAgent类,声明premain方法:
public class MyAgent {
public static void premain(String args, Instrumentation ins){
System.out.println("======Premain Begin=======");
ins.addTransformer(new Agent());
}
}
然后创建Agent类,继承ClassFileTransformer接口,并重写transform方法,同时使用javassit(使用Maven引入),建议选高一点的版本,本人在选择3.20时不知道为什么不能修改字节码,可能也和JDK版本太高有关,我这里选用的是3.29.2,JDK版本为14:
<!--如果Agent和Test是分别两个项目,记得都引入一下-->
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
</dependencies>
使用javassit对其字节码修改,让指定方法提前return:
public class Agent implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.equals("Test")){
try {
ClassPool pool = ClassPool.getDefault();
CtClass clz = pool.get("com.example.Test");
CtMethod method = clz.getDeclaredMethod("echoSomething");
method.insertBefore("return \"Hacker\"");
return clz.toBytecode();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 返回原来的字节码
return classfileBuffer;
}
}
5.3 premain的部署和测试
根据上文配置好配置文件,然后使用mvn clean install
将Agent编译成jar。之后在测试类的启动配置中,添加如下的配置(注意不是添加程序实参,而是VM选项):
如上文所说,在启动参数指定号agent的位置后,就可以启动开始测试了。测试的结果如下:
可以看到,成功的改变了原本的输出,这表明我们的Premain-Agent插入成功。
6. Agentmain-Agent实例
实验目标:利用Agent修改测试类的字节码,改变输出。
6.1 agentmain的实现
根据上文的介绍,我们给MyAgent类添加如下的agentmain方法:
public static void agentmain(String args, Instrumentation ins) throws Exception {
System.out.println("======Agentmain Begin=======");
ins.addTransformer(new Agent(), true);
ins.retransformClasses(Class.forName("com.example.Test"));
}
Agent类和premain中相同,不再赘述。但是注意这里我使用了Class.forName来反射调用,因为我的测试类和Agent项目不在同一个项目里,从上文的截图中也能明显的看到。
6.2 Attach工具
如上文所说,使用agentmain不能再用启动参数,而是要借助Attach工具中的VirtualMachine来获取主程序的在JVM中的pid,然后再把Agent给Attach上去。Attach包在JDK的lib/tools.jar(JDK14 中为jrt-fs.jar)
中,需要从手动引入:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>14</version>
<scope>system</scope>
<systemPath>${JAVA_HOME}\lib\jrt-fs.jar</systemPath>
</dependency>
成功导入后,编写如下的Attach类:
public class MyAttach {
public static void main(String[] args) throws Exception{
// 列出JVM中的描述符列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 遍历列表找到对应的描述符
for (VirtualMachineDescriptor descriptor: list){
if (descriptor.displayName().endsWith("Test")){
// 根据描述符获取pid,并Attach
VirtualMachine virtualMachine = VirtualMachine.attach(descriptor.id());
// 加载Agent
virtualMachine.loadAgent(
"C:\\Users\\沉铝汤\\Desktop\\RASP学习\\Agent\\target\\Agent-1.0-SNAPSHOT.jar",
"参数1"
);
}
}
}
}
6.3 agentmain的部署和测试
在Agent项目的xml(或是META-INF)中指出agentmain的位置:
<Agent-Class>com.example.MyAgent</Agent-Class>
在测试之前,我们还需要再改造一下Test类,使用while循环和sleep函数进行阻塞, 不让他执行完就退出:
public static void main(String[] args) throws Exception{
while (true){
Test test = new Test();
Thread.sleep(5000);
String name = test.echoSomething();
System.out.println(name);
}
}
一个小插曲:在测试过程中,发现Agent中的javassit的pool.get总是找不到类(但是测试premain时没出现这个问题),好在网上给出了一个方法,手动添加ClassPath:
ClassPool pool = ClassPool.getDefault();
ClassClassPath classPath = new ClassClassPath(this.getClass());
pool.insertClassPath(classPath);
首先启动测试类,之后再启动上文的Attach类,结果如下图所示:
可以看到,程序显示按照原来逻辑加载,被Attach上Agent后,后续的逻辑就发生了改变。
本小节参考
http://static.kancloud.cn/alex_wsc/javajvm/1844993
https://mp.weixin.qq.com/s/_JqxUmQKvUpUulmcSMQq2A
https://www.jianshu.com/p/d573456401eb
https://mdnice.com/writing/df581b507be54d229b0d95c6674f6159
https://cloud.tencent.com/developer/article/1650113
0x02 Hook的实现
当前主流的RASP产品都是采用的premain方式进行Hook,原因在于agentmain在插入新方法时存在一定的困难。但是又因为premain的启动方式,需要在程序启动前添加JVM参数实现,所以在部署和对业务的影响上相形见绌。不过因为本篇只是对RASP的初期了解,就不对这些做过多的解释了。
本小节将基于premain方式,并结合javassit实现简单的RASP,通过HOOK对应类和方法来对Java中的命令执行做出响应,旨在让读者大概了解如何使用Agent去实现RASP中的Hook。