从字节码开始到ASM的gadgetinspector源码解析(一)

Intro

目前在CTF比赛中,对于Java反序列化基本上靠codeql、tabby等工具分析利用链,tabby基于字节码的特性会更准确一些。而gadgetinspector作为一个有些年头的基于ASM对字节码进行分析的自动化反序列化链挖掘工具,虽然在实际场景使用中用到的不算很多,但是经过一些功能上的补足和二开后也提高了一部分的准确率。我们主要通过二开后的gadgetinspector来学习一下作者是如何通过ASM来对字节码进行处理并跟踪污点流进行分析。在分析gadgetInspector之前,我们要先对字节码的相关结构有一些了解,所以我们可以按照字节码的固定架构使用十六进制编辑器查看一下字节码中到底存储了些什么东西。

二开后的GadgetInspector:github.com/threedr3am/...

字节码分析

我们以如下类进行分析:

typescript 复制代码
package com.y1zh3e7.Test;​public class ClassTest {    public static void main(String[] args) {        String sayHello = "Hello World!";    }}

编译后class文件扔到hex编辑器里查看十六进制方便分析:

r 复制代码
CA FE BA BE 00 00 00 34 00 18 0A 00 04 00 14 08 00 15 07 00 16 07 00 17 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 1C 4C 63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74 3B 01 00 04 6D 61 69 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 04 61 72 67 73 01 00 13 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 08 73 61 79 48 65 6C 6C 6F 01 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0E 43 6C 61 73 73 54 65 73 74 2E 6A 61 76 61 0C 00 05 00 06 01 00 0C 48 65 6C 6C 6F 20 57 6F 72 6C 64 21 01 00 1A 63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 03 00 04 00 00 00 00 00 02 00 01 00 05 00 06 00 01 00 07 00 00 00 2F 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 02 00 08 00 00 00 06 00 01 00 00 00 03 00 09 00 00 00 0C 00 01 00 00 00 05 00 0A 00 0B 00 00 00 09 00 0C 00 0D 00 01 00 07 00 00 00 3C 00 01 00 02 00 00 00 04 12 02S 4C B1 00 00 00 02 00 08 00 00 00 0A 00 02 00 00 00 05 00 03 00 06 00 09 00 00 00 16 00 02 00 00 00 04 00 0E 00 0F 00 00 00 03 00 01 00 10 00 11 00 01 00 01 00 12 00 00 00 02 00 13

class文件结构如下

0x01 魔术头 Magic Number-4Byte

class文件的魔术头为四字节并且值固定,可以看到为如下内容,这个十六进制表达还是挺有意思的

复制代码
CA FE BA BE

0x02 版本号 Version-2+2Byte

十六进制对应内容为

复制代码
00 00 00 34

前面的0000为次版本号,后面的0034为主版本号,0x0034对应十进制为52,对应版本为jdk1.8,对应的我IDEA中的jdk版本也是1.8

0x03 常量池 Constant Pool-2+nByte

常量池的2+n指的是两字节的常量数量,加上nByte的常量内容,常量池存储如下内容:

接下来我们继续分析十六进制并以此说明:

首先的两个字节代表常量数量,0x0018转换为十进制为24。这里需要注意的是,常量池的常量索引并不是从0开始而是从1开始,因此24表示常量池中共有23个常量,索引以此为1-23,并且在.class文件中,只有常量池的下标是从0开始,后面的接口、属性、方法等下表依然都是从0开始计数:

复制代码
00 18

CONSTANT-1

根据上面的表格,我们可以发现不论是何种类型的常量,都是以u1(1字节)的tag位作为起始,因此我们向下读取一字节,为第一个常量的tag,为0x0A:

复制代码
0A

0x0A对应十进制10,我们在表格中寻找值为10的索引,可以找到该常量类型为CONSTANT_Methodref_info,并且接下来还分别有两个u2的index,我们继续向下读取两个字节,则对应表格中指向声明方法的类描述符的索引项,这些东西的作用我们到后面就会知道了,先继续往下看

复制代码
00 04

继续向下读取两个字节,对应指向名称及类型描述符索引项,值为20

复制代码
00 14

constant#1:

0x0a:Methodref_info

0x00 04:Class_info索引项#4

0x00 14:NameAndType索引项#20

CONSTANT-2

向下读取1B,即为第二个常量的TAG位,值为08,对应表格中CONSTANT_Fieldref_info,依旧是两个u2的index

