Java agent注入技术初探 - 郑瀚Andrew
2023-6-10 21:16:0 Author: www.cnblogs.com(查看原文) 阅读量:43 收藏

拿常规的hook和event callback技术进行类比可以按照如下通俗理解:

  • JVMTI相当于操作系统原生提供的hook和event callback回调框架,它是一个事件驱动的工具实现接口,通过JVMTI才能对JVM内部状态进行感知和交互
  • instrumentation API相当于hook和event callback回调API接口,是具体实现和JVM交互的API实现接口,它原生支持了JVM内部类的Hook劫持机制
  • Agent相当于一个独立于操作系统内核的第三方程序,JVM内部状态感知和交互的事情不太适合在JVM内部集成,而是通过一个外部的第三方Agent来完成
  • 字节码生成库相当于Shellcode/so/exe/elf生成器,用于生成具体的符合JVM规范的hook函数代码逻辑
  • 启动时静态挂载(premain)相当于LD_PRELOAD机制或者Boot启动机制注入,可以实现在JVM应用启动前执行agent的代码逻辑,premain是注入shellcode/so的入口函数
  • 运行时动态挂载(agentmain)相当于ptrace动态so/dll/shellcode注入技术,区别在于JVM原生支持的Attach API内部就集成了进程间通信功能,相比于ptrace shellcode注入技术,VirtualMachine要更加稳定。agentmain是注入shellcode/so的入口函数

0x1:JVMTI介绍

关于JVM TI技术,官方文档的解释如下:

The JVM tool interface (JVM TI) is a native programming interface for use by tools. It provides both a way to inspect the state and to control the execution of applications running in the Java virtual machine (JVM). JVM TI supports the full breadth of tools that need access to JVM state, including but not limited to:

  • profiling
  • debugging
  • monitoring
  • thread analysis
  • coverage analysis tools

Note: JVM TI was introduced at JDK 5.0. JVM TI replaced the Java Virtual Machine Profiler Interface (JVMPI) and the Java Virtual Machine Debug Interface (JVMDI) which, as of JDK 6, are no longer provided.

JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的 Native 接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。

JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。从 Java SE 5 开始,可以使用 Java 的Instrumentation 接口(java.lang.instrument)来编写 Agent。无论是通过 Native 的方式还是通过 Java Instrumentation 接口的方式来编写 Agent,它们的工作都是借助 JVMTI 来进行完成。

JVMTI和Instumentation API的作用很相似,都是一套 JVM 操作和监控的接口,且都需要通过 agent 来启动:

  • Instumentation API​需要打包成 jar,并通过 Java agent 加载(对应启动参数: -javaagent,或者通过attach api动态注入)
  • JVMTI 需要打包成动态链接库(随操作系统,如.dll/.so 文件),并通过 JVMTI agent 加载(对应启动参数:-agentlib/-agentpath,或者通过attach api动态注入)

0x2:加载时机

  • 启动时(Agent_OnLoad)
  • 运行时 Attach(Agent_OnAttach)

0x3:功能

Instumentation API 可以支持 Java 语言实现 agent 功能,但 Instumentation API 本身是基于 JVMTI 实现的,JVMTI 功能比 Instumentation API 更强大,它支持:

  • 获取所有线程、查看线程状态、线程调用栈、查看线程组、中断线程、查看线程持有和等待的锁、获取线程的 CPU 时间、甚至将一个运行中的方法强制返回值……
  • 获取 Class、Method、Field 的各种信息,类的详细信息、方法体的字节码和行号、向 Bootstrap/System Class Loader 添加 jar、修改 System Property……
  • 堆内存的遍历和对象获取、获取局部变量的值、监测成员变量的值……
  • 各种事件的 callback 函数,事件包括:类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、gc 开始和结束、方法调用进入和退出、临界区竞争与等待、VM 启动与退出……
  • 设置与取消断点、监听断点进入事件、单步执行事件……

0x4:JVMTI 与 Java agent

Java agent 是基于 JVMTI 实现,核心部分是 ClassFileLoadHook和TransFormClassFile。

  • ClassFileLoadHook​是一个 JVMTI 事件,该事件是 Instrumentation agent 的一个核心事件,主要是在读取字节码文件回调时调用,内部调用了TransFormClassFile的函数。
  • TransFormClassFile​的主要作用是调用java.lang.instrument.ClassFileTransformer的tranform​方法,该方法由开发者实现,通过Instrumentation的addTransformer方法进行注册。

在字节码文件加载的时候,会触发ClassFileLoadHook​事件,该事件调用TransFormClassFile​,通过经由Instrumentation​ 的 addTransformer 注册的方法完成整体的字节码修改。

对于已加载的类,需要调用retransformClass​函数,然后经由redefineClasses​函数,在读取已加载的字节码文件后,若该字节码文件对应的类关注了ClassFileLoadHook​事件,则调用ClassFileLoadHook事件。后续流程与类加载时字节码替换一致。 

参考链接:

https://docs.oracle.com/javase/8/docs/technotes/guides/jvmti/

关于Java Interface Instrumentation技术,官方文档的解释如下:

public interface Instrumentation

