[备份]深入分析GadgetInspector核心代码

笔者是大四学生,初涉安全的萌新,如果文章有错误之处还请大佬指出!

最近本来在做审计相关的事情,遇到GadgetInspector比较感兴趣,于是做了些深入的分析

 

1 前言

1.1 简介

GadgetInspector是Black Hat 2018提出的一个Java反序列化利用链自动挖掘工具,核心技术的Java ASM,结合字节码的静态分析。根据输入JAR包和JDK已有类进行分析,最终得到利用链

本文的核心是:深入分析数据流模块(PassthroughDataflow)的每一句ASM代码,进而把握最底层的原理

 

1.2 整体流程

整个流程第一步是根据JDK和输入的Jar得到所有的字节码,然后通过MethodDiscovery分析,参考第2,3章。获取所有的方法信息,类信息和继承信息。继承关系InheritanceMap指某个类的父类和实现的接口都有哪些

 

第二步是本文的核心,数据流分析确定:方法的参数和返回值之间的关系。利用第一步获得信息得到方法中的方法调用,结合InheritanceMap的继承关系,将所有方法进行拓扑逆排序(参考8.7)实现最先调用的方法在最前端。然后利用PassthroughDiscovery得到每个方法的参数和返回值之间的关系,也就是返回值能够被哪些参数污染

PassthroughDiscovery的底层是TaintTrakingMethodVisitor,这个类是该项目的核心,参考第5节,他模拟了JVM Stack Frame中的Operand StackLocal Variables Array让代码“”起来,进而根据方法调用流程拿到具体的结果passthroughDataflow,参考第6,7和8节。这个结果从一开始是最底层的调用,所以他的第一步结果可以被第二步分析使用

 

后续利用上文模拟的机制,生成调用图(CallGraph)后结合漏洞触发入口(readObject等)得到discoveredSources,主要保存了方法入口和污染参数信息。在最后一步和之前所有信息合并

 

1.3 加载

主要是区分了SpringBoot Fat JarJar LibWar三种方式:

 

加载字节码的核心方法来自guava库的ClassPath.from(classLoader).getAllClasses()

 

最终获得的都是ClassLoader对象,然后统一获得所有字节码文件

 

加载JDK的rj.jar代码如下,利用String类拿到rt.jar的路径,构造URLClassLoader然后加载

 

经过测试,ClassPath.from方式拿到Jar的class文件是包含rt.jar的,大概在三万多。如果jar数量多,会出现大量的重复,造成不小的性能问题,是否存在一种方式可以直接拿到rt.jar和输入jar的所有class文件的inputStream并且不重复?(已实现,后续开源)

当然,也可能是笔者本地调试的问题,由于一些特殊原因导致出现大量的重复,这点不是文章的重点,顺便提到而已

 

1.4 基础

基础主要是ASM技术的一些基础,需要大致明白ASM如何解析字节码

这里给出ClassVisitMethodVisit的顺序,以便于后续的理解

ClassVisit:大体来看visit->visitAnno->visitField或visitMethod->visitEnd

 

MethodVisit:大体来看visitParam->visitAnno->visitCode->visitFrame或visitXxxInsn->visitMax->visitEnd

 

GadgetInspector模拟了JVM StackFrameOperand StackLocal Variables Array,这一步是基础将在5.1中介绍

 

1.5 杂项

一些细节,比如ClassReference.Handle重写equal

可以看到判断两个类名对象Handle是否相等是根据字符串name做的,因此hashcode只需要根据name

 

处理jar包内的class文件,比较巧妙。可以看到创建了一个临时目录,比如windows在C:\\User\\AppData\\Local\\Temp中。添加一个shutdownHook会在JVM退出的时候调用,在这里面删除这个临时目录。而临时目录中保存的是jar中的所有class文件,用于创建输入流,然后交给ClassReader做分析

 

还有一些小问题,这里就不继续写了,回到重点

 

2 MethodDiscoveryClassVisitor

继承自ASM的ClassVisitor,主要作用是对所有字节码中的类进行观察,下文将根据ASM定义的visit顺序进行分析

 

