0%

Automated Discovery of Deserialization Gadget Chain解读

文章来自Automated Discovery of Deserialization Gadget Chains, Blackhat2018

文章:https://i.blackhat.com/us-18/Thu-August-9/us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains-wp.pdf

演讲视频:https://www.youtube.com/watch?v=wPbW6zQ52w8

工具:https://github.com/JackOfMostTrades/gadgetinspector

原理介绍

看下来的感觉就是将jar包里的(可以反序列化的)所有类的属性设为污点(因为反序列化时攻击者都可以控制),接下来做污点传播,Source为toString(),equals()等方法,Sink为Runtime.exec(),Method.invoke(),URL.openStream等方法,而污点清除函数就是黑名单类中的所有函数。

Step1

枚举包内的所有类和方法,对于类收集其父类和子类集合:

1566118462274

Output:

1
2
3
4
5
6
7
8
9
List<ClassReference> discoveredClasses
ClassReference(name, superClass, interfaces isInterface, members)
ClassReference.Handle(name)
List<MethodReference> discoveredMethod
MethodReference(classReference, name, desc, isStatic)
MethodReference.Handle(classReference, name, desc)
InheritanceMap //记录了一个类的父类和子类集合,因为一个类拥有其父类的所有方法
Map<ClassReference.Handle, Set<ClassReference.Handle>> inheritanceMap
Map<ClassReference.Handle, Set<ClassReference.Handle>> subClassMap

Step2

发现函数传递的数据流,如代码段1中invoke可以传递对象value(0),代码段2中invoke既可以传递arg(1)也可以传递value(0),因为我们假设(a)受污染的对象中,所有成员变量都是受污染的(b)程序中所有分支都是可达的(污染传播都是这样做的),0和1为参数index,0代表this。在此之前要先分析函数调用关系,因为如果有A函数:FuncA(){return FuncB();},那么A函数的返回与B函数有关。

1566118415853

output:

1
2
3
Map<MethodReference.Handle, Set<Integer>> passthroughDataflow
MethodReferenceHandle(classReference, name, desc)
Set<Integer> // 返回一系列污点 0表示this,1为arg1

Step3

构建调用图,即函数A将自身参数arg1传递给函数B的参数arg2 FuncA@1->FuncB@2

1566118496226

Output:

1
2
3
Set<GraphCall> discoveredCalls
GraphCall(caller.Method, targetMethod, callerArgIndex, callerargPath, targetArgIndex)
// 调用函数, 被调函数, 调用函数传递参数idx,路径,被调函数传入参数idx

构图时,只考虑存在污点的边:

1566215882715

这里发现gadgetinspector.TaintTrackingMethodVisitor#getStackTaint有点问题,每次get的时候并未拿到Taint

Step4

枚举入口,如hashCode,toString,程序只能发现已知入口的POP链

1566120657753

Output:

1
2
List<Source> discoveredSources
Source(sourceMethod, taintedArgIndex) //index0=this

Step5

BFS搜索图找到chains

1566302159606

程序分析

断点打gadgetinspector.GadgetInspector#main

污点传播(Step2-Step3)

Step2-Step3都是通过ASM做的,例如Step3:

1566214430085

其中继承了基本Visitor:

gadgetinspector.TaintTrackingMethodVisitor,该类模拟函数执行,主要污点传播逻辑都在这

visitMethodInsn

字节码调用函数时会触发该函数,对于污点传播而言,应该做如下事情

  • 从堆栈取出参数
  • 检查函数的参数是否存在污点
  • 返回函数结果是否存在污点
  • 将结果推入堆栈

实际上就是模拟了一个函数的调用过程,如果函数中有嵌套调用那么情况会更复杂

其属性savedVariableState.StackVars记录着函数参数,及其是否有污点:

1567047086609

如上图,该函数(StringBuilder.append())有两个参数,第一个参数是this,有污点,第二个参数无污点。

特殊的传播

在gadgetinspector.TaintTrackingMethodVisitor#PASSTHROUGH_DATAFLOW处定义了一些特殊的传播,保证污点能够向后传播,比如说String类型A+Taint的传播,idx1,idx2,idx3分别是类,函数名,函数签名,后面是能够传播的污点参数idx,如StringBuilder.append()会将参数0,参数1传播:

