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

莱莱  论坛元老 | 2025-4-27 19:33:58 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1951|帖子 1951|积分 5853

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
Intro

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

我们以如下类进行分析:
  1. package com.y1zh3e7.Test;
  2. public class ClassTest {
  3.    public static void main(String[] args) {
  4.        String sayHello = "Hello World!";
  5.    }
  6. }
复制代码
编译后class文件扔到hex编辑器里查察十六进制方便分析:
  1. 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文件结构如下
[img=720,427.5]https://pic1.imgdb.cn/item/680a07fe58cb8da5c8c8f47d.png[/img]

0x01 魔术头 Magic Number-4Byte

class文件的魔术头为四字节并且值固定,可以看到为如下内容,这个十六进制表达还是挺有意思的
  1. CA FE BA BE
复制代码
0x02 版本号 Version-2+2Byte

十六进制对应内容为
  1. 00 00 00 34
复制代码
前面的0000为次版本号,后面的0034为主版本号,0x0034对应十进制为52,对应版本为jdk1.8,对应的我IDEA中的jdk版本也是1.8
[img=720,286.7142857142857]https://pic1.imgdb.cn/item/680a07fe58cb8da5c8c8f47c.png[/img]

0x03 常量池 Constant Pool-2+nByte

常量池的2+n指的是两字节的常量数目,加上nByte的常量内容,常量池存储如下内容:
[img=720,485.35714285714283]https://pic1.imgdb.cn/item/680a07fe58cb8da5c8c8f47f.png[/img]

接下来我们继续分析十六进制并以此说明:
起首的两个字节代表常量数目,0x0018转换为十进制为24。这里必要注意的是,常量池的常量索引并不是从0开始而是从1开始,因此24表示常量池中共有23个常量,索引以此为1-23,并且在.class文件中,只有常量池的下标是从0开始,后面的接口、属性、方法等下表依然都是从0开始计数:
  1. 00 18
复制代码
CONSTANT-1

根据上面的表格,我们可以发现不论是何种类型的常量,都是以u1(1字节)的tag位作为起始,因此我们向下读取一字节,为第一个常量的tag,为0x0A:
  1. 0A
复制代码
0x0A对应十进制10,我们在表格中寻找值为10的索引,可以找到该常量类型为CONSTANT_Methodref_info,并且接下来还分别有两个u2的index,我们继续向下读取两个字节,则对应表格中指向声明方法的类描述符的索引项,这些东西的作用我们到后面就会知道了,先继续往下看
  1. 00 04
复制代码
继续向下读取两个字节,对应指向名称及类型描述符索引项,值为20
  1. 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:字符串
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)
[img=720,424.2857142857143]https://pic1.imgdb.cn/item/680a07fe58cb8da5c8c8f47e.png[/img]

我们向下读取两个Byte0x0021,代表的是0x0020和0x0001的聚集,意思是该类为public,并且继承object(0x06父类索引)
【----帮助网安学习,以下所有学习资料免费领!加vx:YJ-2021-1,备注 “博客园” 获取!】
 ① 网安学习成长路径思维导图
 ② 60+网安经典常用工具包
 ③ 100+SRC毛病分析报告
 ④ 150+网安攻防实战技术电子书
 ⑤ 最权势巨子CISSP 认证考试指南+题库
 ⑥ 超1800页CTF实战技巧手册
 ⑦ 最新网安大厂面试题合集(含答案)
 ⑧ APP客户端安全检测指南(安卓+IOS)
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中,如果将代码修改如下:
  1. public class ClassTest {
  2.     String sayHello = "Hello World!";
  3. }
复制代码
那么此处的2byte则为0x00 01
0x09 方法-2+nByte