2.1 visit

 

注意到其中的(access & Opcodes.ACC_INTERFACE) != 0为什么这样可以判断是否为接口,因为Opcode中定义如下,发现每一种标识恰好二进制某一位为1,如果按位与,只要不包含该表示,那么得出结果一定是0

 

2.2 visitAnnotation

注解在整个流程中没有什么实际的意义

 

2.3 visitField

 

2.4 visitMethod

 

2.5 visitEnd

 

2.6 作用

得到所有类和方法信息后,进行分析获取进一步的信息,并保存供后续步骤操作

 

 

3 MethodCallDiscoveryClassVisitor

3.1 visit

 

3.2 visitMethod

 

3.3 作用

MethodCallDiscoveryMethodVisitor一起记录所有方法内的所有方法调用

例如这里的test1test2就是方法内的方法调用

 

4 MethodCallDiscoveryMethodVisitor

4.1 构造

 

4.2 visitMethodInsn

方法中的方法相关指令

 

4.3 作用

MethodCallDiscoveryClassVisitor一起记录方法内的方法调用,参考3.3

 

5 TaintTrackingMethodVisitor

5.1 JVM Frame

分析该类离不开JVM的原理。JVM Stack在每个线程被创建时被创建,用来存放一组栈帧(Frame)

 

每次方法调用均会创建一个对应的Frame,方法执行完毕或者异常终止,Frame被销毁

而每个Frame的结构如下,主要由本地变量数组(local variables)和操作栈(operand stack)组成

 

局部变量表所需的容量大小是在编译期确定下来的,表中的变量只在当前方法调用中有效

JVM把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中

例如:方法调用会从当前的Stack里弹出参数,而弹出的参数就到了新的局部变量表里,执行完返回的时候就得把返回值PUSH回Stack。比如5.4中的visitCode做的事就是将参数放到局部变量表

 

之所以介绍JVM Frame,是因为代码模拟了比较完善的Operand Stack和Local Varialbles交互,例如方法调用会从Stack中弹出参数,方法返回值会压入栈中。根据这样的规则,进而执行数据流的分析

 

5.2 SavedVariableState

在5.1中介绍stacklocal variables因为在TaintTrackingMethodVisitor中自行实现了这样的结构

注意到这里保存的Set集合,实际上代码中要么是空Set和Null做占位,要么保存的是实际有意义的值,也就是污染点

污染点的含义是参数索引,进而分析影响返回值的参数是什么。那为什么要用Set不会数组或List呢?因为Set自带去重,分析代码中会往Stack中设置多次污染信息(见后文分析)

 

5.3 构造

有一些变量将在后文分析

 

5.4 visitCode

最先调用visitCode,在进入方法体的时候

这是数据流动的起始位置,注意到根据实际情况在局部变量表里设置参数,这是模拟JVM真实的情况,以便于后续数据流分析

 

5.5 push & pop & get

模拟Stack的push和pop操作

 

5.6 visitFrame

visitFramevisitCode后调用

主要作用是根据ASM给出“正确”的Frame计算方法同步当前模拟的Stack和局部变量表,确保不出现问题

第一步判断的F_NEW原因可以参考ASM源码: Must be Opcodes.F_NEW for expanded frames

 

几个参数的意义参考ASM文档:

 

5.7 visitXxxInsn

根据opcode和操作数进行push和pop操作,模拟了JVM Frame中的OperandStack操作

 

5.7.1 visitInsn

访问0操作数指令

 

5.7.2 visitIntInsn

单int操作数指令

 

5.7.3 visitVarInsn

加载或存储局部变量值的指令

能够进行数据流动正式因为这一步从局部变量表获得或设置数据,而局部变量表数据是从5.4中获得,形成一个完整的流程

模拟JVM进行PUSH/POP操作

 

5.7.4 visitTypeInsn

以类的内部名称作为参数的指令

 

5.7.5 visitFieldInsn

加载或存储对象字段值的指令

程序在这一步并没有过多的操作,只是简单的POP和PUSH

 