constant#2:

0x08:String_info

0x00 15::指向字符串字面量#21

CONSTANT-3

0x07:Class_info

0x00 16:全局限定名常量项索引#22

CONSTANT-4

0x07:Class_info

0x00 17:全局限定名常量项索引#23

CONSTANT-5

0x01:Utf8_info

0x00 06:字符串长度为6

0x3C 69 6E 69 74 3E:字符串<init>

CONSTANT-6

0x01:Utf8-info

0x00 03:字符串长度为3

0x28 29 56:字符串()V

CONSTANT-7

0x01:Utf8-info

0x00 04:字符串长度为4

0x43 6F 64 65:字符串Code

CONSTANT-8

0x01:Utf8-info

0x00 0F:字符串长度为15

0x4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65:字符串LineNumberTable

CONSTANT-9

0x01:Utf8-info

0x00 12:字符串长度为18

0x4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65:字符串LocalVariableTable

CONSTANT-10

0x01:Utf8-info

0x00 04:字符串长度为4

0x74 68 69 73:字符串this

CONSTANT-11

0x01:Utf8-info

0x00 1C:字符串长度为28

0x4C 63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74 3B:字符串Lcom/y1zh3e7/Test/ClassTest;

CONSTANT-12

0x01:Utf8-info

0x00 04:字符串长度为4

0x6D 61 69 6E:字符串main

CONSTANT-13

0x01:Utf8-info

0x00 16:字符串长度为22

