利用Java Agent进行代码植入
利用Java Agent进行代码植入
Java Agent 又叫做 Java 探针,是在 JDK1.5 引入的一种可以动态修改 Java 字节码的技术。可以把javaagent理解成一种代码注入的方式。但是这种注入比起spring的aop更加的优美。
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) {
...
}
JVM 会优先加载带 Instrumentation
签名的方法,加载成功忽略第二种;如果第一种没有,则加载第二种方法。
-
第一个参数
String agentArgs
就是Java agent的参数。 -
Inst
是一个java.lang.instrument.Instrumentation
的实例,可以用来类定义的转换和操作等等。
premain方式
JVM启动时 会先执行 premain
方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,就可以结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。
使用实例:
1)创建应用程序Task.jar
先创建一个Task.jar用于模拟在实际场景中的应用程序,Task.java:
public class Task {
public static void main (String[] args) {
System.out.println("task mian run");
}
}
把Task打成jar包:
此jar包可以单独执行:java -jar Task.jar
2)创建premain方式的Agent
新建一个Agent01.jar,用于在task之前执行:
import java.lang.instrument.Instrumentation;
public class Agent01 {
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premain run----");
}
}
此时项目如果打成jar包,缺少入口main文件,所以需要自己定义一个MANIFEST.MF
文件,用于指明premain
的入口在哪里:
在src/main/resources/
目录下创建META-INF/MANIFEST.MF
:
Manifest-Version: 1.0
Premain-Class: com.test.Agent01
注意:最后一行是空行,不能省略。以下是MANIFEST.MF的其他选项
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(可选)
同样的打成jar包:
回顾下我们之前单独运行task.jar时候,控制台前后并没有打印其他信息
现在我们来使用premain进行注入: java -javaagent:Agent01.jar -jar Task.jar
可以看到premain比task先运行,通过启动时候指定参数javaagent来达到注入的效果
以下是先知社区师傅的流程图:
这种方法存在一定的局限性——只能在启动时使用-javaagent
参数指定。在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。
agentmain方式
同样使用一个案例来说明使用方式
使用实例:
1)创建应用程序Task.jar
和之前的premain方式一样,创建一个Task.jar作为应用程序:
import java.util.Scanner;
public class Task {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
scanner.hasNext();
}
}
把创建的Task.jar运行起来:java -jar Task.jar
2)创建一个agentmain方式的Agent
创建一个agentmain方式的Agent02.jar,Agent02.java:
import java.lang.instrument.Instrumentation;
public class Agent02 {
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("打印全部加载的类:");
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
System.out.println(allLoadedClass.getName());
}
}
}
同样生成jar包的话,需要手动定义一个MANIFEST.MF
文件
Manifest-Version: 1.0
Agent-Class: com.test.Agent02
3)利用VirtualMachine注入
使用VirtualMachine
类来利用前面创建的Agent进行代理类注入,VirtualMachine
类在jdk目录下的lib/tools.jar包,需要手动导入
package com.test;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 App { public static void main( String[] args ) { try { //VirtualMachine 来自tools.jar // VirtualMachine.attach("9444") 9444为线程PID,使用jps查看 VirtualMachine vm = VirtualMachine.attach("9444"); //指定要使用的Agent路径 vm.loadAgent("C:\\Users\\xxx\\Desktop\\Agent02.jar"); } catch (AttachNotSupportedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (AgentLoadException e) { e.printStackTrace(); } catch (AgentInitializationException e) { e.printStackTrace(); } }}
运行这个名为App的类之后,正在运行的Task程序会执行代码:
以下是先知社区的图:
Java Agent 代码植入
利用agentmain配合Javassist,在方法执行前,修改任意类的方法。在演示之前,先来看几个知识点。
Instrumentation类
在agentmain的构造函数中,第二个参数就是Instrumentation
public static void agentmain(String agentArgs, Instrumentation inst)
这个类就是用来进行aop操作的类,能够替换和修改某些类的定义
public interface Instrumentation { // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 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(); ......}
其中addTransformer()
和retransformClasses()
用来篡改Class的字节码。
从源码中看到addTransformer
方法参数中,第一个参数传递的为ClassFileTransformer
类型
ClassFileTransformer接口
这是一个接口,它提供了一个transform
方法:
public interface ClassFileTransformer { default byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { .... }}
接下来就用一个示例来演示利用agentmain配合Javassist进行代码植入的操作
示例:
1)新建一个hello.jar模拟启动的应用程序
//HelloWorld.javapublic class HelloWorld { public static void main(String[] args) { System.out.println("start..."); hello h1 = new hello(); h1.hello(); // 产生中断,等待注入 Scanner sc = new Scanner(System.in); sc.nextInt(); hello h2 = new hello(); h2.hello(); System.out.println("ends..."); }}//hello.javapublic class hello { public void hello(){ System.out.println("hello world"); }}
2)创建javaAgent.jar
//AgentDemo.javapackage com.test;import java.io.IOException;import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class AgentDemo { public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException { Class[] classes = inst.getAllLoadedClasses(); // 判断类是否已经加载 for (Class aClass : classes) { if (aClass.getName().equals(TransformerDemo.editClassName)) { // 添加 Transformer inst.addTransformer(new TransformerDemo(), true); // 触发 Transformer inst.retransformClasses(aClass); } } }}//TransformerDemo.javapackage com.test;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 TransformerDemo implements ClassFileTransformer { // 只需要修改这里就能修改别的函数 public static final String editClassName = "com.test.hello"; public static final String editClassName2 = editClassName.replace('.', '/'); public static final String editMethodName = "hello"; @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { ClassPool cp = ClassPool.getDefault(); if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); cp.insertClassPath(ccp); } CtClass ctc = cp.get(editClassName); CtMethod method = ctc.getDeclaredMethod(editMethodName); String source = "{System.out.println(\"hello transformer\");}"; method.insertBefore(source); byte[] bytes = ctc.toBytecode(); ctc.detach(); return bytes; } catch (Exception e){ e.printStackTrace(); } return null; }}
在MANIFEST.MF
文件中加入
Manifest-Version: 1.0Agent-Class: com.test.AgentDemoCan-Redefine-Classes: trueCan-Retransform-Classes: true
3)利用VirtualMachine注入
package com.test;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 App { public static void main( String[] args ) { try { //VirtualMachine 来自tools.jar // VirtualMachine.attach("9444") 9444为线程PID VirtualMachine vm = VirtualMachine.attach("9444"); //指定要使用的Agent路径 vm.loadAgent("C:\\Users\\xxx\\Desktop\\javaAgent.jar"); } catch (AttachNotSupportedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (AgentLoadException e) { e.printStackTrace(); } catch (AgentInitializationException e) { e.printStackTrace(); } }}
测试:
运行hello.jar
使用VirtualMachine连接VM,进行注入后,第二次调用hello方法已经成功增加了一行hello transformer