This class provides services needed to instrument Java programming language code.

Instrumentation is the addition of byte-codes to methods for the purpose of gathering data to be utilized by tools. Since the changes are purely additive, these tools do not modify application state or behavior.

Examples of such benign tools include

  • monitoring agents
  • profilers
  • coverage analyzers
  • event loggers

There are two ways to obtain an instance of the Instrumentation interface:

  1. When a JVM is launched in a way that indicates an agent class. In that case an Instrumentation instance is passed to the premain method of the agent class.

  2. When a JVM provides a mechanism to start agents sometime after the JVM is launched. In that case an Instrumentation instance is passed to the agentmain method of the agent code.

Once an agent acquires an Instrumentation instance, the agent may call methods on the instance at any time.

JDK™5.0中引入包java.lang.instrument。 该包提供了一个Java编程API,可以用来开发增强Java应用程序的工具,例如监视它们或收集性能信息。 使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP(AOP核心理念是定义切入点(pointcut)以及通知(advice)。程序控制流中所有匹配该切入点的连接点(joinpoint)都将执行这段通知代码)的功能了。

在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能,所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大,包括:

  • 启动后的 instrument。在 Java SE6 里面,最大的改变是运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实践中,有很多的情况,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。
  • 本地代码(native code)instrument。对 native 的 Instrumentation 也是 Java SE 6 的一个崭新的功能,这使以前无法完成的功能 —— 对 native 接口的 instrumentation 可以在 Java SE 6 中,通过一个或者一系列的 prefix 添加而得以完成。
  • 动态改变 classpath。Java SE 6 里的 Instrumentation 也增加了动态添加 class path 的功能。
  • 等等

这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。

java.lang.instrument包的具体实现,依赖于 JVMTI。在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。

Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 –javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。

Instrumentation 的一些主要方法如下:

public interface Instrumentation {
    /**
     * 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
     * Transformer可以直接对类的字节码byte[]进行修改
     */
    void addTransformer(ClassFileTransformer transformer);

    /**
     * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
     * retransformClasses可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

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

    /**
     * 将一个jar加入到bootstrap classloader的 classpath里
     */
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    /**
     * 获取当前被JVM加载的所有类对象
     */
    Class[] getAllLoadedClasses();
}

其中最常用的方法是addTransformer(ClassFileTransformer transformer)​,这个方法可以在类加载时做拦截,对输入的类的字节码进行修改,其参数是一个ClassFileTransformer接口,定义如下:

public interface ClassFileTransformer {

    /**
     * 传入参数表示一个即将被加载的类,包括了classloader,classname和字节码byte[]
     * 返回值为需要被修改后的字节码byte[]
     */
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

addTransformer​方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses​来重新触发这个Transformer​的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。

在运行时,我们可以通过Instrumentation的redefineClasses​方法进行类重定义,在redefineClasses方法上有一段注释需要特别注意: 

     * The redefinition may change method bodies, the constant pool and attributes.
     * The redefinition must not add, remove or rename fields or methods, change the
     * signatures of methods, or change inheritance.  These restrictions maybe be
     * lifted in future versions.  The class file bytes are not checked, verified and installed
     * until after the transformations have been applied, if the resultant bytes are in
     * error this method will throw an exception.

这里面提到,我们不可以增加、删除或者重命名字段和方法,改变方法的签名或者类的继承关系。认识到这一点很重要,当我们通过 ASM 获取到增强的字节码之后,如果增强后的字节码没有遵守这些规则,那么调用redefineClasses方法来进行类的重定义就会失败。   

参考链接:

https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html

Java agent 动态加载是通过 Attach API 实现。

0x1:Attach API 介绍

Attach 机制是 JVM 提供一种 JVM 进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作。

日常很多工作都是通过 Attach API 实现的,例如:

  • JDK 自带的一些命令,如:jstack 打印线程栈、jps 列出 Java 进程、jmap 做内存 dump 等功能
  • Arthas、Greys、btrace 等监控诊断产品,通过 attach 目标 JVM 进程发送指定命令,可以实现方法调用等方面的监控。

0x2:Attach API 用法

由于是进程间通讯,那代表着使用 Attach API 的程序需要是一个独立的 Java 程序,通过 attach 目标进程,与其进行通讯。

下面的代码表示了向进程 pid 为 1234 的 JVM 发起通讯,加载一个名为 agent.jar 的 Java agent。

// VirtualMachine等相关Class位于JDK的tools.jar
VirtualMachine vm = VirtualMachine.attach("1234");  // 1234表示目标JVM进程pid
try {
    vm.loadAgent(".../javaagent.jar");    // 指定agent的jar包路径,发送给目标进程
} finally {
    // attach 动作的相反的行为,从 JVM 上面解除一个代理
    vm.detach();
}

vm.loadAgent 之后,相应的 agent 就会被目标 JVM 进程加载,并执行 agentmain() 方法。

0x3:Attach API 原理

Linux 系统为例。当 external process(attach 发起的进程)执行 VirtualMachine.attach 时,需要通过操作系统提供的进程通信方法,例如信号、socket,进行握手和通信。其具体内部实现流程如下所示: 

上面提到了两个文件:

