GadgetInspector 源码分析

开始啃骨头了。。。

0x01 背景知识

不了解算法原理基础上,去直接啃GI源码,真的是一件很困难的事情,难得有师傅们的经验总结,站在师傅们的肩膀上记录下GI的学习心得。

时间线

当然,还有其他的许多师傅关于GI的文章,这里没有提到,不赘述。

0x02 算法解析

先抛开复杂的代码,GI的工作流程大致如下:

  1. 枚举全部类以及每个类的所有方法:MethodDiscovery
  2. 生成passthrough数据流:PassthroughDiscovery
  3. 枚举passthrough调用图:CallGraphDiscovery
  4. 搜索可用的source:SourceDiscovery
  5. 搜索生成调用链:GadgetChainDiscovery

其中重点的知识点有:

  1. ASM栈模拟
  2. 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的第一个重点知识点。

GadgetInspector 源码分析

最终生成的

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:

GadgetInspector 源码分析

用逆拓扑展开成树状之后如下

GadgetInspector 源码分析

排序结果为:med7、med8、med3、med6、med2、med4、med1。

解决的问题:

  1. 通过逆拓扑排序之后的结果,可以顺序解析,对应的是从树结构的最底端进行分析,不会存在依赖的节点未解析
  2. 解决环的问题

生成这张大逆拓扑排序图之后,接着就进行遍历,计算其污点传播。

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、参数的关系,能否被污染

  1. 把TaintTrackingMethodVisitor 当做用asm模拟了jvm的执行过程即可。
  2. 只需要分析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参数不一样。

GadgetInspector 源码分析 GadgetInspector 源码分析

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并赋值给LocalVars

  • visitFieldInsn

    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
2
3
4
5
6
7
8
9
10
class ParentClass{
    private MyObject obj;
    public void parentMethod(Object arg){
        ...
        TestObject obj1 = new TestObject();
        Object obj2 = obj1.childMethod1(arg);
        this.obj.childMethod(obj2); 
        ...
    }
}

假设

  • arg 是污点
  • passthough 已经算出TestObject.childMehod1 的污点传播为0,1

在class asm 遍历的时候

  1. 是先遇到 ParentClass.parentMethod->obj1.childMehod1,此时会去栈中查找obj1、arg,其中obj1不是污点,arg是污点,得到obj2的污点为1。得出结论ParentClass.parentMethod->TestObject.childMehod1 的传播关系为1->1
  2. 再遇到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
GadgetInspector 源码分析

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中的方法。

GadgetInspector 源码分析

2.5.1 BSF 广度优先搜索

具体可以看discover中的那个循环

  • 通过动态对methodsToExplore 增加元素,实现递归搜索
  • methodsToExplore add,非push增加元素,因此是广度优先
  • exploredMethods 控制了每个Method节点+参数位置 只进行一次搜索

过程中,生成了接口实现方法关系表 methodimpl.data

GadgetInspector 源码分析