继续读取2Byte,0X00 02,说明我们的类中有两个方法,但是代码中我们明明只有一个方法psvm,其实是由于除了接口和抽象类,在javac时会主动生成一个无参构造,我们可以反编译看到他,也可以javap后看到这个构造器:
[img=720,280.92857142857144]https://pic1.imgdb.cn/item/680a07fe58cb8da5c8c8f47b.png[/img]
  1. {
  2.   public com.y1zh3e7.Test.ClassTest();
  3.    descriptor: ()V
  4.    flags: ACC_PUBLIC
  5.    Code:
  6.      stack=1, locals=1, args_size=1
  7.         0: aload_0
  8.         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
  9.         4: return
  10.      LineNumberTable:
  11.        line 3: 0
  12.      LocalVariableTable:
  13.        Start  Length  Slot  Name   Signature
  14.            0       5     0  this   Lcom/y1zh3e7/Test/ClassTest;
  15.   public static void main(java.lang.String[]);
  16.    descriptor: ([Ljava/lang/String;)V
  17.    flags: ACC_PUBLIC, ACC_STATIC
  18.    Code:
  19.      stack=1, locals=2, args_size=1
  20.         0: ldc           #2                  // String Hello World!
  21.         2: astore_1
  22.         3: return
  23.      LineNumberTable:
  24.        line 5: 0
  25.        line 6: 3
  26.      LocalVariableTable:
  27.        Start  Length  Slot  Name   Signature
  28.            0       4     0  args   [Ljava/lang/String;
  29.            3       1     1 sayHello   Ljava/lang/String;
  30. }
复制代码
我们继续向下读取两个方法,方法表结构如下:
  1. method_info {
  2.    u2             access_flags;
  3.    u2             name_index;
  4.    u2             descriptor_index;
  5.    u2             attributes_count;
  6.    attribute_info attributes[attributes_count];
  7. }
复制代码
构造方法解析

我们按照格式来读取第一个方法:0x00 01,访问标志位,代表public方法,给出以下访问标志控制符掩码解析:
  1. 十六进制值   名称  说明
  2. 0x0001  ACC_PUBLIC  方法为 public 权限
  3. 0x0002  ACC_PRIVATE 方法为 private 权限
  4. 0x0004  ACC_PROTECTED   方法为 protected 权限
  5. 0x0008  ACC_STATIC  方法为 static 静态方法
  6. 0x0010  ACC_FINAL   方法为 final(不可被覆盖)
  7. 0x0020  ACC_SYNCHRONIZED    方法为 synchronized(同步方法)
  8. 0x0040  ACC_BRIDGE  方法是由编译器生成的桥接方法(用于泛型类型擦除)
  9. 0x0080  ACC_VARARGS 方法接受可变参数(如 String... args)
  10. 0x0100  ACC_NATIVE  方法为 native(由本地代码实现)
  11. 0x0400  ACC_ABSTRACT    方法为 abstract(抽象方法,无实现)
  12. 0x0800  ACC_STRICT  方法为 strictfp(严格浮点模式)
  13. 0x1000  ACC_SYNTHETIC   方法是由编译器生成的(如默认构造方法、枚举类的 values() 方法等)
复制代码
控制符可以组合利用,如

  • public static 方法:0x0001 (ACC_PUBLIC) | 0x0008 (ACC_STATIC) = 0x0009
  • private final synchronized 方法:0x0002 | 0x0010 | 0x0020 = 0x0032
此中某些标志不能同时存在(如 public、private、protected 只能三选一)。
0x00 05,name_index代表方法索引名,我们去CONSTANT-5进行查找为,这是字节码中对构造方法的专用描述。
0x00 06,方法描述符索引。查找CONSTANT-6,为()V。方法描述符的语法是 (参数类型)返回类型,此中 V 表示 void(即无返回值)。():表示方法没有参数。V:表示方法的返回类型为 void。

  • 为什么构造方法的返回类型是 void?虽然构造方法在 Java 语法中没有显式返回值,但在字节码层面,构造方法的返回类型被标志为 void。实际上,构造方法隐式返回构造的实例对象(this),但这一过程由 JVM 主动处置惩罚,不必要在描述符中体现。
0x00 01,attributes_count,这里引入属性表的概念。属性表可以描述方法的专有信息,这里则代表了该方法的属性表数目为一个。
通用属性表结构如下:
  1. attribute_info {
  2.     u2 attribute_name_index;
  3.     u4 attribute_length;
  4.     u1 info[attribute_length];
  5. }
复制代码
根据通用属性表结构,我们读取一个u2,0x00 07到CONSTANT-7中查找,发现是Code。
在 JVM 的 .class 文件中,Code、LineNumberTable、LocalVariableTable 和 SourceFile 是类文件属性的重要构成部分,分别用于描述方法的行为、调试信息、局部变量与源码的映射关系,以及源码文件的元数据。
Code属性:
Code 属性是方法表(method_info)中的核心属性,作用如下:

  • 存储字节码:包含方法的详细指令(如 aload_0, invokespecial 等)。
  • 定义执行环境:通过 max_stack 和 max_locals 告诉 JVM 怎样分配栈帧内存。
  • 非常处置惩罚:通过 exception_table 定义 try-catch 块的范围和非常类型。
  • 关联调试信息:通过子属性(如 LineNumberTable)将字节码与源码关联。
Code属性结构如下:
  1. Code_attribute {
  2.    u2 attribute_name_index;
  3.    u4 attribute_length;
  4.    u2 max_stack;
  5.    u2 max_locals;
  6.    u4 code_length;
  7.    u1 code[code_length];
  8.    u2 exception_table_length;
  9.    {   u2 start_pc;
  10.        u2 end_pc;
  11.        u2 handler_pc;
  12.        u2 catch_type;
  13.    } exception_table[exception_table_length];
  14.    u2 attributes_count;
  15.    attribute_info attributes[attributes_count];
  16. }
复制代码
继续读取一个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属性结构如下:
  1. LineNumberTable_attribute {
  2.     u2 attribute_name_index;
  3.     u4 attribute_length;
  4.     u2 line_number_table_length;
  5.     {   u2 start_pc;
  6.         u2 line_number;
  7.     } line_number_table[line_number_table_length];
  8. }
复制代码
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属性结构如下:
  1. LocalVariableTable_attribute {
  2.    u2 attribute_name_index;
  3.    u4 attribute_length;
  4.    u2 local_variable_table_length;
  5.    {   u2 start_pc;
  6.        u2 length;
  7.        u2 name_index;
  8.        u2 descriptor_index;
  9.        u2 index;
  10.    } local_variable_table[local_variable_table_length];
  11. }
复制代码
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_index00 07指向常量池第 7 项("Code")attribute_length00 00 00 3C属性总长度:60 字节max_stack00 01操作数栈最大深度:1max_locals00 02局部变量表大小:2(args 和 sayHello)code_length00 00 00 04字节码长度:4 字节字节码12 02 4C B1指令解析: 12 02ldc #2(加载常量 "Hello World!") 4Castore_1(存储到局部变量 1) B1return(方法返回)exception_table_length00 00非常表为空attributes_count00 02包含 2 个子属性 
子属性 1:LineNumberTable
字段十六进制值说明attribute_name_index00 08常量池第 8 项("LineNumberTable")attribute_length00 00 00 0A长度 10 字节line_number_table_length00 022 个行号条目条目 1:start_pc00 00字节码偏移 0 → 源码第 5 行条目 1:line_number00 05 条目 2:start_pc00 03字节码偏移 3 → 源码第 6 行条目 2:line_number00 06   
子属性 2:LocalVariableTable
字段十六进制值说明attribute_name_index00 09常量池第 9 项("LocalVariableTable")attribute_length00 00 00 16长度 22 字节local_variable_table_length00 022 个局部变量条目条目 1:start_pc00 00变量 args 作用域起始偏移 0length00 04作用域长度 4 字节name_index00 0E常量池第 14 项(变量名 args)descriptor_index00 0F常量池第 15 项(类型 [Ljava/lang/String;)index00 00局部变量槽位 0条目 2:start_pc00 03变量 sayHello 作用域起始偏移 3length00 01作用域长度 1 字节name_index00 10常量池第 16 项(变量名 sayHello)descriptor_index00 11常量池第 17 项(类型 Ljava/lang/String;)index00 01局部变量槽位 1 

0x10 属性Attribute-2+nByte

0x00 01:属性数目1
0x0012:属性名称,CONSTANT-18,SourceFile。
SourceFile属性:
类文件的顶级属性,记载 源码文件名,作用如下:

  • 调试支持:在非常堆栈中显示源码文件名(如 Test.java)。
  • 代码溯源:帮助开发者快速定位源码文件。
  • 可读性:反编译时显示原始文件名,而非匿名类名。
SourceFile文件结构如下:
  1. SourceFile_attribute {
  2.    u2 attribute_name_index;  
  3.    u4 attribute_length;      
  4.    u2 sourcefile_index;      
  5. }
复制代码
0x00 00 00 02,attribute_length,属性长度2.
0x00 13,sourcefile_index,指向CONSTANT-19,为ClassTest.java。
gadgetInspector分析

0x01 Intro

工具基于ASM技术来对控制字节码,从而达到对传入jar及war包的classpath下的类进行读取,并依次记载类信息、类方法信息、调用关系信息。最后基于以上收集的信息来进行反序列化链的挖掘,分别对应如下几个类:

  • GadgetInspector:main方法,步伐的入口,做一些配置以及数据的准备工作
  • MethodDiscovery:类、方法数据以及父子类、超类关系数据的搜索
  • PassthroughDiscovery:分析参数能影响到返回值的方法,并收集存储
  • CallGraphDiscovery:记载调用者caller方法和被调用者target方法的参数关联
  • SourceDiscovery:入口方法的搜索,只有具备某种特性的入谈锋会被标志收集
  • GadgetChainDiscovery:整合以上数据,并通过判断调用链的最末端slink特性,从而判断出可利用的gadget chain
0x02 主入口-GadgetInspetcor

该类为整个工具的入口类,基本上是对于相干配置做出初始化处置惩罚,静态代码块中创建准备写入相干结果的文件。main中起首验证是否存在参数,若为空退出。工具在挖掘时必要我们指定差别的gadget-chain,如jdk原生反序列化、jackson等,以及指定classpath的路径。
接下来会对日记进行配置,之后是对历史dat文件(上面提到的类、方法等相干数据的本地化存储)的管理,以及反序列化链类型的指定。我们主要看这一部分是怎样指定反序列化链类型的:
  1. else if (arg.equals("--config")) {
  2.                //--config参数指定fuzz类型
  3.                config = ConfigRepository.getConfig(args[++argIndex]);
  4.                if (config == null) {
  5.                    throw new IllegalArgumentException("Invalid config name: " + args[argIndex]);
  6.                }
复制代码
跟进到getConfig方法中,并且也可以看到所有的gadget-chain是通过差别的Config来实现的,并且都实现了GIConfig接口:
  1. public interface GIConfig {
  2.    String getName();
  3.    SerializableDecider getSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap);
  4.    ImplementationFinder getImplementationFinder(
  5.        Map<Handle, MethodReference> methodMap,
  6.        Map<Handle, Set<Handle>> methodImplMap,
  7.        InheritanceMap inheritanceMap,
  8.        Map<ClassReference.Handle, Set<Handle>> methodsByClass);
  9.    SourceDiscovery getSourceDiscovery();
  10.    SlinkDiscovery getSlinkDiscovery();
  11. }
复制代码
我们以Jackson的实现来看,这些被实现的方法都会在后面用到,他们都是用来对指定gadget-chain进行区分的方法,差别的gadget-chain的特性差别,因此我们可以通过这些方法来确认对应的chain。
  1. package gadgetinspector.config;
  2. import gadgetinspector.ImplementationFinder;
  3. import gadgetinspector.SerializableDecider;
  4. import gadgetinspector.SlinkDiscovery;
  5. import gadgetinspector.SourceDiscovery;
  6. import gadgetinspector.data.ClassReference;
  7. import gadgetinspector.data.InheritanceMap;
  8. import gadgetinspector.data.MethodReference;
  9. import gadgetinspector.data.MethodReference.Handle;
  10. import gadgetinspector.jackson.JacksonImplementationFinder;
  11. import gadgetinspector.jackson.JacksonSerializableDecider;
  12. import gadgetinspector.jackson.JacksonSourceDiscovery;
  13. import java.util.Map;
  14. import java.util.Set;
  15. public class JacksonDeserializationConfig implements GIConfig {
  16.    @Override
  17.    public String getName() {
  18.        return "jackson";
  19.    }
  20.    @Override
  21.    public SerializableDecider getSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap) {
  22.        return new JacksonSerializableDecider(methodMap);
  23.    }
  24.    @Override
  25.    public ImplementationFinder getImplementationFinder(
  26.        Map<Handle, MethodReference> methodMap,
  27.        Map<Handle, Set<Handle>> methodImplMap,
  28.        InheritanceMap inheritanceMap,
  29.        Map<ClassReference.Handle, Set<Handle>> methodsByClass) {
  30.        return new JacksonImplementationFinder(getSerializableDecider(methodMap, inheritanceMap));
  31.    }
  32.    @Override
  33.    public SourceDiscovery getSourceDiscovery() {
  34.        return new JacksonSourceDiscovery();
  35.    }
  36.    @Override
  37.    public SlinkDiscovery getSlinkDiscovery() {
  38.        return null;
  39.    }
  40. }
复制代码
跟进JacksonSerializableDecider,两个map中记载的是可以通过Jackson决策的类和方法:
  1. //类是否通过决策的缓存集合
  2.    private final Map<ClassReference.Handle, Boolean> cache = new HashMap<>();
  3.    //类名-方法集合 映射集合
  4.    private final Map<ClassReference.Handle, Set<MethodReference.Handle>> methodsByClassMap;
复制代码
详细的决策判断逻辑在apply中,在后面的分析中我们也可以看到会调用apply方法来判断类和方法是否通过决策。以jackson的apply来举例,由于jackson的json反序列化是必要以类的无参构造为起始,在java中如果没有显式声明无参构造器,但是显式声明了一个有参构造,那么该类是没有无参构造的,因此代表着该类不可进行jackson反序列化。
  1. @Override
  2.    public Boolean apply(ClassReference.Handle handle) {
  3.        if (isNoGadgetClass(handle)) {
  4.            return false;
  5.        }
  6.        Boolean cached = cache.get(handle);
  7.        if (cached != null) {
  8.            return cached;
  9.        }
  10.        Set<MethodReference.Handle> classMethods = methodsByClassMap.get(handle);
  11.        if (classMethods != null) {
  12.            for (MethodReference.Handle method : classMethods) {
  13.                //该类,只要有无参构造方法,就通过决策
  14.                if (method.getName().equals("<init>") && method.getDesc().equals("()V")) {
  15.                    cache.put(handle, Boolean.TRUE);
  16.                    return Boolean.TRUE;
  17.                }
  18.            }
  19.        }
  20.        cache.put(handle, Boolean.FALSE);
  21.        return Boolean.FALSE;
  22.    }
复制代码
接下往返到Config中,继续看InplementationFinder,在决策时由于Java的多态性,并且gadgetinspector无法在要被检测的jar运行时进行判断,因此当调用到某一接口的方法时,必要查找接口所有的实现类中的该方法,并将这些方法构成实际的调用链去进行污点分析。这些方法是否可进行当前指定的gadget-chain反序列化,还是必要通过apply方法来进行判断:
  1. public class JacksonImplementationFinder implements ImplementationFinder {
  2.    private final SerializableDecider serializableDecider;
  3.    public JacksonImplementationFinder(SerializableDecider serializableDecider) {
  4.        this.serializableDecider = serializableDecider;
  5.    }
  6.    @Override
  7.    public Set<MethodReference.Handle> getImplementations(MethodReference.Handle target) {
  8.        Set<MethodReference.Handle> allImpls = new HashSet<>();
  9.        // For jackson search, we don't get to specify the class; it uses reflection to instantiate the
  10.        // class itself. So just add the target method if the target class is serializable.
  11.        if (Boolean.TRUE.equals(serializableDecider.apply(target.getClassReference()))) {
  12.            allImpls.add(target);
  13.        }
  14.        return allImpls;
  15.    }
  16. }
复制代码
继续看JacksonSourceDiscovery,内部只有一个discover方法,这个方法的作用就是帮我们找到可进行Jackson反序列化的入口方法,对于jackson反序列化来说,会以无参构造为入口,并依次执行setter以及getter。因此discover会查找出通过了apply决策后的类的无参构造(()V代表无参,返回值为viod),以及getter和setter。
  1.  @Override
  2.    public void discover(Map<ClassReference.Handle, ClassReference> classMap,
  3.                         Map<MethodReference.Handle, MethodReference> methodMap,
  4.                         InheritanceMap inheritanceMap, Map<MethodReference.Handle, Set<GraphCall>> graphCallMap) {
  5.        final JacksonSerializableDecider serializableDecider = new JacksonSerializableDecider(methodMap);
  6.        for (MethodReference.Handle method : methodMap.keySet()) {
  7.            if (skipList.contains(method.getClassReference().getName())) {
  8.                continue;
  9.            }
  10.            if (serializableDecider.apply(method.getClassReference())) {
  11.                if (method.getName().equals("<init>") && method.getDesc().equals("()V")) {
  12.                    addDiscoveredSource(new Source(method, 0));
  13.                }
  14.                if (method.getName().startsWith("get") && method.getDesc().startsWith("()")) {
  15.                    addDiscoveredSource(new Source(method, 0));
  16.                }
  17.                if (method.getName().startsWith("set") && method.getDesc().matches("\\(L[^;]*;\\)V")) {
  18.                    addDiscoveredSource(new Source(method, 0));
  19.                }
  20.            }
复制代码
继续向下看GadgetInspector,进入到initJarData方法中,通过for循环读取最后面的参数,从而指定多个jar或war包,通过URLClassLoader,根据绝对路径将这些jar或war包进行加载,并通过ClassResourceEnumerator将jar或war包中的class进行加载:
  1. ClassLoader classLoader = initJarData(args, boot, argIndex, haveNewJar, pathList);
复制代码
  1.                for (int i = 0; i < args.length - argIndex; i++) {
  2.                    String pathStr = args[argIndex + i];
  3.                    if (!pathStr.endsWith(".jar")) {
  4.                        //todo 主要用于大批量的挖掘链
  5.                        //非.jar结尾,即目录,需要遍历目录找出所有jar文件
  6.                        File file = Paths.get(pathStr).toFile();
  7.                        if (file == null || !file.exists())
  8.                            continue;
  9.                        Files.walkFileTree(file.toPath(), new SimpleFileVisitor<Path>() {
  10.                            @Override
  11.                            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
  12.                                if (!file.getFileName().toString().endsWith(".jar"))
  13.                                    return FileVisitResult.CONTINUE;
  14.                                File readFile = file.toFile();
  15.                                Path path = Paths.get(readFile.getAbsolutePath());
  16.                                if (Files.exists(path)) {
  17.                                    if (ConfigHelper.history) {
  18.                                        if (!scanJarHistory.contains(path.getFileName().toString())) {
  19.                                            if (jarCount.incrementAndGet() <= ConfigHelper.maxJarCount) {
  20.                                                pathList.add(path);
  21.                                            }
  22.                                        }
  23.                                    } else {
  24.                                        if (jarCount.incrementAndGet() <= ConfigHelper.maxJarCount) {
  25.                                            pathList.add(path);
  26.                                        }
  27.                                    }
  28.                                }
  29.                                return FileVisitResult.CONTINUE;
  30.                            }
  31.                        });
  32.                        continue;
  33.                    }
  34.                    Path path = Paths.get(pathStr).toAbsolutePath();
  35.                    if (!Files.exists(path)) {
  36.                        throw new IllegalArgumentException("Invalid jar path: " + path);
  37.                    }
  38.                    pathList.add(path);
  39.                
复制代码
getAllParents方法会递归的将当前观察类的所有父类、接口的父类查找出来,并且添加到allParents聚集中
  1.      //类枚举加载器,具有两个方法
  2.            //getRuntimeClasses获取rt.jar的所有class
  3.            //getAllClasses获取rt.jar以及classLoader加载的class          
  4. final ClassResourceEnumerator classResourceEnumerator = new ClassResourceEnumerator(
  5.                    classLoader);
复制代码
最后将类名与整合好的allParents形成映射关系,存储到implicitInheritance中:
  1.  if (!Files.exists(Paths.get("classes.dat")) || !Files.exists(Paths.get("methods.dat"))
  2.                || !Files.exists(Paths.get("inheritanceMap.dat"))) {
  3.            LOGGER.info("Running method discovery...");
  4.            MethodDiscovery methodDiscovery = new MethodDiscovery();
  5.            methodDiscovery.discover(classResourceEnumerator);
  6.            //保存了类信息、方法信息、继承实现信息
  7.            methodDiscovery.save();
  8.        }
复制代码
接下来会用InheritanceMap构造函数将implicitInheritance的子->父的映射关系进行逆转整合。
  1.    public void discover(final ClassResourceEnumerator classResourceEnumerator) throws Exception {
  2.        for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
  3.            try (InputStream in = classResource.getInputStream()) {
  4.                ClassReader cr = new ClassReader(in);
  5.                try {
  6.                    //使用asm的ClassVisitor、MethodVisitor,利用观察模式去扫描所有的class和method并记录
  7.                    cr.accept(new MethodDiscoveryClassVisitor(), ClassReader.EXPAND_FRAMES);
  8.                } catch (Exception e) {
  9.                    LOGGER.error("Exception analyzing: " + classResource.getName(), e);
  10.                }
  11.            } catch (Exception e) {
  12.                e.printStackTrace();
  13.            }
  14.        }
  15.    }
复制代码
此中这一行代码会判断inheritanceMap中每个子类对应的set中的value(parent),是否在subClassMap中,如果不存在执行Lambda表达式,创建一个新的空HashSet,将parent作为key,HashSet作为value存入subClassMap,并且将child添加到HashSet中。最终subClassMap就变成了父类->子类的映射关系。
  1. 1. visit()                  → 访问类的基础信息(版本、类名等)
  2. 2. visitSource()            → 源码信息(可选)
  3. 3. visitModule()            → 模块信息(Java 9+,可选)
  4. 4. visitNestHost()          → 嵌套类宿主(Java 11+,可选)
  5. 5. visitPermittedSubtype()  → sealed类的许可子类(Java 17+,可选)
  6. 6. visitOuterClass()        → 外部类信息(如果是内部类)
  7. 7. visitAnnotation()        → 类上的注解(可能有多个)
  8. 8. visitTypeAnnotation()    → 类上的类型注解(可能有多个)
  9. 9. visitAttribute()         → 类的自定义属性(可能有多个)
  10. 10. visitField()            → 类的字段(按字节码中的顺序访问)
  11. 11. visitMethod()           → 类的方法(按字节码中的顺序访问)
  12. 12. visitEnd()              → 类访问结束
复制代码
举个例子:
假设 inheritanceMap 包含:
  1.   public void visit ( int version, int access, String name, String signature, String superName, String[]interfaces)
  2.        {
  3.            this.name = name;
  4.            this.superName = superName;
  5.            this.interfaces = interfaces;
  6.            this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
  7.            this.members = new ArrayList<>();
  8.            this.classHandle = new ClassReference.Handle(name);//类名
  9.            annotations = new HashSet<>();
  10.            super.visit(version, access, name, signature, superName, interfaces);
  11.        }
复制代码
则 subClassMap 的构建过程如下:

  • 处置惩罚 Dog 的父类 Animal:

    • subClassMap 中没有 Animal,创建HashSet → Animal: {Dog}

  • 处置惩罚 Dog 的父类 Object:

    • 没有 Object,创建HashSet → Object: {Dog}

  • 处置惩罚 Cat 的父类 Animal:

    • Animal 已存在,直接添加 → Animal: {Dog, Cat}

  • 处置惩罚 Cat 的父类 Object:

    • Object 已存在,添加 → Object: {Dog, Cat}

最终 subClassMap 结果:
  1.        @Override
  2.        public FieldVisitor visitField(int access, String name, String desc,
  3.                                       String signature, Object value) {
  4.            if ((access & Opcodes.ACC_STATIC) == 0) {
  5.                Type type = Type.getType(desc);
  6.                String typeName;
  7.                if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
  8.                    typeName = type.getInternalName();
  9.                } else {
  10.                    typeName = type.getDescriptor();
  11.                }
  12.                members.add(new ClassReference.Member(name, access, new ClassReference.Handle(typeName)));
  13.            }
  14.            return super.visitField(access, name, desc, signature, value);
  15.        }
复制代码
最后调用save方法对继承关系进行生存,方法依旧和上面一样,会进行序列化后持久化存储:
  1. private final Member[] members;
复制代码
最终形成的inheritanceMap.dat结构如下:
  1.    public static class Member {
  2.        private final String name;
  3.        private final int modifiers;
  4.        private final ClassReference.Handle type;
  5.        public Member(String name, int modifiers, Handle type) {
  6.            this.name = name;
  7.            this.modifiers = modifiers;
  8.            this.type = type;
  9.        }
复制代码
0x04 入参返回值污染关系收集-PassthroughDiscovery

这一步雷同于污点分析,我们对各个方法的参数对返回值的污染关系做出总结:
  1.    public static class Handle {
  2.        private final String name;
  3.        public Handle(String name) {
  4.            this.name = name;
  5.        }
  6.        public String getName() {
  7.            return name;
  8.        }
  9.        @Override
  10.        public boolean equals(Object o) {
  11.            if (this == o) return true;
  12.            if (o == null || getClass() != o.getClass()) return false;
  13.            Handle handle = (Handle) o;
  14.            return name != null ? name.equals(handle.name) : handle.name == null;
  15.        }
  16.        @Override
  17.        public int hashCode() {
  18.            return name != null ? name.hashCode() : 0;
  19.        }
  20.    }
复制代码
跟进passthroughDiscovery.discover当中,起首会将我们上一步MethodDiscovery所生成的类、方法、继承信息读取进来
  1.        @Override
  2.        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
  3.            boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
  4.            //找到一个方法,添加到缓存
  5.            discoveredMethods.add(new MethodReference(
  6.                    classHandle,//类名
  7.                    name,
  8.                    desc,
  9.                    isStatic));
  10.            return super.visitMethod(access, name, desc, signature, exceptions);
  11.        }
复制代码
接下来通过discoverMethodCalls,来找出所有方法间的调用关系,我们继续跟进
  1.        @Override
  2.        public void visitEnd() {
  3.            ClassReference classReference = new ClassReference(
  4.                    name,
  5.                    superName,
  6.                    interfaces,
  7.                    isInterface,
  8.                    members.toArray(new ClassReference.Member[members.size()]),
  9.                    annotations);//把所有找到的字段封装
  10.            //找到一个方法遍历完成后,添加类到缓存
  11.            discoveredClasses.add(classReference);
  12.            super.visitEnd();
  13.        }
复制代码
在该方法中,依然是通过ASM来先对所有的类进行一次观察,用到的visitor是MethodCallDiscoveryClassVisitor,并且这里的MethodCallDiscoveryClassVisitor内部是做了一些包装的,这一部分的执行顺序可能会有点乱,我会在方法分析结束后总结一下:
  1.    public static <T> void saveData(Path filePath, DataFactory<T> factory, Collection<T> values) throws IOException {
  2.        try (BufferedWriter writer = Files.newWriter(filePath.toFile(), StandardCharsets.UTF_8)) {
  3.            for (T value : values) {
  4.                final String[] fields = factory.serialize(value);
  5.                if (fields == null) {
  6.                    continue;
  7.                }
  8.                StringBuilder sb = new StringBuilder();
  9.                for (String field : fields) {
  10.                    if (field == null) {
  11.                        sb.append("\t");
  12.                    } else {
  13.                        sb.append("\t").append(field);
  14.                    }
  15.                }
  16.                writer.write(sb.substring(1));
  17.                writer.write("\n");
  18.            }
  19.        }
复制代码
分别跟进MCDCV的visit以及visitMethod方法,visit方法中将传入进来的classname进行记载
  1. 类名(例:java/lang/String) 父类 接口A,接口B,接口C 是否接口 字段1!字段1access!字段1类型!字段2!字段2access!字段1类型
复制代码
visitMethod方法又创建了一个MethodCallDiscoveryMethodVisitor,并且可以看到在实例化时将上面的mv也传了进去。但其实我们观察MethodCallDiscoveryClassVisitor的构造函数,在调用父类构造函数时并没有传入任何的classvisitor,因此父类ClassVisitor的cv属性为null,最终返回的也是个null,在这里传入给MCDMV的mv也是个null:
  1. 类名 方法名 方法描述 是否静态方法
复制代码
  1.        Map<ClassReference.Handle, ClassReference> classMap = new HashMap<>();
  2.        for (ClassReference clazz : discoveredClasses) {
  3.            classMap.put(clazz.getHandle(), clazz);
  4.        }
复制代码
跟进MethodCallDiscoveryMethodVisitor,可以发现父类为MethodVisitor,并且调用父类的构造函数时传入了mv,但其实我们这里静态分析可以分析出来mv是null的,即便传入了在调用MethodVisitor.visitXXX时,最终也不会走到cv.visitXXX上,我这里推测是作者为了工具的扩充性,如果我们必要添加其他的visitor来对方法进行其他处置惩罚,那么就可以形成我们之前提到的雷同于责任链的方式,来遍历的调用visitXXX:
  1. InheritanceDeriver.derive(classMap).save();
复制代码
我们继续看,可以看到接下来会将传入的owner(此时正在观察的类名)封装到ClassReference.Handle中,并再将这个CRF.Handle和方法名、方法的相干描述封装到一个MethodReference.Handle中,calledMethods是每次观察到一个方法,都会创建的空HashSet,最终形成了观察方法:{被观察方法调用方法}的映射关系存入到methodCalls中:
  1.    public static InheritanceMap derive(Map<ClassReference.Handle, ClassReference> classMap) {
  2.        LOGGER.debug("Calculating inheritance for " + (classMap.size()) + " classes...");
  3.        Map<ClassReference.Handle, Set<ClassReference.Handle>> implicitInheritance = new HashMap<>();
  4.        //遍历所有类
  5.        for (ClassReference classReference : classMap.values()) {
  6.            if (implicitInheritance.containsKey(classReference.getHandle())) {
  7.                throw new IllegalStateException("Already derived implicit classes for " + classReference.getName());
  8.            }
  9.            Set<ClassReference.Handle> allParents = new HashSet<>();
  10.            //获取classReference的所有父类、超类、接口类
  11.            getAllParents(classReference, classMap, allParents);
  12.            //添加缓存:类名 -> 所有的父类、超类、接口类
  13.            implicitInheritance.put(classReference.getHandle(), allParents);
  14.        }
  15.        //InheritanceMap翻转集合,转换为{class:[subclass]}
  16.        return new InheritanceMap(implicitInheritance);
  17.    }
复制代码
继续向下看,类中另有一个visitMethodInsn方法,当检测到方法内部的调用时就会执行(底层原理是检查到字节码指令INVOKEVIRTUAL、INVOKESPECIAL、INVOKESTATIC、INVOKEINTERFACE),从而将正在观察的方法中调用的方法到场到calledMethods中
  1.    private static void getAllParents(ClassReference classReference, Map<ClassReference.Handle, ClassReference> classMap, Set<ClassReference.Handle> allParents) {
  2.        Set<ClassReference.Handle> parents = new HashSet<>();
  3.        //把当前classReference类的父类添加到parents
  4.        if (classReference.getSuperClass() != null) {
  5.            parents.add(new ClassReference.Handle(classReference.getSuperClass()));
  6.        }
  7.        //把当前classReference类实现的所有接口添加到parents
  8.        for (String iface : classReference.getInterfaces()) {
  9.            parents.add(new ClassReference.Handle(iface));
  10.        }
  11.        for (ClassReference.Handle immediateParent : parents) {
  12.            //从所有类数据集合中,遍历找出classReference的父类、接口
  13.            ClassReference parentClassReference = classMap.get(immediateParent);
  14.            if (parentClassReference == null) {
  15.                LOGGER.debug("No class id for " + immediateParent.getName());
  16.                continue;
  17.            }
  18.            //继续添加到集合中
  19.            allParents.add(parentClassReference.getHandle());
  20.            //继续递归查找,直到把classReference类的所有父类、超类、接口类都添加到allParents
  21.            getAllParents(parentClassReference, classMap, allParents);
  22.        }
  23.    }
复制代码
指令调用类型适用方法特点INVOKEVIRTUAL虚方法调用(动态绑定)普通实例方法(非私有、非构造器、非静态)运行时根据对象实际类型选择方法,支持多态INVOKESPECIAL特殊方法调用(静态绑定)构造器、私有方法、super.xxx()编译时就决定调用哪一个,不支持多态INVOKESTATIC静态方法调用static 修饰的方法无需对象即可调用,直接通过类名调用INVOKEINTERFACE接口方法调用接口定义的方法运行时通过接口表定位目标方法,支持多态  
回到visitMethod中,最后会进行return操作,并且return的是JSRInlinerAdapter。为什么要return这个类呢,由于在早期的java版本中,利用JSR和RET跳转指令来进行步伐流程控制,在后续版本已废弃并利用GOTO指令,因此必要进行兼容处置惩罚。JSRInlinerAdapter会将JSR和RET指令转为GOTO指令,从而兼容了早期项目。
  1. implicitInheritance.put(classReference.getHandle(), allParents);
复制代码
经过这些封装,调用cr.accept(visitor, ClassReader.EXPAND_FRAMES);将封装好的MethodCallDiscoveryClassVisitor传入进行方法调用关系收集。accept执行顺序如下:

  • MethodCallDiscoveryClassVisitor.visit对类进行观察
  • 当观察到方法时调用MethodCallDiscoveryClassVisitor.visitMethod,此中会创建一个MethodCallDiscoveryMethodVisitor实例,并包装为JSRInlinerAdapter返回,创建实例时会主动为观察到的方法添加一个映射关系,即当前观察方法->calledMethods
  • 当触发了visitxxx时,会先把这些visitxxx发给JSRInlinerAdapter,JSRInlinerAdapter通过各个visit方法对JSR和RET跳转指令进行转换。

    JSRInlinerAdapter 本身也是一个 MethodVisitor,它的回调时机完全跟 ASM 的方法遍历流程一致,只不过它在内部额外“钩”了两个地方来做子例程(JSR/RET)内联:

    • visitJumpInsn每当 ASM 在浏览方法字节码时遇到一个跳转指令(visitJumpInsn(int opcode, Label lbl)),就会调用到它的这个方法。

      • 如果 opcode == JSR,它就把这个子例程入口标签记下来,标志说“后面要做内联”

    • visitEnd当 ASM 遍历完一个方法的所有指令并调用到 visitEnd() 时,JSRInlinerAdapter 会先检查在 visitJumpInsn 里有没有记载过任何 JSR。

      • 如果有,就走 markSubroutines() → emitCode() 的流程,把所有老版本的 JSR/RET 全部睁开成 GOTO(以及必要的空值占位等)
      • 然后再把重写后的指令列表一次性转发给它下游的 MethodVisitor(通常是一个 MethodWriter)代码浏览器

    换句话说:

    • 只要你把 JSRInlinerAdapter 插到你的 MethodVisitor 链上(手动 new 一个 大概在利用 ClassWriter.COMPUTE_FRAMES/ClassReader.EXPAND_FRAMES 时 ASM 主动给你插入),
    • 在方法遍历时遇到跳转就会进 visitJumpInsn,
    • 在方法结束时(visitEnd)就会真正触发“内联 JSR→GOTO” 的逻辑。
    这样保证了旧版子例程指令在生成新的字节码之前就被全部消除,适配现代 JVM 对 StackMapFrame 的要求。

    4.JSRInlinerAdapter将指令转换并内联后,会通过其visitend方法再次通过accept将visitXXX传递给下一个visitor,也就是传入的MethodCallDiscoveryMethodVisitor的visitMethodInsn方法,从而将被调用的方法添加到当前观察方法的calledMethods中。
    accept方法结束后还剩一行,还是将类名和classResource的映射关系存储起来并return:
    1.    private final Map<ClassReference.Handle, Set<ClassReference.Handle>> inheritanceMap;
    2.    //父-子关系集合
    3.    private final Map<ClassReference.Handle, Set<ClassReference.Handle>> subClassMap;
    4.    public InheritanceMap(Map<ClassReference.Handle, Set<ClassReference.Handle>> inheritanceMap) {
    5.        this.inheritanceMap = inheritanceMap;
    6.        subClassMap = new HashMap<>();
    7.        for (Map.Entry<ClassReference.Handle, Set<ClassReference.Handle>> entry : inheritanceMap.entrySet()) {
    8.            ClassReference.Handle child = entry.getKey();
    9.            for (ClassReference.Handle parent : entry.getValue()) {
    10.                subClassMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);
    11.            }
    12.        }
    13.    }
    复制代码
    discoverMethodCalls逻辑结束后,接下来是对methodCalls进行一次逆拓扑排序,所谓逆拓扑排序就是把拓扑排序的序列倒过来,什么你还不知道什么是拓扑排序?大概你该学一下数据结构了,大概看一下这篇文章介绍的吧
    https://paper.seebug.org/1034/
    1. subClassMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);
    复制代码
    为什么我们要进行逆拓扑排序,由于在方法的调用链上,假设a方法传递参数给b方法,并且b方法的返回值影响到了a方法的返回值,那么我们在判断方法链的时候就不能从a方法来入手,必要从最深处被调用的b方法来入手,观察b方法的参数与返回值之间是否存在关系,如果存在关系则证明了a方法传入b方法的参数与b方法返回值有关,此时b方法返回值影响到了a方法返回值,那么我们也就可以断定ab方法之间存在污染关系。
    在方法调用的关系中,我们可以将这些调用抽象为有向图,假设a方法内部调用了b方法,那么我们就可以将a方法对应的图节点引出一条有向边,指向b方法。最终将所有的调用关系全部依次类推,就形成了一个有向图。我们将指向其他节点的边的数目叫做一个点的出度,指向自己的边的数目叫做一个节点的入度,如果找到有向图中一个入度为0的节点,将其节点以及所有的边全部消去,并输出该节点。不绝重复这一操作,直到图中所有节点和边全部被消除掉,我们就得到了一组拓扑排序序列,而这一个序列就对应了我们的方法调用顺序。
    但变乱并没有想象中这么顺遂,在方法调用中会出现两种情况,一个是雷同的方法可能会存在重复调用,并且方法调用中由于回调等方式的存在,造成图中可能会出现环路,而环路的出现会导致拓扑排序在某一时候无法找到一个入度为0的点,也就没有拓扑序列的产生了,办理办法上面的文章也提到了。我们用一个例子来看一下详细的执行过程:
    假设有以下方法调用关系:
    1. "Dog" → {"Animal", "Object"}
    2. "Cat" → {"Animal", "Object"}
    复制代码
    对应的调用图为:
    1. "Animal" → {"Dog", "Cat"}
    2. "Object" → {"Dog", "Cat"}
    复制代码

    • 初始调用:从根节点 A 开始。
      1.    public void save() throws IOException {
      2.        //inheritanceMap.dat数据格式:
      3.        //类名 父类或超类或接口类1 父类或超类或接口类2 父类或超类或接口类3 ...
      4.        DataLoader.saveData(Paths.get("inheritanceMap.dat"), new InheritanceMapFactory(), inheritanceMap.entrySet());
      5.    }
      复制代码
    • 处置惩罚节点 A

      • stack 为空,visitedNodes 为空 → 继续。
      • 获取 A 的被调用方法聚集 {B, D}。
      • 将 A 到场 stack(当前路径:[A])。
      • 递归处置惩罚子节点 B:
        1. 类名 父类或超类或接口类1 父类或超类或接口类2 父类或超类或接口类3 ...
        复制代码

    • 处置惩罚节点 B

      • stack 包含 A,不包含 B → 继续。
      • 获取 B 的被调用方法聚集 {C}。
      • 将 B 到场 stack(当前路径:[A, B])。
      • 递归处置惩罚子节点 C:
        1.        if (!Files.exists(Paths.get("passthrough.dat")) && ConfigHelper.taintTrack) {
        2.            LOGGER.info("Analyzing methods for passthrough dataflow...");
        3.            PassthroughDiscovery passthroughDiscovery = new PassthroughDiscovery();
        4.            //记录参数在方法调用链中的流动关联(如:A、B、C、D四个方法,调用链为A->B B->C C->D,其中参数随着调用关系从A流向B,在B调用C过程中作为入参并随着方法结束返回,最后流向D)
        5.            //该方法主要是追踪上面所说的"B调用C过程中作为入参并随着方法结束返回",入参和返回值之间的关联
        6.            passthroughDiscovery.discover(classResourceEnumerator, config);
        7.            passthroughDiscovery.save();
        8.        }
        复制代码

    • 处置惩罚节点 C

      • stack 包含 A, B,不包含 C → 继续。
      • 获取 C 的被调用方法聚集 {}(无子节点)。
      • 将 C 到场 visitedNodes 和 sortedMethods:
        1.    public void discover(final ClassResourceEnumerator classResourceEnumerator, final GIConfig config) throws IOException {
        2.        //加载文件记录的所有方法信息
        3.        Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();
        4.        //加载文件记录的所有类信息
        5.        Map<ClassReference.Handle, ClassReference> classMap = DataLoader.loadClasses();
        6.        //加载文件记录的所有类继承、实现关联信息
        7.        InheritanceMap inheritanceMap = InheritanceMap.load();
        复制代码
      • 返回处置惩罚 B。

    • 回溯节点 B

      • 从 stack 中移除 B(当前路径:[A])。
      • 将 B 到场 visitedNodes 和 sortedMethods:
        1.  //搜索方法间的调用关系,缓存至methodCalls集合,返回 类名->类资源 映射集合
        2.        Map<String, ClassResourceEnumerator.ClassResource> classResourceByName = discoverMethodCalls(classResourceEnumerator);
        复制代码
      • 处置惩罚 B 的下一个子节点(无剩余节点),返回处置惩罚 A。

    • 处置惩罚节点 A 的第二个子节点 D

      • 将 D 到场 stack(当前路径:[A, D])。
      • 递归处置惩罚 D:
        1.    private Map<String, ClassResourceEnumerator.ClassResource> discoverMethodCalls(final ClassResourceEnumerator classResourceEnumerator) throws IOException {
        2.        Map<String, ClassResourceEnumerator.ClassResource> classResourcesByName = new HashMap<>();
        3.        for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
        4.            try (InputStream in = classResource.getInputStream()) {
        5.                ClassReader cr = new ClassReader(in);
        6.                  try {
        7.                    MethodCallDiscoveryClassVisitor visitor = new MethodCallDiscoveryClassVisitor(Opcodes.ASM6);
        8.                    cr.accept(visitor, ClassReader.EXPAND_FRAMES);
        9.                    classResourcesByName.put(visitor.getName(), classResource);
        10.                } catch (Exception e) {
        11.                    LOGGER.error("Error analyzing: " + classResource.getName(), e);
        12.                }
        13.            }
        14.        }
        15.        return classResourcesByName;
        16.    }
        复制代码

    • 处置惩罚节点 D

      • 获取 D 的被调用方法聚集 {}(无子节点)。
      • 将 D 到场 visitedNodes 和 sortedMethods:
        1.         @Override
        2.         public void visit(int version, int access, String name, String signature,
        3.                           String superName, String[] interfaces) {
        4.             super.visit(version, access, name, signature, superName, interfaces);
        5.             if (this.name != null) {
        6.                 throw new IllegalStateException("ClassVisitor already visited a class!");
        7.             }
        8.             this.name = name;
        9.         }
        复制代码
      • 返回处置惩罚 A。

    • 回溯节点 A

      • 从 stack 中移除 A(当前路径:[])。
      • 将 A 到场 visitedNodes 和 sortedMethods:
        1.       @Override
        2.        public MethodVisitor visitMethod(int access, String name, String desc,
        3.                                         String signature, String[] exceptions) {
        4.            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        5.            //在visit每个method的时候,创建MethodVisitor对method进行观察
        6.            MethodCallDiscoveryMethodVisitor modelGeneratorMethodVisitor = new MethodCallDiscoveryMethodVisitor(
        7.                    api, mv, this.name, name, desc);
        8.            return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);
        9.        }
        复制代码



    • 污点分析顺序

      • 先分析 C(无依赖),确定其污点传播规则。
      • 分析 B(依赖 C),利用 C 的结果。
      • 分析 D(无依赖)。
      • 最后分析 A(依赖 B 和 D),确保所有被调用方法已处置惩罚。

    若存在循环调用(如 A → B → A):

    • 处置惩罚 A → B → A 时,第二次进入 A 的递归:

      • stack 包含 A → 触发 if (stack.contains(node)) return;
      • 停止递归,避免死循环。

    逆拓扑排序后,接下来就是对方法参数和返回值之间污染关系的分析:

  1. // MethodCallDiscoveryClassVisitor的构造函数
  2. MethodCallDiscoveryClassVisitor visitor = new MethodCallDiscoveryClassVisitor(Opcodes.ASM6);
  3. //父类ClassVisitor的构造函数
  4.  public ClassVisitor(final int api) {
  5.    this(api, null);
  6.   }
  7.   public ClassVisitor(final int api, final ClassVisitor classVisitor) {
  8.    if (api != Opcodes.ASM6
  9.        && api != Opcodes.ASM5
  10.        && api != Opcodes.ASM4
  11.        && api != Opcodes.ASM7_EXPERIMENTAL) {
  12.      throw new IllegalArgumentException();
  13.    }
  14.    this.api = api;
  15.    this.cv = classVisitor;
  16.   }
  17. //父类ClassVisitor的visitMethod方法
  18. public MethodVisitor visitMethod(
  19.      final int access,
  20.      final String name,
  21.      final String descriptor,
  22.      final String signature,
  23.      final String[] exceptions) {
  24.    if (cv != null) {
  25.      return cv.visitMethod(access, name, descriptor, signature, exceptions);
  26.    }
  27.    return null;
  28.   }
