语言特性 | JavaAgent & Javassist

admin 2024年11月8日16:54:35评论6 views字数 28914阅读96分22秒阅读模式

JavaAgent

简单介绍

Java Agent是Java平台提供的一个强大工具,它可以在运行时修改或增强Java应用程序的行为。是在JDK1.5以后引入的,它能够在不影响正常编译的情况下修改字节码,相当于是在main方法执行之前的拦截器,也叫premain,也就是会先执行premain方法然后再执行main方法。

使用场景如下:

代码注入增强:允许在程序运行时对字节码进行操作,可以实现功能的增强。
性能监控调优:可以监控应用程序方法执行时间、调用次数,类加载的一些信息进行性能检测,以及对一些问题的定位分析,比如一些性能监控和诊断工具如Pinpoint、Skywalking、Zipkin、Arthas等。
日志记录审计:Java Agent可以在方法执行前后记录方法的调用信息,包括方法名、参数、返回值等,动态记录应用程序的运行日志。

具体实现

main & 基本环境搭建 (用于主要测试)

这里我们需要对JavaAgent所需要的基础环境进行一个简单的搭建, 方便下面的学习与理解.

首先创建一个MyMainApp基于Maven的项目, 该项目pom.xml:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>

            <artifactId>maven-jar-plugin</artifactId>

            <version>3.1.2</version>

            <!-- 后续使用 Maven 进行打包即可 -->
            <configuration>
                <archive>
                    <manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>

                </archive>

            </configuration>

        </plugin>

    </plugins>

</build>

<dependencies>
    <dependency>
        <groupId>org.apache.maven.plugins</groupId>

        <artifactId>maven-jar-plugin</artifactId>

        <version>3.1.2</version>

    </dependency>

</dependencies>

在这里我们配置maven-jar-pluginmanifestFile属性, 配置该属性则是为了对我们的MANIFEST.MF文件进行指明.

随后创建resources/META-INF/MANIFEST.MF 文件中进行指明JAR包运行时所运行的主类:

Main-Class: com.heihu577.MyApp (对应了 MyApp::main 方法, 这里需要多打一个换行符)

随后我们创建一个com.heihu577.MyApp类, 内容如下:

public class MyApp {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("[MyApp] Hello, My name is Heihu577~");
        try {
            // 获取虚拟机进程PID
            String jvmPid = ManagementFactory.getRuntimeMXBean().getName();
            System.out.println("[MyApp] 当前Java进程的PID是: " + jvmPid);
        } catch (Exception e) {
            e.printStackTrace();
        }
        while (true) { // 放置程序提前中止, 使用 while 循环进行保持进程
        }
    }
}

随后使用maven打包插件将其打包成jar, 命名为main.jar文件, 并且运行结果如下:

PS C:UsersAdministratorDesktop> java -jar main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 23356@heihubook

这是一个最简单的Java程序案例, 只是简单的将程序进行打包, 随后执行即可.

MANIFEST.MF

我们需要先理解MANIFEST.MF文件是用来做什么的: MANIFEST.MF 文件是 Java 归档文件(如 JAR 文件)中的一个特殊文件,用于存储关于归档文件及其内容的元数据信息。这些元数据信息可以包括主类、类路径、扩展列表、安全证书等。以下是 MANIFEST.MF 文件的一些主要用途.

Implementation-Version: 1.0.0 // 用于记录 JAR 文件的版本信息。
Main-Class: com.example.MyApp // 用于指定当用户运行 JAR 文件时,哪个类的 `main` 方法将被调用。
Class-Path: lib/dependency1.jar lib/dependency2.jar // 用于指定其他依赖的 JAR 文件或类路径。

Extension-List: extension1 extension2 // 用于指定 JAR 文件依赖的扩展。
extension1-Extension-Name: com.example.extension1
extension2-Extension-Name: com.example.extension2

而我们刚刚的案例是在Maven的打包插件中进行引入了MANIFEST.MF文件, 而其实Maven的打包插件也可以通过直接配置参数的方式进行打包, 如下:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>

            <artifactId>maven-jar-plugin</artifactId>

            <version>3.1.2</version>

            <configuration>
                <archive>
                    <!--<manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>-->
                    <manifest> <!-- 使用这种方式进行打包, 无需指明 MANIFEST.MF 文件 -->
                        <mainClass>com.heihu577.MyApp</mainClass>

                    </manifest>

                </archive>

            </configuration>

        </plugin>

    </plugins>

