参考文章:
https://xz.aliyun.com/t/7058
https://paper.seebug.org/1034/
https://gityuan.com/2015/10/24/jvm-bytecode-grammar/
简介 gadget-inspector
是在Black Hat USA 2018
中被提出的,是一个用来自动检测反序列化链的工具。Automated Discovery of Deserialization Gadget Chains Ian Haken
在第一次看PPT的时候,搞不懂”Controlling Data Types => Controlling Code!”具体是什么意思,在PPT中还讲述了什么是反序列化漏洞以及产生的原因,然后就是gadget chain
。
然后看了这篇文章 感觉讲的的很清晰。概括来讲就是因为JAVA的多态性,同一个方法在不同的类里可能会有不同的实现,而有些不恰当的设计可能会导致其成为反序列化漏洞中的一个gadget。
Gadget-inspector 程序入口 在gadaget-inspector
项目中,GadgetInspector.java
是程序的入口文件,程序的主要运行逻辑在GadgetInspector
类中的main()
,程序的开始是对参数的处理,如果带有--resume
,resume
会置为true
,然后继续使用上一次扫描的结果去找gadget-chains
。--config
参数用于指定需要扫描哪种反序列化链,默认为java自带的反序列化,除此之外作者还实现了jackson
和xstream
的反序列化config,在所有参数的最后需要指定待扫描的JAR包或者WAR包,代码会根据给定的jar包生成一个URLClassLoader
,这个URLClassLoader
负责之后的所需类加载以及访问。
主要逻辑 Method discover MethodDiscovery.discovery(classResourceEnumerator)
,先来介绍一下ClassResourceEnumerator
类。
ClassResourceEnumerator 在这个类中定义了一个常量属性classLoader,就是上文提到的urlClassLoader,类中还定义了一个方法getRuntimeClasses()
,此方法用来获取rt.jar包下的所有类。
其中ClassPath.from(classLoader)
返回一个 ClassPath
对象,该对象表示由给定类加载器加载的所有资源和类。
该类中还定义了一个方法 getAllClasses()
,这个方法的作用就是返回所有类的相关信息。具体的返回类型为Collection<ClassResource>
,ClassResource是一个接口,这里所指具体的实现类是ClassLoaderClassResource
,该类保存了类的名字和类加载器。
进入MethodDiscovery.discover()
方法中,这个方法主要是用MethodDiscoveryClassVisitor
对JAR包中的所有类进行分析,分析结果保存在了
MethodDiscoveryClassVisitor
的属性中。具体的工作流程是通过classReader()
接收一个class的inputStream
,之后通过accept()
方法接收ClassVisitor
(MethodDiscoveryClassVisitor extends ClassVisitor)来对class进行观察。ASM框架实现了对class文件的解析,并提供了相关的API让我们操作对应class中的字段和方法,例如classVisitor中的visitField方法会对传入的class文件中的所有字段进行观察,visitField方法有5个参数分别是int access表示字段的访问标志,String name 表示字段的名字,String descriptor表示字段的描述符,String signature表示字段的泛型,Object value表示字段的默认值,只要重写这个方法的内容就能实现对类字段的操作。听起来可能比较抽象,之后会有我写的demo供大家理解。大家如果对ASM有兴趣的话可以去官网下载对应的文档 学习。ASM框架的通用流程简单如下:
这里不涉及到生成增强Class,所以不调用最后的ClassWriter。
MethodDiscoveryClassVisitor
实现了4个方法:visit
、visitField
、visitMethod
和visitEnd
。accept()
方法要接收一个classvisitor
作为参数,然后依次调用该类的方法对传入的class数据流进行分析,具体调用顺序为
visit()
第一个便会调用该方法,version
为class的版本,access
为类的访问标致,signature
为该类的签名,superName
为该类的父类,interfaces
为该类实现的接口。
visit
方法就简单的将类的相关信息保存在自己的属性中,visitField
会依次遍历类中的所有属性,visitMethod
也是一样的。
在visitField
中获取的是非静态属性,因为static 属性不能被序列化,然后将属性的相关信息保存在members
字段中。
在discoveredMethods
保存了类和类中方法的对应关系。
discoveredClasses
中保存了类和类中属性的关系。
然后调用save()
将discoveredClasses
中的信息保存到classes.dat
文件中,将discoveredMethods
中的信息保存到methods.dat
文件中,在方法的最后调用了InheritanceDeriver.derive(classMap).save()
这段代码保存了每个类的继承信息,一个类的所有祖先类信息(包括父类的父类)。
passthroughDiscovery 紧接着会调用PassthroughDiscovery.discover(classResourceEnumerator)->discoverMethodCalls(classResourceEnumerator)
visitMethodInsn(int opcode, String owner, String name, String descriptor)
当产生方法调用的时候将会调用这个方法。 This opcode is either INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE. owner 表示被调用方法的类名 name 表示被调用方法的名字 descriptor 表示被调用方法的描述符
calledMethods
是被调用方法集合。
下面是写的一个小demo:
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 package com.lucifer;import org.objectweb.asm.MethodVisitor;public class MethodCallMethodVisitor extends MethodVisitor { protected MethodCallMethodVisitor (int api, MethodVisitor methodVisitor,String owner,String name) { super (api, methodVisitor); System.out.println("调用者:" +owner+"." +name); } @Override public void visitMethodInsn (int opcode, String owner, String name, String descriptor, boolean isInterface) { System.out.println("被调用者" +owner+"." +name); super .visitMethodInsn(opcode, owner, name, descriptor, isInterface); } } package com.lucifer;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.MethodVisitor;import org.objectweb.asm.Opcodes;public class MethodCallClassVisitor extends ClassVisitor { private String name; protected MethodCallClassVisitor (int api) { super (api); } @Override public void visit (int version, int access, String name, String signature, String superName, String[] interfaces) { this .name = name; super .visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod (int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super .visitMethod(access, name, descriptor, signature, exceptions); mv = new MethodCallMethodVisitor (Opcodes.ASM6, mv, this .name, name); return mv; } } package com.lucifer;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.ClassWriter;import org.objectweb.asm.Opcodes;import java.io.*;public class Main { public static void main (String[] args) throws IOException { File file = new File ("./src/main/java/com/lucifer/Person.class" ); FileInputStream fileInputStream = new FileInputStream (file); org.objectweb.asm.ClassReader classReader = new org .objectweb.asm.ClassReader(fileInputStream); ClassVisitor myVisitor = new MethodCallClassVisitor (Opcodes.ASM6); classReader.accept(myVisitor, ClassReader.EXPAND_FRAMES); } }
methodCalls是一个保存了调用者method对应被调用者methods信息的集合。discoverMethodCalls
其实就是做了这一件简单的事,接下来是topologicallySortMethodCalls()
,通过深度优先搜索实现了逆拓扑排序,算法利用stack解决了环的问题。
在对所有方法进行排序之后就会进入到calculatePassthroughDataflow()
方法中。
<clinit>
是一个特殊的静态初始化方法,在类加载时自动执行,用于初始化类的静态变量和静态初始化块。它由编译器生成,并且在 Java 源代码中不可见,但对类的初始化至关重要,由于是类加载时自动执行的(我们不可控)所以会直接跳过对该方法的处理。
这段代码用来分析class数据流中每个方法的参数和返回结果之间的关系,在TaintTrackingMethodVisitor
中维护了两个变量localVars
和stackVars
分别代表着局部变量表和操作数栈。在JVM中每一个函数调用都会在调用栈中放入一个新的栈帧,栈帧中包括了局部变量表用来放方法参数,如果方法是实例方法的话,第0个参数是this
指向调用方法的实例对象,如果是类方法的话局部变量表中只包含方法参数,操作数栈就是在调用一些字节码指令时的操作数会实现压入操作数栈中。举个例子如果一个方法让两个数相加,字节码指令IADD
的作用是两个int类型的数字相加,在字节码指令执行引擎执行IADD
指令时,必须保证操作数栈中最上面的两个操作数是int类型的数字。ILOAD_x
是将局部变量中第x个变量压入操作数栈中。所以在执行IADD
之前会执行两次ILOAD
。
在调用PassthroughDataflowMethodVisitor.visitCode()
时会根据方法参数的类型去初始化局部变量表。
局部变量表的类型是List<Set<Integer>>
,Set集合中存放的是方法参数的下标索引,PassthroughDataflowMethodVisitor
模拟了JVM指令码对操作数栈的操作,在最后返回时根据返回结果的Set集合中的值确定该方法返回值可以被哪个参数污染,然后保存到private final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow
变量中,如果在方法中调用另一个方法我们希望这个被调用的方法之前已经被处理过了(逆拓扑排序就是为了达到这个效果),这样就可以直接在passthroughDataflow
中找。
这里我也写了一个小demo帮助理解,更好的理解污点传播的规则。
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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 package org.example;import org.objectweb.asm.*;import java.util.ArrayList;import java.util.HashSet;import java.util.List;import java.util.Set;public class ClassAttrVisitor extends ClassVisitor { private Set<Integer> returnTaints; private MethodVisitorDemo taintMethodVisitor; public ClassAttrVisitor () { super (Opcodes.ASM6); returnTaints = new HashSet <>(); } @Override public void visit (int version, int access, String name, String signature, String superName, String[] interfaces) { System.out.println("类名:" +name+" 父类:" +superName); super .visit(version, access, name, signature, superName, interfaces); } @Override public FieldVisitor visitField (int access, String name, String descriptor, String signature, Object value) { System.out.println("属性名字:" +name+" 类型:" +descriptor); return super .visitField(access, name, descriptor, signature, value); } @Override public MethodVisitor visitMethod (int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = super .visitMethod(access, name, descriptor, signature, exceptions); MethodVisitorDemo taintMv = new MethodVisitorDemo (methodVisitor, name, descriptor,access,returnTaints); this .taintMethodVisitor = taintMv; return taintMv; } } class MethodVisitorDemo extends MethodVisitor { private List<Set<Integer>> localVars; private List<Set<Integer>> stackVars; private int access; private Set<Integer> returnTaint; private String methodName; private String desc; protected MethodVisitorDemo (MethodVisitor mv,String methodName,String desc,int access,Set<Integer> returnTaint) { super (Opcodes.ASM6,mv); this .methodName = methodName; this .desc = desc; this .access = access; this .returnTaint = returnTaint; } @Override public void visitCode () { localVars = new ArrayList <>(); stackVars = new ArrayList <>(); int argIndex = 0 ; if ((this .access & Opcodes.ACC_STATIC) == 0 ) { localVars.add(new HashSet <Integer>(){{add(0 );}}); argIndex += 1 ; } for (Type argType : Type.getArgumentTypes(desc)) { for (int i = 0 ; i < argType.getSize(); i++) { HashSet<Integer> args = new HashSet <>(); args.add(argIndex); localVars.add(args); } argIndex += 1 ; } super .visitCode(); } @Override public void visitVarInsn (int opcode, int varIndex) { switch (opcode){ case Opcodes.ILOAD: stackVars.add(localVars.get(varIndex)); } super .visitVarInsn(opcode, varIndex); } @Override public void visitInsn (int opcode) { switch (opcode){ case Opcodes.IADD: Set<Integer> taint1 = stackVars.get(stackVars.size() - 1 ); Set<Integer> taint2 = stackVars.get(stackVars.size() - 2 ); Set<Integer> taint = new HashSet <>(); if (!taint1.isEmpty() || !taint2.isEmpty()) { taint.addAll(taint1); taint.addAll(taint2); } stackVars.remove(stackVars.size() - 1 ); stackVars.remove(stackVars.size() - 1 ); stackVars.add(taint); break ; case Opcodes.IRETURN: returnTaint = stackVars.get(0 ); break ; } super .visitInsn(opcode); } @Override public void visitEnd () { System.out.println(this .methodName+"的返回值被参数" +returnTaint+"污染了" ); super .visitEnd(); } }
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 package org.example;public class User { private String firstName; private String lastName; private String name () { String allName = firstName+lastName; return allName+"!" ; } private int add (int a, int b) { return a+b; } public String getFirstName () { return firstName; } public void setFirstName (String firstName) { this .firstName = firstName; } public String getLastName () { return lastName; } public void setLastName (String lastName) { this .lastName = lastName; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package org.example;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.Opcodes;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException;import java.util.ArrayList;public class Main { public static void main (String[] args) throws IOException { File file = new File ("D:\\javaCMS\\ASMdemo\\src\\main\\java\\org\\example\\User.class" ); FileInputStream fileInputStream = new FileInputStream (file); ClassReader classReader = new ClassReader (fileInputStream); ClassAttrVisitor classAttrVisitor = new ClassAttrVisitor (); classReader.accept(classAttrVisitor,ClassReader.EXPAND_FRAMES); } }
运行结果如下所示:
callGraphDiscovery 毫无疑问这部分代码重要的也是discover
方法。
在discover方法中用ModelGeneratorClassVisitor
对传入的class数据流进行观察,在ModelGeneratorClassVisitor
中重写了visitMethodInsn
,visitMethodInsn
主要观察的字节码指令为 INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE.
这几个字节码指令都是在方法调用时才会触发,在观察一个方法时又调用了另一个方法就会进入到visitMethodInsn
方法中。
这个方法主要观察的是原方法的参数与被调用方法参数的流动关系。例如A方法调用了B方法。
1 2 3 4 5 function A(a,b){ c = a; d = b B(c,d); }
A方法的参数a流入到了B方法参数c的位置,A方法的参数b流入了B方法参数d的位置。这样的话就会在discoveredCalls记录(原方法所属类和方法名字,被调用方法所属类和方法名字,原方法参数下标,原方法参数字段名,被调用方法参数下标),结合这个例子就是discoveredCalls记录(A,B,0,nul,0)、(A,B,1,nul,1),其实就是对应方法参数的流动关系。
jvm小知识点
jvm调用每个方法时都会为其创建栈帧,每一个栈帧包括局部变量表、操作数栈、动态链接和方法返回地址等,在一个方法调用一开始局部变量表里包括方法所需的参数,如果该方法是一个实例方法则局部变量表还会包括调用该方法的实例引用。操作数栈在一开始为空,如果需要用到某个参数会通过字节码指令将参数从局部变量表中加载到操作数栈中。在一个方法中调用另一个方法例如A方法调用B方法,在调用之前B方法所需的所有参数都已经存储在A方法的操作数栈中。JVM此时会暂停A方法的执行并创建B方法的栈帧。方法B的参数从方法A的操作数栈中弹出,并存储到方法B的局部变量表中。当方法B执行完毕后,其栈帧会被销毁,并且返回值(如果有的话)会被压入方法A的操作数栈,方法A继续执行。
一直没有提到TaintTrackingMethodVisitor
,ModelGeneratorClassVisitor extends TaintTrackingMethodVisitor
,在TaintTrackingMethodVisitor
中实现了默认的污点传播规则和模拟JVM模拟堆栈变化的规则(都是根据字节码指定做相应的动作)。
sourceDiscovery 在这个阶段,gadgetinspector会搜索所有反序列化的入口点。不同的反序列化协议有不同的入口,例如hessian反序列化协议会执行Map.put()
,fastJson会执行类的get
和set
方法,java原生的反序列化会自动调用readObject()
方法,想要实现对不同反序列化链的挖掘就要继承SourceDiscovery
类并实现discover()
方法。这里以java原生反序列化来说,SimpleSourceDiscovery.discover()
方法用来搜索java原生反序列化入口点,在搜索方法前会判断方法是否是可序列化的,在java原生反序列化中只有可序列化的类才能作为反序列化漏洞中的一环,然后就在各个类中检索入口点方法,这里除了readObejct
还搜索了finalize、invoke、hashCode、equals、call、doCall
,将方法的名字和被污染的参数记录在discoveredSources中。
gadgetChainDiscovery 在GadgetChainDiscovery.discover()
中,先将caller(方法的调用者)和可以污染到方法放到一个map中。
下面这段代码是将找到的source方法和可以被污染的参数放入到methodsToExplore
列表中。
下面这段代码就是找与methodsToExplore
中的source方法名字相同且可污染参数索引一致的方法添加到链中,并且通过isSink方法判断是否最后一个方法是不是最后的sinkFunction,如果是就将这条链添加到discoveredGadgets中。
最后 若文章有任何问题可以直接在关于页面找到我的邮箱发邮件给我,有任何其他问题也可以直接找我。