复制代码
跟进calculatePassthroughDataflow,起首会遍历sortedMethods,如果是静态初始化代码,即静态代码块,就直接跳过,由于静态代码块是在类加载的时候就加载到JVM当中,我们一般没有办法在步伐运行中进行控制
  1.        public MethodCallDiscoveryMethodVisitor(final int api, final MethodVisitor mv,
  2.                                           final String owner, String name, String desc) {
  3.            super(api, mv);
复制代码
接下来就是对当前所遍历的方法的所属类进行ASM观察:
  1.          
  2. // private final Map<MethodReference.Handle, Set<MethodReference.Handle>> methodCalls = new HashMap<>();
  3.            this.calledMethods = new HashSet<>();
  4.            methodCalls.put(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc), calledMethods);
  5.        }
复制代码
跟进visitor逻辑,查察visit方法,visit方法会判断当前观察的类是否是要准备观察方法的所属类
  1. @Override
  2. public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
  3.    calledMethods.add(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc));
  4.    super.visitMethodInsn(opcode, owner, name, desc, itf);
  5. }
复制代码
接着看visitMethod,我们必要观察的类中的方法只必要是sortedMethod中的方法即可,也就是传入进来的methodToVisit,其他方法是不存在调用关系的:
  1. return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);
复制代码
接下来是对方法进行更细致的观察,依旧看封装后的PassthroughDataflowMethodVisitor
  1. classResourcesByName.put(visitor.getName(), classResource);