</build>

这两种方式是一样的, 只是写法不同而已. 建议使用/META-INF/MANIFEST.MF文件的方式, 因为IDEA工具打包是通过这个文件进行打包的.

建议阅读: https://www.cnblogs.com/leondryu/p/18079568

premain & agentmain

这里将premain & agentmain一起说明, 其原因则是新创建一个Java项目, 其中包含了这两个功能模块.

环境搭建 (用于理解)

这里我们如同上面步骤一样, 先进行一个项目所需要的环境进行搭建. pom.xml:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>

            <artifactId>maven-jar-plugin</artifactId>

            <version>3.1.2</version>

            <configuration>
                <archive>
                    <manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>

                </archive>

            </configuration>

        </plugin>

    </plugins>

</build>

随后我们创建MyPremainTest类, 其内容如下:

public class MyPremainTest {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("[MyPremainTest::premain] 你传递过来的参数: " + agentArgs);
    }

    public static void premain(String agentArgs) {
        System.out.println("[MyPremainTest::premain] 你传递过来的参数: " + agentArgs);
        // 如果存在 public static void premain(String agentArgs, Instrumentation inst), 则不会进入该方法
    }

    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
    }

    public static void agentmain(String agentArgs) {
        System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
        // 如果存在 public static void agentmain(String agentArgs, Instrumentation inst), 则不会进入该方法
    }
}

随后我们创建resources/META-INF/MANIFEST.MF文件, 内容如下:

Manifest-Version: 1.0 (指明版本号)
Can-Redefine-Classes: true (能够重新定义Class)
Can-Retransform-Classes: true (能够转换Class)
Premain-Class: com.heihu577.MyPremainTest (对应了 MyPremainTest::premain 方法)
Agent-Class: com.heihu577.MyPremainTest (对应了 MyPremainTest::agentmain 方法)

创建完毕后, 我们使用package命令进行打包成jar, 命名为agent.jar, 但该文件不能够直接运行.

-javaagent:agent.jar=参数

如果想要运行我们刚刚的agent.jar文件并看到它的效果, 那么我们则需要运行我们main.jar文件进行理解, 首先我们普通运行我们的main.jar文件的结果是这样的:

PS C:UsersAdministratorDesktop> java -jar .main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 2612@heihubook

那么如果我们想达到一个Spring-AOP的效果, 即在main.jar::main方法之前进行执行agent.jar::premain方法, 则我们应该这样运行:

PS C:UsersAdministratorDesktop> java -javaagent:agent.jar=Hacker! -jar .main.jar
[MyPremainTest::premain] 你传递过来的参数: Hacker!
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 10520@heihubook

这里主要是在运行main.jar文件之前增加了-javaagent参数. 而它的运行逻辑如下:

语言特性 | JavaAgent & Javassist

agentmain 何处运行?

上面我们可以看到premain的运行结果, 它的运行必须在运行之前进行指明javaagent参数.

agentmain则无需在运行之前进行指明参数, 即在程序运行时就可以进行执行agent.jar::agentmain方法, 下面我们看一下具体实现.

具体实现

先将我们的普通main.jar跑起来, 看一下普通运行的效果:

PS C:UsersAdministratorDesktop> java -jar main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 15272@heihubook
(阻塞在这一行)

创建一个新项目, 这次新项目我们无需进行打jar包的一个操作, 但我们需要引入JDK自带的tools.jar文件, 因为该文件中有VirtualMachine这个API, 在后面我们需要使用.

首先在pom.xml文件中进行引入:

<dependencies>
    <dependency>
        <groupId>com.sun.jdk</groupId>

        <artifactId>tools</artifactId>

        <version>1.8</version>

        <scope>system</scope>

        <systemPath>${java.home}/../lib/tools.jar</systemPath>

    </dependency>

</dependencies>

随后随意创建一个运行类, 用于attach到我们的main.jar, 代码如下:

public class MyAgentTest {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        System.out.println("AgentDemo4Test"); // 打印测试标识信息

        // 获取当前系统中所有虚拟机实例的描述符列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();