  • .attach_pidXXX 后面的 XXX 代表 pid,例如 pid 为 1234 则文件名为.attach_pid1234。该文件目的是给目标 JVM 一个标记,表示触发 SIGQUIT 信号的是 attach 请求。这样目标 JVM 才可以把 SIGQUIT 信号当做 attach 连接请求,再来做初始化。其默认全路径为/proc/XXX/cwd/.attach_pidXXX,若创建失败则使用/tmp/attach_pidXXX
  • .java_pidXXX 后面的 XXX 代表 pid,例如 pid 为 1234 则文件名为.java_pid1234。由于 Unix domain socket 通讯是基于文件的,该文件就是表示 external process 与 target VM 进行 socket 通信所使用的文件,如果存在说明目标 JVM 已经做好连接准备。其默认全路径为/proc/XXX/cwd/.java_pidXXX,若创建失败则使用/tmp/java_pidXXX

VirtualMachine.attach 动作类似 TCP 创建连接的三次握手,目的就是搭建 attach 通信的连接。而后面执行的操作,例如 vm.loadAgent,其实就是向这个 socket 写入数据流,接收方 target VM 会针对不同的传入数据来做不同的处理。

0x1:javassist

Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库,它使 Java 程序能够在运行时定义一个新类,并在 JVM 加载时修改类文件。

与其他类似的字节码编辑器不同, Javassist 提供了两个级别的 API:

  • 源级别。如果用户使用源级 API,他们可以编辑类文件,而不需要知道 Java 字节码的开发规范。整个 API 只用 Java 语言的词汇来设计。 您甚至可以以源文本的形式指定插入的字节码,Javassist 在运行中编译它
  • 字节码级别。字节码级 API 允许用户直接编辑类文件

0x2:ASM

ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

ASM框架中的核心类有以下几个:

  1. ClassReader:该类用来解析编译过的class字节码文件。
  2. ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。
  3. ClassAdapter:该类也实现了ClassVisitor接口,它将对它的方法调用委托给另一个ClassVisitor对象。

参考链接:

https://central.sonatype.com/artifact/org.ow2.asm/asm/7.0-beta?smo=true
https://codeleading.com/article/62733319970/ 

参考链接:

https://www.51cto.com/article/722419.html
https://www.jianshu.com/p/1b07a8a6a475
https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/%E6%B7%B1%E5%85%A5%E6%8B%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/33%20%20Java%20Agent%E4%B8%8E%E5%AD%97%E8%8A%82%E7%A0%81%E6%B3%A8%E5%85%A5.md
https://leokongwq.github.io/2017/12/21/java-agent-instrumentation.html
https://www.cnblogs.com/liboware/p/12497621.html

主流的 JVM 都提供了Instrumentation​的实现,但是鉴于Instrumentation​的特殊功能,并不适合直接提供在 JDK 的runtime里,而更适合出现在 Java 程序的外层,以上帝视角在合适的时机出现。因此如果想使用Instrumentation功能,拿到 Instrumentation 实例,我们必须通过 Java agent。

Java agent​是一种特殊的 Java 程序(Jar 文件),它是Instrumentation​的客户端。与普通 Java 程序通过 main 方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个 Java 应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。

Java agent与Instrumentation​密不可分,二者也需要在一起使用。因为Instrumentation​的实例会作为参数注入到Java agent的启动方法中。

0x1:Java agent的格式

Java agent 以 jar 包的形式部署在 JVM 中,jar 文件的 manifest 需要指定 agent 的类名。根据不同的启动时机,agent 类需要实现不同的方法(二选一)。

  • premain

  • agentmain

JVM 启动时加载 

[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);

JVM 将首先寻找[1],如果没有发现[1],再寻找[2]。 

JVM 运行时加载

[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);

与 premain()一致,JVM 将首先寻找[1],如果没有发现[1],再寻找[2]。

Java agent 的这些功能都是通过 JVMTI agent,也就是 C agent 来实现的。JVMTI 是一个事件驱动的工具实现接口,通常,我们会在 C agent 加载后的入口方法Agent_OnLoad/Agent_OnAttach处注册各个事件的钩子(hook)方法,当 Java 虚拟机触发了这些事件时,便会调用对应的钩子方法。例如:

  • 可以为 JVMTI 中的ClassFileLoadHook事件设置钩子,从而在 C 层面拦截所有的类加载事件

0x2:Java agent的加载

1、Java agent 与 ClassLoader

Java agent 的包先会被加入到 system class path 中,然后 agent 的类会被system calss loader(默认AppClassLoader)所加载,和应用代码的真实 classLoader 无关。例如:

  • 当启动参数加上-javaagent:my-agent.jar​运行 SpringBoot 打包的 fatjar 时,fatjar 中应用代码和 lib 中的嵌套 jar 是由 org.springframework.boot.loader.LaunchedURLClassLoader​ 加载,但这个 my-agent.jar 依然是在system calss loader(默认AppClassLoader))中加载,而非 org.springframework.boot.loader.LaunchedURLClassLoader 加载。 

该类加载逻辑非常重要,在使用 Java agent 时如果遇到ClassNotFoundException、NoClassDefFoundError,很大可能就是与该加载逻辑有关。