2.5.2 SINKS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
private boolean isSink(MethodReference.Handle method, int argIndex, InheritanceMap inheritanceMap) {
    if (method.getClassReference().getName().equals("java/io/FileInputStream")
            && method.getName().equals("<init>")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/io/FileOutputStream")
            && method.getName().equals("<init>")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/nio/file/Files")
        && (method.getName().equals("newInputStream")
            || method.getName().equals("newOutputStream")
            || method.getName().equals("newBufferedReader")
            || method.getName().equals("newBufferedWriter"))) {
        return true;
    }

    if (method.getClassReference().getName().equals("java/lang/Runtime")
            && method.getName().equals("exec")) {
        return true;
    }
    /*
    if (method.getClassReference().getName().equals("java/lang/Class")
            && method.getName().equals("forName")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/lang/Class")
            && method.getName().equals("getMethod")) {
        return true;
    }
    */
    // If we can invoke an arbitrary method, that's probably interesting (though this doesn't assert that we
    // can control its arguments). Conversely, if we can control the arguments to an invocation but not what
    // method is being invoked, we don't mark that as interesting.
    if (method.getClassReference().getName().equals("java/lang/reflect/Method")
            && method.getName().equals("invoke") && argIndex == 0) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/net/URLClassLoader")
            && method.getName().equals("newInstance")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/lang/System")
            && method.getName().equals("exit")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/lang/Shutdown")
            && method.getName().equals("exit")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/lang/Runtime")
            && method.getName().equals("exit")) {
        return true;
    }

    if (method.getClassReference().getName().equals("java/nio/file/Files")
            && method.getName().equals("newOutputStream")) {
        return true;
    }

    if (method.getClassReference().getName().equals("java/lang/ProcessBuilder")
            && method.getName().equals("<init>") && argIndex > 0) {
        return true;
    }

    if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("java/lang/ClassLoader"))
            && method.getName().equals("<init>")) {
        return true;
    }

    if (method.getClassReference().getName().equals("java/net/URL") && method.getName().equals("openStream")) {
        return true;
    }

    // Some groovy-specific sinks
    if (method.getClassReference().getName().equals("org/codehaus/groovy/runtime/InvokerHelper")
            && method.getName().equals("invokeMethod") && argIndex == 1) {
        return true;
    }

    if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("groovy/lang/MetaClass"))
            && Arrays.asList("invokeMethod", "invokeConstructor", "invokeStaticMethod").contains(method.getName())) {
        return true;
    }

    // This jython-specific sink effectively results in RCE
    if (method.getClassReference().getName().equals("org/python/core/PyCode") && method.getName().equals("call")) {
        return true;
    }

    return false;
}

2.5.3 DEMO流程

还是借用Longofo师傅的图更直观
GadgetInspector 源码分析

BSF流程
  1. source 进入methodsToExplore
  2. 处理source,查找source 相关的callgraph:source-cmed1,source-cmed2,source-cmed3。判断,source-cmed1非sink
    • source-cmed1 add进入methodsToExplore 队尾
    • cmed1 进入exploredMethods白名单
    • cmed2,cmed3 类似
  3. 处理source-cmed1,查找cmed1相关的callgraph:cmed1-cmed4, cmed1-cmed5,重复步骤2
传播流程
  1. 从source出发,source this可控,也即taintArg:0
  2. callgraph中,取出source-method1 参数污染对应关系0->0,那么method1 的arg0被污染,也即taintArg:0
  3. callgraph中,取出method1-method5 参数污染对应关系0->2,那么method5的taintArg:2
  4. callgraph中,取出method5-sink 参数污染对应关系2->2,那么sink的taintArg:2,符合gadget,完成。

0x03 Pratise

Longofo师傅有样例,加了些自己的测试代码,传到了Git上方便点。

1
2
3
4
git clone https://github.com/yangbh/GINote.git
cd GINote
mvn package
java -Xmx2G -jar build/libs/gadget-inspector-all.jar GILearn2-1.0-SNAPSHOT.jar
GadgetInspector 源码分析

0x04 思考

4.1 callgraph 对静态方法的处理

1
2
3
4
    public Object invokeCall(Object arg) throws IOException {
            return Runtime.getRuntime().exec((String)arg);
//        return Runtime.getRuntime().exec(String.ValueOf(arg));
    }

su18师傅的文章更深刻,这里学完再来写吧。

0x05 总结

只能算是学习GI的笔记,至少大致理清了GI的运行逻辑(对ASM还不是特别了解),过程中也发现师傅们提出的一些bug和改进点。

师傅们关于ASM那块都讲的很晦涩,希望能够帮助到同样对此有困惑的人。

0x06 参考

 

 

原文始发于m0d9:GadgetInspector 源码分析

版权声明:admin 发表于 2022年11月13日 下午7:47。
转载请注明:GadgetInspector 源码分析 | CTF导航

相关文章

暂无评论

暂无评论...