Java Instrumentation

admin 2023年1月1日15:48:59评论15 views字数 10473阅读34分54秒阅读模式

一、前言

JDK 1.5 开始,Java新增了 Instrumentation ( Java Agent API )和 JVMTI ( JVM Tool Interface )功能,允许JVM在加载某个 class 文件之前对其字节码进行修改,同时也支持对已加载的 class (类字节码)进行重新加载( Retransform )。

开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 –javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。在类的字节码载入 jvm 前会调用 ClassFileTransformer 的 transform 方法,从而实现修改原类方法的功能,实现 AOP 。

在字节码加载前进行注入,一般有两种写法,重写 ClassLoader 或利用 Instrumentation,而如果重写 ClassLoader,仍然对现有代码进行了修改,而 Instrumentation 则可以做到完全无侵入,利用这种特性,衍生出了诸多新型技术和产品,RASP 就是其中之一。

本篇为相关学习笔记以及个人的一些理解。

二、源码简介

代码位于包 java.lang.instrument 下,共包含如下类和接口:

1. IllegalClassFormatException 异常类

此异常为非法的字节码格式化异常,由ClassFileTransformer.transform 的实现抛出。

抛出此异常的原因是由于初始类文件字节无效,或者由于以前应用的转换损坏了字节码。

2. UnmodifiableClassException 异常类

当程序无法修改制定的类时,会抛出该异常。由 Instrumentation.redefineClasses 的实现抛出。

3. ClassDefinition 绑定/定义类

public final class ClassDefinition {
    /**
     *  要重定义的类
     */
    private final Class<?> mClass;

    /**
     *  用于替换的本地 class ,为 byte 数组
     */
    private final byte[]   mClassFile;

    /**
     *  构造方法,使用提供的类和类文件字节创建一个新的 ClassDefinition 绑定
     */
    public ClassDefinition( Class<?> theClass, byte[]  theClassFile) {
        if (theClass == null || theClassFile == null) {
            throw new NullPointerException();
        }
        mClass      = theClass;
        mClassFile  = theClassFile;
    }

    /**
     * 以下为 getter 方法
     */
    public Class<?>  getDefinitionClass() {
        return mClass;
    }

    public byte[] getDefinitionClassFile() {
        return mClassFile;
    }
}

4. ClassFileTransformer 接口

此接口为转换类文件的代理接口。提供了 transform() 方法用于修改原类的注入。
我们可以在获取到 Instrumentation 对象后通过 addTransformer() 方法添加自定义类文件转换器。

public interface ClassFileTransformer {


    /**
     * 类文件转换方法,重写transform方法可获取到待加载的类相关信息
     *
     * @param loader              定义要转换的类加载器;如果是引导加载器,则为 null
     * @param className           类名,如:java/lang/Runtime
     * @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
     * @param protectionDomain    要定义或重定义的类的保护域
     * @param classfileBuffer     类文件格式的输入字节缓冲区(不得修改)
     * @return 返回一个通过ASM修改后添加了防御代码的字节码byte数组。
     */
        byte[] transform(  ClassLoader loader, 
                String className,
                Class<?> classBeingRedefined,
                ProtectionDomain protectionDomain,
                byte[] classfileBuffer)
        throws IllegalClassFormatException;
}

重写 transform() 方法需要注意以下事项:

  1. ClassLoader 如果是被 Bootstrap ClassLoader (引导类加载器)所加载那么 loader 参数的值是空。
  2. 修改类字节码时需要特别注意插入的代码在对应的 ClassLoader 中可以正确的获取到,否则会报 ClassNotFoundException ,比如修改 java.io.FileInputStream (该类由 Bootstrap ClassLoader 加载)时插入了我们检测代码,那么我们将必须保证 FileInputStream 能够获取到我们的检测代码类。
  3. JVM类名的书写方式路径方式:java/lang/String 而不是我们常用的类名方式:java.lang.String
  4. 类字节必须符合 JVM 校验要求,如果无法验证类字节码会导致 JVM 崩溃或者 VerifyError (类验证错误)。
  5. 如果修改的是 retransform 类(修改已被 JVM 加载的类),修改后的类字节码不得新增方法、修改方法参数、类成员变量。
  6. addTransformer 时如果没有传入 retransform 参数(默认是 false ),就算 MANIFEST.MF 中配置了 Can-Redefine-Classes: true 而且手动调用了retransformClasses()方法也一样无法retransform。
  7. 卸载 transform 时需要使用创建时的 Instrumentation 实例。