2、静态加载 Java agent

Java agent 支持静态加载和动态加载。

静态加载,即 JVM 启动时加载,对应的是 premain()​ 方法。通过 vm 启动参数-javaagent将 agent jar 挂载到目标 JVM 程序,随目标 JVM 程序一起启动。

Java 虚拟机并不限制 Java agent 的数量。你可以在 java 命令后附上多个-javaagent参数。

1)-javaagent 启动参数 

-javaagent​格式:"-javaagent:<jarpath>[=<option>]"。

java -javaagent:agent1.jar=key1=value1&key2=value2 -javaagent:agent2.jar -jar Test.jar

  • [=<option>]​部分可以指定 agent 的参数,可以传递到premain(String agentArgs, Instrumentation inst)​方法的agentArgs入参中
  • 支持可以定义多个 agent,按指定顺序先后执行,上面示例中其中加载顺序为
    • (1) agent1.jar
    • (2) agent2.jar
  • 需要注意的是,不同的顺序可能会导致 agent 对类的修改存在冲突,在实际项目中用到了​pinpoint和SkyWalking​的 agent,当通过-javaagent​先挂载 pinpoint​的 agent ,后挂载 SkyWalking​ 的 agent,出现 SkyWalking​对类的增强发生异常的情况,而先挂载SkyWalking的 agent 则无问题。
  • agent1.jar 的premain(String agentArgs, Instrumentation inst)​方法的agentArgs​值为key1=value1&key2=value2。

2)premain()方法 

premain()方法会在程序 main 方法执行之前被调用,此时大部分 Java 类都没有被加载("大部分"是因为,agent 类本身和它依赖的类还是无法避免的会先加载的),是一个对类加载埋点做手脚(addTransformer)的好机会。

如果此时 premain 方法执行失败或抛出异常,那么 JVM 的启动会被终止。

3)静态加载执行流程

agent 中的 class 由 system calss loader(默认AppClassLoader)加载,premain() 方法会调用 Instrumentation API,然后 Instrumentation API 调用 JVMTI,在需要加载的类需要被加载时,会回调 JVMTI,然后回调 Instrumentation API,触发 ClassFileTransformer.transform()。

可以通过Instrumentation来注册类加载事件的拦截器。该拦截器需要实现ClassFileTransformer接口,并重写其中的transform方法

  • transform方法接收一个 byte 数组类型的参数,它代表的是正在被加载的类的字节码
  • transform方法将返回一个 byte 数组,代表更新过后的类的字节码

当方法返回之后,Java 虚拟机会使用所返回的 byte 数组,来完成接下来的类加载工作;如果transform方法返回 null 或者抛出异常,那么 Java 虚拟机将使用原来的 byte 数组完成类加载工作。

基于这一类加载事件的拦截功能,我们可以实现字节码注入(bytecode instrumentation),往正在被加载的类中插入额外的字节码

4)ClassFileTransformer.transform() 

ClassFileTransformer.transform() 和 ClassLoader.load()的关系

下面是一次 ClassFileTransformer.transform()执行时的方法调用栈,

transform:38, MethodAgentMain$1 (demo)
transform:188, TransformerManager (sun.instrument)
transform:428, InstrumentationImpl (sun.instrument)
defineClass1:-1, ClassLoader (java.lang)
defineClass:760, ClassLoader (java.lang)
defineClass:142, SecureClassLoader (java.security)
defineClass:467, URLClassLoader (java.net)
access$100:73, URLClassLoader (java.net)
run:368, URLClassLoader$1 (java.net)
run:362, URLClassLoader$1 (java.net)
doPrivileged:-1, AccessController (java.security)
findClass:361, URLClassLoader (java.net)
loadClass:424, ClassLoader (java.lang)
loadClass:331, Launcher$AppClassLoader (sun.misc)
loadClass:357, ClassLoader (java.lang)
checkAndLoadMain:495, LauncherHelper (sun.launcher)

可以看到 

  1. ClassLoader.load()​加载类时,ClassLoader.load()​会调用ClassLoader.findClass()
  2. ClassLoader.findClass()​会调用ClassLoader.defefineClass()
  3. ClassLoader.defefineClass()​最终会执行ClassFileTransformer.transform()
  4. ClassFileTransformer.transform()​可以对类进行修改,
  5. --> 所以ClassLoader.load()最终加载 agent 修改后 Class 对象

下面是精简后的 ClassLoader.load() 核心代码:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 判断是否已经加载过了,如果没有,则进行load
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // findClass()内部最终会调用 Java agent 中 ClassFileTransformer.transform()
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
ClassFileTransformer.transform() 和 字节码增强

ClassFileTransformer.transform()​ 中可以对指定的类进行增强,我们可以选择代码生成库修改指定的类的字节码,对类进行增强,比如

  • ASM​
  • CGLIB​
  • Byte Buddy​
  • Javassist

3、动态加载 Java agent

动态加载,即 JVM 启动后的任意时间点(即运行时),通过Attach API​动态地加载 Java agent,对应的是 agentmain() 方法。 

1)agentmain()方法 

