0x0 简介
GadgetInspector是JackOfMostTrades在2018 BLACKHAT USA上发布的一个自动化反序列化链挖掘工具,它通过对字节码形式的JAVA项目进行污点分析,挖掘可能存在的反序列化链。
0x1 原理分析
我们从GadgetInspector的源代码进行分析。根据源代码的逻辑结构,可以看出其分成六个部分:
ClassResourceEnumerator 读取类文件,保存到自定义的数据结构里
MethodDiscovery 分析类文件包含的的类、方法信息
PassthrougthDiscovery 对方法进行局部污点分析
CallGraphDiscovery 枚举污点边
SourceDiscovery 枚举源点
GadgetChainDicovery 生成反序列化链
六个部分层层递进,最终完成反序列化链挖掘。
0x11 ClassResourceEnumerator
GadgetInspector的核心入口类是GadgetInspector
这个类。它的main方法根据输入参数的区别,可以生成war包或者jar包对应的ClassLoader
,并利用ClassLoader
作为参数构造ClassResourceEnumerator
对象
final ClassLoader classLoader;
if (args.length == argIndex+1 && args[argIndex].toLowerCase().endsWith(".war")) {
Path path = Paths.get(args[argIndex]);
classLoader = Util.getWarClassLoader(path);
} else {
final Path[] jarPaths = new Path[args.length - argIndex];
for (int i = 0; i < args.length - argIndex; i++) {
Path path = Paths.get(args[argIndex + i]).toAbsolutePath();
jarPaths[i] = path;
}
classLoader = Util.getJarClassLoader(jarPaths);
}
final ClassResourceEnumerator classResourceEnumerator = new ClassResourceEnumerator(classLoader);
ClassResourceEnumerator
对象主要的作用就是获取用于分析的类文件,我们看一下它的主要方法getAllClasses
。可以看到首先调用getRuntimeClasses
获取JDK中的类文件,然后使用刚才创建的ClassLoader
读取作为分析对象的JAVA项目的类文件
public Collection<ClassResource> getAllClasses() throws IOException {
Collection<ClassResource> result = new ArrayList<>(getRuntimeClasses());
for (ClassPath.ClassInfo classInfo : ClassPath.from(classLoader).getAllClasses()) {
result.add(new ClassLoaderClassResource(classLoader, classInfo.getResourceName()));
}
return result;
}
0x12 MethodDiscovery
在读取了类文件以后,GadgetInspector通过5个阶段的discovery来获取反序列化链,它们的实现框架大致相同:都是根据上一个阶段的分析结果,进行进一步分析,然后保存这个阶段的分析结果。
// Perform the various discovery steps
if (!Files.exists(Paths.get("classes.dat")) || !Files.exists(Paths.get("methods.dat"))
|| !Files.exists(Paths.get("inheritanceMap.dat"))) {
MethodDiscovery methodDiscovery = new MethodDiscovery();
methodDiscovery.discover(classResourceEnumerator);
methodDiscovery.save();
}
对应到具体的方法就是discover
和save
方法。这里先仔细分析MethodDiscovery部分的discover
方法。
这里使用上一步创建的ClassResourceEnumerator
对象获取类输入流
public void discover(final ClassResourceEnumerator classResourceEnumerator) throws Exception {
for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
try (InputStream in = classResource.getInputStream()) {
ClassReader cr = new ClassReader(in);
cr.accept(new MethodDiscoveryClassVisitor(), ClassReader.EXPAND_FRAMES);
}
}
}
注意这里的ClassReader
和MethodDiscoveryClassVistor
用到JAVA中的ASM技术,用来读取类文件并进行分析。
ASM是一种JAVA字节码操作,读取,增强技术。它直接分析类文件中的各种信息,例如常量池,操作码,要求其使用者对于JAVA类文件的结构有一定了解。其主要的类操作API为ClassVisitor类。使用者对ClassVisitor进行个性化实现,满足自己的需求。例如下面这一段代码就是ClassVisitor的API的使用步骤
java public static void main(String[] args) throws Exception{ //实现一个自己的ClassVistor类型的对象 ClassVisitor classVisitor=new ClassVisitor(Opcodes.ASM9) { //重载一个回调方法,当访问字段时会打印字段名称 @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { System.out.println(name); return super.visitField(access, name, desc, signature, value); } }; //获取一个类文件,这里是Hello这个测试类 ClassLoader classLoader= VisitClass.class.getClassLoader(); String path= Hello.class.getCanonicalName().replace(".","/")+".class"; InputStream inputStream=classLoader.getResourceAsStream(path); //使用类文件构造一个ClassReader类型的对象 ClassReader classReader=new ClassReader(inputStream); //使用自定义的ClassVisitor类型的对象并解析ClassReader里面保存的对象流 classReader.accept(classVisitor,0); }
编译运行以后就会打印Hello
这个类的所有字段名称
ASM主要的方法操作API为MethodVisitor类,使用者对MethodVisitor进行个性化实现,可以进一步解析类方法。MethodVisitor的用法下面再介绍
我们需要知道的就是cr.accept
接收一个ClassVistor
类型的对象,并且在分析类文件过程中回调式地触发ClassVistor
类型的对象实现的各种方法。那么我们继续查看MethodDiscoveryClassVistor
。里面两个关键的方法如下
@Override
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
if ((access & Opcodes.ACC_STATIC) == 0) {
Type type = Type.getType(desc);
String typeName;
if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
typeName = type.getInternalName();
} else {
typeName = type.getDescriptor();
}
// 记录成员信息
members.add(new ClassReference.Member(name, access, new ClassReference.Handle(typeName)));
}
return super.visitField(access, name, desc, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
// 记录方法信息
discoveredMethods.add(new MethodReference(
classHandle,
name,
desc,
isStatic));
return super.visitMethod(access, name, desc, signature, exceptions);
}
visitField
方法在访问到成员字段时触发,这里作者在里面添加代码,保存读取到的成员字段信息。visitMethod
方法在访问到成员方法时触发,作者也添加代码,保存方法信息。这些获取到的信息都被保存到MethodDiscovery
的成员变量里面,我们在save
方法里面可以获取到这些信息。
public void save() throws IOException {
DataLoader.saveData(Paths.get("classes.dat"), new ClassReference.Factory(), discoveredClasses);
DataLoader.saveData(Paths.get("methods.dat"), new MethodReference.Factory(), discoveredMethods);
Map<ClassReference.Handle, ClassReference> classMap = new HashMap<>();
for (ClassReference clazz : discoveredClasses) {
classMap.put(clazz.getHandle(), clazz);
}
// 生成继承关系并保存
InheritanceDeriver.derive(classMap).save();
}
save
方法内部再调用savaData
方法,将对应信息保存为文件
0x13 PassthroughDiscovery
PassthroughDiscovery这一部分主要进行局部污点分析。主要使用的技术是JAVA的ASM技术,以及基于它实现的局部变量表和操作数栈的模拟。在分析源代码之前先补充一下局部污点分析的理论。
0x131 局部污点分析
GadgetInspector中用到的局部污点分析考虑这样一个问题:给定一个方法调用,其返回值受到方法的哪些参数影响?
例如下面这个方法
private String a=",";
public String concat(String b,String c)
{
String d="hello"+a+b+c;
return d;
}
我们凭肉眼可以看出,成员a
,参数b
,c
最终都影响到了返回值d
。实际的局部污点分析需要自动化地分析数据流,得出哪些参数影响了方法的返回值
又例如下面这个方法
private Concater a;
public String concat2(String b){
String c=a.concat(b,"!");
return c;
}
它内部涉及到一个方法的调用,返回值c
是否与成员a
,参数b
相关,还依赖于concat
这个方法内部的实现。所以局部污点分析还要求在分析一个方法的数据流之前知道它调用的方法的数据流传递情况。
0x132 拓扑排序
对于上面提到的两种情形,我们着重讨论如何解决第二种情形。根据上面的分析,我们需要按照相互调用的顺序从后向前进行数据流分析,也就是逆拓扑排序。
拓扑排序是图这种数据结构所具有的一种基本算法。对于一个有向无环图G进行拓扑排序,是将G中所有的顶点排成一个线性序列使得图中任意一对顶点u和v,若边<u,v>属于E(G),则u在线性序列中处于之前。如果有一个有向图可以被拓扑排序,那么说明它是无环的。拓扑排序的结果反过来就是逆拓扑排序。
这里我们看PassthroughDiscovery的discover
方法,里面先调用discoverMethodCalls
这个方法使用ASM技术来获取每个方法调用的方法,保存到一个Map
里,对应到拓扑排序算法里面就是边的集合E(G)
,然后调用topologicallySortMethodCalls
这个方法进行逆拓扑排序,逆排序的结果再保存到一个List
里面
public void discover(final ClassResourceEnumerator classResourceEnumerator, final GIConfig config) throws IOException {
Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();
Map<ClassReference.Handle, ClassReference> classMap = DataLoader.loadClasses();
InheritanceMap inheritanceMap = InheritanceMap.load();
// 分析调用关系
Map<String, ClassResourceEnumerator.ClassResource> classResourceByName = discoverMethodCalls(classResourceEnumerator);
// 进行拓扑排序
List<MethodReference.Handle> sortedMethods = topologicallySortMethodCalls();
// 分析数据流传递情况
passthroughDataflow = calculatePassthroughDataflow(classResourceByName, classMap, inheritanceMap, sortedMethods,
config.getSerializableDecider(methodMap, inheritanceMap));
}
0x133 TaintTrackingMethodVistor
在完成了拓扑排序以后,就可以依序进行局部污点分析了。这里作者编写了一个重要的类TaintTrackingMethodVistor
,继承MethodVistor
类,用来模拟局部变量表和操作数栈,辅助进行局部污点分析。这个类重载了多个方法,当ASM访问被分析类的类方法的时候,这些重载的方法就会被自动调用。真正理解这一部分需要读者对JAVA运行时有一定了解。
JAVA的运行时将方法调用信息保存在栈里,并为每个方法调用创建一个栈帧。一个栈帧内部包含着下面几个元素:
- 局部变量表
- 操作数栈
- 方法返回地址
- 动态链
这里我们重点关注局部变量表和操作数栈。局部变量表保存一个方法中的局部变量,我们可以用索引序号的方法读写局部变量表。而操作数栈只能通过
push
和pop
的方式进行操作。对于一个实例方法调用,局部变量表里面从0位开始按序保存着实例对象,方法参数,新定义的局部变量。在运行时,JAVA将局部变量表里面的变量加载到操作数栈上,然后经过运算,得到的结果放到操作数栈顶端,再由其他指令保存回局部变量表。所以说JAVA的局部污点分析,实际就是分析局部变量表和操作数栈之间的数据流动。
下面假定读者已经充分理解JAVA运行时的机制。这里由于页面限制,我们举出一个较短的例子来。下面这个方法visitVarInsn
会在访问到类方法中操作局部变量表相关的操作码时被调用。看代码内容。它首先根据变量索引的需要,扩充局部变量表,向里面添加用来保存局部变量污点信息的Set
,这里的Set
使用整数类型保存污点来源,0代表调用类本身的数据,包括其字段,1代表方法的第一个参数,2代表方法的第二个参数,以此类推。代码用swtich
来体现了不同类型的操作码对应的操作。
@Override
public void visitVarInsn(int opcode, int var) {
// Extend local variable state to make sure we include the variable index
for (int i = savedVariableState.localVars.size(); i <= var; i++) {
savedVariableState.localVars.add(new HashSet<T>());
}
Set<T> saved0;
switch(opcode) {
case Opcodes.ILOAD:
case Opcodes.FLOAD:
push();
break;
case Opcodes.LLOAD:
case Opcodes.DLOAD:
push();
push();
break;
case Opcodes.ALOAD:
push(savedVariableState.localVars.get(var));
break;
case Opcodes.ISTORE:
case Opcodes.FSTORE:
pop();
savedVariableState.localVars.set(var, new HashSet<T>());
break;
case Opcodes.DSTORE:
case Opcodes.LSTORE:
pop();
pop();
savedVariableState.localVars.set(var, new HashSet<T>());
break;
case Opcodes.ASTORE:
saved0 = pop();
savedVariableState.localVars.set(var, saved0);
break;
case Opcodes.RET:
// No effect on stack
break;
default:
throw new IllegalStateException("Unsupported opcode: " + opcode);
}
super.visitVarInsn(opcode, var);
sanityCheck();
}
可以看到这里处理的操作码主要分成两类,一类是LOAD类型,将局部变量表里面的变量加载到操作数栈上。其中对于整数,浮点数,都认为污点不会传递,只是进行对应的push操作,将合适长度的变量加载到操作数栈上;对于对象,会获取本地变量表里面保存的污点集合,然后加载到操作数栈上。另一类是STORE类型,负责将操作数栈顶的变量保存回本地变量表。同理,仅仅对于对象,才将栈顶变量的污点集合保存回局部变量表,其他类型的返回值,只进行pop操作。
另外一个关键的方法是visitMethodInsn
,用来模拟方法调用导致的污点传播。可以看到它首先获取参数中的污点集合argTaint
,然后通过预定义的传递规则和passthroughDataflow
中保存的之前分析好的污点传播信息来进行污点分析,判断哪些参数能够影响到方法的返回值,保存到resultTaint
变量,最后把方法调用导致的污点集合置于操作数栈顶,等待其他方法的处理。
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
final MethodReference.Handle methodHandle = new MethodReference.Handle(
new ClassReference.Handle(owner), name, desc);
// 根据调用的是否是实例方法,扩充this参数
Type[] argTypes = Type.getArgumentTypes(desc);
if (opcode != Opcodes.INVOKESTATIC) {
Type[] extendedArgTypes = new Type[argTypes.length+1];
System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
extendedArgTypes[0] = Type.getObjectType(owner);
argTypes = extendedArgTypes;
}
final Type returnType = Type.getReturnType(desc);
final int retSize = returnType.getSize();
switch (opcode) {
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEINTERFACE:
// 获取参数污点集合
final List<Set<T>> argTaint = new ArrayList<Set<T>>(argTypes.length);
for (int i = 0; i < argTypes.length; i++) {
argTaint.add(null);
}
for (int i = 0; i < argTypes.length; i++) {
Type argType = argTypes[i];
if (argType.getSize() > 0) {
for (int j = 0; j < argType.getSize() - 1; j++) {
pop();
}
argTaint.set(argTypes.length - 1 - i, pop());
}
}
Set<T> resultTaint;
if (name.equals("<init>")) {
// Pass result taint through to original taint set; the initialized object is directly tainted by
// parameters
resultTaint = argTaint.get(0);
} else {
resultTaint = new HashSet<>();
}
// If calling defaultReadObject on a tainted ObjectInputStream, that taint passes to "this"
// 这里如果ObjectInputStream被污染了,那么调用了defaultReadObject后,this也应该是被污染的
if (owner.equals("java/io/ObjectInputStream") && name.equals("defaultReadObject") && desc.equals("()V")) {
savedVariableState.localVars.get(0).addAll(argTaint.get(0));
}
// 查询预先定义的数据流
for (Object[] passthrough : PASSTHROUGH_DATAFLOW) {
if (passthrough[0].equals(owner) && passthrough[1].equals(name) && passthrough[2].equals(desc)) {
for (int i = 3; i < passthrough.length; i++) {
resultTaint.addAll(argTaint.get((Integer)passthrough[i]));
}
}
}
// 查询已经分析好的数据流
if (passthroughDataflow != null) {
Set<Integer> passthroughArgs = passthroughDataflow.get(methodHandle);
if (passthroughArgs != null) {
for (int arg : passthroughArgs) {
resultTaint.addAll(argTaint.get(arg));
}
}
}
...
if (retSize > 0) {
push(resultTaint);
for (int i = 1; i < retSize; i++) {
push();
}
}
break;
default:
throw new IllegalStateException("Unsupported opcode: " + opcode);
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
sanityCheck();
}
0x134 PassthroughDataflowClassVistor
PassthroughDiscovery并不是直接使用上面的TaintTrackingMethodVistor
来进行分析,而是进一步实现其子类PassthroughDataflowMethodVistor
。这个子类进一步完成了把方法调用返回的污点集合保存到局部变量表,以及读取字段导致的污点传播分析的工作。
这里我们看PassthroughDataflowClassVistor
的visitMethod
方法,首先判断访问的方法是不是指定方法(每次只针对某一个类的某一个方法进行分析),这保证污点分析时模拟的局部变量表和操作数栈不会混乱。如果是指定方法,就是使用PassthroughDataflowMethodVisitor
进行污点分析,并保存分析的结果到passthroughDataflow
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
// 判断方法名称和方法描述
if (!name.equals(methodToVisit.getName()) || !desc.equals(methodToVisit.getDesc())) {
return null;
}
if (passthroughDataflowMethodVisitor != null) {
throw new IllegalStateException("Constructing passthroughDataflowMethodVisitor twice!");
}
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
passthroughDataflowMethodVisitor = new PassthroughDataflowMethodVisitor(
classMap, inheritanceMap, this.passthroughDataflow, serializableDecider,
api, mv, this.name, access, name, desc, signature, exceptions);
return new JSRInlinerAdapter(passthroughDataflowMethodVisitor, access, name, desc, signature, exceptions);
}
PassthroughDataflowMethodVistor
相比TaintTrackingMethodVistor
改动不大,读者在了解了TaintTrackingMethodVistor
以后可以自己分析PassthroughDataflowMethodVistor
的具体实现部分
0x14 CallGraphDiscovery
经过了PassthroughDiscovery,我们已经知道了每一个方法的输入参数如何影响它的返回值。那么CallGraphDiscovery这一部分主要的工作就是确定一个方法的输入参数是否能传递到其代码段调用的另一个方法的参数,记录下这种传递关系。
0x141 ModelGeneratorMethodVistor
ModelGeneratorMethodVistor
也是TaintTrackingMethodVistor
的子类。其中最核心的方法是visitMethodInsn
。依赖于TaintTrackingMethodVistor
提供的污点分析能力,在当前访问的方法的代码段中,一个方法被调用时,visitMethodInsn
可以知道每一个参数和当前访问的方法的参数的关系。这部分代码使用一个二重循环,基于不同的调用参数和当前访问的方法的参数的污点传递情况,连接污点边。
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
// 根据调用的是否是实例方法,扩充this参数
Type[] argTypes = Type.getArgumentTypes(desc);
if (opcode != Opcodes.INVOKESTATIC) {
Type[] extendedArgTypes = new Type[argTypes.length+1];
System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
extendedArgTypes[0] = Type.getObjectType(owner);
argTypes = extendedArgTypes;
}
switch (opcode) {
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKEVIRTUAL:
case Opcodes.INVOKESPECIAL:
case Opcodes.INVOKEINTERFACE:
int stackIndex = 0;
// 外层循环遍历调用的方法参数,即目的参数
for (int i = 0; i < argTypes.length; i++) {
int argIndex = argTypes.length-1-i;
Type type = argTypes[argIndex];
Set<String> taint = getStackTaint(stackIndex);
if (taint.size() > 0) {
// 内层循环遍历当前访问的方法参数,即源参数
for (String argSrc : taint) {
if (!argSrc.substring(0, 3).equals("arg")) {
throw new IllegalStateException("Invalid taint arg: " + argSrc);
}
int dotIndex = argSrc.indexOf('.');
int srcArgIndex;
String srcArgPath;
if (dotIndex == -1) {
srcArgIndex = Integer.parseInt(argSrc.substring(3));
srcArgPath = null;
} else {
srcArgIndex = Integer.parseInt(argSrc.substring(3, dotIndex));
srcArgPath = argSrc.substring(dotIndex+1);
}
// 保存污点边
discoveredCalls.add(new GraphCall(
new MethodReference.Handle(new ClassReference.Handle(this.owner), this.name, this.desc),
new MethodReference.Handle(new ClassReference.Handle(owner), name, desc),
srcArgIndex,
srcArgPath,
argIndex));
}
}
stackIndex += type.getSize();
}
break;
default:
throw new IllegalStateException("Unsupported opcode: " + opcode);
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
ModelGeneratorMethodVistor
使用GraphCall
类型的对象来记录这种传递关系。GraphCall
类型的对象保存了源方法、源方法的参数索引以及目的方法、目的方法的参数索引。
0x15 SourceDiscovery
SourceDiscovery这一部分讨论反序列化的源点发现。对于JAVA原生反序列化,GadgetInspector提供了SimpleSourceDiscovery
这个实现类。对前面保存的方法信息,类信息进行分析,保存源点。判断源点的方式主要就是简单的匹配类名称和方法名称来确定是否是已经预先标记的源点。目前的源点如下
类或者父类 | 方法名称 | 方法描述 | 污点参数 |
---|---|---|---|
任意 | finalize | ()V | 0 |
任意 | readObject | (Ljava/io/ObjectInputStream;)V | 1 |
java/lang/reflect/InvocationHandler | invoke | (Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object; | 0 |
任意 | hashCode | ()I | 0 |
任意 | equals | (Ljava/lang/Object;)Z | 0,1 |
groovy/lang/Closure | call,doCall | 和具体方法有关 | 和具体方法有关 |
0x16 GadgetChainDiscovery
GadgetChainDiscovery作为GadgetInspector的最后一部分。集中地完成了反序列化链挖掘的工作。
首先确定一些概念
- GraphCall:表示一个方法的参数传递到另一个方法的参数
- Link:表示一个方法和它的一个污点参数
- GadgetChain:一个由Link构成的有序列表
GadgetChainDiscovery的核心代码如下。可以看到首先将上一步SourceDiscovery发现的源点作为GadgetChain
保存到methodsToExplore
里面,然后开启迭代。
迭代部分基于BFS算法,将methodsToExplore
作为一个队列,每次从队首pop
出一个GadgetChain
,然后从CallGraphDiscovery部分保存的GraphCall
信息中查找使得GadgetChain
尾部的Link
与GraphCall
的源Link
匹配的GraphCall
,然后将这个GraphCall
的目标Link
加入GadgetChain
的尾部。如果找到的这个Link
就是预先确定的汇点,那么宣告找到了一条反序列化链。如果不是,则将这个Link
保存到exploredMethods
中,下次不再访问这个Link
。
Set<GadgetChainLink> exploredMethods = new HashSet<>();
LinkedList<GadgetChain> methodsToExplore = new LinkedList<>();
for (Source source : DataLoader.loadData(Paths.get("sources.dat"), new Source.Factory())) {
GadgetChainLink srcLink = new GadgetChainLink(source.getSourceMethod(), source.getTaintedArgIndex());
if (exploredMethods.contains(srcLink)) {
continue;
}
// 从源点构建反序列化链
methodsToExplore.add(new GadgetChain(Arrays.asList(srcLink)));
exploredMethods.add(srcLink);
}
long iteration = 0;
Set<GadgetChain> discoveredGadgets = new HashSet<>();
while (methodsToExplore.size() > 0) {
if ((iteration % 1000) == 0) {
LOGGER.info("Iteration " + iteration + ", Search space: " + methodsToExplore.size());
}
iteration += 1;
// 队首弹出一个Link
GadgetChain chain = methodsToExplore.pop();
GadgetChainLink lastLink = chain.links.get(chain.links.size()-1);
// 查询满足条件的GraphCall
Set<GraphCall> methodCalls = graphCallMap.get(lastLink.method);
if (methodCalls != null) {
for (GraphCall graphCall : methodCalls) {
if (graphCall.getCallerArgIndex() != lastLink.taintedArgIndex) {
continue;
}
Set<MethodReference.Handle> allImpls = implementationFinder.getImplementations(graphCall.getTargetMethod());
for (MethodReference.Handle methodImpl : allImpls) {
GadgetChainLink newLink = new GadgetChainLink(methodImpl, graphCall.getTargetArgIndex());
if (exploredMethods.contains(newLink)) {
continue;
}
// 保存反序列化链
GadgetChain newChain = new GadgetChain(chain, newLink);
if (isSink(methodImpl, graphCall.getTargetArgIndex(), inheritanceMap)) {
discoveredGadgets.add(newChain);
} else {
methodsToExplore.add(newChain);
exploredMethods.add(newLink);
}
}
}
}
}
0x2 总结和问题
这篇文章从ASM实现和污点分析原理两个方面介绍了GadgetInspector。理解GadgetInspector对于安全开发和漏洞挖掘具有重要的意义。
0x21 问题讨论
0x211 GadgetInspector中反序列化链挖掘的实际过程?
GadgetInspector的反序列化链实际过程是寻找一个入口函数的参数在不同的函数参数之间传递链,最终到达危险调用参数的过程,本质是参数的传播。
例如官方文档展示的调用链,就是从readObject
方法的参数1,即传入参数ObjectInputStream
,传递给add
方法的参数0,即FastArrayList
本身···最后传递给invoke
方法的参数0,发生了危险调用。
net/sf/jasperreports/charts/design/JRDesignPieDataset.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/commons/collections/FastArrayList.add(Ljava/lang/Object;)Z (0)
java/util/ArrayList.clone()Ljava/lang/Object; (0)
org/jfree/data/KeyToGroupMap.clone()Ljava/lang/Object; (0)
org/jfree/data/KeyToGroupMap.clone(Ljava/lang/Object;)Ljava/lang/Object; (0)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
0x212 反序列化链挖掘和常规漏洞挖掘的异同?
- 同:都可以使用污点分析,发掘危险调用链
- 异:就JAVA原生反序列化来说,有两点不同,一是反序列化的入口函数往往是固定的魔术方法,例如
readObject
、readExternal
,二是反序列化要求调用链上出现的对象都可以序列化,即都实现了java.io.Serializable
接口
针对第一点不同,GadgetInspector通过明确源点的方式进行处理。对于第二点不同,GadgetInspector设计了SerializableDecider
类,判断读取的类文件是否能够被反序列化
protected static final boolean couldBeSerialized(SerializableDecider serializableDecider, InheritanceMap inheritanceMap, ClassReference.Handle clazz) {
if (Boolean.TRUE.equals(serializableDecider.apply(clazz))) {
return true;
}
Set<ClassReference.Handle> subClasses = inheritanceMap.getSubClasses(clazz);
if (subClasses != null) {
for (ClassReference.Handle subClass : subClasses) {
if (Boolean.TRUE.equals(serializableDecider.apply(subClass))) {
return true;
}
}
}
return false;
}
当调整了源点的定义,同时解除序列化的限制以后,GadgetInspector提供的污点分析能力就可以用于常规漏洞挖掘
0x213 GadgetInspector对于动态特性的处理?
GadgetInspector是一个基于污点传播的静态分析工具,语言的动态特性往往是静态分析工具遇到的一大问题。对于JAVA语言中的一些动态特性,GadgetInspector也做了一些特殊处理。这里以多态为例子讨论一下。
首先是PassthroughDiscovery部分,这一部分负责分析出一个方法的参数能否影响到这个方法的返回值。当一个方法的代码段调用了其他方法时,也就是上面提到的污点分析情形二,需要知道被调方法的数据流传递情况。当这个被调用的方法是一个接口方法时,由于之前分析时ASM会自动跳过接口方法的分析,导致无法获取接口方法的数据流传递情况,以至于污点分析会直接断开。例如下面这一段代码,污点参数this.command
经过了一个接口方法调用transform
,获取的返回值command1
就无法再保持this.command
中的污点,导致反序列化链的缺失
private void readObject(ObjectInputStream ois) throws Exception {
ois.defaultReadObject();
transformerInterface=new ReinforceTransformer();
String command1=(String)transformerInterface.transform(command);
sonInterface.exec(command1);
}
其次是GadgetChainDiscovery部分,这一部分负责生成反序列化链。在使用GraphCall
连接GadgetChain
的过程中,如果GraphCall
指向的目标方法是一个接口方法,那么GadgetChainDiscovery会寻找这个接口方法所有的实现方法,尝试连接污点边,一定程度地解决了多态导致的问题。例如下面这一段代码,虽然exec
是一个接口方法,但是GadgetChainDiscovery可以自动找到实现了这个接口的危险调用Son.exec
private void readObject(ObjectInputStream ois) throws Exception {
ois.defaultReadObject();
sonInterface.exec(command1);
}