跳转至

附录:字节码编辑工具

1530 个字 261 行代码 预计阅读时间 12 分钟

ASM

ASM 是一种通用 Java 字节码操作和分析框架,它可以直接以二进制形式修改一个现有的类或动态生成类文件。ASM 官方用户手册

ASM 提供了三个基于ClassVisitor API的核心 API,用于生成和转换类:

  1. ClassReader类用于解析 class 文件或二进制流;
  2. ClassWriter类是ClassVisitor的子类,用于生成类二进制;
  3. ClassVisitor是一个抽象类,自定义ClassVisitor重写visitXXX方法,可获取捕获 ASM 类结构访问的所有事件;

ClassVisitor ClassReader

ClassReader类用于解析类字节码,创建ClassReader对象可传入类名、类字节码数组或者类输入流对象。

创建完ClassReader对象就会触发字节码解析(解析 class 基础信息,如常量池、接口信息等,所以可以直接通过ClassReader对象获取类的基础信息

调用ClassReader类的accpet方法需要传入自定义的ClassVisitor对象,ClassReader会按照如下顺序,依次调用该ClassVisitor的类方法。

visit
        [ visitSource ] [ visitModule ][ visitNestHost ][ visitPermittedclass ][ visitOuterClass ]
        ( visitAnnotation | visitTypeAnnotation | visitAttribute )*
        ( visitNestMember | visitInnerClass | visitRecordComponent | visitField | visitMethod )*
        visitEnd
Image title
ClassVisitor 类图

MethodVisitor AdviceAdapter

MethodVisitorClassVisitor,重写MethodVisitor类方法可获取捕获到对应的visit事件

AdviceAdapter的父类是GeneratorAdapterLocalVariablesSorter,在MethodVisitor类的基础上封装了非常多的便捷方法,同时还为我们做了非常有必要的计算,所以我们应该尽可能的使用AdviceAdapter来修改字节码。

AdviceAdapter类实现了一些非常有价值的方法,如onMethodEnter(方法进入时回调方法onMethodExit(方法退出时回调方法,如果我们自己实现很容易掉进坑里面,因为这两个方法都是根据条件推算出来的。比如我们如果在构造方法的第一行直接插入了我们自己的字节码就可能会发现程序一运行就会崩溃,因为 Java 语法中限制我们第一行代码必须是super(xxx)

GeneratorAdapter封装了一些栈指令操作的方法,如loadArgArray方法可以直接获取方法所有参数数组、invokeStatic方法可以直接调用类方法、push方法可压入各种类型的对象等。

LocalVariablesSorter类实现了计算本地变量索引位置的方法,如果要在方法中插入新的局部变量就必须计算变量的索引位置,我们必须先判断是否是非静态方法、是否是long/double类型的参数(宽类型占两个位,否则计算出的索引位置还是错的。使用AdviceAdapter可以直接调用mv.newLocal(type)计算出本地变量存储的位置,为我们省去了许多不必要的麻烦。

读取类信息

code

package cc.asm;

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

import java.io.IOException;
import java.util.Arrays;

import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.Opcodes.ASM9;

public class readinfo {

    public static void main(String[] args) {
        // 定义需要解析的类名称
        String className = "cc.asm.testhw";

        try {
            // 创建ClassReader对象,用于解析类对象,可以根据类名、二进制、输入流的方式创建
            final ClassReader cr = new ClassReader(className);

            System.out.println(
                    "解析类名:" + cr.getClassName() + ",父类:" + cr.getSuperName() +
                            ",实现接口:" + Arrays.toString(cr.getInterfaces())
            );

            System.out.println("-----------------------------------------------------------------------------");

            // 使用自定义的ClassVisitor访问者对象,访问该类文件的结构
            cr.accept(new ClassVisitor(ASM9) {
                @Override
                public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                    System.out.println(
                            "变量修饰符:" + access + "\t 类名:" + name + "\t 父类名:" + superName +
                                    "\t 实现的接口:" + Arrays.toString(interfaces)
                    );

                    System.out.println("-----------------------------------------------------------------------------");

                    super.visit(version, access, name, signature, superName, interfaces);
                }

                @Override
                public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
                    System.out.println(
                            "变量修饰符:" + access + "\t 变量名称:" + name + "\t 描述符:" + desc + "\t 默认值:" + value
                    );

                    return super.visitField(access, name, desc, signature, value);
                }

                @Override
                public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

                    System.out.println(
                            "方法修饰符:" + access + "\t 方法名称:" + name + "\t 描述符:" + desc +
                                    "\t 抛出的异常:" + Arrays.toString(exceptions)
                    );

                    return super.visitMethod(access, name, desc, signature, exceptions);
                }
            }, EXPAND_FRAMES);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}
运行结果: alt text

修改类

大多数使用 ASM 库的目的是修改类方法的字节码,在原方法执行的前后动态插入新的 Java 代码,从而实现类似于 AOP 的功能。修改类方法字节码的典型应用场景如:APM RASPAPM 需要统计和分析每个类方法的执行时间,而 RASP 需要在 Java 底层 API 方法执行之前插入自身的检测代码,从而实现动态拦截恶意攻击。

使用ClassWriter可以实现类修改功能,使用 ASM 修改类字节码时如果插入了新的局部变量、字节码,需要重新计算max_stackmax_locals,否则会导致修改后的类文件无法通过 JVM 校验。手动计算max_stackmax_locals是一件比较麻烦的事情,ASM 为我们提供了内置的自动计算方式,只需在创建ClassWriter的时候传入COMPUTE_FRAMES即可:new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

code

package cc.asm;

import org.javaweb.utils.FileUtils;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;

import java.io.File;
import java.io.IOException;

import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.Opcodes.ASM9;

public class modifyasm {

    public static void main(String[] args) {
        // 定义需要解析的类名称
        String className = "cc.asm.testhw";

        try {
            // 创建ClassReader对象,用于解析类对象,可以根据类名、二进制、输入流的方式创建
            final ClassReader cr = new ClassReader(className);

            // 创建ClassWriter对象,COMPUTE_FRAMES会自动计算max_stack和max_locals
            final ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

            // 使用自定义的ClassVisitor访问者对象,访问该类文件的结构
            cr.accept(new ClassVisitor(ASM9, cw) {
                @Override
                public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                    if (name.equals("hello")) {
                        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);

                        // 创建自定义的MethodVisitor,修改原方法的字节码
                        return new AdviceAdapter(api, mv, access, name, desc) {
                            int newArgIndex;

                            // 获取String的ASM Type对象
                            private final Type stringType = Type.getType(String.class);

                            @Override
                            protected void onMethodEnter() {
                                // 输出hello方法的第一个参数,因为hello是非static方法,所以0是this,第一个参数的下标应该是1
                                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                                mv.visitVarInsn(ALOAD, 1);
                                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

                                // 创建一个新的局部变量,newLocal会计算出这个新局部对象的索引位置
                                newArgIndex = newLocal(stringType);

                                // 压入字符串到栈顶
                                mv.visitLdcInsn("colemak.top");
                                storeLocal(newArgIndex, stringType);
                            }

                            @Override
                            protected void onMethodExit(int opcode) {
                                dup(); // 复制栈顶的返回值

                                // 创建一个新的局部变量,并获取索引位置
                                int returnValueIndex = newLocal(stringType);

                                // 将栈顶的返回值压入新生成的局部变量中
                                storeLocal(returnValueIndex, stringType);

                                // 输出hello方法的返回值
                                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                                mv.visitVarInsn(ALOAD, returnValueIndex);
                                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

                                // 压入方法进入(onMethodEnter)时存入到局部变量的var2值到栈顶
                                loadLocal(newArgIndex);

                                // 返回一个引用类型,即栈顶的var2字符串,return var2;
                                // 需要特别注意的是不同数据类型应当使用不同的RETURN指令
                                mv.visitInsn(ARETURN);
                            }
                        };
                    }

                    return super.visitMethod(access, name, desc, signature, exceptions);
                }
            }, EXPAND_FRAMES);

            File classFilePath = new File(new File(System.getProperty("user.dir"), "src/main/java/cc/asm/"), "THW.class");

            // 修改后的类字节码
            byte[] classBytes = cw.toByteArray();

            // 写入修改后的字节码到class文件
            FileUtils.writeByteArrayToFile(classFilePath, classBytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
编译后发现在指定路径下生成了THW.class文件,反编译后发现hello方法的字节码已经被修改:

    public String hello(String content) {
    System.out.println(content);
    String var2 = "colemak.top";
    String str = "Hello:";
    String var4 = str + content;
    System.out.println(var4);
    return var2;
}
运行结果如下: alt text

Javassist

Javassist是一个开源的分析、编辑和创建 Java 字节码的类库;相比 ASMJavassist提供了更加简单便捷的 API,使用Javassist我们可以像写 Java 代码一样直接插入 Java 代码片段,让我们不再需要关注 Java 底层的字节码的和栈操作,仅需要学会如何使用Javassist API 即可实现字节码编辑。学习Javassist可以阅读官方的入门教程:Getting Started with Javassist

Javassist API 和标识符

Javassist提供类似于 Java 反射机制的 API

描述
ClassPool ClassPool 是一个存储 CtClass 的容器,如果调用get方法会搜索并创建一个表示该类的 CtClass 对象
CtClass CtClass 表示的是从 ClassPool 获取的类对象,可对该类就行读写编辑等操作
CtMethod 可读写的类方法对象
CtConstructor 可读写的类构造方法对象
CtField 可读写的类成员变量对象

Javassist使用了内置的标识符来表示一些特定的含义,如:$_表示返回值。我们可以在动态插入类代码的时候使用这些特殊的标识符来表示对应的对象。

表达式 描述
$0, $1, $2, ... this和方法参数
$args Object[]类型的参数数组
$$ 所有的参数,如m($$)等价于m($1,$2,...)
$cflow(...) cflow 变量
$r 返回类型,用于类型转换
$w 包装类型,用于类型转换
$_ 方法返回值
$sig 方法签名,返回java.lang.Class[]数组类型
$type 返回值类型,java.lang.Class类型
$class 当前类,java.lang.Class类型

读取类信息

javassist 读取进程类信息较为简单

code

package cc.asm;

import javassist.*;
import java.util.Arrays;

public class javassistreadi {

    public static void main(String[] args) {
        // 创建ClassPool对象
        ClassPool classPool = ClassPool.getDefault();

        try {
            CtClass ctClass = classPool.get("cc.asm.testhw");

            System.out.println(
                    "解析类名:" + ctClass.getName() + ",父类:" + ctClass.getSuperclass().getName() +
                            ",实现接口:" + Arrays.toString(ctClass.getInterfaces())
            );

            System.out.println("-----------------------------------------------------------------------------");

            // 获取所有的构造方法
            CtConstructor[] ctConstructors = ctClass.getDeclaredConstructors();

            // 获取所有的成员变量
            CtField[] ctFields = ctClass.getDeclaredFields();

            // 获取所有的成员方法
            CtMethod[] ctMethods = ctClass.getDeclaredMethods();

            // 输出所有的构造方法
            for (CtConstructor ctConstructor : ctConstructors) {
                System.out.println(ctConstructor.getMethodInfo());
            }

            System.out.println("-----------------------------------------------------------------------------");

            // 输出所有成员变量
            for (CtField ctField : ctFields) {
                System.out.println(ctField);
            }

            System.out.println("-----------------------------------------------------------------------------");

            // 输出所有的成员方法
            for (CtMethod ctMethod : ctMethods) {
                System.out.println(ctMethod);
            }
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
    }

}
运行结果: alt text

修改类方法

Javassist实现类方法修改只需调用CtMethod类的对应的 APICtMethod提供了类方法修改的 API,如:setModifiers可修改类的访问修饰符,insertBeforeinsertAfter能够实现在类方法执行的前后插入任意的 Java 代码片段,setBody可以修改整个方法的代码等。

code

package cc.asm;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import org.javaweb.utils.FileUtils;
import java.io.File;

public class modifymethodjas {
    public static void main(String[] args) throws Exception {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass cc = pool.get("cc.asm.testhw");
            CtMethod helloMethod = cc.getDeclaredMethod("hello", new CtClass[]{pool.get("java.lang.String")});
            helloMethod.setBody("{System.out.println(\"hello insert\");"
                    + "System.out.println(\"当前参数=\" + $1);" +
                    "return \"o.O\";}");
            File classFilePath = new File(new File(System.getProperty("user.dir"), "src/main/java/cc/asm/"), "testhw.class");
            byte[] bytes = cc.toBytecode();
            FileUtils.writeByteArrayToFile(classFilePath, bytes);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}
修改后的hello方法字节码如下: alt text 运行结果如下: alt text

bytebuddy

参考资料


最后更新: 2024年7月22日 19:50:51
创建日期: 2024年7月22日 19:50:51

评论