对于 VM 启动后加载的Java agent​,其agentmain()​方法会在加载之时立即执行。如果agentmain执行失败或抛出异常,JVM 会忽略掉错误,不会影响到正在 running 的 Java 程序。

一般 agentmain() 中会编写如下步骤:

  • 注册类的ClassFileTransformer
  • 调用retransformClasses 方法对指定的类进行重加载

redefine和retransform针对的是已加载的类,并要求用户传入所要redefine或者retransform的类实例。

  • redefine指的是舍弃原本的字节码,并替换成由用户提供的 byte 数组。该功能比较危险,一般用于修复出错了的字节码
  • retransform则将针对所传入的类,重新调用所有已注册的ClassFileTransformer的transform方法  
    • 在执行premain或者agentmain方法前,Java 虚拟机早已加载了不少类,而这些类的加载事件并没有被拦截,因此也没有被注入。使用retransform功能可以注入这些已加载但未注入的类
    • 在定义了多个 Java agent,多个注入的情况下,我们可能需要移除其中的部分注入。当调用Instrumentation.removeTransformer去除某个注入类后,我们可以调用retransform功能,重新从原始 byte 数组开始进行注入

参考链接:

https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html 

0x1:premain Instrumentation agent注入

1、创建Java Agent 规范的Jar

创建如下结构文件

PreDemo.java内容为:

import java.lang.instrument.Instrumentation;

public class PreDemo {
    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中
     * 并被同一个System ClassLoader装载
     * 被统一的安全策略(security policy)和上下文(context)管理
     */
    public static void premain(String agentOps, Instrumentation inst) {

        System.out.println("====premain1 execute====");
        System.out.println(agentOps);
    }

    /**
     * 如果不存在 premain(String agentOps, Instrumentation inst)
     * 则会执行 premain(String agentOps)
     */
    public static void premain(String agentOps) {

        System.out.println("====premain2 execute====");
        System.out.println(agentOps);
    }
}

MANIFEST.MF内容为:

Manifest-Version: 1.0
Premain-Class: PreDemo
Can-Redefine-Classes: true

打包agent代码为一个独立的jar包,

cd /Users/zhenghan/Projects/JavaAgent_Demo/src

# 编译成.class 文件
javac PreDemo.java

# 加入META-INF/MANIFEST.MF,打包
jar -cvfm JavaAgent_Demo.jar META-INF/MANIFEST.MF PreDemo.class

2、创建被注入的jar包程序 - 以hello world为例

写一个简单的hello world,用于演示premian注入特定java应用。 

创建如下结构文件

HelloWorldMain.java内容,

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class HelloWorldMain {
    public static void main(String[] args) {
        // Press Opt+Enter with your caret at the highlighted text to see how
        // IntelliJ IDEA suggests fixing it.
        System.out.printf("Hello and welcome!");

        // Press Ctrl+R or click the green arrow button in the gutter to run the code.
        for (int i = 1; i <= 5; i++) {

            // Press Ctrl+D to start debugging your code. We have set one breakpoint
            // for you, but you can always add more by pressing Cmd+F8.
            System.out.println("i = " + i);
        }
    }
}

MANIFEST.MF 文件内容,

Manifest-Version: 1.0
Main-Class: HelloWorldMain
Can-Redefine-Classes: true

打包代码为一个独立的jar包,

cd /Users/zhenghan/Projects/HelloWorld/src

# 编译成.class 文件
javac HelloWorldMain.java

# 加入META-INF/MANIFEST.MF,打包
jar -cvfm HelloWorld.jar META-INF/MANIFEST.MF HelloWorldMain.class

3、执行premain注入 

通过 -javaagent  参数来指定我们的Java代理包,-javaagent 这个参数的个数是不限的,如果指定了多个,则会按指定的先后执行,执行完各个 agent 后,才会执行主程序的 main 方法。 

cd /Users/zhenghan/Projects
java -javaagent:JavaAgent_Demo/src/JavaAgent_Demo.jar=javaagent_args -jar HelloWorld/src/HelloWorld.jar

整个流程大致如下:

0x2:agentmian Instrumentation agent注入

1、创建 Java Agent 规范的Jar

创建如下结构文件

agantmainDemo.java内容为:

import java.lang.instrument.Instrumentation;
import java.io.*;

public class agantmainDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        for (int i = 0; i < 10; i++) {
            System.out.println("hello I`m agentMain!!!");
        }
    }

    public static void premain(String args, Instrumentation inst) throws Exception
    {
        System.out.println("Pre Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes)
        {
            System.out.println(clazz.getName());
        }
    }
}

MANIFEST.MF内容为: 

Manifest-Version: 1.0
Agent-Class: agantmainDemo
Premain-Class: agantmainDemo
Can-Redine-Classes: true
Can-Retransform-Classes: true

打包代码为一个独立的jar包,

cd /Users/zhenghan/Projects/JavaAgent_Demo/src

# 编译成.class 文件
javac agantmainDemo.java

# 加入META-INF/MANIFEST.MF,打包
jar -cvfm JavaAgent_Demo.jar META-INF/MANIFEST.MF agantmainDemo.class

2、创建被注入的jar包程序 - 以hello world为例