5.7.6 visitMethodInsn

方法调用,比较核心的方法

根据方法调用需要的参数,在Stack中POP,这是对真实情况的模拟

如果是构造方法,那么argTaint第0位的this添加到污染

如果是void ObjectInputStream.defaultReadObject()不传参,这时候对象本身this就是污染,给当前局部变量表第0位设置污染(这种情况下这一步拿不到污染,在后续的数据流中得到污染)

如果目前的方法恰好匹配到白名单(很可能存在漏洞)那么白名单函数的参数位置设置到污染(其实白名单就是简化了分析,固定出了哪些类的哪些函数是存在漏洞的,它的第几个参数是可被污染的,如果匹配到白名单,直接设置该参数即可)

根据已有的passthroughDataflow得到与返回值有关的参数索引Set,加入污染(这一步是外部生成的,也就是Visit其他类的生成的,根据已有信息进行污点设置。参考8.7中的分析,调用链最末端的最优先被分析,因此调用到的方法必然已被visit分析过。由于Set的特性,并不会冲突,只是一次补充的效果)

如果当前类是集合类子类,认为集合中所有元素都是污染,这里不得到结果,只是设置污染,然后继续分析;如果返回对象或数组,认为返回是污染,这个结果是要并入PUSH的污染中的

最后把污染结果入栈,这模拟的就是执行完方法的PUSH返回值(代码第二次的PUSH是为了补位)

 

进一步的分析参考7.5

 

给出一个非STATIC方法调用的图

 

白名单

 

5.7.7 visitInvokeDynamicInsn

动态调用方法

 

5.7.8 visitJumpInsn

跳到其他操作的操作

jump后应该处理Stack和局部变量表的问题

 

5.7.9 visitLabel

标签指定了紧接着它将被访问的指令

 

5.7.10 visitLdcInsn

常量池操作

 

5.7.11 visitTableSwitchInsn

通过索引访问跳转表并跳转

 

5.7.12 visitLookupSwitchInsn

通过键匹配和跳转访问跳转表

任何跳转都需要处理Stack和局部变量表

 

5.7.13 visitMultiANewArrayInsn

创建新的多维数组

 

5.7.14 visitOthers

这部分基本没有业务逻辑,也没有POP/PUSH操作

 

5.8 xxxTaint

一些出入栈和局部变量表的操作

这些方法并不影响Stack的POP/PUSH,只是往已有的位置设置污染信息

 

5.9 couldBeSerialized

是否能被序列化,决策者其实就是一个方法,用来判断什么情况下可以被序列化,什么情况下存在漏洞

 

比如Jackson的决策者

 

5.10 作用

该类模拟了JVM中的operand Stack和Local Varaibles,个人理解相当于把完全静态的代码做成了半动态,结合业务逻辑代码,实现数据流动分析。POP和PUSH的都是空Set,如果分析中认为存在污点,那么就把对应Set位置设为污染

 

6 PassthroughDataflowClassVisitor

该类不是重点,第7条PassthroughDataflowMethodVisitor应重点关注

 

6.1 构造

 

6.2 visit

 

6.3 visitMethod

 

6.4 getReturnTaint

 

6.5 作用

与7结合分析数据流之间的污染

 

7 PassthroughDataflowMethodVisitor

继承自TaintTrackingMethodVisitor

 

7.1 构造

 

7.2 visitCode

在进入方法体的时候调用

父类先清空stack和局部变量表,重新设置局部变量表为正确的值

然后交给子类给该方法每个参数位置设置污染到局部变量表

 

 

7.3 visitInsn

无操作数的操作,注意子类和父类的顺序不可乱

子类:

如果操作是return,将返回值加入到返回污点中

父类:

进行POP/PUSH等正常操作

 

 

7.4 visitFieldInsn

字段(属性)相关的操作

子类:

如果opcode是GETFIELD,这个操作需要从Stack里POP出一个对象再把得到的值PUSH进去

如果这个字段类型包括基类都可以被反序列化,那么目前栈顶的这个对象就是污染

可以看到,先暂存了这个污染对象