0x28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56:字符串([Ljava/lang/String;)V

CONSTANT-14

0x01:Utf8-info

0x00 04:字符串长度为4

0x61 72 67 73:字符串args

CONSTANT-15

0x01:Utf8-info

0x00 13:字符串长度为19

0x5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B:字符串[Ljava/lang/String;

CONSTANT-16

0x01:Utf8-info

0x00 08:字符串长度为8

0x73 61 79 48 65 6C 6C 6F:字符串sayHello

CONSTANT-17

0x01:Utf8-info

0x00 08:字符串长度为18

0x4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B:字符串Ljava/lang/String;

CONSTANT-18

0x01:Utf8-info

0x00 0A:字符串长度为10

0x53 6F 75 72 63 65 46 69 6C 65:字符串SourceFile

CONSTANT-19

0x01:Utf8-info

0x00 0A:字符串长度为14

0x43 6C 61 73 73 54 65 73 74 2E 6A 61 76 61:字符串ClassTest.java

CONSTANT-20

0x0C:NameAndType_info

0x00 05:字段或方法名常量项索引#5

0x00 06:字段或方法描述符常量索引#6

CONSTANT-21

0x01:Utf8-info

0x00 0C:字符串长度为12

0x48 65 6C 6C 6F 20 57 6F 72 6C 64 21:字符串Hello World!

CONSTANT-22

0x01:Utf8-info

0x00 1A:字符串长度为26

0x63 6F 6D 2F 79 31 7A 68 33 65 37 2F 54 65 73 74 2F 43 6C 61 73 73 54 65 73 74:字符串com/y1zh3e7/Test/ClassTest

CONSTANT-23

0x01:Utf8-info

0x00 10:字符串长度为16

0x6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74:字符串java/lang/Object

0x04 访问标志位 Access Flags-2Byte

访问标志位包括一个class文件的属性(如是类还是接口,是否被定义成public,是否是abstract,是否是final)

我们向下读取两个Byte0x0021,代表的是0x0020和0x0001的集合,意思是该类为public,并且继承object(0x06父类索引)

0x05 类索引-2Byte

类索引可以确定类的全局限定名称,我们读取两个字节为0x00 03,对应常量池第三个常量CONSTANT-3,可以发现CONSTANT-3:0x00 16:全局限定名常量项索引#22,所以继续去CONSTANT-22查找对应常量,得到全局限定类名com/y1zh3e7/Test/ClassTest

0x06 父类索引-2Byte

0X00 04,对应CONSTANT-4,0x00 17:全局限定名常量项索引#23,对应java/lang/Object

0X07 接口索引-2+n

2+n依旧指两个字节代表接口数量,n代表接口表,我们向下读取两个字节0X00 00,即接口数量为0,自然也没有n了

0x08 字段表集合-2+nByte

字段表中包含了类中声明的变量,以及实例化后的变量,但是不包括方法内声明的局部变量,因此继续向下读取两个字节,可以发现也是0x00 00,因为我们的变量是定义在psvm中,如果将代码修改如下:

arduino 复制代码
public class ClassTest {	String sayHello = "Hello World!";}

那么此处的2byte则为0x00 01

0x09 方法-2+nByte

继续读取2Byte,0X00 02,说明我们的类中有两个方法,但是代码中我们明明只有一个方法psvm,其实是因为除了接口和抽象类,在javac时会自动生成一个无参构造,我们可以反编译看到他,也可以javap后看到这个构造器:

vbnet 复制代码
{  public com.y1zh3e7.Test.ClassTest();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V         4: return      LineNumberTable:        line 3: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       5     0  this   Lcom/y1zh3e7/Test/ClassTest;​  public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=1, locals=2, args_size=1         0: ldc           #2                  // String Hello World!         2: astore_1         3: return      LineNumberTable:        line 5: 0        line 6: 3      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       4     0  args   [Ljava/lang/String;            3       1     1 sayHello   Ljava/lang/String;}​

我们继续向下读取两个方法,方法表结构如下:

css 复制代码
method_info {    u2             access_flags;    u2             name_index;    u2             descriptor_index;    u2             attributes_count;    attribute_info attributes[attributes_count];}

构造方法解析

我们按照格式来读取第一个方法: 0x00 01,访问标志位,代表public方法,给出以下访问标志控制符掩码解析:

java 复制代码
十六进制值	名称	说明0x0001	ACC_PUBLIC	方法为 public 权限0x0002	ACC_PRIVATE	方法为 private 权限0x0004	ACC_PROTECTED	方法为 protected 权限0x0008	ACC_STATIC	方法为 static 静态方法0x0010	ACC_FINAL	方法为 final(不可被覆盖)0x0020	ACC_SYNCHRONIZED	方法为 synchronized(同步方法)0x0040	ACC_BRIDGE	方法是由编译器生成的桥接方法(用于泛型类型擦除)0x0080	ACC_VARARGS	方法接受可变参数(如 String... args)0x0100	ACC_NATIVE	方法为 native(由本地代码实现)0x0400	ACC_ABSTRACT	方法为 abstract(抽象方法,无实现)0x0800	ACC_STRICT	方法为 strictfp(严格浮点模式)0x1000	ACC_SYNTHETIC	方法是由编译器生成的(如默认构造方法、枚举类的 values() 方法等)

控制符可以组合使用,如

  • public static 方法:0x0001 (ACC_PUBLIC) | 0x0008 (ACC_STATIC) = 0x0009

  • private final synchronized 方法:0x0002 | 0x0010 | 0x0020 = 0x0032

其中某些标志不能同时存在(如 publicprivateprotected 只能三选一)。

0x00 05,name_index代表方法索引名,我们去CONSTANT-5进行查找为<init>,这是字节码中对构造方法的专用描述。

0x00 06,方法描述符索引。查找CONSTANT-6,为()V。方法描述符的语法是 (参数类型)返回类型,其中 V 表示 void(即无返回值)。():表示方法没有参数。V:表示方法的返回类型为 void

  • 为什么构造方法的返回类型是 void

    虽然构造方法在 Java 语法中没有显式返回值,但在字节码层面,构造方法的返回类型被标记为 void。实际上,构造方法隐式返回构造的实例对象(this),但这一过程由 JVM 自动处理,不需要在描述符中体现。

0x00 01,attributes_count,这里引入属性表的概念。属性表可以描述方法的专有信息,这里则代表了该方法的属性表数量为一个。

通用属性表结构如下:

css 复制代码
attribute_info {    u2 attribute_name_index;    u4 attribute_length;    u1 info[attribute_length];}

根据通用属性表结构,我们读取一个u2,0x00 07到CONSTANT-7中查找,发现是Code。

在 JVM 的 .class 文件中,CodeLineNumberTableLocalVariableTableSourceFile 是类文件属性的重要组成部分,分别用于描述方法的行为、调试信息、局部变量与源码的映射关系,以及源码文件的元数据。

Code属性:

Code 属性是方法表(method_info)中的核心属性,作用如下:

  • 存储字节码 :包含方法的具体指令(如 aload_0, invokespecial 等)。

  • 定义执行环境 :通过 max_stackmax_locals 告诉 JVM 如何分配栈帧内存。

  • 异常处理 :通过 exception_table 定义 try-catch 块的范围和异常类型。

  • 关联调试信息 :通过子属性(如 LineNumberTable)将字节码与源码关联。

Code属性结构如下:

css 复制代码
Code_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 max_stack;    u2 max_locals;    u4 code_length;    u1 code[code_length];    u2 exception_table_length;    {   u2 start_pc;        u2 end_pc;        u2 handler_pc;        u2 catch_type;    } exception_table[exception_table_length];    u2 attributes_count;    attribute_info attributes[attributes_count];}

继续读取一个u4,attribute_length,0x0000002F代表接下来的47个字节为Code属性的指令字节码。

读取一个u2,0x00 01,max_stack,代表操作数栈最大深度1,一会我们在分析字节码指令时就知道这是什么意思了。

0X00 01,max_locals,代表方法的局部变量表大小为1,局部变量为this,因为所有实例方法 (非静态方法)和构造方法 的第一个局部变量槽位(索引 0)都存储了当前对象的引用(即 this)。这是 JVM 的隐式规则,无需在代码中显式声明,因此在psvm这个静态方法中就不会包含this了。此外如果该构造方法为有参构造,那么max_locals数量会+n(参数列表的参数数量)

0x00 00 00 05,code_length为指令长度,也就是说接下来的五个字节为指令。

2A B7 00 01 B1,我们分别来分析这几条指令的作用。2A对应指令aload_0,用于加载局部方法表中的参数到操作数栈中,因此这一步会将this加载到操作数栈上。B7 对应指令invokespecial ,00 01对应CONSTANT-1,即调用父类构造方法。B1对应指令return,方法返回。

0x00 00,exception_table_length,代表异常表为空。

0x00 02,attributes_count,代表该Code属性中还包含了两个子属性。

0x00 08,对应CONSTANT-8,LineNumberTable,则说明该子属性为一个LineNumberTable。

LineNumberTable 属性:

Code 属性的子属性,记录 字节码偏移量源码行号 的映射关系,作用如下:

  • 调试支持 :在 IDE 或异常堆栈中显示源码行号(如 Exception in thread "main" java.lang.NullPointerException at Test.java:12)。

  • 反编译辅助 :帮助工具(如 javap)生成更易读的反编译结果。

  • 优化限制:若省略此属性,JIT 编译器可能无法进行某些优化(如基于行号的 Profiling)。

LineNumberTable属性结构如下:

css 复制代码
LineNumberTable_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 line_number_table_length;    {   u2 start_pc;        u2 line_number;     } line_number_table[line_number_table_length];}

0x00 00 00 06,attribute_length,代表接下来的六字节为属性。

00 01,line_number_table_length为1,代表了下面的line_number_table长度为1。

每个line_number_table包含两个字段,0x00 00对应start_pc,0x00 03对应line_number,这两个字段负责将字节码偏移量与源码行数进行映射,start_pc对应字节码偏移量,line_number对应源码行数,因此0003意思是将第0行开始的字节码指令全部与第三行源码进行对应。如果line_number_table长度不为1,还会有多个start_pc来负责映射字节码指令和源码的关系。比如如果还有一组start_pc=3,line_number=4,那么两组映射关系意思是字节码偏移量0-2对应源码第三行,字节码偏移量3及之后的指令对应源码第四行。

我们继续向下读取第Code的第二个子属性,0x00 09,对应CONSTANT-9,LocalVariableTable。

LocalVariableTable属性:

Code 属性的子属性,记录 局部变量名类型 及其在局部变量表中的 槽位 和作用域,作用如下:

  • 调试支持 :在 IDE 中显示局部变量名和值(如调试时查看 sayHello 变量的内容)。

  • 反射支持 :通过 Method.getParameters() 获取参数名(需编译时启用 -parameters 选项)。

  • 反编译辅助 :帮助反编译器还原变量名(否则变量名会变成 var1, var2)。

LocalVariableTable属性结构如下:

perl 复制代码
LocalVariableTable_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 local_variable_table_length;    {   u2 start_pc;        u2 length;        u2 name_index;        u2 descriptor_index;        u2 index;    } local_variable_table[local_variable_table_length];}