HelloWorldMain.java内容,

import java.lang.Thread;

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class HelloWorldMain {
    public static void main(String[] args) {
        // Press Opt+Enter with your caret at the highlighted text to see how
        // IntelliJ IDEA suggests fixing it.
        System.out.printf("Hello and welcome!");

        // Press Ctrl+R or click the green arrow button in the gutter to run the code.
        for (int i = 1; i <= 999999; i++) {

            // Press Ctrl+D to start debugging your code. We have set one breakpoint
            // for you, but you can always add more by pressing Cmd+F8.
            System.out.println("i = " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

MANIFEST.MF 文件内容,

Manifest-Version: 1.0
Main-Class: HelloWorldMain
Can-Redefine-Classes: true

打包代码为一个独立的jar包,

cd /Users/zhenghan/Projects/HelloWorld/src

# 编译成.class 文件
javac HelloWorldMain.java

# 加入META-INF/MANIFEST.MF,打包
jar -cvfm HelloWorld.jar META-INF/MANIFEST.MF HelloWorldMain.class

3、创建用于实现attach api的jar包程序 - 类似于ptrace的作用

官方为了实现启动后加载jar包,提供了Attach API。

Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面。着重关注的是VitualMachine这个类。

VirtualMachine字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息、 loadAgent,Attach 和 Detach 等方法,可以实现的功能可以说非常之强大 。

  • List:获取当前所有JVM列表
  • attach:该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上。
  • detach:断开连接
  • loadAgent:通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例。

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

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,代码如下:

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 attacherDemo {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        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");
    }
}

MANIFEST.MF内容为: 

Manifest-Version: 1.0
Main-Class: attacherDemo

打包代码为一个独立的jar包,

cd /Users/zhenghan/Projects/JavaAgent_Demo/src

# 编译成.class 文件
# 加入tool.jar外部依赖,指定本地系统中tool.jar路径
javac -cp ".:/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/lib/*" attacherDemo.java

# 加入META-INF/MANIFEST.MF,打包
jar -cvfm JavaAgent_Attach_Demo.jar META-INF/MANIFEST.MF attacherDemo.class

4、执行agentmain注入 

启动被注入进程HelloWorld.jar,

获取被注入进程HelloWorld.jar的pid,

启动JavaAgent_Attach_Demo.jar,将JavaAgent_Demo.jar注入到目标进程HelloWorld.jar中,

cd /Users/zhenghan/Projects/JavaAgent_Demo/src

jar tf "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Demo.jar"

java -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/lib/tools.jar -jar "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Attach_Demo.jar" 86149 "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Demo.jar"

整个过程的流程图大致如下图所示: 

0x3:通过Instrumentation和JVMTIAgent进行交互从而达到修改类字节码的目的

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

下面列出这个类的一些方法,更加详细的介绍和方法,可以参照官方文档

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();

    ......
}

1、使用Instrumentation获取被注入JVM当前加载类

修改之前写的agantmainDemo.java:

import java.lang.instrument.Instrumentation;
import java.io.*;

public class agantmainDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        Class[] classes = inst.getAllLoadedClasses();
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(new File("/tmp/classesInfo"));
            for (Class aClass : classes) {
                String result = "class ==> " + aClass.getName() + "\n\t" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\n";
                fileOutputStream.write(result.getBytes());
            }
            fileOutputStream.close();
        } catch (IOException fnfe) {
            System.out.println(fnfe);
        } finally {
            //
        }
    }

    public static void premain(String args, Instrumentation inst) throws Exception
    {
        System.out.println("Pre Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes)
        {
            System.out.println(clazz.getName());
        }
    }
}

重新attach到某个JVM,在/tmp/classesInfo文件中有如下信息:

得到了目标JVM上所有已经加载的类,并且知道了这些类能否被修改。

除此之外,Java agent 还提供了一套 instrumentation 机制,允许应用程序拦截类加载事件,并且更改该类的字节码。

2、使用Instrumentation劫持目标类加载流程

接下来来讲讲如何使用addTransformer()和retransformClasses()进行类加载劫持和重新。

首先看一下这两个方法的声明:

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;

    ......
}

在addTransformer()方法中,有一个参数ClassFileTransformer transformer。这个参数将帮助我们完成字节码的修改工作。

这是一个接口,它提供了一个transform方法:

public interface ClassFileTransformer {
    default byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer) {
        ....
    }
}

简单概括一下:

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

在开始修改字节码之前,我们先来看一下通过addTransformer和retransformClasses,对目标类进行劫持重加载,并打印出被劫持类的字节码。

为了方便演示修改特定class的字节码,修改HelloWorldMain.java内容,增加了对hello类的调用,

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class HelloWorldMain {
    public static void main(String[] args) {
        // Press Opt+Enter with your caret at the highlighted text to see how
        // IntelliJ IDEA suggests fixing it.
        System.out.printf("Hello and welcome!");

        hello h2 = new hello();
        h2.hello();
        System.out.println("ends...");
    }
}

新建一个hello.java文件,

import java.lang.Thread;