还需要理解的是,在以下三种情形下 ClassFileTransformer.transform() 会被执行:

  1. 新的 class 被加载。
  2. Instrumentation.redefineClasses 显式调用。
  3. addTransformer 第二个参数为 true 时,Instrumentation.retransformClasses 显式调用。

5. Instrumentation 接口

java.lang.instrument.Instrumentation 是 Java 提供的监测运行在 JVM 程序的 API 。利用 Instrumentation 我们可以实现如下功能:

类方法 功能
void addTransformer(ClassFileTransformer transformer, boolean canRetransform) 添加一个 Transformer,是否允许 reTransformer
void addTransformer(ClassFileTransformer transformer) 添加一个 Transformer
boolean removeTransformer(ClassFileTransformer transformer) 移除一个 Transformer
boolean isRetransformClassesSupported() 检测是否允许 reTransformer
void retransformClasses(Class<?>... classes) 重加载(retransform)类
boolean isModifiableClass(Class<?> theClass) 确定一个类是否可以被 retransformation 或 redefinition 修改
Class[] getAllLoadedClasses() 获取 JVM 当前加载的所有类
Class[] getInitiatedClasses(ClassLoader loader) 获取指定类加载器下所有已经初始化的类
long getObjectSize(Object objectToSize) 返回指定对象大小
void appendToBootstrapClassLoaderSearch(JarFile jarfile) 添加到 BootstrapClassLoader 搜索
void appendToSystemClassLoaderSearch(JarFile jarfile) 添加到 SystemClassLoader 搜索
boolean isNativeMethodPrefixSupported() 是否支持设置 native 方法 Prefix
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix) 通过允许重试,将前缀应用到名称,此方法修改本机方法解析的失败处理
boolean isRedefineClassesSupported() 是否支持类 redefine
void redefineClasses(ClassDefinition... definitions) 重定义(redefine)类

三、原理

这部分由于参考作者 throwable 总结较好,直接引用。

instrument 的底层实现依赖于 JVMTI ,也就是 JVM Tool Interface ,它是 JVM 暴露出来的一些供用户扩展的接口集合, JVMTI 是基于事件驱动的, JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。 JVMTIAgent 是一个利用 JVMTI 暴露出来的接口提供了代理启动时加载(agent on load)、代理通过 attach 形式加载(agent on attach)和代理卸载(agent on unload)功能的动态库。而 instrument agent 可以理解为一类 JVMTIAgent 动态库,别名是 JPLISAgent (Java Programming Language Instrumentation Services Agent),也就是专门为 Java 语言编写的插桩服务提供支持的代理。

四、用法

可以看到,是非常简单和清晰的一个包,有了这些方法之后我们就可以通过代理,在main函数运行前或后动态的改变类的定义和其他处理操作。

接下来我们来看用法,首先定义一个类,这个类就是我们将要修改的类:

package org.su18;

public class MyClass {

	public static void sayNice() {
		System.out.println("Nice!");
	}

}

毫无疑问,此类的 sayNice() 方法在运行时将打印出字符串 “Nice!” 。

1. 使用 premain 让代理在函数运行前执行

然后接下来进行定义自己的 Transformer,如下代码,我这里使用判断如果类名为指定类的名称,则使用ClassHandler.replaceBytes() 方法进行字节码的替换。

package org.su18;

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

public class Transformer implements ClassFileTransformer {

