开始啃骨头了。。。
0x01 背景知识
不了解算法原理基础上,去直接啃GI源码,真的是一件很困难的事情,难得有师傅们的经验总结,站在师傅们的肩膀上记录下GI的学习心得。
时间线
- 2018年Black Hat公布GadgetInspector
- 2019年Longofo师傅《Java 反序列化工具 gadgetinspector 初窥》,详细跟踪了除ASM之外的知识点,重点是逆拓扑排序
- 2020年threedr3am《java反序列化利用链自动挖掘工具gadgetinspector源码浅析》,重点分析了Longofo师傅未提到的ASM
- 2022年su18师傅《高效挖掘反序列化漏洞——GadgetInspector改造》,提到了GI的不足以及改进思路
当然,还有其他的许多师傅关于GI的文章,这里没有提到,不赘述。
0x02 算法解析
先抛开复杂的代码,GI的工作流程大致如下:
- 枚举全部类以及每个类的所有方法:MethodDiscovery
- 生成passthrough数据流:PassthroughDiscovery
- 枚举passthrough调用图:CallGraphDiscovery
- 搜索可用的source:SourceDiscovery
- 搜索生成调用链:GadgetChainDiscovery
其中重点的知识点有:
- ASM栈模拟
- PassthroughDiscovery 的函数间污点传播
2.1 MethodDiscovery
枚举全部类以及每个类的所有方法,保存在classes.dat、methods.dat中,分别对应类、方法,结构如下
classes.dat
字段 | Demo |
---|---|
类名 | com/m0d9/sec/AbstractTableModel |
父类名 | java/lang/Object |
所有接口 | java/io/Serializable |
是否是接口 | false |
成员 | __clojureFnMap!2!java/util/HashMap |
methods.dat
字段 | Demo |
---|---|
类名 | com/m0d9/sec/AbstractTableModel |
方法名 | hashCode |
方法描述信息 | ()I |
是否是静态方法 | false |
继承关系表:inheritanceMap.dat
字段 | Demo |
---|---|
类 | com/m0d9/sec/AbstractTableModel |
直接父类+间接父类 | java/lang/Object、java/io/Serializable |
其中的字段结构,Longofo师傅文章里都有解释,这里不赘述。
2.2 PassthroughDiscovery
生成passthrough数据流,这里涉及到 静态分析-污点分析 的知识点了,是GI的第一个重点知识点。
最终生成的
passthrough.dat:
字段 | Demo |
---|---|
类名 | com/m0d9/sec/FnConstant |
方法名 | invokeCall |
方法描述 | (Ljava/lang/Object;)Ljava/lang/Object; |
污点 | 0 |
2.2.1 污点传播函数表示约定
简单来讲,这一步是计算所有的方法的污点参数污播规则。
例如:r = o.m(p0,…)
注意这里的前提:
- 只考虑返回值r的污染情况
那么和以下因素有关
- o,也即调用者,在函数内部也可以理解为this
- p0,…,参数
- m,方法,为什么有关?因为多态和继承。这里还没跟踪文章是如何实现的,待补充。
具体规则如下:
- 如果r和o有关,那么意味着r可以被o污染,在污点字段记为0
- 如何r和p有关,那么意味着r可以被参数p污染,参数从1开始计数
PPT上有提到如何定义有关
- Assumption #1: All members of a “tainted” object are also tainted (and recursively, etc)
- Assumption #2: All branch conditions are satisfiable
思考:
如果方法内部还有调用,要怎么处理?
2.2.2 逆拓扑排序
为了保证正在分析的函数,其内部调用的函数都是分析过的,或者无其他内部函数,需要对methods 进行逆拓扑排序,Longofo师傅讲的很仔细了,这里不赘述,直接引用师傅的结论。
原始的Call Graph:
用逆拓扑展开成树状之后如下
排序结果为:med7、med8、med3、med6、med2、med4、med1。
解决的问题:
- 通过逆拓扑排序之后的结果,可以顺序解析,对应的是从树结构的最底端进行分析,不会存在依赖的节点未解析
- 解决环的问题
生成这张大逆拓扑排序图之后,接着就进行遍历,计算其污点传播。
2.2.3 过程内污点分析的实现
这块threedr3am师傅有详细讲解,具体在PassthroughDataflowMethodVisitor& TaintTrackingMethodVisitor 内,这里也只是简要理解其算法。
这块需要两点知识
- JVM
- ASM 的一些基础知识。
1. JVM
JAVA的运行时将方法调用信息保存在栈里,并为每个方法调用创建一个栈帧。一个栈帧内部包含着下面几个元素:
- 局部变量表
- 操作数栈
- 方法返回地址
- 动态链
这里我们重点关注局部变量表和操作数栈。局部变量表保存一个方法中的局部变量,我们可以用索引序号的方法读写局部变量表。而操作数栈只能通过push和pop的方式进行操作。对于一个实例方法调用,局部变量表里面从0位开始按序保存着实例对象,方法参数,新定义的局部变量。在运行时,JAVA将局部变量表里面的变量加载到操作数栈上,然后经过运算,得到的结果放到操作数栈顶端,再由其他指令保存回局部变量表。所以说JAVA的局部污点分析,实际就是分析局部变量表和操作数栈之间的数据流动。
2. ASM Visitor
GadgetInspector 是用ASM来模拟方法的调用,入参、返回,从而达到程序内污点跟踪。
接口类
asm.ClassVisitor
- visit
- visitMethod : 指定method的相关操作
- visitEnd
- …
asm.MethodVisitor
- visitCode:在进入方法的第一时间,ASM会先调用这个方法
- visitInsn:在方法体中,每一个字节码操作指令的执行,ASM都会调用这个方法
- visitFieldInsn:对于字段的调用,ASM都会调用这个方法
- visitMethodInsn:方法体内,一旦调用了其他方法,都会触发这个方法的调用
- visitVarInsn:在方法体内字节码操作变量时,会被调用
简单理解
抓住关键点:只关注ret 与this、参数的关系,能否被污染
把TaintTrackingMethodVisitor 当做用asm模拟了jvm的执行过程即可。
只需要分析PassthroughDataflowMethodVisitor 中关于stackVars(操作数栈)、localVars(局部变量表)的操作:
- visitCode(在进入方法的第一时间),将(0,p(this)=0),(1,p(ag1)),…,那么需要将参数加入临时变量localVars,其中的p为污点占位
- visitFieldInsn(调用字段),如果调用了this.field,则通过setStackTaint(0, taint) 设置stackVars污点
- visitMethodInsn(方法调用),通过getStackTaint(retSize-1).addAll(resultTaint) 设置stackVars的污点
- visitInsn return的时候,取当前栈上的最新的为污点分析结果。
注意这里 ,stackVars 用的ArrayList实现的,因此pop 和push看起来会比较晦涩,可以直接把stackVars当作栈来看待,不过get 获取的是倒过来的即可。
例如visitMethodInsn中的getStackTaint(retSize-1).addAll(resultTaint),就可以理解为把最新的那个栈元素重新赋值,这样会好理解很多。
这里只是简单解释下流程,在后文CallGraphDiscovery 中在详细解释。
2.3 CallGraphDiscovery
这一步和上一步类似,但检查的不再是参数与返回结果的关系,而是方法的,参数与其所调用的子方法的关系,即子方法的参数是否可以被父方法的参数所影响。
这里强调个师傅们没讲的点,虽然都是遍历,但是有处很大的不同
- PassthroughDiscovery 有两次遍历
- 遍历所有class的methods,生成逆拓扑图
- 遍历逆拓扑图,生成passthrough
- CallGraphDiscovery 只有一次遍历,遍历的是所有class
两次方法 TaintTrackingMethodVisitor构造中,passthroughDataflow参数不一样。
callgraph.dat
字段 | Demo |
---|---|
父方法类名 | com/m0d9/sec/AbstractTableModel |
父方法 | hashCode |
父方法描述 | ()I |
子方法类名 | com/m0d9/sec/IFn |
子方法 | invokeCall |
子方法描述 | (Ljava/lang/Object;)Ljava/lang/Object; |
父方法第几参 | 0 |
参数对象的哪个field被传递 | __clojureFnMap |
子方法第几参 | 0 |
为什么callgraph需要用到passthrough?代码中CallGraphDiscovery 中除了给TaintTrackingMethodVisitor 中用到了之前的passthrough 结果,就没有其他地方用到。看师傅们文章,都对这里的解释都很模糊
2.3.1 ASM 详细流程
localVars, 本地变量污染表,包含this、入参、以及函数中的其他临时变量,结构为(id,taint),有如下接口:
- add:
- remove:
- get:
- set:
- setLocalTaint:设置local变量中的某个元素
- getLocalTaint:
stackVars 模拟的JVM栈,结果同样为(id,taint),有如下接口:
- pop:出栈
- push:入栈
- getStackTaint:获取stack中的某个元素
- setStackTaint:设置stack中的某个元素
- get:和getStackTaint类似
下面详细解释一下三个ASM MethodVisitor 对应的具体localVars、stackVars 上的操作
- TaintTrackingMethodVisitor
- PassthroughDataflowMethodVisitor
- CallGraph#ModelGeneratorMethodVisitor
1. TaintTrackingMethodVisitor
visitCode
localVars 增加this, arg1, arg0, …,内容暂时为空
visitVarInsn 在方法体内字节码操作变量时,会被调用
Load 将localVars中push到stack
STORE 将stack中pop并赋值给LocalVarsvisitFieldInsn
get 时push入栈,结果在stackVars中
put 时pop出栈visitMethodInsn
处理调用返回
疑问:
此时参数已经在栈内了,哪里入栈的?是LDC/DUP之类指令
具体的处理过程中,有两个变量:
- argTaint是参数的污点表,是从栈中pop取得
- resultTaint 是最终的返回值污染情况
具体的逻辑:
如果被调用的函数为java.io.ObjectInputStream#defaultReadObject, 那么localVars[0] 设为argTaint[0],localVars[0]是this
如果被调用函数在内置的PASSTHROUGH_DATAFLOW 中,那么resultTaint 加上所有的污染点id
如果被调用函数在passthroughDataflow 中,那么同样resultTaint 加上所有的污染点id
如果参数0,也就是this,是java.util.Collection/java.util.Map 接口的实现,那么也认为argTaint[0]包含所有的参数污点,并且如果返回值是return的话,那么resultTaint也要加入这些污点参数。
最后将resultTaint 入栈
2. PassthroughDataflowMethodVisitor
visitCode
localVars this, arg1, arg0, …中赋值,值为当前id
visitFieldInsn
get GETFIELD时,如果是非Transient,那么可以当作污点,把在TaintTrackingMethodVisitor中push入栈的结果打上taint标,内容为0(因为this id为0)
visitMethodInsn
可以当作是TaintTrackingMethodVisitor的补充,丰富resultTaint
具体做法是如果是构造函数,那么resultTaint取之依赖this,否则还是设成空。
接着获取调用的函数的passthrough,获得它的return 和 参数关系,然后通过argTaint获取参数的实际污染情况,最终加入resultTaint
最后调用父类的visitMethodInsn,将resultTaint 合并
3. CallGraph#ModelGeneratorMethodVisitor
visitCode
localVars this, arg1, arg0, …中赋值,值为arg0,arg1,arg2…visitFieldInsn
get GETFIELD时,如果是非Transient,那么可以当作污点,把在TaintTrackingMethodVisitor中push入栈的结果打上taint标,内容为fieldname,如果已经存在,则用.拼接visitMethodInsn
对于每一个参数,通过getStackTaint 获取该的参数的污点信息。
2.3.2 passthrough->callgraph
有了以上个ASM MethodVisitor的了解,再结合Lengo师傅的例子具体走一遍分析流程:
1 | class ParentClass{ |
假设
- arg 是污点
- passthough 已经算出TestObject.childMehod1 的污点传播为0,1
在class asm 遍历的时候
是先遇到 ParentClass.parentMethod->obj1.childMehod1,此时会去栈中查找obj1、arg,其中obj1不是污点,arg是污点,得到obj2的污点为1。得出结论ParentClass.parentMethod->TestObject.childMehod1 的传播关系为1->1
再遇到ParentClass.parentMethod->obj1.this.obj.childMethod(obj2),此时obj2已经入栈,污点值为1,那算得return值为0、1,不过因为return结果没有赋值,不入栈。得出结论ParentClass.parentMethod-MyObject.childMethod 的传播关系为0-obj->0 和1->1
可见passthrough 会影响ret栈的值,从而影响callgraph的结果。
2.4 SourceDiscovery
这一步是内置source源,以java 原声反序列化为例,在JavaSourceDiscovery#discover 中实现了定义。
- jackson
- javaserial
- xstream
sources.dat
字段 | Demo |
---|---|
类 | java/lang/Enum |
方法 | readObject |
方法描述 | (Ljava/io/ObjectInputStream;)V |
污染参数 | 1 |
以javaserial 为例,在SimpleSourceDiscovery内。除了source原,还有几个接口,以支持以上的不同反序列化框架。
- SourceDiscovery:源source点
- SerializableDecider:决策
- ImplementationFinder:解决多态问题
2.5 GadgetChainDiscovery
这一步会从source 开始遍历,并在callgraph.dat中递归查找所有可以继续传递污点参数的子方法调用,直至遇到sink中的方法。
2.5.1 BSF 广度优先搜索
具体可以看discover中的那个循环
- 通过动态对methodsToExplore 增加元素,实现递归搜索
- methodsToExplore add,非push增加元素,因此是广度优先
- exploredMethods 控制了每个Method节点+参数位置 只进行一次搜索
过程中,生成了接口实现方法关系表 methodimpl.data
2.5.2 sinks
1 | private boolean isSink(MethodReference.Handle method, int argIndex, InheritanceMap inheritanceMap) { |
2.5.3 Demo流程
还是借用Longofo师傅的图更直观
BSF流程
- source 进入methodsToExplore
- 处理source,查找source 相关的callgraph:source-cmed1,source-cmed2,source-cmed3。判断,source-cmed1非sink
- source-cmed1 add进入methodsToExplore 队尾
- cmed1 进入exploredMethods白名单
- cmed2,cmed3 类似
- 处理source-cmed1,查找cmed1相关的callgraph:cmed1-cmed4, cmed1-cmed5,重复步骤2
传播流程
- 从source出发,source this可控,也即taintArg:0
- callgraph中,取出source-method1 参数污染对应关系0->0,那么method1 的arg0被污染,也即taintArg:0
- callgraph中,取出method1-method5 参数污染对应关系0->2,那么method5的taintArg:2
- callgraph中,取出method5-sink 参数污染对应关系2->2,那么sink的taintArg:2,符合gadget,完成。
0x03 Pratise
Longofo师傅有样例,加了些自己的测试代码,传到了Git上方便点。
1 | git clone https://github.com/yangbh/GINote.git |
0x04 思考
4.1 callgraph 对静态方法的处理
1 | public Object invokeCall(Object arg) throws IOException { |
su18师傅的文章更深刻,这里学完再来写吧。
0x05 总结
只能算是学习GI的笔记,至少大致理清了GI的运行逻辑(对ASM还不是特别了解),过程中也发现师傅们提出的一些bug和改进点。
师傅们关于ASM那块都讲的很晦涩,希望能够帮助到同样对此有困惑的人。
0x06 参考
- [1] 高效挖掘反序列化漏洞——GadgetInspector改造(by:su18)
- [2] us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains.pdf (by:网飞 Platform Security Team)
- [3] https://github.com/kezibei/Urldns(by:珂字辈)
- [4] java反序列化利用链自动挖掘工具gadgetinspector源码浅析(by:threedr3am)
- [5] Java 反序列化工具 gadgetinspector 初窥(by:Longofo)
- [6] GadgetInspector源码分析(by:fynch3r)
(https://www.anquanke.com/post/id/251814) - [7] gadgetinspector源码浅析(by:kingkk)