// hello.java
public class hello {
    public void hello() {
        System.out.println("hello world");

        // Press Ctrl+R or click the green arrow button in the gutter to run the code.
        for (int i = 1; i <= 999999; i++) {

            // Press Ctrl+D to start debugging your code. We have set one breakpoint
            // for you, but you can always add more by pressing Cmd+F8.
            System.out.println("i = " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

打包代码为一个独立的jar包,

cd /Users/zhenghan/Projects/HelloWorld/src

# 编译成.class 文件
javac HelloWorldMain.java

# 加入META-INF/MANIFEST.MF,打包
jar -cvfm HelloWorld.jar META-INF/MANIFEST.MF HelloWorldMain.class hello.class

修改之前写的agantmainDemo.java:

import java.io.*;

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


public class agantmainDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        Class[] classes = inst.getAllLoadedClasses();
        // 判断类是否已经加载
        for (Class aClass : classes) {
            if (aClass.getName().equals(TransformerDemo.editClassName)) {
                // 添加 Transformer
                inst.addTransformer(new TransformerDemo(), true);
                // 触发 Transformer
                try {
                    inst.retransformClasses(aClass);
                } catch (Exception e) {
                }
            }
        }
    }
}

新建一个TransformerDemo.java文件

import java.io.*;

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


// TransformerDemo.java
// 如果在使用过程中找不到javassist包中的类,那么可以使用URLCLassLoader+反射的方式调用
public class TransformerDemo implements ClassFileTransformer {
    // 只需要修改这里就能修改别的函数
    public static final String editClassName = "hello";
    public static final String editVNClassName = new String(editClassName).replaceAll("\\.", "\\/");;
    public static final String editMethodName = "hello";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 判断类名是否为目标类名
        if (!className.equals(this.editVNClassName)) {
            return classfileBuffer;
        }
        System.out.printf("Loaded %s: 0x%X%X%X%X\n", className, classfileBuffer[0], classfileBuffer[1],
                classfileBuffer[2], classfileBuffer[3]);
        return null;
    }
}

在上面这段代码中,

  • transform方法将接收一个 byte 数组类型的参数,它代表的是正在被加载的类的字节码。在上面这段代码中,我将打印该数组的前四个字节,也就是 Java class 文件的魔数(magic number)0xCAFEBABE。
  • transform方法将返回一个 byte 数组,代表更新过后的类的字节码。当方法返回之后,Java 虚拟机会使用所返回的 byte 数组,来完成接下来的类加载工作。不过,如果transform方法返回 null 或者抛出异常,那么 Java 虚拟机将使用原来的 byte 数组完成类加载工作。

基于这一类加载事件的拦截功能,我们可以实现字节码注入(bytecode instrumentation)等更复杂的功能,例如往正在被加载的类中插入额外的字节码。

打包编译为独立jar文件,

cd /Users/zhenghan/Projects/JavaAgent_Demo/src

# 编译成.class 文件,引入本地包含javassist.jar的lib路径
javac -cp ".:./*" agantmainDemo.java 

# 加入META-INF/MANIFEST.MF,打包
jar -cvfm JavaAgent_Demo.jar META-INF/MANIFEST.MF agantmainDemo.class TransformerDemo.class

attach到HelloWorld目标进程,

ps -ef | grep HelloWorld
# 501 96170 86164   0 11:47AM ttys001    0:04.26 /usr/bin/java -jar HelloWorld.jar

cd /Users/zhenghan/Projects/JavaAgent_Demo/src

java -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/lib/tools.jar:/Users/zhenghan/Projects/JavaAgent_Demo/src/javassist.jar -jar "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Attach_Demo.jar" "96170" "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Demo.jar"

3、使用Instrumentation重写目标类字节码 - 使用javaassist

接下来我们不满足于仅仅劫持重加载目标类,我们更进一步看看如何自定义修改目标类的字节码逻辑,具体的做法还需要用到另一个工具——javassist。 

agantmainDemo.java保持不变,TransformerDemo.java文件修改为,

import java.io.*;

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

import javassist.*;
import javassist.ClassPath;


// TransformerDemo.java
// 如果在使用过程中找不到javassist包中的类,那么可以使用URLCLassLoader+反射的方式调用
public class TransformerDemo implements ClassFileTransformer {
    // 只需要修改这里就能修改别的函数
    public static final String editClassName = "hello";
    public static final String editVNClassName = new String(editClassName).replaceAll("\\.", "\\/");;
    public static final String editMethodName = "hello";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 判断类名是否为目标类名
        if (!className.equals(this.editVNClassName)) {
            return classfileBuffer;
        }
        try {
            ClassPool cp = ClassPool.getDefault();
            cp.appendClassPath("/Users/zhenghan/Projects/JavaAgent_Demo/src/");

            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                cp.insertClassPath(ccp);
            }
            CtClass ctc = cp.get(this.editClassName);
            CtMethod method = ctc.getDeclaredMethod(this.editMethodName);
            String source = "{System.out.println(\"hello transformer\");}";
            method.setBody(source);
            byte[] bytes = ctc.toBytecode();