	public byte[] transform(ClassLoader loader, String className,
	                        Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
	                        byte[] classfileBuffer) throws IllegalClassFormatException {

		// 将常用的类名转换为 JVM 认识的类名
		className = className.replace("/", ".");

		// 如果类名为我们指定的类
		if (className.equals("org.su18.MyClass")) {
			// 进一步进行处理,替换掉输出字符串
			return ClassHandler.replaceBytes(className, classfileBuffer);
		}
		return classfileBuffer;
	}
}

字节处理代码:

package org.su18;

import java.util.Arrays;

public class ClassHandler {

	public static byte[] replaceBytes(String className, byte[] classBuffer) {

		// 将类字节码转换成byte字符串
		String bufferStr = Arrays.toString(classBuffer);
		System.out.println(className + "类替换前的字节码:" + bufferStr);

		bufferStr = bufferStr.replace("[", "").replace("]", "");

		// 查找需要替换的Java二进制内容
		byte[] findBytes = "Nice!".getBytes();

		// 把搜索的字符串byte转换成byte字符串
		String findStr = Arrays.toString(findBytes).replace("[", "").replace("]", "");

		// 二进制替换后的byte值,注意这个值需要和替换的字符串长度一致,不然会破坏常量池
		byte[] replaceBytes = "Fxxk!".getBytes();

		// 把替换的字符串byte转换成byte字符串
		String replaceStr = Arrays.toString(replaceBytes).replace("[", "").replace("]", "");

		bufferStr = bufferStr.replace(findStr, replaceStr);

		// 切割替换后的byte字符串
		String[] byteArray = bufferStr.split("\\s*,\\s*");

		// 创建新的byte数组,存储替换后的二进制
		byte[] bytes = new byte[byteArray.length];

		// 将byte字符串转换成byte
		for (int i = 0; i < byteArray.length; i++) {
			bytes[i] = Byte.parseByte(byteArray[i]);
		}

		System.out.println(className + "类替换后的字节码:" + Arrays.toString(bytes));

		// 返回修改后的二进制
		return bytes;
	}

}

可以看到,这里将类字节码转换为byte字符串,并进行字符串查找,替换后再转回 byte,这里为了演示是一种取巧的方式,在实际项目中将使用 ASM 或 javassist 等对类字节码进行处理。

接下来定义 Premain,类名随意,类中定义了 premain() 方法添加自己的 Transformer。
其中参数 agentArgs 是 premain 函数得到的程序参数,随同 “-javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。

package org.su18;

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

public class Premain {

	public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
		inst.addTransformer(new Transformer());
	}

}

最后需要在 MANIFEST.MF 中修改 :Premain-Class: org.su18.Premain

并且在 pom.xml 中加入如下配置:

<plugin>
     <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>2.3.2</version>
     <configuration>
         <archive>
             <manifestFile>src/main/resources/MANIFEST.MF</manifestFile>
         </archive>
     </configuration>
</plugin>

然后使用 maven 构建 jar 包:mvn clean install

打包之后,在运行程序时加入如下参数:

-javaagent:/Users/phoebe/IdeaProjects/AgentTest/target/AgentTest-1.0.jar org.su18.MyClass 

运行 MyClass 程序,输出如下:

可以看到,输出的内容已经成功被修改。

2. 使用 agentmain

JDK 1.6 新增了attach (附加方式)方式,可以对运行中的 Java 进程附加 Agent 。

这就是我们说的 agentmain ,使用方式和 permain 十分相似,包括编写 MANIFEST.MF 和生成代理 Jar 包。但是,它并不需要通过-javaagent 命令行形式引入代理 Jar ,而是在运行时通过 attach 工具激活指定代理即可。

同样的,我们简单修改下 MyClass,使程序每过三秒打印一次 “Nice!” 字符串。

package org.su18;

public class MyClass {

	public static void sayNice() {
		System.out.println("Nice!");
	}