0x00 0000 0C,attribute_length,代表接下来12字节为属性长度。

0x00 01,代表一个局部变量条目,因此下面的local_variable_table[1]中即为描述局部变量this的相关信息。 0x00 00,start_pc,代表this的作用域从字节码偏移量0开始,作用域覆盖0x00 05length,共五个字节。

0x00 0A,name_index,指向CONSTANT-10,局部变量名为this。

0x00 0B,类的全局限定名,指向CONSTANT-11,Lcom/y1zh3e7/Test/ClassTest

0x00 00,index,指该局部变量存储在局部变量表的槽位 0(实例方法的 this 固定占用槽位 0)

main方法解析

0x00 09:访问标志,0x01和0x08的集合,即public static。

0x00 0C:name_index,指向CONSTANT-12,类名main,

0x00 0D:descriptor_index,指向CONSTANT-13,([Ljava/lang/String;)V,方法接收参数为String,返回类型为viod。

0x00 01:attributes_count,属性数量为1。

继续解析属性:

字段

十六进制值

十进制值/说明

attribute_name_index

00 07

指向常量池第 7 项("Code")

attribute_length

00 00 00 3C

属性总长度:60 字节

max_stack

00 01

操作数栈最大深度:1

max_locals

00 02

局部变量表大小:2(argssayHello

code_length

00 00 00 04

字节码长度:4 字节

字节码

12 02 4C B1

指令解析:

12 02

ldc #2(加载常量 "Hello World!")

4C

astore_1(存储到局部变量 1)

B1

return(方法返回)

exception_table_length

00 00

异常表为空

attributes_count

00 02

包含 2 个子属性

子属性 1:LineNumberTable

字段

十六进制值

说明

attribute_name_index

00 08

常量池第 8 项("LineNumberTable")

attribute_length

00 00 00 0A

长度 10 字节

line_number_table_length

00 02

2 个行号条目

条目 1:start_pc

00 00

字节码偏移 0 → 源码第 5 行

条目 1:line_number

00 05

条目 2:start_pc

00 03

字节码偏移 3 → 源码第 6 行

条目 2:line_number

00 06

子属性 2:LocalVariableTable

字段

十六进制值

说明

attribute_name_index

00 09

常量池第 9 项("LocalVariableTable")

attribute_length

00 00 00 16

长度 22 字节

local_variable_table_length

00 02

2 个局部变量条目

条目 1:start_pc

00 00

变量 args 作用域起始偏移 0

length

00 04

作用域长度 4 字节

name_index

00 0E

常量池第 14 项(变量名 args

descriptor_index

00 0F

常量池第 15 项(类型 [Ljava/lang/String;

index

00 00

局部变量槽位 0

条目 2:start_pc

00 03

变量 sayHello 作用域起始偏移 3

length

00 01

作用域长度 1 字节

name_index

00 10

常量池第 16 项(变量名 sayHello

descriptor_index

00 11

常量池第 17 项(类型 Ljava/lang/String;

index

00 01

局部变量槽位 1

0x10 属性Attribute-2+nByte

0x00 01:属性数量1

0x0012:属性名称,CONSTANT-18,SourceFile。

SourceFile属性:

类文件的顶级属性,记录 源码文件名,作用如下:

  • 调试支持 :在异常堆栈中显示源码文件名(如 Test.java)。

  • 代码溯源:帮助开发者快速定位源码文件。

  • 可读性:反编译时显示原始文件名,而非匿名类名。

SourceFile文件结构如下:

ini 复制代码
SourceFile_attribute {    u2 attribute_name_index;      u4 attribute_length;          u2 sourcefile_index;      }

0x00 00 00 02,attribute_length,属性长度2.

0x00 13,sourcefile_index,指向CONSTANT-19,为ClassTest.java。

相关推荐
工业互联网专业9 小时前
基于springboot+vue的悠扬乐器管理系统
java·vue.js·spring boot·毕业设计·源码·课程设计
苏近之2 天前
深入浅出 Rust 异步运行时原理
rust·源码
董可伦2 天前
Flink 源码编译
大数据·flink·源码
工业互联网专业3 天前
基于JavaWeb的花店销售系统设计与实现
java·vue.js·spring boot·毕业设计·源码·课程设计·花店销售系统
echoVic3 天前
PixiJS 源码揭秘 - 8. 插件机制深度解析
前端·源码·数据可视化
CYRUS_STUDIO3 天前
详解 Android APP 启动流程
android·操作系统·源码
明远湖之鱼4 天前
手把手带你实现一个自己的简易版 Webpack
前端·webpack·源码
工业互联网专业4 天前
基于springboot+vue的医院管理系统
java·vue.js·spring boot·毕业设计·源码·课程设计·医院管理系统