少女祈祷中...

java Agent

JDK1.5开始,Java新增了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,允许JVM在加载某个class文件之前对其字节码进行修改,同时也支持对已加载的class(类字节码)进行重新加载(Retransform)。
利用Java Agent这一特性衍生出了APM(Application Performance Management,应用性能管理)、RASP(Runtime application self-protection,运行时应用自我保护)、IAST(Interactive Application Security Testing,交互式应用程序安全测试)等相关产品,它们都无一例外的使用了Instrumentation/JVMTI的API来实现动态修改Java类字节码并插入监控或检测代码。

简介

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 内存马

对于 Agent(代理)来讲,其大致可以分为两种,一种是在 JVM 启动前加载的premain-Agent,另一种是 JVM 启动之后加载的 agentmain-Agent。这里我们可以将其理解成一种特殊的 Interceptor(拦截器),如下图
Premain-Agent

agentmain-Agent

Java agent的使用方式有两种:

  • 实现premain方法,在JVM启动前加载。
  • 实现agentmain方法,在JVM启动后加载。

premain和agentmain函数声明如下,拥有Instrumentation inst参数的方法优先级更高

public static void agentmain(String agentArgs, Instrumentation inst) {
...
}

public static void agentmain(String agentArgs) {
...
}

public static void premain(String agentArgs, Instrumentation inst) {
...
}

public static void premain(String agentArgs) {
...
}

第一个参数String agentArgs就是Java agent的参数。

ava Agent还限制了我们必须以jar包的形式运行或加载,我们必须将编写好的Agent程序打包成一个jar文件。除此之外,Java Agent还强制要求了所有的jar文件中必须包含/META-INF/MANIFEST.MF文件,且该文件中必须定义好Premain-Class(Agent模式)或Agent-Class:(Agent模式)配置

使用

premain

构建
编写agent项目

package com.yyjccc;

import java.lang.instrument.Instrumentation;

public class PerMain {
public static void premain(String args, Instrumentation instrumentation) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("my Agent --"+i);
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}).start();
}
}

打包成jar
使用idea打包

agent不要选取主类,选择通过清单链接
在src/main/resources/目录下修改META-INF/MANIFEST.MF,需要指定Premain-Class,注意最后的换行

Manifest-Version: 1.0
Premain-Class: com.yyjccc.PerMain

然后idea输出构建工件

最后得到需要附加的Agnet jar包

使用
测试主程序

package com.yyjccc;

public class Main {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
System.out.println(i);
Thread.sleep(1000*2);
}
}
}

配置启动设置
添加虚拟机选项VM option
设置-javaagent 值为刚才的jar包路径

-javaagent:F:\code\java\myAgent\myAgent\target\artifacts\myAgent_jar\myAgent.jar

运行程序

附加的包的premain方法会在Main函数之前执行(这里是多开了一个线程)
还有直接使用命令

java  -javaagent:F:\code\java\myAgent\myAgent\target\artifacts\myAgent_jar\myAgent.jar -jar .\common.jar

缺陷
这种方法存在一定的局限性——只能在启动时使用-javaagent参数指定。在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。

agentmain

写一个agentmain和premain差不多,只需要在META-INF/MANIFEST.MF中加入Agent-Class:即可,且编写的是agentmain方法

Manifest-Version: 1.0
Premain-Class: com.yyjccc.PerMain
Agent-Class: com.yyjccc.AgentMain

附加Agent的代码

package com.yyjccc;

import java.lang.instrument.Instrumentation;

public class AttachAgent {
public static void agentmain(String args, Instrumentation instrumentation) {
for (int i = 0; i < 10; i++) {
System.out.println("Attach Agent --"+i);
}
}
}

同样的要将Agent程序打包成jar

不同的是,这种方法不是通过JVM启动前的参数来指定的,官方为了实现启动后加载,提供了Attach API。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面。着重关注的是VitualMachine这个类。

VirtualMachine

JDK 默认有tools.jar,JRE 默认没有。并且 Linux 和 Windows 之间是存在一个适配问题
字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息、 loadAgent,Attach 和 Detach 等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例
(注意jdk8 , 依赖是tool.jar)在jdk安装目录中的lib目录下,idea中手动加一下

下面列几个这个类提供的方法:

public abstract class VirtualMachine {
// 获得当前所有的JVM列表
public static List<VirtualMachineDescriptor> list() { ... }

// 根据pid连接到JVM
public static VirtualMachine attach(String id) { ... }

// 断开连接
public abstract void detach() {}

// 加载agent,agentmain方法靠的就是这个方法
public void loadAgent(String agent) { ... }

}

根据提供的api,可以写出一个attacher
实现注入的代码

