附录:字节码编辑工具 ¶
约 1530 个字 261 行代码 预计阅读时间 12 分钟
ASM¶
ASM 是一种通用 Java 字节码操作和分析框架,它可以直接以二进制形式修改一个现有的类或动态生成类文件。ASM 官方用户手册
ASM 提供了三个基于ClassVisitor API
的核心 API,用于生成和转换类:
ClassReader
类用于解析 class 文件或二进制流;ClassWriter
类是ClassVisitor
的子类,用于生成类二进制;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

MethodVisitor 和 AdviceAdapter ¶
MethodVisitor
同ClassVisitor
,重写MethodVisitor
类方法可获取捕获到对应的visit
事件
AdviceAdapter
的父类是GeneratorAdapter
和LocalVariablesSorter
,在MethodVisitor
类的基础上封装了非常多的便捷方法,同时还为我们做了非常有必要的计算,所以我们应该尽可能的使用AdviceAdapter
来修改字节码。
AdviceAdapter
类实现了一些非常有价值的方法,如onMethodEnter
(方法进入时回调方法onMethodExit
(方法退出时回调方法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();
}
}
}

修改类 ¶
大多数使用 ASM 库的目的是修改类方法的字节码,在原方法执行的前后动态插入新的 Java 代码,从而实现类似于 AOP 的功能。修改类方法字节码的典型应用场景如:APM 和 RASP;APM 需要统计和分析每个类方法的执行时间,而 RASP 需要在 Java 底层 API 方法执行之前插入自身的检测代码,从而实现动态拦截恶意攻击。
使用ClassWriter
可以实现类修改功能,使用 ASM 修改类字节码时如果插入了新的局部变量、字节码,需要重新计算max_stack
和max_locals
,否则会导致修改后的类文件无法通过 JVM 校验。手动计算max_stack
和max_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;
}

Javassist¶
Javassist
是一个开源的分析、编辑和创建 Java 字节码的类库;相比 ASM,Javassist
提供了更加简单便捷的 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();
}
}
}

修改类方法 ¶
Javassist
实现类方法修改只需调用CtMethod
类的对应的 API。CtMethod
提供了类方法修改的 API,如:setModifiers
可修改类的访问修饰符,insertBefore
和insertAfter
能够实现在类方法执行的前后插入任意的 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
方法字节码如下:


bytebuddy¶
参考资料 ¶
创建日期: 2024年7月22日 19:50:51