Java Agent

介绍

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

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

agent 主要分为两种:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<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

测试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 方法:

1
-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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 号:

1
jps -l

image

image

VirtualMachine 还有其他方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 进行交互,从而达到修改数据的效果。

就是这里的参数:

1
agentmain(String agentArgs, Instrumentation inst) 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 的方法去做修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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. 对方法进行修改
  4. retransformClasses(Class<?>... classes) 重新加载类

实现转换器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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​ 里面写一下参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 路径需要填绝对路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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


Java Agent
https://liancccc.github.io/2023/09/15/技术/开发/Java-Agent/
作者
守心
发布于
2023年9月15日
许可协议