	public static void main(String[] args) throws InterruptedException {
		while (true) {
			sayNice();
			Thread.sleep(1000 * 3);
		}
	}
}

Transformer 和程序处理逻辑不变,将 Premain 修改为 AgentMain。

package org.su18;

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

public class AgentMain {

	public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException,
			ClassNotFoundException {
		inst.addTransformer(new Transformer(), true);
		inst.retransformClasses(Class.forName("org.su18.MyClass"));
	}
}

这里可以看到和 premain 的区别在于,我们在 addTransformer 的参数中指定了 true,而且使用了 retransformClasses 重新加载了指定的类。

然后我们再编写 AttachTest 类用来将我们的程序 attach 进去。

package org.su18;

import com.sun.tools.attach.*;

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

public class AttachTest {

	public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException,
			AgentInitializationException, InterruptedException {

		// 获取正在运行 JVM 列表
		List<VirtualMachineDescriptor> list = VirtualMachine.list();

		// 遍历列表
		for (VirtualMachineDescriptor descriptor : list) {

			// 根据进程名字获取进程ID, 并使用 loadAgent 注入进程
			if (descriptor.displayName().endsWith("MyClass")) {
				VirtualMachine virtualMachine = VirtualMachine.attach(descriptor.id());
				virtualMachine.loadAgent("/Users/phoebe/IdeaProjects/AgentTest/target/AgentTest-1.0.jar", "arg1");
				virtualMachine.detach();
			}
		}
	}
}

别忘了修改 MANIFEST.MF 文件:Agent-Class: org.su18.AgentMain

然后同样进行打包,如果找不到 tools 的话可以指定 classpath,还有一种简单粗暴的方式:

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8.0</version>
            <scope>system</scope>
            <systemPath>/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar</systemPath>
        </dependency>

打包后先运行 MyClass,然后运行 AttachTest 进行注入,可看到效果。

可以看到,使用 attach 进行附加进程的方式可以在程序无需重启的情况下进行注入和修改,是更加方便的方式,两种方式可以看情况选择。

但是使用 attach 方式进行进程注入时,需要注意的点为:

  1. java agent 中的所有依赖,在原进程中的 classpath 中都要能找到,否则在注入时原进程会报错NoClassDefFoundError。
  2. java agent 的 pom 文件中包含如下内容,以在 jar 包中包含 MANIFEST.MF 并设置 Agent-Class 和 Can-Retransform-Classes 属性。
  3. agent 进程的 classpath 中必须有 tools.jar(提供 VirtualMachine attach api ),jdk 默认有 tools.jar,jre 默认没有。

3. 动态加载 classpath:BootClassPath / SystemClassPath

如果我们需要在虚拟机启动之后来加载某些 jar 进入 bootclasspath,可以使用 appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch 方法进行动态添加,或使用配置文件在 agent 启动时进行添加。

4. prefix:对 native 代码的 instrument 方式

由于博主发量有限,这部分暂不涉及。

以上案例中,我们均使用了 retransform 来重新进行类加载,而 Instrumentation 还提供了 redefine,这两者有什么异同呢?

以下节选自参考文章:Java 5就提供了 Class Redifine 的能力,而 Java 6 才支持 Class Retransform ,可以认为 Retransform 是 Redifine 的一种升级版本,更加方便使用,两者能实现的功能是一致的,只是调用方式有些区别。

五、参考链接

  1. https://javasec.org/javase/JavaAgent/
  2. https://www.cnblogs.com/yelao/p/9841810.html
  3. http://throwable.coding.me/2019/06/29/java-understand-instrument-first/
  4. https://blog.csdn.net/warren288/article/details/82828989
  5. https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html
  6. https://github.com/anbai-inc/javaweb-expression
  7. https://javaweb.org/?p=1862

FROM:素十八[su18]

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年1月1日15:48:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java Instrumentationhttps://cn-sec.com/archives/1493413.html

发表评论

匿名网友 填写信息