        // 遍历虚拟机实例描述符列表
        for (VirtualMachineDescriptor vmd : list) {
            // 检查虚拟机实例的显示名称是否为 "main.jar"
            if (vmd.displayName().equals("main.jar")) {
                // 附加到指定的虚拟机实例
                VirtualMachine vm = VirtualMachine.attach(vmd.id());

                // 打印虚拟机实例的 ID
                System.out.println(vm.id());

                // 打印虚拟机实例的信息
                System.out.println(vm);

                // 加载指定的 Java Agent
                vm.loadAgent("C:\Users\Administrator\Desktop\agent.jar""Hacker!");
                // 参数1: 指明具体的 agent 包路径
                // 参数2: 给该 agent 方法传递的参数

                // 从目标虚拟机断开连接
                vm.detach();
            }
        }
    }
}

编译并运行该程序, 那么main.jar那边就会执行agent.jar::agentmain方法, 如下:

PS C:UsersAdministratorDesktop> java -jar main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 15272@heihubook
[MyPremainTest::agentmain] 你传递过来的参数: Hacker!
(阻塞在这一行)

可以用如下图进行解释:

语言特性 | JavaAgent & Javassist

Instrumentation

接口简介

刚刚我们成功执行了agentmain方法, 我们知道的是, agentmain & premain方法都有第二个参数, 它们是Instrumentation类型的, 看一下这个接口的定义:

public interface Instrumentation {

    /**
     * 增加一个Class文件的转换器,转换器用于改变Class二进制流的数据。
     * 参数canRetransform设置是否允许重新转换。
     * @param transformer 转换器实例
     * @param canRetransform 是否允许重新转换
     */

    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    /**
     * 在类加载之前,重新定义Class文件。
     * 如果在类加载之后,需要使用retransformClasses方法重新定义。
     * addTransformer方法配置之后,后续的类加载都会被Transformer拦截。
     * 对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。
     * 类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
     * @param transformer 转换器实例
     */

    void addTransformer(ClassFileTransformer transformer);

    /**
     * 删除一个类转换器。
     * @param transformer 要删除的转换器实例
     * @return 如果成功删除返回true,否则返回false
     */

    boolean removeTransformer(ClassFileTransformer transformer);

    /**
     * 检查是否支持类的重新转换。
     * @return 如果支持返回true,否则返回false
     */

    boolean isRetransformClassesSupported();

    /**
     * 在类加载之后,重新定义Class。
     * 这个方法是1.6之后加入的,事实上,该方法是更新了一个类。
     * @param classes 要重新定义的类数组
     * @throws UnmodifiableClassException 如果类无法被修改
     */

    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    /**
     * 检查是否支持类的重新定义。
     * @return 如果支持返回true,否则返回false
     */

    boolean isRedefineClassesSupported();

    /**
     * 重新定义类。
     * @param definitions 包含新定义的类数组
     * @throws ClassNotFoundException 如果类未找到
     * @throws UnmodifiableClassException 如果类无法被修改
     */

    void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;

    /**
     * 检查类是否可修改。
     * @param theClass 要检查的类
     * @return 如果类可修改返回true,否则返回false
     */

    boolean isModifiableClass(Class<?> theClass);

    /**
     * 获取所有已加载的类。
     * @return 已加载的类数组
     */

    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    /**
     * 获取由指定类加载器初始化的类。
     * @param loader 指定的类加载器
     * @return 由指定类加载器初始化的类数组
     */

    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);

    /**
     * 获取一个对象的大小。
     * @param objectToSize 要获取大小的对象
     * @return 对象的大小(以字节为单位)
     */

    long getObjectSize(Object objectToSize);

    /**
     * 将JAR文件添加到引导类加载器的搜索路径中。
     * @param jarfile 要添加的JAR文件
     */

    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    /**
     * 将JAR文件添加到系统类加载器的搜索路径中。
     * @param jarfile 要添加的JAR文件
     */

    void appendToSystemClassLoaderSearch(JarFile jarfile);

    /**
     * 检查是否支持本地方法前缀。
     * @return 如果支持返回true,否则返回false
     */

    boolean isNativeMethodPrefixSupported();

    /**
     * 为转换器设置本地方法前缀。
     * @param transformer 转换器实例
     * @param prefix 本地方法前缀
     */

    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

其中我们挑出addTransformer & retransformClasses进行重点理解.

使用案例

接下来的所有案例, 操作步骤将重新定义agentmain方法, 并重新打包jarattach执行, 过程就不再描述.

public static void agentmain(String agentArgs, Instrumentation inst) {
    System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
    Class[] allLoadedClasses = inst.getAllLoadedClasses(); // 得到当前 JVM 加载所有的类
    for (Class clazz : allLoadedClasses) {
        System.out.println(clazz.getName());
        /*
        com.heihu577.MyPremainTest
        com.heihu577.MyApp
        sun.instrument.InstrumentationImpl$1
        ... 当前 JVM 已加载的类, 太多了, 就不粘贴了.
        */

    }
}

随后比较重要的是addTransformer方法, 而理解该方法我们需要先在premain方法中进行理解.

ClassFileTransformer 理解
premain 中进行理解

这里我们将通过premain & -javaagent参数进行理解, 定义premain方法如下:

public static void premain(String agentArgs, Instrumentation inst) {
    System.out.println("[MyPremainTest::premain] 你传递过来的参数: " + agentArgs);
    inst.addTransformer(new MyClassFileTransformer()); // 对 jvm 准备加载的所有类增加转换器, 当加载到具体类时, 会先走到 MyClassFileTransformer::transform 方法中
}

static class MyClassFileTransformer implements ClassFileTransformer {
    /**
     * 转换类的字节码。
     * @param loader              加载该类的类加载器。如果类是通过引导类加载器加载的,则此参数为null。
     * @param className           类的全限定名(包括包名),格式为"com/example/MyClass"。
     * @param classBeingRedefined 如果这个方法是响应于retransformClasses调用而被调用的,则此参数表示正在被重新转换的类;
     *                            如果是类首次加载时被调用的,则此参数为null。
     * @param protectionDomain    类的保护域,包含了类的安全策略和权限信息。
     * @param classfileBuffer     类文件的字节码数组,即类文件在磁盘上的二进制表示。
     *                            这个数组在传递给transform方法之前,已经被JVM读取并加载到内存中。
     * @return 修改后的类文件的字节码数组。如果不需要修改类文件,可以直接返回传入的classfileBuffer参数。
     * 如果返回null,则表示拒绝加载该类(这通常是不被建议的,因为它会导致类加载失败)。
     * @throws IllegalClassFormatException 如果转换后的类文件格式不正确,抛出此异常。
     */

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("Transforming class: " + className);
        return classfileBuffer; // 返回修改后的字节码,这里直接返回原字节码, 并没有对其进行修改
    }
}

编译, 重新打jar包后, 我们指明javaagent参数执行:

PS C:UsersAdministratorDesktop> java -javaagent:agent.jar -jar main.jar
[MyPremainTest::premain] 你传递过来的参数: null
Transforming class: java/lang/invoke/MethodHandleImpl
Transforming class: java/lang/invoke/MethodHandleImpl$1
Transforming class: java/lang/invoke/MethodHandleImpl$2
Transforming class: java/util/function/Function
... more
[MyApp] Hello, My name is Heihu577~
Transforming class: java/util/LinkedHashMap$LinkedHashIterator
Transforming class: java/util/LinkedList$ListItr
Transforming class: java/net/InetAddress$CacheEntry
... more
[MyApp] 当前Java进程的PID是: 23428@heihubook

可以看到的是, 我们在main方法之前调用premain方法, 在premain方法中增加了一个类转换器, 那么接下来JVM加载的类就会调用到MyClassFileTransformer::transform方法支持其修改字节码等操作.

agentmain 中进行理解

从刚刚的案例中, 成功理解了ClassFileTransformer的使用, 但是是在premain中进行理解的, 依赖于-javaagent参数指明.

那么如果在agentmain中进行使用呢?这里我们需要配合retransformClasses方法的调用, 给出案例:

public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
    System.out.println("[MyPremainTest::agentmain] 你传递过来的参数: " + agentArgs);
    inst.addTransformer(new MyClassFileTransformer(), true); // 参数2: 是否支持类的转换
    Class[] allLoadedClasses = inst.getAllLoadedClasses(); // 得到当前 JVM 加载的所有类
    ArrayList<Class> transformClasses = new ArrayList<>(); // 准备待转换的类
    for (Class clazz : allLoadedClasses) {
        String className = clazz.getName();
        if (className.equals("com.heihu577.MyApp")) {
            System.out.println("ADD " + className);
            transformClasses.add(clazz); // 将 com.heihu577.MyApp 这个类加入
        }
    }
    inst.retransformClasses(transformClasses.toArray(new Class[0])); // 进行转换为Class[]
}

static class MyClassFileTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 注意:这里打印的 className 可能是替换过的,与 clazz.getName() 不完全匹配
        System.out.println("Transforming class: " + className);
        // 如果需要修改类,请在这里处理 classfileBuffer
        return classfileBuffer; // 目前返回原始字节码, 如果返回 null 则不做一丁点修改
    }
}

生成agent.jar后, 运行我们的main.jar, 随后进行attach的结果如下:

PS C:UsersAdministratorDesktop> java -jar main.jar
[MyApp] Hello, My name is Heihu577~
[MyApp] 当前Java进程的PID是: 2456@heihubook
[MyPremainTest::agentmain] 你传递过来的参数: Hacker!
ADD com.heihu577.MyApp
Transforming class: com/heihu577/MyApp

可以看到的是, 目前是已经调用到MyClassFileTransformer::transform方法了, 我们可以在该方法中使用javassist进行修改类的字节码!

javassist

Javassist(JAVA programming ASSISTant)是一个开源的分析、编辑和创建Java字节码的类库。允许在运行时定义新类和修改类文件,提供了创建、修改类和方法的能力。它的使用方法如下:

语言特性 | JavaAgent & Javassist

那么先引入javassist包如下:

<dependencies>
    <dependency>
        <groupId>org.javassist</groupId>

        <artifactId>javassist</artifactId>

        <version>3.30.2-GA</version>

    </dependency>

</dependencies>

入门案例 - 创建类并实例化

我们新创建一个项目进行简单的使用:

public class Main {
    public static void main(String[] args) throws Exception {
        ClassPool classPool = new ClassPool(true);
        classPool.insertClassPath(new LoaderClassPath(Main.class.getClassLoader()))// 添加搜索路径, 当前是 AppClassLoader
        CtClass ctClass = classPool.makeClass("com.heihu577.Cat"); // 创建一个 com.heihu577.Cat 类
        /*
         * class com.heihu577.Cat {}
         * */

        ctClass.addInterface(classPool.get(Animal.class.getName()))// 让其实现 Animal 接口
        /*
         * class com.heihu577.Cat implements com.heihu577.Main$Animal {}
         * */

        CtMethod eatMethod = new CtMethod(CtClass.voidType, "eat"new CtClass[]{classPool.get(String.class.getName())}, ctClass);
        /**
         * 参数1: 指明方法返回类型
         * 参数2: 指明方法名称
         * 参数3: 方法参数类型
         * 参数4: 在哪个类下创建方法
         */

        /*
         * class com.heihu577.Cat implements com.heihu577.Main$Animal {
         *   void eat(String);
         * }
         * */

        String src = "{System.out.println("Eating... " + $1);}"// javassist 中的 $1 参数, 代表第一个参数
        eatMethod.setBody(src); // 设置方法体
        /*
         * class com.heihu577.Cat implements com.heihu577.Main$Animal {
         *   void eat(String){
         *      System.out.println("Eating... " + $1);
         *   }
         * }
         * */

        ctClass.addMethod(eatMethod); // 在该类上添加一个方法
        Class clazz = ctClass.toClass(); // 将写好的类, 转换为 Class 对象
        Animal animal = (Animal) clazz.newInstance(); // 创建实例, 这里使用接口接收, 调用方法时, 不需要通过反射调用
        animal.eat("rice"); // 调用 eat 方法
    }

    public interface Animal {
        void eat(String food);
    }
}

有一种在JavaScript中创建DOM节点操作的感觉, 而它的运行结果为:

class com.heihu577.Cat
Eating... rice

特殊符号

在上面的案例中, src变量中存在$1, 这是遵循了javassist的语法. 而它的特殊符号语法如下:

符号 含义
$0, $1, $2, ... $0 = this; $1 = args[1] .....
$args 方法参数数组.它的类型为 Object[]
$$ 所有实参。例如, m($$) 等价于 m($1,$2,...)
$cflow(...) cflow 变量
$r 返回结果的类型,用于强制类型转换
$w 包装器类型,用于强制类型转换
$_ 返回值

实际案例

修改方法体

在上面我们只是简单的通过javassist进行创建一个类, 并且在其中定义了我们自定义的一个方法, 那么如果我们想要修改某个类下的某个方法体应该如何操作呢?

public class MainTest2 {
    public static class MyStringUtils {
        public String returnA() {
            return "A";
        }
    }

    public static void main(String[] args) throws Exception {
//        MyStringUtils myStringUtils = new MyStringUtils();
//        System.out.println(myStringUtils.repeatA(10));
        ClassPool classPool = new ClassPool(true);
        classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
        CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils"); // 得到具体类模型, 这里必须使用字符串, 否则将抛出已加载过异常
        CtMethod repeatA = ctClass.getDeclaredMethod("returnA"); // 得到 returnA 方法
        repeatA.setBody("{return "Heihu577";}"); // 修改 returnA 方法体
        ctClass.toClass(); // 使用 ClassLoader 进行加载
        System.out.println(new MyStringUtils().returnA()); // 返回 Heihu577
    }
}

通过上述案例, 我们已经成功的将returnA的方法体改为了return "Heihu577".

创建新方法

当然, 除了修改我们也可以在原有类基础上进行添加方法. 实现如下:

public class MainTest2 {
    public static class MyStringUtils {
        public String returnA() {
            return "A";
        }
    }

    public static void main(String[] args) throws Exception {
        ClassPool classPool = new ClassPool(true);
        classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
        CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils"); // 得到具体类模型, 这里必须使用字符串, 否则将抛出已加载过异常

        CtMethod sayHello = new CtMethod(CtClass.voidType, "sayHello"new CtClass[]{}, ctClass); // 返回值 void, 方法名 sayHello, 方法参数无, 在哪个类上
        sayHello.setBody("{System.out.println("Hello~World~");}"); // 设置方法体
        ctClass.addMethod(sayHello); // 将该方法加入到 CtClass 中
        ctClass.toClass(); // 使用 ClassLoader 进行加载

        MyStringUtils myStringUtils = new MyStringUtils();
        myStringUtils.getClass().getDeclaredMethod("sayHello").invoke(myStringUtils); // 调用刚刚添加的 sayHello 方法, 输出 Hello~World~
    }
}

由于编译时, 并无法扫描到sayHello方法, 所以我们这里只能通过反射进行调用.

修改方法参数类型

public class MainTest2 {
    public static class MyStringUtils {
        public void tester(String name) {
            System.out.println("tester starting...");
            System.out.println(name);
        }
    }

    public static void main(String[] args) throws Exception {
        ClassPool classPool = new ClassPool(true);
        classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
        CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils");
        CtMethod testerMethod = ctClass.getDeclaredMethod("tester"new CtClass[]{classPool.get("java.lang.String")});
        ClassMap classMap = new ClassMap();
        classMap.put("java.lang.String""java.lang.Object"); // 把 String 类型改成 Object
        CtMethod testerNewMethod = CtNewMethod.copy(testerMethod, "tester", ctClass, classMap); // 参数四用于修改参数类型
        ctClass.removeMethod(testerMethod); // 删除原有的 tester 方法
        ctClass.addMethod(testerNewMethod); // 将 copy 创建出来的 method, 加入到 ctClass 中
        ctClass.toClass(); // 将类加载到内存
        MyStringUtils myStringUtils = new MyStringUtils();
        Method[] declaredMethods = myStringUtils.getClass().getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println(declaredMethod); // 可以看到 public void com.heihu577.MainTest2$MyStringUtils.tester(java.lang.Object)
        }
        myStringUtils.getClass().getDeclaredMethod("tester", Object.class).invoke(myStringUtils, 123)// 传入 Integer 类型仍然可行
    }
}

AOP 实现

通过代理调用手段

在调用方法前输出method start, 在调用方法完毕后输出method end. 使用如下案例:

public class MainTest2 {
    public static class MyStringUtils {
        public void tester(String name) {
            System.out.println("Hi: " + name);
        }
    }

    public static void main(String[] args) throws Exception {
        ClassPool classPool = new ClassPool(true);
        classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
        CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils"); // 得到 MyStringUtils 类
        CtMethod tester = ctClass.getDeclaredMethod("tester"new CtClass[]{classPool.get(String.class.getName())})// 得到 tester 方法
        // 创建 tester$agent 方法, 方法体和 tester 方法相同
        CtMethod tester$agent = CtNewMethod.copy(tester, tester.getName() + "$agent", ctClass, null);
        // 先添加 tester$agent 方法
        ctClass.addMethod(tester$agent);
        // 定义新的方法体
        String src = "{" +
                "System.out.println("method start");" +
                "this." + tester.getName() + "$agent($$);" + // 调用 tester$agent 方法
                "System.out.println("method end");" +
                "}";
        // 将原来的 tester 方法体, 改为调用 tester$agent 方法的方法体
        tester.setBody(src);
        // 转换为Java类
        Class<?> clazz = ctClass.toClass();
        // 创建实例并调用方法
        MyStringUtils instance = (MyStringUtils) clazz.getDeclaredConstructor().newInstance();
        instance.tester("aaa");
        /*
            method start
            Hi: aaa
            method end
        */

    }
}
通过插入代码块手段

当然我们也可以通过insertBefore & insertAfter进行操作类字节码:

public class MainTest2 {
    public static class MyStringUtils {
        public void tester(String name) {
            System.out.println("Hello " + name);
        }
    }

    public static void main(String[] args) throws Exception {
        ClassPool classPool = new ClassPool(true);
        classPool.insertClassPath(new LoaderClassPath(MainTest2.class.getClassLoader()));
        CtClass ctClass = classPool.get("com.heihu577.MainTest2$MyStringUtils");
        CtMethod testerMethod = ctClass.getDeclaredMethod("tester"new CtClass[]{classPool.get(String.class.getName())});
        testerMethod.insertBefore("System.out.println("Before..." + name);"); // 在方法前增加代码
        testerMethod.insertAfter("System.out.println("After...");"); // 在方法后增加代码
        ctClass.toClass();
        MyStringUtils myStringUtils = new MyStringUtils();
        myStringUtils.tester("World");
    }
}

但是这种方式的缺陷则是, 假设在insertBefore中进行定义了a变量, 而在insertAfter中是无法获取到a变量的值的.

Javassist & JavaAgent 联动使用

JavaAgent 可以通过agentmain方法, 进行劫持到正常的 Java 程序中, 支持将已加载的类进行重新加载, 提供修改字节码操作功能.

Javassist 提供修改字节码功能操作, 下面来看一下这两者联合使用的案例. 本次将重新构建新项目进行说明.

搭建正常服务

这里我们新建一个项目, 创建一个正常服务:

package com.heihu577;

public class Server {
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            sayHello();
            Thread.sleep(1000);
        }
    }

    public static void sayHello() {
        System.out.println("[Server] Hello ~");
    }
}

这个代码很简单, 每过一秒进行输出[Server] Hello ~, 随后我们配置打包插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>

            <artifactId>maven-jar-plugin</artifactId>

            <version>3.1.2</version>

            <configuration>
                <archive>
                    <manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>

                </archive>

            </configuration>

        </plugin>

    </plugins>

</build>

因为是不需任何依赖, 我们只需要使用maven-jar-plugin就可以了. 随后我们创建META-INF/MANIFEST.MF文件内容如下:

Main-Class: com.heihu577.Server

配置完毕后, 打成server.jar包并运行:

PS C:UsersAdministratorDesktop> java -jar server.jar
[Server] Hello ~
[Server] Hello ~
[Server] Hello ~
... (持续输出)

创建 JavaAgent

随后我们再新建一个项目, 用于生成我们javaAgent.jar包,创建主程序如下:

public class MyAgent {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new MyClassFileTransformer(), true);
        Class<?>[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class<?> allLoadedClass : allLoadedClasses) {
            if (allLoadedClass.getName().equals("com.heihu577.Server")) {
                try {
                    inst.retransformClasses(allLoadedClass); // 重新加载 com.heihu577.Server
                } catch (UnmodifiableClassException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    static class MyClassFileTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            try {
                System.out.println("Transforming class: " + className);
                ClassPool classPool = new ClassPool(true);
                classPool.insertClassPath(new LoaderClassPath(loader));
                CtClass ctClass = classPool.get(className.replace("/""."));
                CtMethod tester = ctClass.getDeclaredMethod("sayHello"); // 得到 sayHello 方法
                tester.setBody("{System.out.println("[MyAgent] Heihu577!");}");
                return ctClass.toBytecode(); // 返回修改后的字节码
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

        }
    }
}

由于这里我们的transform方法使用了javassist库中的内容, 这里打包方式以及扩展引入在pom.xml中是这样定义的:

<dependencies>
    <dependency>
        <groupId>org.javassist</groupId>

        <artifactId>javassist</artifactId>

        <version>3.30.2-GA</version>

    </dependency>

</dependencies>

<build>
    <plugins>
        <plugin>
            <artifactId>maven-assembly-plugin</artifactId>

            <configuration>
                <archive>
                    <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>

                </archive>

                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>

                </descriptorRefs>

            </configuration>

            <executions>
                <execution>
                    <id>make-assembly</id>

                    <phase>package</phase>

                    <goals>
                        <goal>single</goal>

                    </goals>

                </execution>

            </executions>

        </plugin>

    </plugins>

</build>

使用maven-assembly-plugin可以进行带扩展打包, 随后指明我们的/resources/META-INF/MANIFEST.MF文件内容如下:

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: com.heihu577.MyAgent

定义完毕之后, 可以进行打包. 打成agent.jar包.

运行 attach 应用

再新建一个项目, 写入attach代码块:

public class Test {
    public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException {
        System.out.println("AgentDemo4Test"); // 打印测试标识信息
        // 获取当前系统中所有虚拟机实例的描述符列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        // 遍历虚拟机实例描述符列表
        for (VirtualMachineDescriptor vmd : list) {
            // 检查虚拟机实例的显示名称是否为 "main.jar"
            if (vmd.displayName().equals("server.jar")) {
                // 附加到指定的虚拟机实例
                VirtualMachine vm = VirtualMachine.attach(vmd.id());
                // 打印虚拟机实例的 ID
                System.out.println(vm.id());
                // 打印虚拟机实例的信息
                System.out.println(vm);
                // 加载指定的 Java Agent
                vm.loadAgent("C:\Users\Administrator\Desktop\agent.jar""Hacker!");
                // 参数1: 指明具体的 agent 包路径
                // 参数2: 给该 agent 方法传递的参数
                // 从目标虚拟机断开连接
                vm.detach();
            }
        }
    }
}

这里我们需要引入tools.jar:

<dependencies>
    <dependency>
        <groupId>com.sun.jdk</groupId>

        <artifactId>tools</artifactId>

        <version>1.8</version>

        <scope>system</scope>

        <systemPath>${java.home}/../lib/tools.jar</systemPath>

    </dependency>

</dependencies>

随后运行我们的server.jar:

PS C:UsersAdministratorDesktop> java -jar server.jar
[Server] Hello ~
[Server] Hello ~

运行我们的attach应用, 使用javaAgent && javassist技术修改正在运行的server.jar中的Server::sayHello内容:

语言特性 | JavaAgent & Javassist

Reference

Javassist 视频课: https://www.bilibili.com/video/BV1ov4y1Z7tr

Javassist 使用指南(一):https://www.jianshu.com/p/43424242846b

Javassist 使用指南(二):https://www.jianshu.com/p/b9b3ff0e1bf8

Javassist 使用指南(三):https://www.jianshu.com/p/7803ffcc81c8

Java Agent 从入门到内存马: https://xz.aliyun.com/t/9450

Java Agent 使用指南: https://www.cnblogs.com/rickiyang/p/11368932.html

Java Agent 使用: https://blog.csdn.net/zhangyifang_009/article/details/137936613

Java Agent 通灵之术: https://lsieun.github.io/article/java-agent-summoning-jutsu.html

原文始发于微信公众号(Heihu Share):语言特性 | JavaAgent & Javassist

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月8日16:54:35
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   语言特性 | JavaAgent & Javassisthttps://cn-sec.com/archives/3373902.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息