复制代码
下面作者用代码模拟了方法调用的过程,从而在模拟的局部变量表(污点变量表)中对参数进行污点标志。我们先往返顾JVM在进行方法调用时都做了哪些变乱。假设现在A方法中要调用B方法,那么此时我们是在A方法内部的,那么JVM中会有A方法的栈帧,栈帧中主要两部分,一个是局部变量表,一个是操作数栈,当A方法内部准备调用B方法时,会先将要传给B方法的参数生存到A方法栈帧的操作数栈上,此时JVM会为B方法创建其对应的栈帧,然后在A方法操作数栈上的参数会被弹到B方法栈帧的局部变量表中。B方法内部利用这些参数时,会通过LOAD指令将其从局部变量表加载到操作数栈上,再进行利用。这里的思想就是用代码去模仿JVM的行为,从而将JVM的方法调用流程可视化。
下面的分析过程基于如下例子,这一段代码调用包含了入参与返回结果雷同,返回结果与入参有关的情况,我们分别来看:
  1. List<MethodReference.Handle> sortedMethods = topologicallySortMethodCalls();
复制代码
逆拓扑排序后的结果为:
  1. A → B → C
  2. A → D
复制代码
A.method1

因此我们先从A.method1来进行分析:
这里我们看到visitCode方法,在进入方法的第一时间,ASM会先调用这个方法。对于非静态方法来说,方法参数插槽的第一个0号位位this,对于静态方法,0号位为参数,所以这里将方法内的所有参数生存在一个利用Java代码模拟的局部变量表中,localIndex为参数在局部变量表中的位置,由于参数的类型差别,所以其在局部变量表中占用的大小也差别。而argIndex对应了参数在方法中的索引,通过setLocalTaint方法,形成了局部变量表与方法参数索引之间的映射关系
  1. outgoingReferences = {
  2.     A: {B, D},
  3.     B: {C},
  4.     C: {},
  5.     D: {}
  6. }
