沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

浅触Rasp的实现

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选项):

image-20221025214256565

如上文所说,在启动参数指定号agent的位置后,就可以启动开始测试了。测试的结果如下:

image-20221025220007766

可以看到,成功的改变了原本的输出,这表明我们的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类,结果如下图所示:

image-20221026204032938

可以看到,程序显示按照原来逻辑加载,被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。

Hook点