Skip to content

JavaSec 入门-8-Java Agent

约 4341 字大约 14 分钟

Java

2025-03-18

前言

(8和9篇章之后补) Java Agent 是 Java 平台提供的一个强大工具, jdk 引入了 java.lang.instrument 包,该包提供了检测 Java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent,它可以在运行时修改或增强 Java 应用程序的行为。是在 JDK1.5 以后引入的,它能够在不影响正常编译的情况下修改字节码,相当于是在 main 方法执行之前的拦截器,也叫 premain ,也就是会先执行 premain 方法然后再执行 main 方法。

学内存马就会遇到 Java Agent 类型内存马,简单记录一下基础知识以及后续利用

基础利用

Agent 主要就是两个部分 premain 和 agentmain ,premain 顾名思义,在 JVM 加载 和 main 方法之前触发,另一种是 JVM 启动之后加载的 agentmain,这里我们可以将其理解成一种特殊的 Interceptor

简单示例

main.jar 组成部分

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.agent</groupId>
    <artifactId>AgentTest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>

                <artifactId>maven-jar-plugin</artifactId>

                <version>3.1.2</version>

                <configuration>
                    <archive>
                        <manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>

                    </archive>

                </configuration>

            </plugin>

        </plugins>

    </build>
    <dependencies>
        <dependency>
            <groupId>org.apache.maven.plugins</groupId>

            <artifactId>maven-jar-plugin</artifactId>

            <version>3.1.2</version>

        </dependency>

    </dependencies>
</project>

premain & agentmain

agent.jar 组成部分,这两个内容示例写在一起了

pom.xml
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>

            <artifactId>maven-jar-plugin</artifactId>

            <version>3.1.2</version>

            <configuration>
                <archive>
                    <manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>

                </archive>

            </configuration>

        </plugin>

    </plugins>

</build>

agent.jar 无法直接运行,要借助之前写的 main.jar 去使用 premain

java -javaagent:agent.jar=Hacker! -jar .\main.jar

而 agentmain 先启动 main.jar,再通过一个 VirtualMachine 的 attach 去使用,新建个项目,建议使用 jdk8

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>VirtualMachine</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.sun.jdk</groupId>

            <artifactId>tools</artifactId>

            <version>1.8</version>

            <scope>system</scope>

            <systemPath>${java.home}/../lib/tools.jar</systemPath>

        </dependency>

    </dependencies>

</project>

VirtualMachine

那么什么是 VirtualMachine ?

com.sun.tools.attach.VirtualMachine 类可以实现获取JVM信息,内存 dump、线程 dump、类信息统计(例如 JVM 加载的类)等功能。 该类允许我们通过给 attach 方法传入一个 JVM 的 PID,来远程连接到该 JVM上 ,之后我们就可以对连接的 JVM 进行各种操作,如注入 Agent。下面是该类的主要方法

//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()
 
//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent()
 
//获得当前所有的JVM列表
VirtualMachine.list()
 
//解除与特定JVM的连接
VirtualMachine.detach()

VirtualMachineDescriptor

com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
 
import java.util.List;
 
public class get_PID {
    public static void main(String[] args) {
        
        //调用VirtualMachine.list()获取正在运行的JVM列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for(VirtualMachineDescriptor vmd : list){
            
            //遍历每一个正在运行的JVM,如果JVM名称为get_PID则返回其PID
            if(vmd.displayName().equals("get_PID"))
            System.out.println(vmd.id());
        }
 
    }
}

Instrumentation

Instrumentation 是 java.lang.instrument 包中的接口,由 JVM 提供,允许 Agent 在运行时操作类定义和 JVM 状态。

主要方法

  • addTransformer(ClassFileTransformer transformer, boolean canRetransform):注册一个 ClassFileTransformer,用于修改类字节码。
  • retransformClasses(Class<?>... classes):触发对指定类的重新转换,调用已注册的 Transformer。
  • getAllLoadedClasses():返回当前 JVM 中所有已加载的类。
  • isRetransformClassesSupported():检查是否支持类的重新转换。

用途

  • 提供动态修改类的能力,例如代码注入、性能监控、调试等。

Javassist

Javassist(Java Assist)是一个开源的 Java 字节码操作库,广泛用于运行时动态修改类的字节码。它比直接使用低级的 ASM 库更易用,提供了一个高层次的 API,让开发者可以通过简单的 Java 代码操作类的结构和行为。

导入依赖

<dependency>
  <groupId>org.javassist</groupId>
  <artifactId>javassist</artifactId>
  <version>3.25.0-GA</version>
</dependency>

主要类和方法