复制代码
接下来执行A.method1方法内部逻辑时(即return param),要将局部变量表中的参数通过ALOAD指令读取到操作数栈上,继续模拟,在检测到ALOAD指令时(包罗其他访问局部变量表的指令),会回调visitVarInsn,将参数push到模拟的污点栈上,这里的参数可以看到是列表localVars的值,也就是局部变量表中对应的参数索引
  1. dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, A);
复制代码
接下来判断方法是否是构造器,如果是构造器的话意味着在当前调用方法(B.method2)当中会有这么一段代码:
  1. dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, B);
复制代码
因此可以确定被调用方法(C.method3)的返回值结果受到了this(C类实例对象)的污染,那么将argTaint中的0号索引取出,即为this,并将其到场resultTaint。如果不是构造器,那么就创造一个空的HashSet来存储后面的resultTaint。
从passthroughDataflow中拿到被调用方法C.method3的参数与返回值污点分析关系,并判断污点分析关系中的参数是否在当前的argTaint中,如果在则说明被调用方法的返回值被调用者传入的参数污染,这也就是为什么要进行逆拓扑排序。
  1. dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, C);
复制代码
最后还是return,将B.method2的结果存到passthroughDataflow中
</ol>main方法
  1. visitedNodes = {C}, sortedMethods = [C]