package com.yyjccc;
/**
* jdk>=9
*/

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class AgentMain {
public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException {
String id = args[0];
String jarName = args[1];

System.out.println("id ==> " + id);
System.out.println("jarName ==> " + jarName);

VirtualMachine virtualMachine = VirtualMachine.attach(id);
virtualMachine.loadAgent(jarName);
virtualMachine.detach();

System.out.println("ends");
}
}

通过pid向对应java程序进程注入附加Agent
执行这段代码,就能将jar包附加上对应的进程

VirtualMachineDescriptor

com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例

package com.drunkbaby;  

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名称为common.jar则返回其PID
if(vmd.displayName().equals("common.jar"))
System.out.println(vmd.id());
}

}
}

如果我们启动一个comm.jar
也可以使用这个类的方法,使用如下代码注入Agent,就不要手动找pid了

package com.yyjccc.Agent;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class Attacher {
public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException {
String agentJar="F:\\code\\java\\myAgent\\myAgent\\target\\artifacts\\myAgent_jar\\myAgent.jar";
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){

//遍历每一个正在运行的JVM,如果JVM名称为common.jar则返回其PID
if(vmd.displayName().equals("common.jar")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent(agentJar);
virtualMachine.detach();
System.out.println("attached pid "+vmd.id());
System.out.println("attached jar "+agentJar);
}

}
}
}

以附加agentmain的方式可以不断使用上面程序进行注入

这里就附加了三次

Instrumentation

Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。

上面的只是附加运行代码,而Instrumentation能实现读取、修改已加载的字节码操作

其在 Java 中是一个接口,常用方法如下

public interface Instrumentation {

//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);

//删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);


//在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;



//判断一个类是否被修改
boolean isModifiableClass(Class<?> theClass);

// 获取目标已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();

//获取一个对象的大小
long getObjectSize(Object objectToSize);

}

ClassFileTransformer

转换类文件,该接口下只有一个方法:transform,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在 java agent 内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与 addTransformer 搭配使用。

//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。  
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

简单概括一下:

  1. 使用Instrumentation.addTransformer()来加载一个转换器。
  2. 转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。
  3. 对于没有加载的类,会使用ClassLoader.defineClass()定义它;对于已经加载的类,会使用ClassLoader.redefineClasses()重新定义,并配合Instrumentation.retransformClasses进行转换。

下面给出一个修改已加载类的例子
程序运行下面代码

package com.yyjccc.Agent;

import static java.lang.Thread.sleep;

public class SayHello {
public static void main(String[] args) throws InterruptedException {
while(true) {
hello();
sleep(3000);
}
}

private static void hello() {
System.out.println("hello world ");
}
}

编写agent:
编写一个ClassFileTransformer的实现类,重写transform方法,然后使用javassist修改方法体

package com.yyjccc.transformer;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Hello_Transform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();
//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}

//获取目标类
CtClass ctClass = classPool.get("com.yyjccc.Agent.SayHello");
//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("hello");

//设置方法体
String body = "{System.out.println(\"Hacker!\");}";
ctMethod.setBody(body);

//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;

}catch (Exception e){
e.printStackTrace();
}
return null;
}

}

agentmain

package com.yyjccc;

import com.yyjccc.transformer.Hello_Transform;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class EvalAgent {
public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses();

//获取目标JVM加载的全部类
for(Class cls : classes){
if (cls.getName().equals("com.yyjccc.Agent.SayHello")){
//添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new Hello_Transform(),true);
inst.retransformClasses(cls);
}
}

}
}

MANFIEST.MF设置运行可以重新类加载

Manifest-Version: 1.0
Premain-Class: com.yyjccc.PerMain
Agent-Class: com.yyjccc.EvalAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

最后打jar包

然后注入

package com.yyjccc.Agent;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class Attacher {
public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException {
String agentJar="F:\\code\\java\\myAgent\\myAgent\\target\\artifacts\\myAgent_jar\\myAgent.jar";
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){

//遍历每一个正在运行的JVM,如果JVM名称为common.jar则返回其PID
if(vmd.displayName().equals("common.jar")){
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent(agentJar);
virtualMachine.detach();
System.out.println("attached pid "+vmd.id());
System.out.println("attached jar "+agentJar);
}

}
}
}

局限性

大多数情况下,我们使用 Instrumentation 都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:
premain 和 agentmain 两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有 Class 类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换 (Class Transform),类转换其实最终都回归到类重定义 Instrumentation#redefineClasses 方法,此方法有以下限制:

  1. 新类和老类的父类必须相同
  2. 新类和老类实现的接口数也要相同,并且是相同的接口
  3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
  4. 新类和老类新增或删除的方法必须是 private static/final 修饰的
  5. 可以修改方法体

Maven插件

不去手动写MANIFREST.MF文件的方式,使用maven插件:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>com.yyjccc.EvalAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>

Reference