一、简介
Java Agent技术,也被称为Java代理、Java探针,它允许程序员利⽤其构建⼀个独⽴于应⽤程序的代理程序。
Java Agent 本质上就是一个 jar 包,对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行。使用Java Agent可以实现在Java程序运行的过程中对其进行修改。
Java Agent的jar包主要包含两个部分:实现代码与配置文件。配置文件名为MANIFEST.MF,放在META - INF文件夹下,包含下列配置项:
Manifest-Version: 版本号
Created-By: 创作者
Premain-Class :包含 premain 方法的类
Agent-Class :包含 agentmain 方法的类
Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false
Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false
Java Agent的入口是premain或agentmain,具体选用哪一个以及其中的内容需要根据应用场景决定。
二、使用场景
Java Agent 技术有以下主要功能:
这些功能使得Java Agent在作为一个独立于Java应用程序的代理程序的同时,可以协助监测、运行甚至替换 JVM 上的程序。Agent的应用十分广泛,可用于实现Java IDE的调试功能、热部署功能、线上诊断⼯具和性能分析⼯具。例如,百度网络攻击防护工具OpenRASP中就使用了Java Agent来对敏感函数进行插桩,以此实现攻击检测。
三、使用方法
Java Agent分为两种:静态agent与动态agent,这两种方式的启动方法与运行机制是不同的。
1. 静态agent
这种方式是使用premain作为agent的入口方法,以vm参数的方式载入,在Java程序的 main 方法执行之前执行。要使用agent,需要在premain中实现我们想要的功能。agent技术使用JVMTI提供的接口来实现对应的功能,下面是一些官方的Instrumentation接口:
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
//注册ClassFileTransformer实例,注册多个会按照注册顺序进行调用。所有的类被加载完毕之后会调用ClassFileTransformer实例,相当于它们通过了redefineClasses方法进行重定义。布尔值参数canRetransform决定这里被重定义的类是否能够通过retransformClasses方法进行回滚。
void addTransformer(ClassFileTransformer transformer)
//相当于addTransformer(transformer, false),也就是通过ClassFileTransformer实例重定义的类不能进行回滚。
boolean removeTransformer(ClassFileTransformer transformer)
//移除(反注册)ClassFileTransformer实例。
void retransformClasses(Class>... classes)
//已加载类进行重新转换的方法,重新转换的类会被回调到ClassFileTransformer的列表中进行处理。
void appendToBootstrapClassLoaderSearch(JarFile jarfile)
//将某个jar加入到Bootstrap Classpath里优先其他jar被加载。
void appendToSystemClassLoaderSearch(JarFile jarfile)
//将某个jar加入到Classpath里供AppClassloard去加载。
Class[] getAllLoadedClasses()
//获取所有已经被加载的类。
Class[] getInitiatedClasses(ClassLoader loader)
//获取所有已经被初始化过了的类。
long getObjectSize(Object objectToSize)
//获取某个对象的(字节)大小,注意嵌套对象或者对象中的属性引用需要另外单独计算。
boolean isModifiableClass(Class> theClass)
//判断对应类是否被修改过。
boolean isNativeMethodPrefixSupported()
//是否支持设置native方法的前缀。
boolean isRedefineClassesSupported()
//返回当前JVM配置是否支持重定义类(修改类的字节码)的特性。
boolean isRetransformClassesSupported()
//返回当前JVM配置是否支持类重新转换的特性。
void redefineClasses(ClassDefinition... definitions)
//重定义类,也就是对已经加载的类进行重定义,ClassDefinition类型的入参包括了对应的类型Class>对象和字节码文件对应的字节数组。
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)
//设置某些native方法的前缀,主要在找native方法的时候做规则匹配。
使用静态agent的步骤如下:
1)编写premain函数,该函数应包含下面两个方法的其中之一:
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
如果两个方法都被实现了,那么带Instrumentation参数的会被优先调用。agentArgs是premain函数得到的程序参数,通过命令行传入。
例如,OpenRASP中的premin函数:
/**
* 启动时加载的agent入口方法
*
* @param agentArg 启动参数
* @param inst {@link Instrumentation}
*/
public static void premain(String agentArg, Instrumentation inst) {
init(START_MODE_NORMAL, START_ACTION_INSTALL, inst);
}
2)定义一个MANIFEST.MF文件,其中必须包含Premain-Class选项。例如OpenRASP工具中rasp.jar包内的MANIFEST.MF文件如下:
Manifest-Version: 1.0
Build-Time: 2022-03-11T11:55:29Z
Last-Commit-User-Email: [email protected]
Built-By: root
Premain-Class: com.baidu.openrasp.Agent
Created-By: Apache Maven 3.2.3
Git-Branch: 1.3.7
Git-Commit: f264cc6
Last-Commit-User-Name: lixinkai
Build-Jdk: 1.6.0_45
Project-Version: 1.3.7
Agent-Class: com.baidu.openrasp.Agent
Can-Redefine-Classes: true
Main-Class: com.baidu.openrasp.Agent
Can-Retransform-Classes: true
Archiver-Version: Plexus Archiver
3)将包含premain的类与MANIFEST.MF文件打包成一个 jar 包
4)在启动java程序时添加启动参数 -javaagent:[path],其中的path为对应的agent的jar包路径。此时java程序的入口函数被agent中的premain取代,premain中的内容将在主程序运行前先执行。
例如,在安装了OpenRASP的tomcat服务器中,catalina.sh文件中会多出Java Agent的启动参数:
### BEGIN OPENRASP - DO NOT MODIFY ###
if [ "$RASP_DISABLE"x != "Y"x ]; then JAVA_OPTS="-javaagent:${CATALINA_HOME}/rasp/rasp.jar ${JAVA_OPTS}"; fi
JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED"
export JDK_JAVA_OPTIONS
### END OPENRASP ###
2. 动态agent
与静态方式不同,动态agent允许代理的目标程序的JVM先启动,再通过attach机制载入。
和premain模式不同,不再通过添加启动参数的方式来连接agent和主程序了。attach方式使用了com.sun.tools.attach包下的VirtualMachine工具类。需要注意的是这个包不是jvm标准规范,需要引入依赖。以attach方式启动需要在agent中实现如下方法:
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);
使用动态agent的方式如下:
1)编写agentmain函数 ,如上面所述。例如OpenRASP也包含了动态agent的入口函数:
/**
* attach 机制加载 agent
*
* @param agentArg 启动参数
* @param inst {@link Instrumentation}
*/
public static void agentmain(String agentArg, Instrumentation inst) {
init(Module.START_MODE_ATTACH, agentArg, inst);
}
2)定义一个MANIFEST.MF文件,与静态agent不同的是,此时必须包含Agent-Class选项。
3)将包含agentmain的类和MANIFEST.MF文件打成一个jar包。
4)在主程序开始执行后,通过attach工具加载Agent。Agent启动后会监听VMInit事件,在 JVM 初始化完成之后创建 InstrumentationImpl 对象,监听 ClassFileLoadHook 事件,然后调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,该方法中会调用Agent里MANIFEST.MF所指定的Agent-Class类的agentmain方法。
在编写agentmain方法时会用以下方法来实现attach机制:
List list = VirtualMachine.list();
// 列出所有VM实例
VirtualMachine.attach(descriptor.id());
// attach目标VM
VirtualMachine#loadAgent("代理Jar路径","命令参数");
// 目标VM加载Agent