复制代码
第一步,执行visitCode存储入参到局部变量表
第二步,执行visitVarInsn参数入栈
第三步,执行visitMethodInsn调用A.method1,A.method1被污染的返回结果,也就是参数索引会被放在栈顶
第四步,执行visitVarInsn把放在栈顶的污染参数索引,放入到本地变量表
第五步,执行visitVarInsn参数入栈
第六步,执行visitMethodInsn调用B.method2,被污染的返回结果会被放在栈顶
第七步,执行visitInsn,返回栈顶数据,缓存到passthroughDataflow,也就是main方法的污点分析结果
最后通过passthroughDiscovery.save方法生存分析数据
  1. visitedNodes = {C, B}, sortedMethods = [C, B]
复制代码
最后持久化的passthrough.dat文件的数据格式如下:
  1. dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, D);
复制代码
0x05 方法调用污染关联-CallGraphDiscovery

我们用这个例子进行分析:
  1. visitedNodes = {C, B, D}, sortedMethods = [C, B, D]
复制代码
跟进callGraphDiscovery.discover,读取前面收集的数据,然后利用ModelGeneratorClassVisitor进行观察,visitCode观察每一个类,visitMethod观察类中的每一个方法,继续跟进ModelGeneratorMethodVisitor
  1. visitedNodes = {C, B, D, A}, sortedMethods = [C, B, D, A]
复制代码
进入main方法内部,触发visitCode,main方法不是静态,将this以及参数args存入局部变量表,此处与前面差别的是会在参数索引前加一个arg前缀来进行标识:
  1. passthroughDataflow = calculatePassthroughDataflow(classResourceByName, classMap, inheritanceMap, sortedMethods,
  2.                config.getSerializableDecider(methodMap, inheritanceMap));
