Java字节码

前言

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:
#1 = Methodref #8.#21 // java/lang/Object."<init>":()V
#2 = String #22 // H3rmesk1t
#3 = Fieldref #7.#23 // Bytecode.name:Ljava/lang/String;
#4 = Fieldref #7.#24 // Bytecode.age:I
#5 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #27.#28 // java/io/PrintStream.println:(I)V
#7 = Class #29 // Bytecode
#8 = Class #30 // java/lang/Object
#9 = Utf8 name
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 age
#12 = Utf8 I
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 add
#18 = Utf8 ()I
#19 = Utf8 SourceFile
#20 = Utf8 Bytecode.java
#21 = NameAndType #13:#14 // "<init>":()V
#22 = Utf8 H3rmesk1t
#23 = NameAndType #9:#10 // name:Ljava/lang/String;
#24 = NameAndType #11:#12 // age:I
#25 = Class #31 // java/lang/System
#26 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#27 = Class #34 // java/io/PrintStream
#28 = NameAndType #35:#36 // println:(I)V
#29 = Utf8 Bytecode
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (I)V
{
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 #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String H3rmesk1t
7: putfield #3 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 21
13: putfield #4 // Field age:I
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 // Field age:I
4: iconst_1
5: iadd
6: istore_1
7: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
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 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 {
// 调用父类构造方法, 使用ASM Opcodes版本
protected ASMClassVisitor() {
super(Opcodes.ASM5);
}

// 重写visit方法, 输出类名
@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);
}

// 重写visitMethod方法, 输出方法名
@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);
}

// 重写visitField, 输出变量名
@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 {
// 实例化自定义Visitor
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_stackmax_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();
// 添加return指令
methodVisitor.visitInsn(Opcodes.RETURN);
// 设置方法的最大操作数栈深度和最大局部变量表大小,空方法设置00即可
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的功能。修改类方法字节码的典型应用场景如:APMRASPAPM需要统计和分析每个类方法的执行时间,而RASP需要在Java底层API方法执行之前插入自身的检测代码,从而实现动态拦截恶意攻击。

假设我们需要修改org.example.asm.demo.BytecodeTest类的process方法,实现以下两个需求:

  1. 在原业务逻辑执行前打印出该方法的参数值
  2. 修改该方法的返回值
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 classReader = new ClassReader(inputStream);
// 创建ClassWriter对象, COMPUTE_FRAMES会自动计算max_stack和max_locals
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);

// 使用自定义的ClassVisitor访问者对象, 访问该类文件的结构
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);

// 创建自定义的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() {
// 输出process方法的第一个参数, 因为process是非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("RASP is so cool!");

// 将"RASP is so cool!"字符串压入到新生成的局部变量中, String var2 = "RASP is so cool!";
storeLocal(newArgIndex, stringType);
}

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

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

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

// 输出process方法的返回值
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);
}

}, 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
// 使用ClassWriter生成类字节码
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用于生成类字节码
ClassWriter cw = new ClassWriter(0);
// 创建MethodVisitor
MethodVisitor mv;

// 创建一个字节码版本为JDK1.8的org.example.asm.demo.BytecodeCalculatorTest类
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();

// 创建execute方法
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();

// 创建自定义类加载器, 加载ASM创建的类字节码到JVM
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);
}
}
};

// 反射调用通过ASM生成的BytecodeCalculatorTest类的execute方法
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 {
// 给ctClass类添加一个string类型的字段为name并初始化该字段
CtField ctField = new CtField(classPool.get("java.lang.String"), "name", ctClass);
ctField.setModifiers(Modifier.PRIVATE);
ctClass.addField(ctField, CtField.Initializer.constant("H3rmesk1t"));

// 生成对应的get/set方法
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);

// 添加Main函数
CtMethod ctMethod = CtMethod.make("public static void main(String[] args) { Runtime.getRuntime().exec(\"open -a Calculator\");}", ctClass);
ctClass.addMethod(ctMethod);

// 写入class文件
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 = 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();
}
}
}