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 { public static List<VirtualMachineDescriptor> list () { ... } public static VirtualMachine attach (String id) { ... } public abstract void detach () {} public void loadAgent (String agent) { ... } }
根据提供的api,可以写出一个attacher 实现注入的代码
package com.yyjccc;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) { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ 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" ; List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ 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 { void addTransformer (ClassFileTransformer transformer, boolean canRetransform) ; void addTransformer (ClassFileTransformer transformer) ; boolean removeTransformer (ClassFileTransformer transformer) ; void retransformClasses (Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass (Class<?> theClass) ; @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); long getObjectSize (Object objectToSize) ; }
转换类文件,该接口下只有一个方法:transform,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在 java agent 内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与 addTransformer 搭配使用。
void addTransformer (ClassFileTransformer transformer, boolean canRetransform) ;
简单概括一下:
使用Instrumentation.addTransformer()来加载一个转换器。
转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。
对于没有加载的类,会使用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 { 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(); for (Class cls : classes){ if (cls.getName().equals("com.yyjccc.Agent.SayHello" )){ 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" ; List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ 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 方法,此方法有以下限制:
新类和老类的父类必须相同
新类和老类实现的接口数也要相同,并且是相同的接口
新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
新类和老类新增或删除的方法必须是 private static/final 修饰的
可以修改方法体
Maven插件 不去手动写MANIFREST.MF文件的方式,使用maven插件:
<plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-jar-plugin</artifactId > <version > 3.1.0</version > <configuration > <archive > <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