1566974563078

而gadgetinspector.TaintTrackingMethodVisitor#visitMethodInsn这里需要处理函数参数调用传播污点的情况,如A(B(xxx))

SINK点

Sink点定义在gadgetinspector.GadgetChainDiscovery#isSink:

1566288878759

判断该类是否可以反序列化

对于java原生序列化,判断逻辑在:gadgetinspector.javaserial.SimpleSerializableDecider#applyNoCache,其他序列化方法找对应的包名。

黑名单

对于java原生序列化,逻辑在:gadgetinspector.javaserial.SimpleSerializableDecider#isBlacklistedClass,可以追加黑名单。

程序的一个Issue

原版污点传播存在一个Bug,考虑如下情况:

1
2
3
4
5
6
7
8
9
10
11
12
class A {
String cmd;
public String getcmd(){
return cmd;
}
}

class B extends A {
public String func() throws Exception{
Runtime.getRuntime().exec(getcmd());
}
}

这里程序无法发现gadget,因为在读取getcmd()字节码后,污点传播执行如下代码(TaintTrackingMethodVisitor.java#L675)向外传播污点:

1566980149628

这一意思是说,如果passthroughDataflow(第二步)计算得到getcmd能够传播污点,那么该污点将作为参数推入堆栈(即传播到exec()函数参数上),但是这里的passthroughDataflow并没有B.getcmd()的记录,只有A.getcmd()记录——因为B.getcmd()继承A.getcmd(),因此污点传播中断了。

因此需要寻找其父类查看是否有该方法,将图中的代码块换成如下:

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
if (passthroughDataflow != null) {
Set<Integer> passthroughArgs = passthroughDataflow.get(methodHandle);
if (passthroughArgs == null && inheritanceMap!=null) {
// FIXME ClassA->ClassB->ClassC
// ClassA.func(), ClassB.func(), ClassC.func()==ClassB.func()!=ClassA.func();
// now taint will passthrough if ClassB.func() or ClassA.func() can passthrough.
// So FP will arrise.
Set<ClassReference.Handle> superClasses=inheritanceMap.getSuperClasses(clzHandle);
if(superClasses!=null){
for (ClassReference.Handle superClzHandle: superClasses){
List<Set<Integer>> list=passthroughDataflow.entrySet().stream()
.filter(e->(e.getKey().getClassReference().equals(superClzHandle)
&& e.getKey().getName().equals(name)
&& e.getKey().getDesc().equals(desc)))
.map(e->e.getValue())
.collect(Collectors.toList());
if(!list.isEmpty()){
passthroughArgs=list.get(0);
break;
}
}
}
}
if (passthroughArgs != null) {
for (int arg : passthroughArgs) {
resultTaint.addAll(argTaint.get(arg));
}
}
}

但是这样就还存在一个问题,因为gadgetinspector.data.InheritanceMap#getSuperClasses返回的是一个集合,因此,我们并没法知道真实调用exec()的是哪一个函数:

1566981439728

考虑如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
String cmd;
public String getcmd(){
return cmd;
}
}
class B extends A{
public String getcmd() {
return "whoami";
}
class C extends B{
public String func() throws Exception{
Runtime.getRuntime().exec(getcmd());
}
}

在这里,前面的代码会返回getcmd()传播污点——父类的任意一个getcmd()能传污点就有污点,但是实际上其并没有污点,因为实际上getcmd()调用的是B.getcmd(),因此会产生误报,预计使用接口的话这一点会更加明显。

缺陷

  • 无视控制流,导致误报

  • 无法识别反射的SINK点,导致漏报

相关代码:https://github.com/Anemone95/gadgetinspector

附录:ASM源码

其污点传播用ASM做的,因此记一下关于ASM的一些使用方法

ClassReader

ClassVisitor

定义在读取Class字节码时会触发的事件,如类头解析完成、注解解析、字段解析、方法解析等。

MethodVisitor

定义在解析方法时触发的事件,如方法上的注解、属性、代码等。这里只是操作码不含操作数

visitInsn:IALOAD~SALOAD触发该函数

visitVarInsn(opcode, var):ILOAD~ALOAD触发该函数,var为操作数

B4 getfield