32 - ASM之Class Transformation的原理
Class-Reader/Visitor/Writer
我们使用ClassReader、ClassVisitor和ClassWriter类来进行Class Transformation操作的整体思路是这样的:
ClassReader --> ClassVisitor(1) --> ... --> ClassVisitor(N) --> ClassWriter
其中,ClassReader类负责“读”Class,ClassWriter负责“写”Class,而ClassVisitor则负责进行“转换”(Transformation)。在Class Transformation过程中,可以有多个ClassVisitor参与。不过要注意,ClassVisitor类是一个抽象类,我们需要写代码来实现一个ClassVisitor类的子类才能使用。
class transform为了解释清楚Class Transformation是如何工作的,我们从两个问题来入手:
- 第一个问题,在编写代码的时候(程序未运行),ClassReader、ClassVisitor和ClassWriter三个类的实例之间,是如何建立联系的?
- 第二个问题,在执行代码的时候(程序开始运行),类内部visitXxx()方法的调用顺序是什么样的?
建立联系
我们在进行Class Transformation操作时,一般是这样写代码的:
import lsieun.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);
//(1)构建ClassReader
ClassReader cr = new ClassReader(bytes1);
//(2)构建ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//(3)串连ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv1 = new ClassVisitor1(api, cw);
ClassVisitor cv2 = new ClassVisitor2(api, cv1);
// ...
ClassVisitor cvn = new ClassVisitorN(api, cvm);
//(4)结合ClassReader和ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cvn, parsingOptions);
//(5)生成byte[]
byte[] bytes2 = cw.toByteArray();
FileUtils.writeBytes(filepath, bytes2);
}
}
ClassReader-ClassVisitor
第一步,将ClassReader类与ClassVisitor类建立联系是通过ClassReader.accept()方法实现的。
public void accept(ClassVisitor classVisitor, int parsingOptions)
在accept()方法中,ClassReader类会不断调用ClassVisitor类当中的visitXxx()方法。
public void accept(ClassVisitor classVisitor, int parsingOptions) {
//......
classVisitor.visit(readInt(cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);
//...
// Visit the fields and methods.
int fieldsCount = readUnsignedShort(currentOffset);
currentOffset += 2;
while (fieldsCount-- > 0) {
currentOffset = readField(classVisitor, context, currentOffset);
}
int methodsCount = readUnsignedShort(currentOffset);
currentOffset += 2;
while (methodsCount-- > 0) {
currentOffset = readMethod(classVisitor, context, currentOffset);
}
// Visit the end of the class.
classVisitor.visitEnd();
}
ClassVisitor-ClassVisitor
第二步,就是将一个ClassVisitor类与另一个ClassVisitor类建立联系。因为在进行Class Transformation的过程中,可能需要多个ClassVisitor类的参与。
当前ClassVisitor类与下一个ClassVisitor类建立初步联系,是通过构造方法来实现的:
public abstract class ClassVisitor {
protected final int api;
protected ClassVisitor cv;
public ClassVisitor(final int api, final ClassVisitor classVisitor) {
this.api = api;
this.cv = classVisitor;
}
}
当前ClassVisitor类与下一个ClassVisitor类建立后续联系,是通过visitXxx()方法来实现的:
public abstract class ClassVisitor {
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if (cv != null) {
return cv.visitField(access, name, descriptor, signature, value);
}
return null;
}
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
return null;
}
public void visitEnd() {
if (cv != null) {
cv.visitEnd();
}
}
}
ClassVisitor-ClassWriter
第三步,就是将一个ClassVisitor类与一个ClassWriter类建立联系。由于ClassWriter类是继承自ClassVisitor类,所以第三步的原理和第二步的原理是一样的。不过,ClassWriter类是一个特殊的ClassVisitor类,它的主要作用是得到一个符合ClassFile结构的字节数组(byte[])。
执行顺序
对于Class Transformation来说,它具体的代码执行顺序如下图所示:
执行顺序为了方便理解代码的执行顺序,我们也结合生活当中的一些经验,来进行这样的类比:在生活当中,天下降下了雨水,这些雨水会慢慢的向下ssR,穿过不同的地层,最后形成地下水;这种“雨水向下ssR,穿过不同地层”的形式,它与多个ClassVisitor对象调用同名的visitXxx()方法是非常相似的。
顺序示意图- 第一步,可以将ClassReader类想像成最上面的土层
- 第二步,可以将ClassWriter类想像成最下面的土层
- 第三步,可以将ClassVisitor类想像成中间的多个土层
当调用visitXxx()方法的时,就像水在多个土层之间ssR:由最上面的ClassReader“土层”开始,经历一个一个的ClassVisitor中间“土层”,最后进入最下面的ClassWriter“土层”。
执行顺序的代码演示
为了查看代码的执行顺序,我们可以添加一个自定义的ClassVisitor类。在这个类当中,我们可以添加一些打印语句,将类名、方法名以及方法的接收的参数都打印出来,这样我们就能知道代码的执行过程了。
首先,我们来定义一个InfoClassVisitor类,它继承自ClassVisitor;在InfoClassVisitor类当中,我们只打印了visit()、visitField()、visitMethod()和visitEnd()这4个方法的信息。
- 在visitField()方法中,我们自定义了一个InfoFieldVisitor对象
- 在visitMethod()方法中,我们也自定义了一个InfoMethodVisitor对象
另外,在InfoClassVisitor类当中,也定义了一个getAccess()方法,为了简便,我们只判断了public、protected和private标识符。大家可以根据自己的兴趣,来对这个类进行扩展。
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class InfoClassVisitor extends ClassVisitor {
public InfoClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
String line = String.format("ClassVisitor.visit(%d, %s, %s, %s, %s, %s);",
version, getAccess(access), name, signature, superName, Arrays.toString(interfaces));
System.out.println(line);
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
String line = String.format("ClassVisitor.visitField(%s, %s, %s, %s, %s);",
getAccess(access), name, descriptor, signature, value);
System.out.println(line);
FieldVisitor fv = super.visitField(access, name, descriptor, signature, value);
return new InfoFieldVisitor(api, fv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
String line = String.format("ClassVisitor.visitMethod(%s, %s, %s, %s, %s);",
getAccess(access), name, descriptor, signature, exceptions);
System.out.println(line);
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new InfoMethodVisitor(api, mv);
}
@Override
public void visitEnd() {
String line = String.format("ClassVisitor.visitEnd();");
System.out.println(line);
super.visitEnd();
}
private String getAccess(int access) {
List<String> list = new ArrayList<>();
if ((access & Opcodes.ACC_PUBLIC) != 0) {
list.add("ACC_PUBLIC");
}
else if ((access & Opcodes.ACC_PROTECTED) != 0) {
list.add("ACC_PROTECTED");
}
else if ((access & Opcodes.ACC_PRIVATE) != 0) {
list.add("ACC_PRIVATE");
}
return list.toString();
}
}
接着,是我们的InfoFieldVisitor类,它继承自FieldVisitor类。这个类很简单,它只打印其中的visitEnd()方法。
import org.objectweb.asm.FieldVisitor;
public class InfoFieldVisitor extends FieldVisitor {
public InfoFieldVisitor(int api, FieldVisitor fieldVisitor) {
super(api, fieldVisitor);
}
@Override
public void visitEnd() {
String line = String.format(" FieldVisitor.visitEnd();");
System.out.println(line);
super.visitEnd();
}
}
再接下来,是我们的InfoMethodVisitor类,它继承自MethodVisitor类。在这个类当中,我们打印的方法比较多,但是想将这些方法呈现为4个类型:
- visitCode()
- visitXxxInsn()
- visitMaxs()
- visitEnd()
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.util.Printer;
public class InfoMethodVisitor extends MethodVisitor {
public InfoMethodVisitor(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitCode() {
String line = String.format(" MethodVisitor.visitCode();");
System.out.println(line);
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
String line = String.format(" MethodVisitor.visitInsn(%s);", Printer.OPCODES[opcode]);
System.out.println(line);
super.visitInsn(opcode);
}
@Override
public void visitIntInsn(int opcode, int operand) {
String line = String.format(" MethodVisitor.visitIntInsn(%s, %s);", Printer.OPCODES[opcode], operand);
System.out.println(line);
super.visitIntInsn(opcode, operand);
}
@Override
public void visitVarInsn(int opcode, int var) {
String line = String.format(" MethodVisitor.visitVarInsn(%s, %s);", Printer.OPCODES[opcode], var);
System.out.println(line);
super.visitVarInsn(opcode, var);
}
@Override
public void visitTypeInsn(int opcode, String type) {
String line = String.format(" MethodVisitor.visitTypeInsn(%s, %s);", Printer.OPCODES[opcode], type);
System.out.println(line);
super.visitTypeInsn(opcode, type);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
String line = String.format(" MethodVisitor.visitFieldInsn(%s, %s, %s, %s);",
Printer.OPCODES[opcode], owner, name, descriptor);
System.out.println(line);
super.visitFieldInsn(opcode, owner, name, descriptor);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
String line = String.format(" MethodVisitor.visitMethodInsn(%s, %s, %s, %s, %s);",
Printer.OPCODES[opcode], owner, name, descriptor, isInterface);
System.out.println(line);
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
@Override
public void visitJumpInsn(int opcode, Label label) {
String line = String.format(" MethodVisitor.visitJumpInsn(%s, %s);", Printer.OPCODES[opcode], label);
System.out.println(line);
super.visitJumpInsn(opcode, label);
}
@Override
public void visitLabel(Label label) {
String line = String.format(" MethodVisitor.visitLabel(%s);", label);
System.out.println(line);
super.visitLabel(label);
}
@Override
public void visitLdcInsn(Object value) {
String line = String.format(" MethodVisitor.visitLdcInsn(%s);", value);
System.out.println(line);
super.visitLdcInsn(value);
}
@Override
public void visitIincInsn(int var, int increment) {
String line = String.format(" MethodVisitor.visitIincInsn(%s, %s);", var, increment);
System.out.println(line);
super.visitIincInsn(var, increment);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
String line = String.format(" MethodVisitor.visitMaxs(%s, %s);", maxStack, maxLocals);
System.out.println(line);
super.visitMaxs(maxStack, maxLocals);
}
@Override
public void visitEnd() {
String line = String.format(" MethodVisitor.visitEnd();");
System.out.println(line);
super.visitEnd();
}
}
现在,准备工作已经做好了,我们只需要将自定义的InfoClassVisitor类加入到Class Transformation的过程中就可以了:
import lsieun.core.info.InfoClassVisitor;
import lsieun.utils.FileUtils;
import org.objectweb.asm.*;
public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);
//(1)构建ClassReader
ClassReader cr = new ClassReader(bytes1);
//(2)构建ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//(3)串连ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new InfoClassVisitor(api, cw);
//(4)结合ClassReader和ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
//(5)生成byte[]
byte[] bytes2 = cw.toByteArray();
FileUtils.writeBytes(filepath, bytes2);
}
}
准备一个示例代码:
public class HelloWorld {
public int intValue;
public String strValue;
public int add(int a, int b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
}
输出结果:
ClassVisitor.visit(52, [ACC_PUBLIC], sample/HelloWorld, null, java/lang/Object, []);
ClassVisitor.visitField([ACC_PUBLIC], intValue, I, null, null);
FieldVisitor.visitEnd();
ClassVisitor.visitField([ACC_PUBLIC], strValue, Ljava/lang/String;, null, null);
FieldVisitor.visitEnd();
ClassVisitor.visitMethod([ACC_PUBLIC], <init>, ()V, null, null);
MethodVisitor.visitCode();
MethodVisitor.visitVarInsn(ALOAD, 0);
MethodVisitor.visitMethodInsn(INVOKESPECIAL, java/lang/Object, <init>, ()V, false);
MethodVisitor.visitInsn(RETURN);
MethodVisitor.visitMaxs(1, 1);
MethodVisitor.visitEnd();
ClassVisitor.visitMethod([ACC_PUBLIC], add, (II)I, null, null);
MethodVisitor.visitCode();
MethodVisitor.visitVarInsn(ILOAD, 1);
MethodVisitor.visitVarInsn(ILOAD, 2);
MethodVisitor.visitInsn(IADD);
MethodVisitor.visitInsn(IRETURN);
MethodVisitor.visitMaxs(2, 3);
MethodVisitor.visitEnd();
ClassVisitor.visitMethod([ACC_PUBLIC], sub, (II)I, null, null);
MethodVisitor.visitCode();
MethodVisitor.visitVarInsn(ILOAD, 1);
MethodVisitor.visitVarInsn(ILOAD, 2);
MethodVisitor.visitInsn(ISUB);
MethodVisitor.visitInsn(IRETURN);
MethodVisitor.visitMaxs(2, 3);
MethodVisitor.visitEnd();
ClassVisitor.visitEnd();
串联的Field/MethodVisitors
经过上面内容的讲解,相信大家已经了解到多个ClassVisitor之间是相互连接的,或者说是串联到一起的。
classvistor串联示意图其实,还有一些“细微之处”的连接,我们也要注意到:不同ClassVisitor对象里,对应同一个字段的多个FieldVisitor对象,也是串联到一起;不同ClassVisitor对象里,对应同一个方法的多个MethodVisitor对象,也是串联到一起,如下图所示。
methodvistor串联示意图我们在讲删除“字段”和删除“方法”的时候,就是其中的某一个FieldVisitor或MethodVisitor不工作了,也就是不向后“传递数据”了,那么,相应的“字段”和“方法”就丢失了,就达到了“删除”的效果。类似的,添加“字段”和“方法”,其实就是“传递了额外的数据”,那么就会出现新的字段和方法,就达到了添加字段和方法的效果。
Class Transformation的本质
对于Class Transformation来说,它的本质就是“中间人公(攻)鸡(击)”(Man-in-the-middle attack)。
在 Wiki当中,是这样描述Man-in-the-middle attack的:
In cryptography and computer security, a man-in-the-middle(MITM) attack is a cyberattack where the attacker secretly relays and possibly alters the communications between two parties who believe that they are directly communicating with each other.
在计算机安全领域,我们应该尽量的避免遭受到“中间人公(攻)鸡(击)”,这样我们的信息就不会被窃取和篡改。但是,在Java ASM当中,Class Transformation的本质就是利用了“中间人公(攻)鸡(击)”的方式来实现对已有的Class文件进行修改或转换。
更详细的来说,我们自己定义的ClassVisitor类就是一个“中间人”,那么这个“中间人”可以做什么呢?可以做三种类型的事情:
- 对“原有的信息”进行篡改,就可以实现“修改”的效果。对应到ASM代码层面,就是对ClassVisitor.visitXxx()和MethodVisitor.visitXxx()的参数值进行修改。
- 对“原有的信息”进行扔掉,就可以实现“删除”的效果。对应到ASM代码层面,将原本的FieldVisitor和MethodVisitor对象实例替换成null值,或者对原本的一些ClassVisitor.visitXxx()和MethodVisitor.visitXxx()方法不去调用了。
- 伪造一条“新的信息”,就可以实现“添加”的效果。对应到ASM代码层面,就是在原来的基础上,添加一些对于ClassVisitor.visitXxx()和MethodVisitor.visitXxx()方法的调用。
小结
本文主要对Class Transformation的工作原理进行介绍,内容总结如下:
- 第一点,在代码开始执行之前,ClassReader、ClassVisitor和ClassWriter这三者之间是如何建立最初的联系的。例如,ClassReader与ClassVisitor建立联系是通过ClassReader.accept()方法来实现的;而ClassVisitor与ClassWriter建立联系是通过ClassVisitor的构造方法来建立联系的。
- 第二点,在代码的执行过程中,其中涉及到ClassVisitor.visitXxx()和MethodVisitor.visitXxx()方法的执行顺序是什么样的。
- 第三点,在进行Class Transformation的过程中,内在的多个FieldVisitor和MethodVisitor是串联到一起的。
- 第四点,Class Transformation的本质就是“中间人公(攻)鸡(击)”(Man-in-the-middle attack)。