[备份]学习Java Agent

介绍

JavaAgent技术是什么:自行搜索

为什么要学JavaAgent:破解Java软件,Agent内存马,RASP等

其中Agent的Jar由:Manifest,Agent Class,ClassFile Transformer组成

他们之间的关联大致如下:

根据Mainifest中定义的属性找到具体的AgentClass,调用对应的方法

在方法中通过Instrumentation对象使用ClassFileTransformer

而操作字节码则需要了解Class文件和ASM或Javassist的使用

 

Manifest

首先来看Manifest中与Java Agent相关的属性

这里做一个简单的介绍,对相关属性有大体的了解,结合后续实例进一步理解

(1)Premain-Class

使用Java Agent有两种方式:Load-Time和Dynamic,也就是说一种是启动时,一种是运行时。当我们使用启动时Agent需要加入-javaagent参数,这时候就会在Manifest中寻找Premain-Class属性

这个属于记录的是一个类名,要求这个类中必须包含premain方法

(2)Agent-Class

使用另一种Dynamic方式的Java Agent需要寻找Manifest的Agent-Class属性

该属性记录的也是一个类名,要求这个类中必须包含agentmain方法

(3)Can-Redefine-Classes

这是一个Boolean类型的属性,默认False

判断当前JVM的配置是否支持类的重新定义(后续介绍)

(4)Can-Retransform-Classes

这是一个Boolean类型的属性,默认False

如果需要调用Instrumentation.retransformClasses等方法需要设置为True

(5)Can-Set-Native-Method-Prefix

这是一个Boolean类型的属性,默认False

是否支持设置本地方法前(后续介绍)

(6)Class-Path与Boot-Class-Path

JVM有三个ClassLoader:Bootstrap,Extension,Application(System)

其中Class-Path指定的Jar包将由Application(System) ClassLoader加载

其中Boot-Class-Path指定的Jar包将由Bootstrap ClassLoader加载

之所以要提到Boot-Class-Path属性,因为在某些情况下必须使用该属性

(7)Launcher-Agent-Class

该属性是Java9引用的属性,记录的是一个类名,类似Premain-Class和Agent-Class

可用于在main方法执行之前做一些事情

 

Agent Class

(1)LoadTimeAgent

注意到上文:LoadTimeAgent需要配置Premain-Class属性且要求premain方法存在

这个方法有两种使用方式

另一种

实际上大多数情况下需要使用的是第一种方式,因为我们的目的是修改某个Class文件,如果需要修改Class文件则离不开Instrumentation对象

其实JVM在加载Agent的时候,也是优先选择第一种方法,如果无法找到则会选择第二种

(2)DynamicAgent

注意到上文:DynamicAgent需要配置Agent-Class属性且要求agentmain方法存在

这个方法也有两种使用方式

另一种

可以发现与LoadTimeAgent几乎一致

 

ClassFileTransformer

上文提到:如果需要修改Class文件则离不开Instrumentation对象

Instrumentation是一个接口,关于这个接口更多的方法暂不介绍

先来看最基本的两个方法:添加和移除ClassFileTransformer的方法

如果我们需要修改字节码,需要实现ClassFileTransformer接口并重写transform方法

 

LoadTime示例

目标:利用JavaAgent技术打印正在加载的类(LoadTimeAgent)

 

测试程序

一个提供加法和减法的方法

一个简单的类:随机生成数字并做加减的运算

编译运行

观察到如下的打印

 

编写Agent

首先编写manifest.txt文件(结尾必须是一个换行)

编写AgentClass

编写ClassFileTransformer实现简单的打印功能

 

手动打包

找到所有的Java文件并将文件名写入sources.txt

编译

复制manifest.txt文件到输出目录

使用命令生成Agent Jar

使用-javaagnet启动,观察到很多打印内容说明成功

 

修改字节码示例

注意到上文的LoadTime示例并没有修改字节码

目标:打印出方法接收的参数,使用LoadTimeAgent实现

 

ASM代码

测试程序不变,由于调用打印方法需要修改字节码,所以编写ASM代码

这里使用JDK自带的ASM框架,对于JDK8而言,ASM对应版本是5

该ClassVisitor的目的是遇到非构造方法和非静态代码块时,交给自定义MethodAdapter处理

在对应的MethodAdapter中,逻辑是这样:一开始进入方法时,打印方法名等信息,然后遍历方法参数,使用ILOAD指令将每个参数入栈。之所以这样做是因为后续打印方法的调用需要弹栈,取出这个参数

由于System.out.println打印方法实际上对应的字节码指令比较复杂,所以自行写了一个ParameterUtils提供静态打印方法

 

编写Agent

其中manifest.txt文件不变(结尾必须是一个换行)

修改LoadTimeAgent类的代码:使用inst.addTransformer

编写ASMTransformer代码:由于我们只处理sample/HelloWorld类所以判断类名是否equals然后用ASM的一套流程对字节码进行修改即可

 

手动打包

找到所有的Java文件并将文件名写入sources.txt

编译(当我们使用javac编译时候并没有直接和rt.jar做关联,而是使用到ct.sym,如果需要指定使用rt.jar则加上-XDignore.symbol.file参数才可以,否则会报错)

复制manifest.txt文件到输出目录

使用命令生成Agent Jar

使用-javaagnet启动,观察到很多打印内容说明成功

打印如下

 

Dynamic示例

以上两个示例都是LoadTimeAgent

这个示例将使用DynamicAgent来实现和示例二一样的功能:打印方法参数

大部分代码都是相同的,区别在于Agent的编写

 

编写Agent

编写DynamicAgent需要在manifest.txt中添加两个属性

编写DynamicAgent代码:注意retransform之后要remove了防止影响过多

使用DynamicAgent需要特殊的一个类VMAttach

关于这个类相关的介绍将放在后文,本文只是使用

 

手动打包

对于DynamicAgent而言想要打包其实比较麻烦,例如上文的VMAttach类需要tools.jar

所以建议分来来编译和打包

首先对测试程序本身进行编译

可以进入out目录运行测试

对Agent进行打包

这时候sources.txt里会有VMAttach的部分,这个编译会报错,所以手动删除

然后进行编译

复制manifest.txt文件

使用命令生成Agent Jar

最后是对VMAttach进行编译(这是针对GIT模拟的Linux命令行MINGW64

在Linux中应该是这样

纯Windows应该是这样

最后运行(同样分三种)

纯Linux

纯Windows

注意:需要先跑起来测试程序,然后再跑VMAttach程序

如果VMAttach打印如下且测试程序开始输出参数则说明成功

 

Maven打包

不难发现,自行编译打包非常复杂,所以迫切需要学习自动打包方式

加入必须的依赖(可以用ASM库而不是JDK内置了)

一些属性配置

由于DynamicAgent需要tools.jar所以另外添加

编译插件(必须)

 

dependency

JAR插件

这时候如果执行代码会出问题,找不到依赖

所以需要另一个插件:作用是把所有依赖复制过来

这时候就可以运行Agent观察效果了

 

assembly

另一种办法是使用assembly把所有jar都打包进去

 

shade

使用shade插件,可以在assembly基础上进一步缩小体积

 

运行测试

对于LoadTimeAgent的测试

对于DynamicAgent的测试

先启动测试程序

再启动Agent

测试成功,用Maven自动打包的确比手动方便很多

(DynamicInstrumentation类就是上文的VMAttach类)