ClassPool

  • 作用:管理类的加载和查找。
  • 常用方法
    • ClassPool.getDefault():获取默认的 ClassPool 实例。
    • get(String className):根据类名获取 CtClass 对象。
    • insertClassPath(ClassPath cp):添加类路径(如当前 ClassLoader)。
    • makeClass(String className):创建新类。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.agent.Server");

CtClass

  • 作用:表示一个可修改的类。
  • 常用方法
    • getDeclaredMethod(String name):获取指定方法。
    • getDeclaredField(String name):获取指定字段。
    • addMethod(CtMethod m):添加新方法。
    • addField(CtField f):添加新字段。
    • toBytecode():生成字节码(返回 byte[])。
    • toClass():将类加载到 JVM。

CtMethod

  • 作用:操作类中的方法。
  • 常用方法
    • setBody(String src):设置方法体(用 Java 代码字符串表示)。
    • insertBefore(String src):在方法开始处插入代码。
    • insertAfter(String src):在方法结束处插入代码。
    • addLocalVariable(String name, CtClass type):添加局部变量。

总结就是ClassPool对象是Ctclass的容器,首先创建容器 pool,再在 pool 中创建Ctclass,对其进行自定义修改操作,结束后将其保存在容器中,方便我们之后取出,CtClass对应的实际上就是 class 文件

生成类示例

package com.aniale;

import javassist.*;

/**
 * @author rickiyang
 * @date 2019-08-06
 * @Desc
 */
public class javassistsDemo {

    /**
     * 创建一个Person 对象
     *
     * @throws Exception
     */
    public static void createPseson() throws Exception {
        ClassPool pool = ClassPool.getDefault();

        // 1. 创建一个空类
        CtClass cc = pool.makeClass("com.aniale.javassit.Person");

        // 2. 新增一个字段 private String name;
        // 字段名为name
        CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
        // 访问级别是 private
        param.setModifiers(Modifier.PRIVATE);
        // 初始值是 "xiaoming"
        cc.addField(param, CtField.Initializer.constant("xiaoming"));

        // 3. 生成 getter、setter 方法
        cc.addMethod(CtNewMethod.setter("setName", param));
        cc.addMethod(CtNewMethod.getter("getName", param));

        // 4. 添加无参的构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = \"xiaohong\";}");
        cc.addConstructor(cons);

        // 5. 添加有参的构造函数
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // $0=this / $1,$2,$3... 代表方法参数
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);

        // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        cc.addMethod(ctMethod);

        //这里会将这个创建的类对象编译为.class文件
        cc.writeFile("D:\\Java\\Agentdemo");
    }

    public static void main(String[] args) {
        try {
            createPseson();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

效果:

image-20250320082302668

通过反射方式调用

package com.aniale;

import javassist.*;

import java.lang.reflect.Method;

/**
 * @author rickiyang
 * @date 2019-08-06
 * @Desc
 */
public class javassistsDemo {

    /**
     * 创建一个Person 对象
     *
     * @throws Exception
     */
    public static void createPseson() throws Exception {
        ClassPool pool = ClassPool.getDefault();

        // 1. 创建一个空类
        CtClass cc = pool.makeClass("com.aniale.javassit.Person");

        // 2. 新增一个字段 private String name;
        // 字段名为name
        CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
        // 访问级别是 private
        param.setModifiers(Modifier.PRIVATE);
        // 初始值是 "xiaoming"
        cc.addField(param, CtField.Initializer.constant("xiaoming"));

        // 3. 生成 getter、setter 方法
        cc.addMethod(CtNewMethod.setter("setName", param));
        cc.addMethod(CtNewMethod.getter("getName", param));

        // 4. 添加无参的构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = \"xiaohong\";}");
        cc.addConstructor(cons);

        // 5. 添加有参的构造函数
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // $0=this / $1,$2,$3... 代表方法参数
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);

        // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        cc.addMethod(ctMethod);
        Object person = cc.toClass().newInstance();
        //    设置值
        Method setName = person.getClass().getDeclaredMethod("setName", String.class);
        //    触发
        setName.invoke(person,"aniale");
        //    获取值
        Method printName = person.getClass().getDeclaredMethod("printName");
        //    触发
        printName.invoke(person);

        //这里会将这个创建的类对象编译为.class文件
        cc.writeFile("D:\\Java\\Agentdemo");
    }

    public static void main(String[] args) {
        try {
            createPseson();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

通过.class文件调用

package com.aniale;

import javassist.ClassPool;
import javassist.CtClass;

import java.lang.reflect.Method;

public class javassistsDemo {

    /**
     * 创建一个Person 对象
     *
     * @throws Exception
     */
    public static void createPseson() throws Exception {

        ClassPool pool = ClassPool.getDefault();
// 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬
        pool.appendClassPath("D:\\Java\\Agentdemo");
        //获取Person的class对象
        CtClass ctClass = pool.get("com.aniale.javassit.Person");
        Object person = ctClass.toClass().newInstance();
//  ...... 下面和通过反射的方式一样去使用
        //    设置值
        Method setName = person.getClass().getDeclaredMethod("setName", String.class);
        //    触发
        setName.invoke(person,"aniale");
        //    获取值
        Method printName = person.getClass().getDeclaredMethod("printName");
        //    触发
        printName.invoke(person);

    }

    public static void main(String[] args) {
        try {
            createPseson();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Javassist & JavaAgent 联动使用

先写一个正常服务 server.jar

package com.agent
public class Server {
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            sayHello();
            Thread.sleep(1000);
        }
    }

    public static void sayHello() {
        System.out.println("[Server] Hello ~");
    }
}

agent.jar,这里我们用了一个 maven 的打包插件,使用maven-assembly-plugin可以进行带扩展打包,直接 package 会生成两个 jar 包,选择带 dependencies 的

MyAgent.java
package com.aniale;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;

public class MyAgent {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new MyClassFileTransformer(), true);
        Class<?>[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class<?> allLoadedClass : allLoadedClasses) {
            if (allLoadedClass.getName().equals("com.agent.Server")) {
                try {
                    inst.retransformClasses(allLoadedClass);
                } catch (UnmodifiableClassException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    static class MyClassFileTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            try {
                System.out.println("Transforming class: " + className);
                ClassPool classPool = new ClassPool(true);
                classPool.insertClassPath(new LoaderClassPath(loader));
                CtClass ctClass = classPool.get(className.replace("/", "."));
                CtMethod tester = ctClass.getDeclaredMethod("sayHello"); // 得到 sayHello 方法
                tester.setBody("{System.out.println(\"[MyAgent] aniale!\");}");
                return ctClass.toBytecode(); // 返回修改后的字节码
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

        }
    }
}

attach 部分,配置和之前相同

public class Test {
    public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException {
        System.out.println("AgentDemo4Test"); // 打印测试标识信息
        // 获取当前系统中所有虚拟机实例的描述符列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        // 遍历虚拟机实例描述符列表
        for (VirtualMachineDescriptor vmd : list) {
            // 检查虚拟机实例的显示名称是否为 "main.jar"
            if (vmd.displayName().equals("server.jar")) {
                // 附加到指定的虚拟机实例
                VirtualMachine vm = VirtualMachine.attach(vmd.id());
                // 打印虚拟机实例的 ID
                System.out.println(vm.id());
                // 打印虚拟机实例的信息
                System.out.println(vm);
                // 加载指定的 Java Agent
                vm.loadAgent("C:\\Users\\Administrator\\Desktop\\agent.jar", "Hacker!");
                // 参数1: 指明具体的 agent 包路径
                // 参数2: 给该 agent 方法传递的参数
                // 从目标虚拟机断开连接
                vm.detach();
            }
        }
    }
}

image-20250320093028544

Instrumentation的局限性

大多数情况下,我们使用 Instrumentation 都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性: premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:

  1. 新类和老类的父类必须相同
  2. 新类和老类实现的接口数也要相同,并且是相同的接口
  3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
  4. 新类和老类新增或删除的方法必须是private static/final修饰的
  5. 可以修改方法体

用 Agent 实现 Filter 型的内存马

起一个 Spring 项目看一下 Controller 调用流程,

image-20250324102747666

这里存在一个递归调用,去依次加载 Filter ,最后调用 service ,我们只要动态修改internalDoFilter或者是DoFilter,就可以注入 Agent 的内存马,这里用internalDoFilter的话很明显会执行五次,而单纯的 Tomcat 会执行两次。

所以我们用org.apache.catalina.core.StandardWrapperValve#invoke,这个在 Tomcat 只会执行一次,Spring 两次,在这里我们把 attach 和 agent 部分写在一起最后打包成一个 jar

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>VirtualMachine</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.sun.tools.attach</groupId>
            <artifactId>MyTools</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <!-- 将 ${java.home}/../lib/tools.jar 拷贝到当前目录 -->
            <systemPath>${pom.basedir}/lib/GenericAgentTools.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.30.2-GA</version>
        </dependency>
    </dependencies>
</project>

项目文件结构如图

image-20250324132452380

工件位置要把这俩依赖放进去

image-20250324132542119

然后选择 IDEA 自己的打包

image-20250324132613707

最后

java -jar your_inject.jar

image-20250324132715721

对于 Spring 来说,在 Main 类里面改一下那个CLASSNAME就好

展望

其他如文件不落地或者结合反序列化的以后再更新进来

参考文章

Agent内存马剖析

https://mp.weixin.qq.com/s/3Zy6P3lB9CpJ6Y0EICP0Lg

https://wjlshare.com/archives/1582