前言 Java 源文件( .java )在经过编译后会变成 class 文件( .class ), class 文件有着固定的二进制格式,即字节码。字节码是一套设计用来在 Java 虚拟机中执行的高度优化的指令集,由十六进制值组成, JVM 以两个十六进制为一组,按照字节为单位进行读取。
Java源文件的编译解析流程如下图所示。
字节码结构 JVM对于字节码有着严格规范要求,每个字节码文件都要由十部分按照规定的顺序组成,分别为魔数(magic)、版本号(version)、常量池(constant pool)、访问标记(access flag)、类索引(this class)、超类索引(super class)、接口表索引(interface)、字段表(fields)、方法表(methods)和属性表(attributes)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
魔数 所有的class文件的前四个字节都是魔数,固定值为0xCAFEBABE,JVM可以根据文件的开头来判断这个文件是否可能是一个class文件,判断通过后才会进行后续的操作。
版本号 魔数之后的四个字节为版本号,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。例如00 00 00 34中次版本号转换成十进制为0,主版本号转换成十进制为52,所以编译该文件的JDK版本号为1.8。
常量池 版本号后的字节为常量池,常量池中存储着两类常量,分别是字面量与符号引用。字面量为代码中声明为 Final 的常量值,符号引用为类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。
常量池分为两部分,分别为常量池计数器和常量池数据区。
常量池计数器(constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。例如字节码前10个字节为CA FE BA BE 00 00 00 34 00 24,将十六进制的24转化为十进制值为36,排除掉下标0,也就是说,这个类文件中共有35个常量。
常量池数据区:数据区是由(constant_pool_count-1)个cp_info结构组成,一个cp_info结构对应一个常量。在字节码中共有14种类型的cp_info,每种类型的结构都是固定的。
访问标记 常量池结束之后的两个字节为访问标记,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述,例如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC|ACC_FINAL,即0x0001|0x0010=0x0011。
类索引 访问标志后的两个字节为当前类名,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引位置,根据索引位置就能在常量池中找到这个类的全限定名。
超类索引 类索引后的两个字节为超类索引,描述父类的全限定名,与类索引一样,超类索引保存的也是常量池中的索引值。
接口表索引 超类索引后的两个字节为的接口索引表,描述了该类或父类实现的接口数量,紧接着的n个字节是所有接口名称的字符串常量的索引值。
字段表 字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。
字段的访问标记共有9种,例如public static final int DEFAULT_NUM = 0,其访问标记的值为0x0019,由ACC_PUBLIC|ACC_STATIC|ACC_FINAL组成。
对于第三部分的字段描述符,其用来表示某个字段的类型,在JVM中,根据字段类型的不同,字段描述符主要分为以下几种:
基本数据类型:byte-B、char-C、double-D、float-F、int-I、long-J、short-S、boolean-Z
特殊类型:void-V
对象类型:描述符用字符L;加上对象的全限定名来表示,为了防止多个连续的引用类型描述符出现混淆,引用类型描述符最后都加了一个;作为结束,比如字符串类型String的描述符为Ljava/lang/String;
数组类型:JVM使用一个前置的[来表示数组类型,例如,int[]类型的描述符为[I,字符串数组String[]的描述符为[Ljava/lang/String;,每增加一个维度则在对应的字段描述符前增加一个[,比如,Object[][][]类型的描述符为[[[Ljava/lang/Object;
方法表 字段表结束后的为方法表,方法表也是由两部分组成,第一部分为两个字节,描述方法的个数;第二部分为每个方法的详细信息methods_info。方法的详细信息较为复杂,包括访问标记、方法名索引、方法描述符索引以及方法的属性。
方法的访问标记共有12种,例如private static synchronized void demo,其访问标记的值为0x002a,由ACC_PRIVATE|ACC_STATIC|ACC_SYNCHRONIZED组成。
对于第三部分的字段描述符,其格式如下:(参数1类型 参数2类型 参数3类型 … )返回值类型,例如,方法Object demo(int i, double d, String s)的描述符为(IDLjava/lang/String;)Ljava/lang/Object;。
属性表 属性表是class文件的最后一部分内容,属性出现的地方比较广泛,除了字段和方法中,在顶层的class文件中也会出现,该项存放了在该文件中类或接口所定义属性的基本信息。
字节码解析 javap 在Java中内置一个反编译命令javap,使用javap可以反编译class文件,将二进制格式的字节码转换成易于理解的格式,从而来查看一个类的结构,包括构造方法、方法、字段等。
1 2 3 javap <options> <classes> javap -c -l -v Bytecode.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ❯ javap -help 用法: javap <options> <classes> 其中, 可能的选项包括: -help --help -? 输出此用法消息 -version 版本信息 -v -verbose 输出附加信息 -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示所有类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的 系统信息 (路径, 大小, 日期, MD5 散列) -constants 显示最终常量 -classpath <path> 指定查找用户类文件的位置 -cp <path> 指定查找用户类文件的位置 -bootclasspath <path> 覆盖引导类文件的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 ❯ javap -c -l -v Bytecode.class Classfile /Users/alphag0/Desktop/Bytecode.class Last modified 2024-2-16; size 487 bytes MD5 checksum f48d6d0524598aa13585226cdb2ff798 Compiled from "Bytecode.java" public class Bytecode minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: { public int age; descriptor: I flags: ACC_PUBLIC public Bytecode(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial 4: aload_0 5: ldc 7: putfield 10: aload_0 11: bipush 21 13: putfield 16: return LineNumberTable: line 1: 0 line 2: 4 line 3: 10 public int add(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=1 0: aload_0 1: getfield 4: iconst_1 5: iadd 6: istore_1 7: getstatic 10: iload_1 11: invokevirtual 14: iload_1 15: ireturn LineNumberTable: line 6: 0 line 7: 7 line 8: 14 } SourceFile: "Bytecode.java"
jclasslib 相较于javap命令,jclasslib作为一个插件,能够可视化呈现反编译结果,同时还提供了修改jar包中的class文件的API。
字节码增强 ASM Java字节码库允许我们通过字节码库的API动态创建或修改Java类、方法、变量等操作而被广泛使用。ASM是一种通用Java字节码操作和分析框架,它可以直接以二进制形式修改一个现有的类或动态生成类文件。
ASM字节码处理流程:目标类class bytes->ClassReader解析->ClassVisitor增强修改字节码->ClassWriter生成增强后的class bytes->通过Instrumentation解析加载为新的字节码。
ASM框架提供了三个基于ClassVisitor API的核心API,用于生成和转换类:
ClassReader:用于解析class文件或二进制流,并将所有字节码传递给ClassWriter
ClassVisitor:抽象类,负责访问class文件的各个元素,可以解析或者修改class文件的内容,自定义ClassVisitor重写visitXXX方法,可获取捕获ASM类结构访问的所有事件
ClassWriter:是ClassVisitor的子类,用于生成类二进制
ClassReader&ClassVisitor&ClassWriter ClassReader类用于解析类字节码,创建ClassReader对象可传入类名、类字节码数组或者类输入流对象。创建完ClassReader对象会触发字节码解析(解析class基础信息,如常量池、接口信息等),可以直接通过ClassReader对象获取类的基础信息。
调用ClassReader类的accpet方法需要传入自定义的ClassVisitor对象,ClassReader会按照如下顺序,依次调用该ClassVisitor的类方法。
1 2 3 4 5 visit [ visitSource ] [ visitModule ][ visitNestHost ][ visitPermittedclass ][ visitOuterClass ] ( visitAnnotation | visitTypeAnnotation | visitAttribute )* ( visitNestMember | visitInnerClass | visitRecordComponent | visitField | visitMethod )* visitEnd
ClassWriter直接以二进制形式生成编译后的类,他会生成一个字节数组形式的输出,其中包含了已编译的类,可以调用toByteArray方法来提取。
读取和解析字节码 ClassReader读取字节码 1 2 3 4 5 6 7 8 9 10 11 byte [] bytecode = Files.readAllBytes(Paths.get("path/to/MyClass.class" )); FileInputStream bytecode = new FileInputStream ("path/to/MyClass.class" ); InputStream is = getClass().getClassLoader().getResourceAsStream("com/example/MyClass.class" );byte [] bytecode = is.readAllBytes();ClassReader classReader = new ClassReader (bytecode);
ClassVisitor解析字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package org.example.asm.demo;import java.io.Serializable;public class BytecodeTest implements Serializable { private String name = "H3rmesk1t" ; private int age = 22 ; public int getAge () { return age; } public void setAge (int age) { this .age = age; } public String getName () { return name; } public void setName (String name) { this .name = name; } public void process () { System.out.println("Name: " + name); System.out.println("Age: " + age); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package org.example.asm.demo;import org.objectweb.asm.*;import java.util.Arrays;public class ASMClassVisitor extends ClassVisitor { protected ASMClassVisitor () { super (Opcodes.ASM5); } @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 MethodVisitor visitMethod (int access, String name, String descriptor, String signature, String[] exceptions) { System.out.println("变量修饰符: " + access + "\t 变量名称: " + name + "\t 描述符: " + descriptor + "\t 抛出的异常: " + Arrays.toString(exceptions)); return super .visitMethod(access, name, descriptor, signature, exceptions); } @Override public FieldVisitor visitField (int access, String name, String descriptor, String signature, Object value) { System.out.println("变量修饰符:" + access + "\t 变量名称:" + name + "\t 描述符:" + descriptor + "\t 默认值:" + value); return super .visitField(access, name, descriptor, signature, value); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package org.example.asm.demo;import org.objectweb.asm.ClassReader;import java.io.FileInputStream;import java.util.Arrays;public class ASMMain { public static void main (String[] args) throws Exception { ASMClassVisitor asmClassVisitor = new ASMClassVisitor (); FileInputStream inputStream = new FileInputStream ("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/asm/demo/BytecodeTest.class" ); ClassReader classReader = new ClassReader (inputStream); System.out.println("解析类名: " + classReader.getClassName() + ",父类: " + classReader.getSuperName() + ",实现接口: " + Arrays.toString(classReader.getInterfaces())); System.out.println("-----------------------------------------------------------------------------" ); classReader.accept(asmClassVisitor, 0 ); } }
修改字节码 使用 ClassWriter 可以实现类修改功能,使用 ASM 修改类字节码时如果插入了新的局部变量、字节码,需要重新计算 max_stack 和 max_locals ,否则会导致修改后的类文件无法通过 JVM 校验。 ASM 提供了内置的自动计算方式,只需在创建 ClassWriter 的时候传入 COMPUTE_FRAMES 即可: new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); 。
添加&删除Field
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package org.example.asm.demo;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.FieldVisitor;import org.objectweb.asm.Opcodes;public class ASMUpdateFieldClassVisitor extends ClassVisitor { private String deleteFieldName; private int addFieldAccess; private String addFieldName; private String addFieldDesc; private boolean flag = false ; protected ASMUpdateFieldClassVisitor (ClassVisitor classVisitor, String deleteFieldName, int addFieldAccess, String addFieldName, String addFieldDesc) { super (Opcodes.ASM5, classVisitor); this .deleteFieldName = deleteFieldName; this .addFieldAccess = addFieldAccess; this .addFieldName = addFieldName; this .addFieldDesc = addFieldDesc; } @Override public FieldVisitor visitField (int access, String name, String descriptor, String signature, Object value) { if (name.equals(deleteFieldName)) { return null ; } if (name.equals(addFieldName)) { flag = true ; } return super .visitField(access, name, descriptor, signature, value); } @Override public void visitEnd () { if (!flag) { FieldVisitor fieldVisitor = super .visitField(addFieldAccess, addFieldName, addFieldDesc, null , null ); if (fieldVisitor != null ) { fieldVisitor.visitEnd(); } } super .visitEnd(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package org.example.asm.demo;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Opcodes;import java.io.FileInputStream;import java.io.FileOutputStream;import java.util.Arrays;public class ASMUpdateFieldMain { public static void main (String[] args) throws Exception { FileInputStream inputStream = new FileInputStream ("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/asm/demo/BytecodeTest.class" ); ClassReader classReader = new ClassReader (inputStream); ClassWriter classWriter = new ClassWriter (classReader, ClassWriter.COMPUTE_FRAMES); ASMUpdateFieldClassVisitor asmUpdateFieldClassVisitor = new ASMUpdateFieldClassVisitor (classWriter, "age" , Opcodes.ACC_PRIVATE, "sex" , "Ljava/lang/String;" ); classReader.accept(asmUpdateFieldClassVisitor, ClassReader.EXPAND_FRAMES); FileOutputStream fileOutputStream = new FileOutputStream ("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/asm/demo/BytecodeUpdateFieldTest.class" ); byte [] updateByte = classWriter.toByteArray(); fileOutputStream.write(updateByte); fileOutputStream.close(); ASMClassVisitor asmClassVisitor = new ASMClassVisitor (); ClassReader reader = new ClassReader (updateByte); System.out.println("解析类名: " + reader.getClassName() + ",父类: " + reader.getSuperName() + ",实现接口: " + Arrays.toString(reader.getInterfaces())); System.out.println("-----------------------------------------------------------------------------" ); reader.accept(asmClassVisitor, 0 ); } }
添加&删除Method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package org.example.asm.demo;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.MethodVisitor;import org.objectweb.asm.Opcodes;public class ASMUpdateMethodClassVisitor extends ClassVisitor { private String deleteMethodName; private String deleteMethodDesc; private int addMethodAccess; private String addMethodName; private String addMethodDesc; private boolean flag = false ; protected ASMUpdateMethodClassVisitor (ClassVisitor classVisitor, String deleteMethodName, String deleteMethodDesc, int addMethodAccess, String addMethodName, String addMethodDesc) { super (Opcodes.ASM5, classVisitor); this .deleteMethodName = deleteMethodName; this .deleteMethodDesc = deleteMethodDesc; this .addMethodAccess = addMethodAccess; this .addMethodName = addMethodName; this .addMethodDesc = addMethodDesc; } @Override public MethodVisitor visitMethod (int access, String name, String descriptor, String signature, String[] exceptions) { if (name.equals(deleteMethodName) && descriptor.equals(deleteMethodDesc)) { return null ; } if (name.equals(addMethodName) && name.equals(addMethodDesc)) { flag = true ; } return super .visitMethod(access, name, descriptor, signature, exceptions); } @Override public void visitEnd () { if (!flag) { MethodVisitor methodVisitor = super .visitMethod(addMethodAccess, addMethodName, addMethodDesc, null , null ); if (methodVisitor != null ) { methodVisitor.visitCode(); methodVisitor.visitInsn(Opcodes.RETURN); methodVisitor.visitMaxs(0 , 0 ); methodVisitor.visitEnd(); } } super .visitEnd(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package org.example.asm.demo;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Opcodes;import java.io.FileInputStream;import java.io.FileOutputStream;import java.util.Arrays;public class ASMUpdateMethodMain { public static void main (String[] args) throws Exception { FileInputStream inputStream = new FileInputStream ("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/asm/demo/BytecodeTest.class" ); ClassReader classReader = new ClassReader (inputStream); ClassWriter classWriter = new ClassWriter (classReader, ClassWriter.COMPUTE_FRAMES); ASMUpdateMethodClassVisitor asmUpdateMethodClassVisitor = new ASMUpdateMethodClassVisitor (classWriter, "process" , "()V" , Opcodes.ACC_PUBLIC, "newMethod" , "()V" ); classReader.accept(asmUpdateMethodClassVisitor, ClassReader.EXPAND_FRAMES); FileOutputStream fileOutputStream = new FileOutputStream ("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/asm/demo/BytecodeUpdateMethodTest.class" ); byte [] updateByte = classWriter.toByteArray(); fileOutputStream.write(updateByte); fileOutputStream.close(); ASMClassVisitor asmClassVisitor = new ASMClassVisitor (); ClassReader reader = new ClassReader (updateByte); System.out.println("解析类名: " + reader.getClassName() + ",父类: " + reader.getSuperName() + ",实现接口: " + Arrays.toString(reader.getInterfaces())); System.out.println("-----------------------------------------------------------------------------" ); reader.accept(asmClassVisitor, 0 ); } }
修改方法指令 大多数使用 ASM 库的目的其实是修改类方法的字节码,在原方法执行的前后动态插入新的 Java 代码,从而实现类似于 AOP 的功能。修改类方法字节码的典型应用场景如: APM 和 RASP ; APM 需要统计和分析每个类方法的执行时间,而 RASP 需要在 Java 底层 API 方法执行之前插入自身的检测代码,从而实现动态拦截恶意攻击。
假设我们需要修改 org.example.asm.demo .BytecodeTest 类的 process 方法,实现以下两个需求:
在原业务逻辑执行前打印出该方法的参数值
修改该方法的返回值
1 2 3 4 5 public String process (String name) { String str = "Hello: " ; return str + name; }
1 2 3 4 5 6 7 8 9 public String process (String name) { System.out.println(var1); String var2 = "RASP is so cool!" ; String str = "Hello: " ; String var4 = str + name; System.out.println(var4); return var2; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package org.example.asm.demo;import org.objectweb.asm.commons.AdviceAdapter;import org.objectweb.asm.*;import java.io.FileInputStream;import java.io.FileOutputStream;import java.util.Arrays;public class ASMModMethodAdapterMain { public static void main (String[] args) { try { FileInputStream inputStream = new FileInputStream ("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/asm/demo/BytecodeTest.class" ); ClassReader classReader = new ClassReader (inputStream); ClassWriter classWriter = new ClassWriter (classReader, ClassWriter.COMPUTE_FRAMES); classReader.accept(new ClassVisitor (Opcodes.ASM9, classWriter) { @Override public MethodVisitor visitMethod (int access, String name, String desc, String signature, String[] exceptions) { if (name.equals("process" )) { MethodVisitor mv = super .visitMethod(access, name, desc, signature, exceptions); return new AdviceAdapter (api, mv, access, name, desc) { int newArgIndex; private final Type stringType = Type.getType(String.class); @Override protected void onMethodEnter () { 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 ); newArgIndex = newLocal(stringType); mv.visitLdcInsn("RASP is so cool!" ); storeLocal(newArgIndex, stringType); } @Override protected void onMethodExit (int opcode) { dup(); int returnValueIndex = newLocal(stringType); storeLocal(returnValueIndex, stringType); 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 ); loadLocal(newArgIndex); mv.visitInsn(ARETURN); } }; } return super .visitMethod(access, name, desc, signature, exceptions); } }, ClassReader.EXPAND_FRAMES); FileOutputStream fileOutputStream = new FileOutputStream ("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/asm/demo/BytecodeModMethodAdapterTest.class" ); byte [] updateByte = classWriter.toByteArray(); fileOutputStream.write(updateByte); fileOutputStream.close(); ASMClassVisitor asmClassVisitor = new ASMClassVisitor (); ClassReader reader = new ClassReader (updateByte); System.out.println("解析类名: " + reader.getClassName() + ",父类: " + reader.getSuperName() + ",实现接口: " + Arrays.toString(reader.getInterfaces())); System.out.println("-----------------------------------------------------------------------------" ); reader.accept(asmClassVisitor, 0 ); } catch (Exception e) { e.printStackTrace(); } } }
动态生成字节码 可以使用 ClassWriter 来动态创建出一个 Java 类的二进制文件,然后通过自定义的类加载器就可以将动态生成的类加载到 JVM 中。
1 2 3 4 5 6 7 8 9 package org.example.asm.demo;public class BytecodeCalculatorTest { public static void execute () { Runtime.getRuntime().exec("open -a Calculator" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package org.example.asm.demo;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.MethodVisitor;import org.objectweb.asm.Opcodes;public class ASMBytecodeCalculatorMain { private static final String CLASS_NAME = "org.example.asm.demo.BytecodeCalculatorTest" ; private static final String CLASS_NAME_ASM = "org/example/asm/demo/BytecodeCalculatorTest" ; public static byte [] dump() { ClassWriter cw = new ClassWriter (0 ); MethodVisitor mv; cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, CLASS_NAME_ASM, null , "java/lang/Object" , null ); cw.visitSource("BytecodeCalculatorTest.java" , null ); MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>" , "()V" , null , null ); constructor.visitCode(); constructor.visitVarInsn(Opcodes.ALOAD, 0 ); constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object" , "<init>" , "()V" , false ); constructor.visitInsn(Opcodes.RETURN); constructor.visitMaxs(1 , 1 ); constructor.visitEnd(); MethodVisitor executeMethod = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "execute" , "()V" , null , null ); executeMethod.visitCode(); executeMethod.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Runtime" , "getRuntime" , "()Ljava/lang/Runtime;" , false ); executeMethod.visitLdcInsn("open -a Calculator" ); executeMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Runtime" , "exec" , "(Ljava/lang/String;)Ljava/lang/Process;" , false ); executeMethod.visitInsn(Opcodes.POP); executeMethod.visitInsn(Opcodes.RETURN); executeMethod.visitMaxs(2 , 0 ); executeMethod.visitEnd(); cw.visitEnd(); return cw.toByteArray(); } public static void main (String[] args) throws Exception { final byte [] classBytes = dump(); ClassLoader classLoader = new ClassLoader (ASMBytecodeCalculatorMain.class.getClassLoader()) { @Override protected Class<?> findClass(String name) { try { return super .findClass(name); } catch (ClassNotFoundException e) { return defineClass(CLASS_NAME, classBytes, 0 , classBytes.length); } } }; classLoader.loadClass(CLASS_NAME).getMethod("execute" ).invoke(null ); } }
Javassist Javassist是一个开源的分析、编辑和创建Java字节码的类库;相比ASM,Javassist提供了更加简单便捷的API,使用Javassist可以像写Java代码一样直接插入Java代码片段,不再需要关注Java底层的字节码的和栈操作,仅需要学会如何使用Javassist的API即可实现字节码编辑。官方教程:Getting Started with 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类型
生成字节码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package org.example.bytecode.javassist.demo;import javassist.*;import java.io.FileOutputStream;public class JavassistTest { public static void main (String[] args) { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("org.example.bytecode.javassist.demo.JavassistCalculatorTest" ); try { CtField ctField = new CtField (classPool.get("java.lang.String" ), "name" , ctClass); ctField.setModifiers(Modifier.PRIVATE); ctClass.addField(ctField, CtField.Initializer.constant("H3rmesk1t" )); ctClass.addMethod(CtNewMethod.getter("getName" , ctField)); ctClass.addMethod(CtNewMethod.setter("setName" , ctField)); CtConstructor ctConstructor = new CtConstructor (new CtClass []{}, ctClass); ctConstructor.setBody("{name = \"AlphaG0\";}" ); ctClass.addConstructor(ctConstructor); CtConstructor ctConstructor1 = new CtConstructor (new CtClass []{classPool.get("java.lang.String" )}, ctClass); ctConstructor1.setBody("{$0.name = $1;}" ); ctClass.addConstructor(ctConstructor1); CtMethod ctMethod = CtMethod.make("public static void main(String[] args) { Runtime.getRuntime().exec(\"open -a Calculator\");}" , ctClass); ctClass.addMethod(ctMethod); FileOutputStream fileOutputStream = new FileOutputStream ("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/bytecode/javassist/JavassistCalculatorTest.class" ); byte [] updateByte = ctClass.toBytecode(); fileOutputStream.write(updateByte); fileOutputStream.close(); } catch (Exception e) { e.printStackTrace(); } } }
读取类&成员变量&方法信息 Javassist 读取类信息非常简单,使用 ClassPool 对象获取到 CtClass 对象后就可以像使用 Java 反射 API 一样去读取类信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package org.example.bytecode.javassist.demo;import javassist.*;import java.util.Arrays;public class JavassistClassAccessTest { public static void main (String[] args) { ClassPool classPool = ClassPool.getDefault(); try { CtClass ctClass = classPool.get("org.example.bytecode.javassist.demo.JavassistCalculatorTest" ); System.out.println("解析类名: " + ctClass.getName() + "\t 父类: " + ctClass.getSuperclass().getName() + "\t 实现接口: " + 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 (Exception e) { e.printStackTrace(); } } }
修改类方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package org.example.bytecode.javassist.demo;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.reflect.Method;public class JavassistClassModifyTest { public static void main (String[] args) { ClassPool classPool = ClassPool.getDefault(); try { CtClass ctClass = classPool.get("org.example.bytecode.javassist.demo.JavassistCalculatorTest" ); CtMethod mainMethod = ctClass.getDeclaredMethod("main" , new CtClass []{classPool.get("java.lang.String[]" )}); mainMethod.insertBefore("System.out.println(\"Start...\");" ); mainMethod.insertAfter("System.out.println(\"End...\");" ); Object o = ctClass.toClass().newInstance(); Method main = o.getClass().getMethod("main" , String[].class); main.invoke(o, (Object) new String []{}); } catch (Exception e) { e.printStackTrace(); } } }