            ctc.writeFile("/Users/zhenghan/Projects/JavaAgent_Demo/src/dump");
            ctc.detach();
            return bytes;
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

下载javassist.jar到当前编译目录,

4、使用Instrumentation重写目标类字节码 - 使用ASM库

agantmainDemo.java保持不变

import java.io.*;
import java.lang.instrument.*;

public class agantmainDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        Class[] classes = inst.getAllLoadedClasses();
        // 判断类是否已经加载
        for (Class aClass : classes) {
            if (aClass.getName().equals(TransformerDemo.editClassName)) {
                // 添加 Transformer
                inst.addTransformer(new TransformerDemo(), true);
                // 触发 Transformer
                try {
                    inst.retransformClasses(aClass);
                } catch (Exception e) {
                }
            }
        }
    }
}

TransformerDemo.java文件修改为,

import java.io.*;

import java.lang.instrument.*;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;


// TransformerDemo.java
public class TransformerDemo implements ClassFileTransformer, Opcodes {
    // 只需要修改这里就能修改别的函数
    public static final String editClassName = "hello";
    public static final String editVNClassName = new String(editClassName).replaceAll("\\.", "\\/");;
    public static final String editMethodName = "hello";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 判断类名是否为目标类名
        if (!className.equals(this.editVNClassName)) {
            return classfileBuffer;
        }

        ClassReader cr = new ClassReader(classfileBuffer);
        ClassNode classNode = new ClassNode(ASM7);
        cr.accept(classNode, ClassReader.SKIP_FRAMES);

        for (MethodNode methodNode : classNode.methods) {
            if (editMethodName.equals(methodNode.name)) {
                InsnList instrumentation = new InsnList();
                instrumentation.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
                instrumentation.add(new LdcInsnNode("Hello, Instrumentation!"));
                instrumentation
                        .add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));

                methodNode.instructions.insert(instrumentation);
            }
        }

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        classNode.accept(cw);
        return cw.toByteArray();
    }
}

上面代码使用ClassReader读取所传入的 byte 数组,并将其转换成ClassNode。然后我们将遍历ClassNode中的MethodNode节点,也就是该类中的构造器和方法。

当遇到名字为"hello"的方法时,我们会在方法的入口处注入System.out.println("Hello, Instrumentation!");。

下载的tree 包(依赖于基础包)到编译目录下,

打包编译为独立jar文件,

cd /Users/zhenghan/Projects/JavaAgent_Demo/src

# 编译成.class 文件,引入本地包含javassist.jar的lib路径
javac -cp ".:./*" agantmainDemo.java 

# 加入META-INF/MANIFEST.MF,打包
jar -cvfm JavaAgent_Demo.jar META-INF/MANIFEST.MF agantmainDemo.class TransformerDemo.class

运行结果如下所示,

ps -ef | grep HelloWorld
# 501 96170 86164   0 11:47AM ttys001    0:04.26 /usr/bin/java -jar HelloWorld.jar

cd /Users/zhenghan/Projects/JavaAgent_Demo/src

java -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/lib/tools.jar:/Users/zhenghan/Projects/JavaAgent_Demo/src/asm.jar:/Users/zhenghan/Projects/JavaAgent_Demo/src/asm-tree.jar -jar "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Attach_Demo.jar" "13680" "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Demo.jar"

5、使用Instrumentation处理重复修改、二次覆盖等复杂场景

Java agent 还提供了另外两个功能redefine和retransform。这两个功能针对的是已加载的类,并要求用户传入所要redefine或者retransform的类实例。

  • redefine指的是舍弃原本的字节码,并替换成由用户提供的 byte 数组。该功能比较危险,一般用于修复出错了的字节码。
  • retransform则将针对所传入的类,重新调用所有已注册的ClassFileTransformer的transform方法。它的应用场景主要有如下两个。
    • 第一,在执行premain或者agentmain方法前,Java 虚拟机早已加载了不少类,而这些类的加载事件并没有被拦截,因此也没有被注入。使用retransform功能可以注入这些已加载但未注入的类。
    • 第二,在定义了多个 Java agent,多个注入的情况下,我们可能需要移除其中的部分注入。当调用Instrumentation.removeTransformer去除某个注入类后,我们可以调用retransform功能,重新从原始 byte 数组开始进行注入。 

参考链接:

https://www.cnblogs.com/yyhuni/p/15371534.html
https://github.com/DanielJyc/premain-simple-demo/tree/master 
https://blog.csdn.net/21aspnet/article/details/81252656 
https://toforu.com/blog/article/1062
https://www.118pan.com/o14107
https://xz.aliyun.com/t/9450 
https://www.cnblogs.com/rickiyang/p/11336268.html 
https://www.cnblogs.com/rickiyang/p/11368932.html 
https://central.sonatype.com/artifact/org.ow2.asm/asm-tree/7.0-beta/versions?smo=true
https://central.sonatype.com/artifact/org.ow2.asm/asm/7.0-beta/versions?smo=true
https://zhuanlan.zhihu.com/p/340541234 
https://ggball.top/pages/a1a9db/#%E7%B2%BE%E9%80%89%E8%AF%84%E8%AE%BA

文章来源: https://www.cnblogs.com/LittleHann/p/17462796.html
如有侵权请联系:admin#unsafe.sh