复制代码
我在写到这里的时候有一点疑问,对于visitVarInsn的调用时机。我们来看如下两个例子:
  1.   final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = new HashMap<>();
  2.         //遍历所有方法,然后asm观察所属类,经过前面DFS的排序,调用链最末端的方法在最前面
  3.         for (MethodReference.Handle method : sortedMethods) {
  4.             //跳过static静态初始化代码
  5.             if (method.getName().equals("<clinit>")) {
  6.                 continue;
  7.             }
复制代码
我们先来看第一个例子,new A()的字节码指令大概如下,可以看到是没有LOAD指令的,在调用构造方法时直接消耗的是操作数栈上的A对象引用:
  1.   ClassResourceEnumerator.ClassResource classResource = classResourceByName.get(method.getClassReference().getName());
  2.             try (InputStream inputStream = classResource.getInputStream()) {
  3.                 ClassReader cr = new ClassReader(inputStream);
  4.                 try {
  5.                     PassthroughDataflowClassVisitor cv = new PassthroughDataflowClassVisitor(classMap, inheritanceMap,
  6.                             passthroughDataflow, serializableDecider, Opcodes.ASM6, method);
  7.                     cr.accept(cv, ClassReader.EXPAND_FRAMES);
复制代码
接下来由于要把对象引用存到a中,因此会把对象引用存储到局部变量表中(假设在局部变量表2号位,局部变量表1号位存储args),即ASTORE指令,此时会触发一次visitVarInsn。那么接下来在调用a.method1(args)时必要进行两次ALOAD,起首把a的对象引用加载到操作数栈上,再把args加载到操作数栈上,从而接着触发了两次visitVarInsn
  1.    @Override
  2.         public void visit(int version, int access, String name, String signature,
  3.                           String superName, String[] interfaces) {
  4.             super.visit(version, access, name, signature, superName, interfaces);
  5.             this.name = name;
  6.             //不是目标观察的class跳过
  7.             if (!this.name.equals(methodToVisit.getClassReference().getName())) {
  8.                 throw new IllegalStateException("Expecting to visit " + methodToVisit.getClassReference().getName() + " but instead got " + this.name);
  9.             }
  10.         }
复制代码
继续我们看第二个例子,当构造函数执行完毕后,不必要进行ASTORE,并且再调用method1时也不必要从局部变量表中加载a的对象引用,因此最终只有加载args时才会调用一次visitVarInsn
  1.     //不是目标观察的method需要跳过,上一步得到的method都是有调用关系的method才需要数据流分析
  2.             if (!name.equals(methodToVisit.getName()) || !desc.equals(methodToVisit.getDesc())) {
  3.                 return null;
  4.             }
复制代码
检测到字节码指令new,触发visitTypeInsn,会push一个空的HashSet到污点栈中:
  1. MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
  2.             passthroughDataflowMethodVisitor = new PassthroughDataflowMethodVisitor(
  3.                     classMap, inheritanceMap, this.passthroughDataflow, serializableDecider,
  4.                     api, mv, this.name, access, name, desc, signature, exceptions);
复制代码
字节码指令INVOKESPECIALA.()V,调用A的构造器,触发visitMethodInsn,判断是否是构造器,被调用方法为构造器,将this设置为argTypes第一个参数:
  1. public class Main {
  2.   public String main(String args) throws IOException {
  3.     String cmd = new A().method1(args);
  4.     return new B().method2(cmd);
  5.   }
  6. }
  7. class A {
  8.   public String method1(String param) {
  9.     return param;
  10.   }
  11. }
  12. class B {
  13.   public String method2(String param) {
  14.     return new C().method3(param);
  15.   }
  16. }
  17. class C {
  18.   public String method3(String param) {
  19.     return param;
  20.   }
  21. }
复制代码
jiee下来检测启动工具时参数是否要进行污点分析,如果不进行污点分析,则直接把调用方法以及被调用方法封装为GraphCall,到场discoveredCalls中:
  1. A.method1
  2. C.method3
  3. B.method2
  4. main
复制代码
启动污点分析后的逻辑接着往下看,会从污点栈中取出对应的参数,但我们这里由于没有进入到visitVarInsn,因此污点栈现在只有一个在visitInsn中push进去的一个空的set,这一步不会对discoverdCalls做任何变乱
接着我们分析method1(args,name)的调用情况,起首必要加载args,触发visitVarInsn,ALOAD指令,将args(arg1)推入污点栈,然后调用visitMethodInsn。由于要传递的参数name是a的属性,因此必要加载this,从this中拿到name属性。触发ALOAD指令,将this(arg0)推入污点栈。此时污点栈中为如下内容:
  1.        @Override
  2.        public void visitCode() {
  3.            super.visitCode();
  4.            int localIndex = 0;
  5.            int argIndex = 0;
  6.            if ((this.access & Opcodes.ACC_STATIC) == 0) {
  7.                //非静态方法,第一个局部变量应该为对象实例this
  8.                //添加到局部变量表集合
  9.                setLocalTaint(localIndex, argIndex);
  10.                localIndex += 1;
  11.                argIndex += 1;
  12.            }
  13.            for (Type argType : Type.getArgumentTypes(desc)) {
  14.                //判断参数类型,得出变量占用空间大小,然后存储
  15.                setLocalTaint(localIndex, argIndex);
  16.                localIndex += argType.getSize();
  17.                argIndex += 1;
  18.            }
  19.        }
  20.        
  21.           protected void setLocalTaint(int index, T ... possibleValues) {
  22.        Set<T> values = new HashSet<T>();
  23.        for (T value : possibleValues) {
  24.            values.add(value);
  25.        }
  26.        savedVariableState.localVars.set(index, values);
  27.    }
  28.        
复制代码
接下来必要读入实例a的name字段,检测到字节码指令GETFIELD,触发visitFieldInsn,起首在ClassReference中不绝遍历,直到找到该字段,判断该字段是否是transient,如果是transient就没必要到场污点栈。如果是非transient属性,就把栈顶当前的arg0修改为arg0.name到场污点栈中
  1. @Override
  2.    public void visitVarInsn(int opcode, int var) {
  3.        // Extend local variable state to make sure we include the variable index
  4.        for (int i = savedVariableState.localVars.size(); i <= var; i++) {
  5.            savedVariableState.localVars.add(new HashSet<T>());
  6.        }
  7.        //变量操作,var为操作的本地变量索引
  8.        Set<T> saved0;
  9.        switch(opcode) {
  10.            case Opcodes.ILOAD:
  11.            case Opcodes.FLOAD:
  12.                push();
  13.                break;
  14.            case Opcodes.LLOAD:
  15.            case Opcodes.DLOAD:
  16.                push();
  17.                push();
  18.                break;
  19.            case Opcodes.ALOAD:
  20.                //从局部变量表取出变量数据入操作数栈,这个变量数据可能是被污染的
  21.                push(savedVariableState.localVars.get(var));
  22.                break;
  23.            case Opcodes.ISTORE:
  24.            case Opcodes.FSTORE:
  25.                pop();
  26.                savedVariableState.localVars.set(var, new HashSet<T>());
  27.                break;
  28.            case Opcodes.DSTORE:
  29.            case Opcodes.LSTORE:
  30.                pop();
  31.                pop();
  32.                savedVariableState.localVars.set(var, new HashSet<T>());
  33.                break;
  34.            case Opcodes.ASTORE:
  35.                //从栈中取出数据存到局部变量表,这个数据可能是被污染的(主要还是得看调用的方法,返回值是否可被污染)
  36.                saved0 = pop();
  37.                savedVariableState.localVars.set(var, saved0);
  38.                break;
  39.            case Opcodes.RET:
  40.                // No effect on stack
  41.                break;
  42.            default:
  43.                throw new IllegalStateException("Unsupported opcode: " + opcode);
  44.        }
  45.        super.visitVarInsn(opcode, var);
  46.        sanityCheck();
  47.    }
  48.    private void push(Set<T> possibleValues) {
  49.        // Intentionally make this a reference to the same set
  50.        savedVariableState.stackVars.add(possibleValues);
  51.    }
复制代码
非静态方法,argTypes第一个为A(this),第二个为String(args),第三个为String(name),对应了污点栈上的[{},{"arg1"}, {"arg0"} ](从左到右为栈底到栈顶),for循环i从0到2,分别从污点栈中拿到了arg0.name,arg1和空set。起首对arg0.name进行拆解,最终拆解出来dotIndex为4,srcArgIndex为0,srcArgPath为name,并记载到了discoverdCalls当中。继续拆解arg1,dotindex为-1,srcArgIndexn为1,srcArgPath为null,记载到discoverdCalls中。
  1.         @Override
  2.         public void visitInsn(int opcode) {
  3.             switch(opcode) {
  4.                 case Opcodes.IRETURN://从当前方法返回int
  5.                 case Opcodes.FRETURN://从当前方法返回float
  6.                 case Opcodes.ARETURN://从当前方法返回对象引用
  7.                     returnTaint.addAll(getStackTaint(0));//栈空间从内存高位到低位分配空间
  8.                     break;
  9.                 case Opcodes.LRETURN://从当前方法返回long
  10.                 case Opcodes.DRETURN://从当前方法返回double
  11.                     returnTaint.addAll(getStackTaint(1));
  12.                     break;
  13.                 case Opcodes.RETURN://从当前方法返回void
  14.                     break;
  15.                 default:
  16.                     break;
  17.             }
  18.             super.visitInsn(opcode);
  19.         }
复制代码
最后save生存数据,持久化后的callgraph.dat格式如下:
  1.         final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = new HashMap<>();
  2. passthroughDataflow.put(method, cv.getReturnTaint());
复制代码
0x06 利用链入口搜索-SourceDiscovery

在一开始我们也说到了,在挖掘反序列化链的时候必要指定类型,所以此处先获得对应的sourceDiscovery,我们这里以Jackson反序列化分析
  1. class B {
  2.   public String method2(String param) {
  3.     return new C().method3(param);
  4.   }
  5. }
复制代码
跟进SourceDiscovery.discover在jackson中的实现,可以发现对于Jackson反序列化来说,source必要判断方法是否是无参构造、setter和getter,只有这些方法才能作为jackson反序列化的入口:
  1.    @Override
  2.    public void discover(Map<ClassReference.Handle, ClassReference> classMap,
  3.                         Map<MethodReference.Handle, MethodReference> methodMap,
  4.                         InheritanceMap inheritanceMap, Map<MethodReference.Handle, Set<GraphCall>> graphCallMap) {
  5.        final JacksonSerializableDecider serializableDecider = new JacksonSerializableDecider(methodMap);
  6.        for (MethodReference.Handle method : methodMap.keySet()) {
  7.            if (skipList.contains(method.getClassReference().getName())) {
  8.                continue;
  9.            }
  10.            if (serializableDecider.apply(method.getClassReference())) {
  11.                if (method.getName().equals("<init>") && method.getDesc().equals("()V")) {
  12.                    addDiscoveredSource(new Source(method, 0));
  13.                }
  14.                if (method.getName().startsWith("get") && method.getDesc().startsWith("()")) {
  15.                    addDiscoveredSource(new Source(method, 0));
  16.                }
  17.                if (method.getName().startsWith("set") && method.getDesc().matches("\\(L[^;]*;\\)V")) {
  18.                    addDiscoveredSource(new Source(method, 0));
  19.                }
  20.            }        }    }
复制代码
最后还是将方法生存持久化为sources.dat,格式如下:
  1.                     final List<Set<Integer>> argTaint = new ArrayList<Set<Integer>>(argTypes.length);
  2.                     for (int i = 0; i < argTypes.length; i++) {
  3.                         argTaint.add(null);
  4.                     }
  5.                     int stackIndex = 0;
  6.                     for (int i = 0; i < argTypes.length; i++) {
  7.                         Type argType = argTypes[i];
  8.                         if (argType.getSize() > 0) {
  9.                             //根据参数类型大小,从栈顶获取入参,参数入栈是从左到右的
  10.                             argTaint.set(argTypes.length - 1 - i, getStackTaint(stackIndex + argType.getSize() - 1));
  11.                         }
  12.                         stackIndex += argType.getSize();
  13.                     }
复制代码
0x07 gadgetChain挖掘-GadgetChainDiscovery

跟进GadgetChainDiscovery.discover,起首进行所有重写方法的扫描,在一开始我们也说了工具没有办法在运行时进行扫描,所以对于各种方法的重写我们没有办法确定到底调用的是哪个方法
  1. C c = new C();
复制代码
跟进InheritanceDeriver.getAllMethodImplementations,获取之前收集到的method的类,并通过之前收集到的继承关系来获取类的所有子孙类,最终形成类->子孙类的映射关系:
  1.                    Set<Integer> passthrough = passthroughDataflow.get(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc));
  2.                    if (passthrough != null) {
  3.                        for (Integer passthroughDataflowArg : passthrough) {
  4.                            //判断是否和同一方法体内的其它方法返回值关联,有关联则添加到栈底,等待执行return时保存
  5.                            resultTaint.addAll(argTaint.get(passthroughDataflowArg));
  6.                        }
复制代码
接下来遍历所有的方法,并遍历subclasses,如果某一个subclass中存在与当前遍历的方法名和返回值一致的方法,就将其到场overridingMethods,最后整合所有重写的方法,形成方法名到重写方法之间的映射关系,由于静态方法不可重写,因此遇到静态方法直接跳过:
  1. public class Main {
  2.  public String main(String args) throws IOException {
  3.    String cmd = new A().method1(args);
  4.    return new B().method2(cmd);
  5.   }
  6. }
复制代码
然后下面的一大堆逻辑就是对重写方法关系的持久化存储,最终的methodimpl.dat格式如下:
  1. public static class PassThroughFactory implements DataFactory<Map.Entry<MethodReference.Handle, Set<Integer>>> {
  2.    ...
  3.    @Override
  4.    public String[] serialize(Map.Entry<MethodReference.Handle, Set<Integer>> entry) {
  5.        if (entry.getValue().size() == 0) {
  6.            return null;
  7.        }
  8.        final String[] fields = new String[4];
  9.        fields[0] = entry.getKey().getClassReference().getName();
  10.        fields[1] = entry.getKey().getName();
  11.        fields[2] = entry.getKey().getDesc();
  12.        StringBuilder sb = new StringBuilder();
  13.        for (Integer arg : entry.getValue()) {
  14.            sb.append(Integer.toString(arg));
  15.            sb.append(",");
  16.        }
  17.        fields[3] = sb.toString();
  18.        return fields;
  19.    }
  20. }
复制代码
接下来对callgraph.dat的调用关系进行整合,对于同一个方法发起的调用,整合成caller->被调用方法聚集之间的映射关系:
  1. 类名 方法名 方法描述 能污染返回值的参数索引1,能污染返回值的参数索引2,能污染返回值的参数索引3...
复制代码
剩下的挖掘逻辑我们用一个例子来分析:
设我们有如下方法间调用:

  • :A.sources() 污染参数 0
  • A.sources(0) → 调用 B.load(0)
  • B.load(0) → 调用接口方法 C.handle(0)
  • C.handle(0) 在实现类 CImpl 中有实现 CImpl.handle(0)
  • CImpl.handle(0) → 调用 D.sink(1)(这里假设它把参数 1 污染到 sink)
  • D.sink(1) 是最终的 sink
对应的数据结构:

  • sources.dat 只包含一个 Source(A.sources, taintedArgIndex=0)
  • graphCallMap
    1. public class Main {
    2.  private String name;
    3.  public void main(String args) throws IOException {
    4.    new A().method1(args, name);
    5.   }
    6. }
    7. class A {
    8.  public String method1(String param, String param2) {
    9.    return param + param2;
    10.   }
    11. }
    复制代码
  • implementationFinder.getImplementations(C.handle) → { CImpl.handle }
  • isSink(D.sink,1) → true
对于是否为sink点的判断逻辑如下:
  1.        @Override
  2.        public MethodVisitor visitMethod(int access, String name, String desc,
  3.                                         String signature, String[] exceptions) {
  4.            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
  5.            ModelGeneratorMethodVisitor modelGeneratorMethodVisitor = new ModelGeneratorMethodVisitor(classMap,
  6.                    inheritanceMap, passthroughDataflow, serializableDecider, api, mv, this.name, access, name, desc, signature, exceptions);
  7.            return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);
  8.        }
复制代码
配置参数:
  1.        public void visitCode() {
  2.            super.visitCode();
  3.            int localIndex = 0;
  4.            int argIndex = 0;
  5.            //使用arg前缀来表示方法入参,后续用于判断是否为目标调用方法的入参
  6.            if ((this.access & Opcodes.ACC_STATIC) == 0) {
  7.                setLocalTaint(localIndex, "arg" + argIndex);
  8.                localIndex += 1;
  9.                argIndex += 1;
  10.            }
  11.            for (Type argType : Type.getArgumentTypes(desc)) {
  12.                setLocalTaint(localIndex, "arg" + argIndex);
  13.                localIndex += argType.getSize();
  14.                argIndex += 1;
  15.            }
  16.        }
复制代码
1️⃣ 初始化
  1. // example 1
  2. A a = new A();
  3. a.method1(args);
  4. //example 2
  5. new A().method1(args);
复制代码
2️⃣ 第一次迭代

  • iteration=0 → pop first chain
    1. NEW A //创建A类实例
    2. DUP   //创建对象引用
    3. INVOKESPECIAL A.<init>()V //调用构造方法
    复制代码
  • 长度检查:1 < maxChainLength → 通过
  • 取出 graphCallMap.get(A.sources) → { GC1 }

    • GC1: (callerArgIndex=0 → targetMethod=B.load, targetArgIndex=0)

  • taintTrack:GC1.callerArgIndex(0) == lastLink.taintedArgIndex(0) → 通过
  • 找实现:allImpls = getImpls(B.load) → { B.load }(普通方法)
  • 遍历 impls

    • methodImpl = B.load
    • newLink = (B.load,0)
    • 去重:exploredMethods 不含 → 继续
    • 新链:newChain = [ A.sources(0), B.load(0) ]
    • sink 检测:isSink(B.load,0) → false
    • 到场队列
      1. NEW A
      2. DUP
      3. INVOKESPECIAL A.<init>()V
      4. ASTORE 2        // 存到局部槽 2 —> visitVarInsn(ASTORE,2)
      5. ALOAD 2         // 再加载回来 —> visitVarInsn(ALOAD,2)
      6. ALOAD 1         // 加载 args  —> visitVarInsn(ALOAD,1)
      7. INVOKEVIRTUAL A.method1…
      复制代码

3️⃣ 第二次迭代

  • iteration=1 → pop
    1. NEW A
    2. DUP
    3. INVOKESPECIAL A.<init>()V
    4. // 上一步执行完 new A(),操作数栈上已经有了 A 的实例
    5. ALOAD 1          // 将 args(槽 1)加载到栈顶 — 触发一次 visitVarInsn(AL OAD,1)
    6. INVOKEVIRTUAL A.method1:(Ljava/lang/String;)Ljava/lang/Strin
    复制代码
  • graphCallMap.get(B.load) → { GC2 }

    • GC2: (callerArgIndex=0 → targetMethod=C.handle, targetArgIndex=0)

  • taintTrack:匹配 → 通过
  • impls:getImpls(C.handle) → { },fallback 父类查找也无(接口),所以按注释 “GadgetInspector bug”,跳到父类去搜,依次找到 C.handle 本身,到场。
  • impls 变为 → { C.handle }
  • for each impl

    • newLink = (C.handle,0)
    • 去重通过
    • newChain = [A.sources(0),B.load(0),C.handle(0)]
    • isSink(C.handle,0) → false
    • 到场:
      1.   @Override
      2.    public void visitTypeInsn(int opcode, String type) {
      3.        switch(opcode) {
      4.            case Opcodes.NEW:
      5.                push();
      6.                break;
      7.            case Opcodes.ANEWARRAY:
      8.                pop();
      9.                push();
      10.                break;
      11.            case Opcodes.CHECKCAST:
      12.                // No-op
      13.                break;
      14.            case Opcodes.INSTANCEOF:
      15.                pop();
      16.                push();
      17.                break;
      18.            default:
      19.                throw new IllegalStateException("Unsupported opcode: " + opcode);
      20.        }
      复制代码

4️⃣ 第三次迭代

  • chain = [A.sources(0),B.load(0),C.handle(0)]
  • graphCallMap.get(C.handle) → { GC3 }

    • GC3: (callerArgIndex=0 → targetMethod=C.handle, targetArgIndex=0) // 发自实现类

  • taintTrack:匹配
  • impls:getImpls(C.handle) → { CImpl.handle }
  • for each

    • newLink = (CImpl.handle,0)
    • 去重通过
    • newChain = [A.sources(0),B.load(0),C.handle(0),CImpl.handle(0)]
    • isSink(CImpl.handle,0) → false
    • 入队 & 到场 exploredMethods

5️⃣ 第四次迭代

  • chain = [ …, CImpl.handle(0)]
  • graphCallMap.get(CImpl.handle) → { GC4 }

    • GC4: (callerArgIndex=0 → targetMethod=D.sink, targetArgIndex=1)

  • taintTrack:匹配
  • impls:getImpls(D.sink) → { D.sink }
  • for each

    • newLink = (D.sink,1)
    • 去重通过
    • newChain = [ …, CImpl.handle(0), D.sink(1)]
    • isSink(D.sink,1) → true
    • 到场 discoveredGadgets

此时 methodsToExplore 可能为空,循环结束。
接下来进行链路聚合优化
  1.            Type[] argTypes = Type.getArgumentTypes(desc);
  2.            if (opcode != Opcodes.INVOKESTATIC) {
  3.                Type[] extendedArgTypes = new Type[argTypes.length+1];
  4.                System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
  5.                extendedArgTypes[0] = Type.getObjectType(owner);
  6.                argTypes = extendedArgTypes;
  7.            }
复制代码

  • 比如如果我们由于 opLevel 限制,把某条中间链放进了 methodsToExploreRepeat 而没睁开到 sink,那么这段逻辑就能 把这些中途链 主动补全到 已知的完整 Chain,得到更多发现。
更多网安技能的在线实操训练,请点击这里>>
  

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

莱莱

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表