本文简要介绍 Java Agent,快速入门 Java Agent
建议先学习 Javassist 和 Java ASM 后再来学习 Java Agent,这两部分在我的博客上都有介绍
什么是 Java Agent
Java Agent 本质上可以理解为一个插件,该插件就是一个精心提供的 Jar 包。只是启动方式和普通 Jar 包有所不同,对于普通的 Jar 包,通过指定类的 main 函数进行启动。但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行,在面向切面编程方面应用比较广泛
Java Agent 的 Jar 包通过 JVMTI(JVM Tool Interface)完成加载,最终借助 JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。主要功能如下:
- 可以在加载 Java 文件之前做拦截把字节码做修改
- 可以在运行期将已经加载的类的字节码做变更
在 JDK1.5 版本开始,Java 增加了 Instrumentation(Java Agent API)和 JVMTI(JVM Tool Interface)功能,该功能可以实现 JVM 在加载某个 class 文件对其字节码进行修改,也可以对已经加载的字节码进行一个重新的加载。而在 1.6 版本新增了 Attach(附加)方式,可以对运行中的 Java 进程插入 Agent。Java Agent 可以去实现字节码插桩、动态跟踪分析等,比如 RASP 产品和 Java Agent 内存马
Java Agent 有两种模式:
启动 Java 程序时添加-javaagent
(Instrumentation API 实现方式)或-agentpath/-agentlib
(JVMTI 的实现方式)参数,如:
1 | java -javaagent:XXX.jar Test |
JDK1.6 新增了 Attach(附加)方式,可以对运行中的 Java 进程附加 Agent
这两种运行方式的最大区别在于第一种方式只能在程序启动时指定 Agent 文件,而 Attach 方式可以在 Java 程序运行后根据进程 ID 动态注入 Agent 到 JVM,所以类似于想要注入 Agent 型内存马,一般会用 Attach 的方式
Java Agent 简介
Java Agent 是 java 命令的一个参数,参数 Java Agent 可以用于指定一个 Jar 包,Java Agent 和普通的 Java 类并没有任何区别,普通的 Java 程序中规定了 main 方法为程序入口,而 Java Agent 则将 premain(Agent 模式)和 agentmain(Attach 模式)作为了 Agent 程序的入口,两者所接受的参数是完全一致的,如下:
1 | public static void premain(String args, Instrumentation inst) |
而在 Attach 模式下的premain()
方法有两种写法,如下:
1 | public static void premain(String agentArgs, Instrumentation inst) |
JVM 会去优先加载带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法,Java Agent 还限制了我们必须以 Jar 包的形式运行或加载,我们必须将编写好的 Agent 程序打包成一个 Jar 文件。除此之外,Java Agent 还强制要求了所有的 Jar 文件中必须包含/META-INF/MANIFEST.MF
文件,且该文件中必须定义好Premain-Class
或Agent-Class
配置,如:
1 | Premain-Class: com.anbai.sec.agent.CrackLicenseAgent |
如果我们需要修改已经被 JVM 加载过的类的字节码,那么还需要设置在MANIFEST.MF
中添加:
1 | Can-Retransform-Classes: true |
以下是几个主要的概念:
1.ClassFileTransformer
ClassFileTransformer 是一个转换类文件的代理接口,我们可以在获取到 Instrumentation 对象后通过 addTransformer 方法添加自定义类文件转换器
使用addTransformer
方法可以注册一个我们自定义的 Transformer 到 Java Agent,当有新的类被 JVM 加载时 JVM 会自动回调用我们自定义的 Transformer 类的 transform 方法,传入该类的 transform 信息(类名、类加载器、类字节码等),我们可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后我们将新的类字节码返回给 JVM,JVM 会验证类和相应的修改是否合法,如果符合类加载要求 JVM 会加载我们修改后的类字节码
重写 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 实例
2.Instrumentation
java.lang.instrument.Instrumentation
是监测运行在 JVM 程序的 Java API,利用 Instrumentation 我们可以实现如下功能:
- 动态添加或移除自定义的 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)
Java Agent 实例
写几个小 Demo 来实际体验下 Java Agent,我们需要新建两个项目,一个用来写我们的 Agent(起名:JavaAgent),另一个用来测试 Agent(起名:JavaAgentTest)
Agent 模式
先来写一个最简单的 Demo,创建我们的DefineTransformer
:
1 | import java.lang.instrument.ClassFileTransformer; |
我们的主类Agent
:
1 | import java.lang.instrument.Instrumentation; |
然后需要添加一些依赖来打包和生成MANIFEST.MF
:
1 | <build> |
然后能生成这样的MANIFEST.MF
:
1 | Manifest-Version: 1.0 |
一些可能会用到的参数说明:
- Premain-Class :包含 premain 方法的类(类的全路径名)
- Agent-Class :包含 agentmain 方法的类(类的全路径名)
- Boot-Class-Path :设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符
/
开头,则为绝对路径,否则为相对路径。相对路径根据代理 Jar 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径(可选)- Can-Redefine-Classes :true 表示能重定义此代理所需的类,默认值为 false(可选)
- Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
- Can-Set-Native-Method-Prefix: true 表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
接下来编写我们的测试类:
1 | public class Main { |
先使用 Maven 进行打包(打包为 Jar 文件),再添加对应的 VM 选项:
1 | -javaagent:E:\Zodog\Temp\javassist\JavaAgent\target\JavaAgent-1.0-SNAPSHOT.jar |
运行测试类,可以看见:
1 | agentArgs: null |
执行 main 方法之前会加载所有的类,包括系统类和自定义类。而在ClassFileTransformer
中会去拦截系统类和自己实现的类对象,逻辑则是在ClassFileTransformer
实现类的transform
方法中定义
而在这里transform
类似于一个filter
会去拦截、遍历一些要在 JVM 中加载的类,而在transform
方法中我们可以定义一些逻辑,比如if className == xxx
时走入一个逻辑去实现 AOP。而其中就可以利用如 Javassist 技术修改字节码并作为transform
方法的返回值,这样就在该类在 JVM 中加载前(-javaagent
模式)修改了字节码
再来看另一个例子,使用 Javassist 修改字节码,首先需要在所有项目里添加依赖:
1 | <dependencies> |
我们的DefineTransformer
:
1 | import javassist.*; |
其它的类不用变化,运行就可以发现打印了:
1 | agentArgs: null |
然后弹出了计算器
Attach API
在 Java SE 6 以后在Instrumentation
接口中提供了新的方法agentmain
可以在 main 函数开始运行之后再运行
1 | // 采用 attach 机制,被代理的目标程序 VM 有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助 |
同样,agentmain 方法中带 Instrumentation 参数的方法也比不带优先级更高。开发者必须在MANIFEST.MF
文件里面设置Agent-Class
来指定包含 agentmain 函数的类,在 Java6 以后实现启动后加载的新实现是 Attach API。Attach API 很简单,只有 2 个主要的类,都在com.sun.tools.attach
包里面:
- VirtualMachine 字面意义表示一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息,比如获取内存 dump、线程 dump,类信息统计,比如已加载的类以及实例个数等, loadAgent,Attach 和 Detach (Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给 attach 方法传入一个 JVM 的 PID(进程 id),远程连接到 JVM上 。代理类注入操作只是它众多功能中的一个,通过 loadAgent 方法向 JVM 注册一个代理程序 agent,在该 agent 的代理程序中会得到一个 Instrumentation 实例,该实例可以在 class 加载前改变 class 的字节码,也可以在 class 加载后重新加载。在调用 Instrumentation 实例的方法时,这些方法会使用 ClassFileTransformer 接口中提供的方法进行处理
- VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能
Attach 实现动态注入的原理如下:通过 VirtualMachine 类的attach(pid)
方法,便可以 attach 到一个运行中的 java 进程上,之后便可以通过loadAgent(agentJarPath)
来将 agent 的 Jar 包注入到对应的进程,然后对应的进程会调用 agentmain 方法
Attach 模式
在 JavaAgent 项目中新编写一个 AgentMain 类:
1 | import java.lang.instrument.Instrumentation; |
我们需要编写一个 AgentMainTransformer 类,去修改方法:
1 | import javassist.*; |
在 Agent 项目里编写 AgentMainTest,用于注入 Agent:
1 | import com.sun.tools.attach.*; |
注意修改 Jar 的路径,另外修改 Agent 的pom.xml
:
1 | <dependencies> |
还需要去除我们在前面为测试 Agent 而添加的 VM 选项,改变测试 Agent 项目以前的 Main 类:
1 | public class Main { |
然后先运行测试 Agent 的类,再运行 Agent 的类,就会出现:
这样就成功了,还有些要注意的内容:
- 已加载的 Java 类是不会再被 Agent Attach 处理的,所以我们需要调用
retransformClasses
- premain 和 agentmain 两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有 Class 类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类
- 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义
Instrumentation#redefineClasses()
方法,此方法有以下限制:- 新类和老类的父类必须相同
- 新类和老类实现的接口数也要相同,并且是相同的接口
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
- 新类和老类新增或删除的方法必须是
private static/final
修饰的 - 可以修改方法体
- Java Agent 中的所有依赖,在原进程中的 classpath 中都要能找到,否则在注入时原进程会报错 NoClassDefFoundError
- Agent 进程的 classpath 中必须有
tools.jar
(提供 VirtualMachine Attach API ),JDK 默认有tools.jar
,JRE 默认没有。并且 Linux 和 Windows 之间是存在一个适配问题
到此介绍完毕
本文参考链接: