JAVA安全|浅谈ASM结合JavaAgent的字节码插桩技术

0x00 前言

字节码增强技术是一类对现有字节码进行修改或者动态生成全新字节码文件的技术,它在网络安全领域中的作用之一就是用来以“零侵入“方式插入恶意字节码,达到权限维持和RCE的目的,故而我更愿意叫它字节码插桩技术。

0x01 ASM&&JDPA&&JavaAgent介绍

一、ASM

ASM是一个字节码操作框架,它直接从字节码的层面来修改现有类或动态生成类,官网地址:https://asm.ow2.io。使用ASM需要了解字节码文件结构与JVM指令,并理解访问者设计模式。

二、JPDA

要介绍JavaAgent追根溯源就得先介绍JPDA。

JPDA英文全称为Java Platform Debugger Architecture,即Java平台调试体系。如果JVM启动时开启了JPDA,那么类是允许被重新加载的。在这种情况下,已被加载的旧版本类信息可以被卸载,然后重新加载新版本的类。

正如JPDA名称中的Debugger,这个体系为开发人员提供了一整套用于调试 Java 程序的 API,是一套用于开发 Java 调试工具的接口和协议。本质上说,它是我们通向虚拟机,考察虚拟机运行态的一个通道,一套工具。另外,我们要注意的是,JPDA 是一套标准,任何的 JDK 实现都必须完成这个标准。

JPDA定义了一整套完整的体系,它由三个相对独立的层次共同组成,而且规定了它们三者之间的交互方式,或者说定义了它们通信的接口。这三个层次由低到高分别是 Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP)以及 Java 调试接口(JDI):
JDPA.png

这三个模块把调试过程分解成三个很自然的概念:调试者(debugger)和被调试者(debuggee),以及他们中间的通信器。被调试者运行于我们想调试的 Java 虚拟机之上,它可以通过 JVMTI 这个标准接口,监控当前虚拟机的信息;调试者定义了用户可使用的调试接口,通过这些接口,用户可以对被调试虚拟机发送调试命令,同时调试者接受并显示调试结果。在调试者和被调试着之间,调试命令和调试结果,都是通过 JDWP 的通讯协议传输的。所有的命令被封装成 JDWP 命令包,通过传输层发送给被调试者,被调试者接收到 JDWP 命令包后,解析这个命令并转化为 JVMTI 的调用,在被调试者上运行。类似的,JVMTI 的运行结果,被格式化成 JDWP 数据包,发送给调试者并返回给 JDI 调用。而调试器开发人员就是通过 JDI 得到数据,发出指令。

1.JDI

JDI英文全称Java Debug Interface,即 Java 调试接口,由Java语言实现,通过它,调试工具开发人员就能通过前端虚拟机上的调试器来远程操控后端虚拟机上被调试程序的运行。

2.JDWP

JDWP英文全称Java Debug Wire Protocol,即Java 调试线协议,是一个为 Java 调试而设计的一个通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。

3.JVMTI

这是本文中需要重点认识的。

JVMTI英文全称Java Virtual Machine Tool Interface,即 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口,它处于整个 JPDA 体系的最底层,所有调试功能本质上都需要通过 JVMTI 来提供。

通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。JVMTI 的前身是 JVMDI(Java Virtual Machine Debug Interface) 和 JVMPI(Java Virtual Machine Profiler Interface),它们原来分别被用于提供调试 Java 程序以及 Java 程序调节性能的功能。

JVMTI就是JVM提供的一套对JVM进行操作的工具接口。通过JVMTI,可以实现对JVM的多种操作,它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。

有关JPDA的更详细内容可以参考这篇文章:

JPDA 体系概览

三、JavaAgent

1.JavaAgent本质

Agent就是JVMTI的一种实现,它有两种启动方式:

一、随Java进程启动而启动,经常见到的java -agentlib就是这种方式;

二、运行时载入,通过Attach API,将模块(jar包)动态地Attach到指定进程id的Java进程内

什么又是Attach API(附加应用程序接口)呢?

Attach API是JVMTI的一种机制,其作用是提供JVM进程间通信的能力,比如说我们为了让另外一个JVM进程把线上服务的线程Dump出来,会运行jstack或jmap的进程,并传递pid的参数,告诉它要对哪个进程进行线程Dump,这就是Attach API做的事情。

2.JavaAgent “表象”

JavaAgent的表象就是java命令的一个参数,该参数可以用于指定一个jar包,jar包内至少含有一个遵循一组严格约定的常规Java类,对该jar包有两个要求:

1.这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。

2.Premain-Class 指定的那个类必须实现 premain() 方法。

premain即运行在main函数之前,当JVM启动时,在执行main函数之前,会先运行-javaagent所指定jar包内Premain-Class类的premain方法。

我们可以通过在命令行输入java看到与java agent相关的参数:

相关参数.png

在-javaagent中提到了java.lang.instrument,它是在rt.jar中定义的一个包:

instrument.png

这个包提供了允许Java编程语言代理程序检测在JVM上运行的程序的服务,检测机制是修改方法的字节码。也可以理解为它提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。故而可以将JavaAgent看作一个Class 类型的转换器,其中对Class类型进行修改重要的是Instrumentation和ClassFileTransformer接口。

javaagent与instrument的关系就是javaagent的实现使用到了java.lang.instrument软件包。

四、Instrumetation和ClassFileTransformer接口

Instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持,它需要依赖JVMTI的Attach API机制实现。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。

下面就介绍下java.lang.instrument包中两个重要的接口:

1.ClassFileTransformer

要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。

该接口定义如下:

public interface ClassFileTransformer {
    //transform()方法会在类文件被加载时调用,而在transform方法里,可以利用ASM、Javassist等对传入的字节码进行改写或替换,生成新的字节码数组后返回。
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}
2.Instrumentation

通过Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至可以替换和修改某些类的定义。

该接口的定义和重要方法如下:

public interface Instrumentation {
	//增加一个Class文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    //删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    boolean isRetransformClassesSupported();

    //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    }

0x02 使用JavaAgent进行字节码插桩

一、JVM启动时进行插桩

1.premain方法

要实现在JVM启动时进行插桩,即随JavaAgent随Java进程启动而启动,那么在javaagent命令中指定的jar包的Premain-class类中必须要有premain()方法。

premain方法有两种格式:

public static void premain(String agentArgs, Instrumentation inst)

public static void premain(String agentArgs)

JVM 会优先加载带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。这个逻辑在rt.jar包中的sun.instrument.InstrumentationImpl 类的loadClassAndStartAgent()方法中:
premain逻辑.png

我们如何来写个简单代码来实现下呢?

2.代码实现

首先,我们建一个普通java工程,工程名为Test,有一个类Test01如下:

public class Test01 {
    public static void main(String[] args) {
        System.out.println("test");
    }
}

我们想通过插桩,使得在打印test的前后打印start和end,且修改字节码部分通过ASM来实现。

新建一个Maven项目Enhancement01,自写适配器代码如下:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.*;

public class Adapter extends ClassVisitor {
    public Adapter(ClassVisitor cv){
        super(ASM5,cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        if(!name.equals("<init>") && mv != null){
            mv = new MyMethodVisitor(mv);
        }
        return mv;
    }

    class MyMethodVisitor extends MethodVisitor{
        public MyMethodVisitor(MethodVisitor mv){
            super(ASM5,mv);
        }

        @Override
        public void visitCode() {
            //在方法访问前打印start
            super.visitCode();
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("start");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }

        @Override
        public void visitInsn(int opcode) {
            //在方法返回前打印end
            if((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW){
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("end");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
            mv.visitInsn(opcode);
        }
    }
}

再在Enhancement01项目中写个PremainAgent类,该类即为Premain-Class,需要有一个premain()方法。

PremainAgent类的代码如下:

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class PremainAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("agentArgs : " + agentArgs);
        inst.addTransformer(new TestTransformer(),true);
    }

    static class TestTransformer implements ClassFileTransformer{
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            if(!className.equals("Test01")){
                return null;
            }

            ClassReader cr = null;
            try {
                cr = new ClassReader(className);
            } catch (IOException e) {
                e.printStackTrace();
            }
            ClassWriter cw = new ClassWriter(0);
            ClassVisitor cv = new Adapter(cw);
            cr.accept(cv,0);

            byte[] b = cw.toByteArray();

            //将转换后的字节码写入到文件
            String filePath = "F:\\JAVA\\Test01\\src\\Test01_1.class";
            try {
                FileOutputStream fos = new FileOutputStream(filePath);
                fos.write(b);
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("转化后的字节码已写入 " + filePath);
            //返回字节码
            return b;

        }
    }
}
3.打包

通过maven将Enhancement01项目打包成jar包,我的pom.xml完整内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>ByteCode</groupId>
    <artifactId>Enhancement</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <appendAssemblyId>false</appendAssemblyId>
                    <finalName>Premain_Agent</finalName>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>PremainAgent</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>assembly</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.2</version>
        </dependency>
    </dependencies>

</project>

打包时会将项目中的所有依赖都打进jar包,且会在jar包为我们生成包含如下内容的MANIFEST.MF文件:

Premain-Class: PremainAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

通过Maven–>Lifecycle–>clean–>package可得到jar包:

打包步骤.png
4.JVM启动时运行javaagent

有两种方式:

(1)在IDEA中运行Test01时,在VM options中指定-javaagent:

premain运行参数.png

(2)在cmd中运行:

cmd运行premain.png

运行完成会生成一个Test01_1.class文件,通过IDEA 打开:

Test01_1class.png

Perfect!!!该class即为成功插桩后加载进入jvm运行的字节码,是不是帅到飞起~

二、JVM运行中进行插桩

上面的实验只是在java程序启动时才能进行插桩,实战意义并不大,那么如何对一个正在运行中的java进程进行插桩呢?这是在JDK1.6之后才能进行的操作。

1.agentmain方法

在 Java SE 6 的 Instrumentation 当中,提供了一个新的代理操作方法:agentmain,可以在 main 函数开始运行之后再运行。跟premain函数一样, 开发者可以编写一个含有agentmain函数的 Java 类。

agentmain也有两种方式:

//采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调
public static void agentmain (String agentArgs, Instrumentation inst)

public static void agentmain (String agentArgs)

同样,带Instrumentation参数的方法比不带优先级更高。开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。

2.Attach API及注入原理

在Java6 以后实现启动后加载的新实现是Attach api,有 2 个主要的类VirtualMachine和VirtualMachineDescriptor,都在 tools.jar中的com.sun.tools.attach 包里:

attach api.png

注:tools.jar包默认是没有载入到项目的JDK中的,因为该包位于JDK安装目录下的/lib中,我们需要通过 file–>project structer–>SDKs将/lib/tools.jar加入到ClassPath中:

tools加入path.png

下面分别介绍下VirtualMachine和VirtualMachineDescriptor:

(1)VirtualMachine类

该类提供了获取系统信息比如获取内存dump、线程dump,类信息统计(比如已加载的类以及实例个数等)的方法,以及 loadAgent,Attach 和 Detach (Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法,可以实现的功能可以说非常之强大 。

该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。

(2)VirtualMachineDescriptor类

一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

Attach实现动态注入原理

通过VirtualMachine类的attach(pid)方法,attach到一个运行中的java进程上,之后通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。

3.代码实现

首先,我们在Test工程中写一个Test02类,该类将作为我们的运行中的java程序:

public class Test02 {
    public static void main(String[] args) {
        while (true) {
            try {
                Thread.sleep(5000L);
            } catch (Exception e) {
                break;
            }
            process();
        }
    }

    public static void process(){
        System.out.println("process...");
    }
}

我们希望在process方法中打印process后弹出计算器,即我们希望插桩成功后process方法的代码是这样的:

public static void process(){
    System.out.println("process...");
    try {
        Runtime.getRuntime().exec("calc");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

那么如何使用ASM写出对应的代码呢?这里用到一个ASM ByteCode Outline的插件,在IDEA中通过setting–>plugins进行搜索下载安装即可。

安装后我们编写一个Test03.java,其process方法如上所示,将其编译,然后右键–>show bytecode outline,在ASMified模块即可看到用ASM生成该方法的代码:

弹窗代码生成.png

新建一个maven项目Enhancement02,自写适配器如下:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import static org.objectweb.asm.Opcodes.*;

public class Adapter extends ClassVisitor {
    public Adapter(ClassVisitor cv){
        super(ASM5,cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        if(name.equals("process")){
            mv = new MyMethodVisitor(mv);
        }

        return mv;
    }

    class MyMethodVisitor extends MethodVisitor{
        public MyMethodVisitor(MethodVisitor mv){
            super(ASM5,mv);
        }

        @Override
        public void visitInsn(int opcode) {
            //在方法返回前弹计算器
            if((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW){
                Label l0 = new Label();
                Label l1 = new Label();
                Label l2 = new Label();
                mv.visitTryCatchBlock(l0, l1, l2, "java/io/IOException");
                mv.visitLabel(l0);
                mv.visitLineNumber(24, l0);
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false);
                mv.visitLdcInsn("calc");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "(Ljava/lang/String;)Ljava/lang/Process;", false);
                mv.visitInsn(POP);
                mv.visitLabel(l1);
                mv.visitLineNumber(27, l1);
                Label l4 = new Label();
                mv.visitJumpInsn(GOTO, l4);
                mv.visitLabel(l2);
                mv.visitLineNumber(25, l2);
                mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/io/IOException"});
                mv.visitVarInsn(ASTORE, 0);
                Label l5 = new Label();
                mv.visitLabel(l5);
                mv.visitLineNumber(26, l5);
                mv.visitVarInsn(ALOAD, 0);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/IOException", "printStackTrace", "()V", false);
                mv.visitLabel(l4);
                mv.visitLineNumber(28, l4);
                mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
            }
            mv.visitInsn(opcode);
        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(maxStack, maxLocals + 1);
        }
    }
}

再在Enhancement02项目中写个AgentMainAgent类,该类即为Agent-Class,需要有一个agentmain()方法。

AgentMainAgent类的代码如下:

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;

public class AgentMainAgent {
    public static void agentmain(String agentArgs, Instrumentation inst){
        inst.addTransformer(new TestTransformer(),true);
//        try {
//            inst.retransformClasses(Test02.class);
//            System.out.println("Agent Load Done.");
//        } catch (UnmodifiableClassException e) {
//            System.out.println("Agent Load Fail.");
//        }
        Class[] classes = inst.getAllLoadedClasses();
        for (int i = 0; i < classes.length ; i++) {
            if(classes[i].getName().equals("Test02")){
                try {
                    inst.retransformClasses(classes[i]);
                } catch (UnmodifiableClassException e) {
                    e.printStackTrace();
                }
                break;
            }

        }

    }

    static class TestTransformer implements ClassFileTransformer {
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            if(!className.equals("Test02")){
                return null;
            }

            ClassReader cr = null;
            try {
                cr = new ClassReader(className);
            } catch (IOException e) {
                e.printStackTrace();
            }
            ClassWriter cw = new ClassWriter(0);
            ClassVisitor cv = new Adapter(cw);
            cr.accept(cv,0);

            byte[] b = cw.toByteArray();

            //将转换后的字节码写入到文件
            String filePath = "F:\\JAVA\\Test01\\src\\Test02_1.class";
            try {
                FileOutputStream fos = new FileOutputStream(filePath);
                fos.write(b);
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("转化后的字节码已写入 " + filePath);
            //返回字节码
            return b;
        }
    }
}
3.pom.xml及打包

pom.xml文件内容只要按照上面的配置指定Agent-class即可,打包就不说了。

4.JVM运行中attach Agent

另外新建一个工程,编写如下测试类,使得在JVM运行时通过Attach机制将Agent_Main.jar注入指定pid的虚拟机:

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class AgentMainTest {
    public static void main(String[] args) throws Exception {
        //获取当前系统中所有 运行中的 虚拟机
        System.out.println("当前系统中所有运行中的JVM虚拟机: ");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
            //然后加载 Agent_Main.jar 发送给该虚拟机
            System.out.println(vmd.displayName());
            if (vmd.displayName().endsWith("Test02")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("F:\\JAVA\\Enhancement02\\target\\Agent_Main.jar");
                virtualMachine.detach();
            }
        }
    }
}

我们先运行Test02,再运行AgentMainTest,接下来就是见证奇迹的时候:

弹计算器.png
注入Agent_Main.jar后,Test02只要在运行就会每隔5秒打印一个process并弹出一个计算器。实现了以“零侵入“方式插入字节码!!!有没有帅到屌爆~

0x03 打包时到的坑

有时候按照上面的配置进行打包后,MANIFEST.MF会被默认配置覆盖掉,导致我们生成的jar包中没有指定Premain-class或Agent-class,那么我们的Agent就不能运行,有什么解决办法呢?

方法一:关闭IDEA并重新启动

方法二:解压jar包,将jar包中的MANIFEST.MF替换为我们想要的配置,然后再通过jar -cfM0 xxx.jar *重新打包为jar包。

参考:https://www.zhuyanbin.com/?p=863

方法三:不在IDEA中打包为jar,然后安装maven并配置好后,使用mvn package半自动进行打包(狗头)

0x04 总结

一、本文会产生纯属意外,起因是学习java常见反序列化链时,看到一些POC中使用到了javassist,于是就去网上搜相关资料,基本能看到的文章都会把javassist和ASM放在一起提一下。

作为一个学东西喜欢搞懂原理的人,javassist并不能满足我的求知欲,于是就想去学ASM,后来无意看到美团的这篇文章字节码增强技术探索 – 美团技术团队 (meituan.com)。文章很长,知识点很多,很干,写文章的师傅很强。插桩很有意思,于是就开启了字节码之旅。

参考文章

本文参考了很多资料和文章,还看过原版的ASM手册,参考的文章每一篇都很好,本文只是自己的一个学习记录,并没有把这些文章的内容做整合,每位师傅都写得很好,各位师傅可以看看:

字节码增强技术探索 – 美团技术团队 (meituan.com)

Java字节码技术(一)static、final、volatile、synchronized关键字的字节码体现_

Java字节码技术(二)字节码增强之ASM、JavaAssist、Agent、Instrumentation_字节码修改_

javaagent使用指南

JPDA 体系概览

本文作者:walker1995

转载自FreeBuf.COM

© 版权声明
THE END
喜欢就支持一下吧
点赞7赏点小钱 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    请登录后查看评论内容