Java Agent机制
在JDK1.5版本开始,Java增加了
Instrumentation(Java Agent API)
和JVMTI(JVM Tool Interface)
功能,该功能可以实现JVM再加载某个class文件对其字节码进行修改,也可以对已经加载的字节码进行一个重新的加载。
利用该机制能够实现许多技术,如RASP、内存马、IDEA破解。
Java Agent
有两种运行模式:
- 启动
Java程序
时添加-javaagent(Instrumentation API实现方式)
或-agentpath/-agentlib(JVMTI的实现方式)
参数,如java -javaagent:/data/XXX.jar Test
。 JDK1.6
新增了attach(附加方式)
方式,可以对运行中的Java进程
附加Agent
。
第一种方式只能在程序启动时指定Agent
文件,而attach
方式可以在Java程序
运行后根据进程ID
动态注入Agent
到JVM
。
前置知识
使用Java Agent会加载一个jar包(我们的程序也必须打包成一个jar包)。如下有一些规范:
- jar文件中必须包含
/META-INF/MANIFEST.MF
文件 MANIFEST.MF
文件中必须定义好Premain-Class
(Agent模式)或Agent-Class:
(Attach模式)- 如果我们需要修改已经被JVM加载过的类的字节码,那么还需要设置在
MANIFEST.MF
中添加Can-Retransform-Classes: true
或Can-Redefine-Classes: true
。
Java Agent和普通的Java类并没有任何区别,普通的Java程序中规定了main
方法为程序入口,而Java Agent则将premain
(Agent模式)和agentmain
(Attach模式)作为了Agent程序的入口,如下:
public static void premain(String args, Instrumentation inst) {
}
public static void agentmain(String args, Instrumentation inst) {
}
简单来说就是在运行main方法前会去加载-javaagent指定的jar包里面的Premain-Class类中的premain方法。
premain方法其实还可以简写成:(agentmain同样)
public static void premain(String agentArgs)
JVM会去优先加载带 Instrumentation
签名的premain方法,加载成功忽略无签名的,如果第一种没有,则加载第二种方法。
java.lang.instrument
这个包提供了Java运行时,动态修改系统中的Class类型的功能。
这里面有2个重要的接口Instrumentation
和 ClassFileTransformer
Instrumentation
java.lang.instrument.Instrumentation是监测运行在
JVM程序的
Java API,用一下javasec里面的一张图
利用该类可以实现如下功能:
- 动态添加或移除自定义的
ClassFileTransformer
(addTransformer/removeTransformer
),JVM会在类加载时调用Agent中注册的ClassFileTransformer
;- 动态修改
classpath
(appendToBootstrapClassLoaderSearch
、appendToSystemClassLoaderSearch
),将Agent程序添加到BootstrapClassLoader
和SystemClassLoaderSearch
(对应的是ClassLoader类的getSystemClassLoader方法
,默认是sun.misc.Launcher$AppClassLoader
)中搜索;- 动态获取所有
JVM
已加载的类(getAllLoadedClasses
);- 动态获取某个类加载器已实例化的所有类(
getInitiatedClasses
)。- 重定义某个已加载的类的字节码(
redefineClasses
)。- 动态设置
JNI
前缀(setNativeMethodPrefix
),可以实现Hook native方法。- 重新加载某个已经被JVM加载过的类字节码
retransformClasses
)。
ClassFileTransformer
java.lang.instrument.ClassFileTransformer
是一个转换类文件的代理接口,我们可以在获取到Instrumentation
对象后通过addTransformer
方法添加自定义类文件转换器。
我们可以使用addTransformer
注册一个我们自定义的Transformer
到Java Agent
,当有新的类被JVM
加载时JVM
会自动回调用我们自定义的Transformer
类的transform
方法,传入该类的transform
信息(类名、类加载器、类字节码
等),我们可以根据传入的类信息决定是否需要修改类字节码。修改完字节码后我们将新的类字节码返回给JVM
,JVM
会验证类和相应的修改是否合法,如果符合类加载要求JVM
会加载我们修改后的类字节码。
该接口中有只有一个transform方法,里面的参数内容对应的信息分别是:
ClassLoader loader 定义要转换的类加载器;如果是引导加载器,则为 null String className 加载的类名,如:java/lang/Runtime Class<?> classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null ProtectionDomain protectionDomain 要定义或重定义的类的保护域 byte[] classfileBuffer 类文件格式的输入字节缓冲区(不得修改)
重写
transform
方法注意事项:
ClassLoader
如果是被Bootstrap ClassLoader(引导类加载器)
所加载那么loader
参数的值是空。- 修改类字节码时需要特别注意插入的代码在对应的
ClassLoader
中可以正确的获取到,否则会报ClassNotFoundException
,比如修改java.io.FileInputStream(该类由Bootstrap ClassLoader加载)
时插入了我们检测代码,那么我们将必须保证FileInputStream
能够获取到我们的检测代码类。JVM
类名的书写方式路径方式:java/lang/String
而不是我们常用的类名方式:java.lang.String
。- 类字节必须符合
JVM
校验要求,如果无法验证类字节码会导致JVM
崩溃或者VerifyError(类验证错误)
。- 如果修改的是
retransform
类(修改已被JVM
加载的类),修改后的类字节码不得新增方法
、修改方法参数
、类成员变量
。addTransformer
时如果没有传入retransform
参数(默认是false
)就算MANIFEST.MF
中配置了Can-Redefine-Classes: true
而且手动调用了retransformClasses
方法也一样无法retransform
。- 卸载
transform
时需要使用创建时的Instrumentation
实例。
技术实现
看了一堆概念不如代码案例来得实在
JVM运行前(Agent模式)
创建我们的agent类
package com.study.agent;
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("args: "+agentArgs);
inst.addTransformer(new OurTransformer(),true);//add a Transformer
}
}
OurTransformer类
package com.study.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class OurTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out