父类:

模拟JVM的POP/PUSH操作,这时候栈顶就是PUSH进去的值

根据暂存的污染对象,把目前栈顶的值设置为污染

这样做的目的是能够让污染传递下去,从右边到左边

 

 

7.5 visitMethodInsn

方法的调用需要从Stack里面取参数,最后把返回值压入Stack,参考5.7.6中的图片

关于passthroughDataflow,已经进行DFS排序,调用链最末端最先被visit,因此,调用到的方法必然已被visit分析过(参考三梦师傅)

 

子类做的事:

得到模拟Stack中应该获取的参数,设置到argTaint

如果是构造方法,那么argTaint第0位this就是污染

根据已有的passthroughDataflow得到与返回值有关的参数索引Set,加入污染

父类做的事:

参考5.7.6,根据方法调用需要的参数,在Stack中POP

如果是构造方法,那么argTaint第0位this就是污染

如果是void ObjectInputStream.defaultReadObject()不传参,这时候对象本身this就是污染,给局部变量表第0位设置污染

如果目前的方法恰好匹配到白名单(很可能存在漏洞)那么白名单函数的参数位置设置到污染

根据已有的passthroughDataflow得到与返回值有关的参数索引Set,加入污染

如果当前类是集合类子类,认为集合中所有元素都是污染;如果返回对象或数组,认为返回也是污染

最后把污染结果入栈,这模拟的就是执行完方法的PUSH返回值

子类继续做:

这时候子类取到Stack顶的RETURN值,在父类的污染中再加入子类得到的污染

 

注意:重复添加了很多污染,会不会重复?不会,因为污染是参数的位置int值组成的Set,Set特性是不会重复

 

 

关于这个passthroughDataFlow是个全局变量,是一个缓存,visit一个类就缓存一次

 

7.6 作用

与6结合分析数据流之间的污染,由5 TaintTrackingMethodVisitor作为驱动,模拟JVM执行代码

 

8 PassthroughDiscovery

业务逻辑代码,同样是重点

8.1 属性

 

8.2 discover

这里需要注意一个流程:先DFS排序,然后再进行PassthroughDataflowMethodVisitor的数据流污染分析,分析见8.7

 

8.3 discoverMethodCalls

主要是结合3,4做方法内的方法调用收集,没有什么难度

 

8.4 topologicallySortMethodCalls

核心代码

 

8.5 dfsTsort

遍历集合中的起始方法,进行递归深度优先搜索DFS,实现逆拓扑排序。最终结果是调用链的最末端排在最前面,这样才能实现入参、返回值、函数调用链之间的污点影响(参考三梦师傅)

stack保证了在进行逆拓扑排序时不会形成环,visitedNodes避免了重复排序(参考Longofo师傅)

深入分析参考8.7

 

8.6 calculatePassthroughDataflow

 

8.7 分析

关于逆拓扑排序,参考Longofo师傅的文章,对图片做了一些优化和精简

 

这是一个方法调用关系:

 

在排序中的stack和visited和sorted过程如下:

只要有子方法,就一个个地入栈

到达method7发现没有子方法,那么弹出并加入visited和sorted

回溯上一层,method3还有一个method8子方法,压栈

method8没有子方法,回溯上一层method3也没有,都弹出并进入右侧

到达method6,有子方法,压栈,找到method6下的method1,压栈,注意这里是Set结构不重复,所以压了等于没压

回溯后method6和method2都没有子方法了,弹出并进入右边

往后执行遇到method1的两个子方法method3和method4,由于method3已在visited,直接return,把method4压栈。然后method4没有子方法弹栈,最后剩下的method1也没有子方法,弹栈

最终得到的排序结果就是7836241,达到了最末端在最开始的效果

 

9 参考

原版代码:https://github.com/JackOfMostTrades/gadgetinspector

三梦师傅代码:https://github.com/threedr3am/gadgetinspector

Oracle JVM Doc:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

三梦师傅的文章:https://xz.aliyun.com/t/7058

Longofo师傅的文章:https://paper.seebug.org/1034/