1 前言
1.1 简介
GadgetInspector是Black Hat 2018提出的一个Java反序列化利用链自动挖掘工具,核心技术的Java ASM,结合字节码的静态分析。根据输入JAR包和JDK已有类进行分析,最终得到利用链
本文的核心是:深入分析数据流模块(PassthroughDataflow)的每一句ASM代码,进而把握最底层的原理
1.2 整体流程
整个流程第一步是根据JDK和输入的Jar得到所有的字节码,然后通过MethodDiscovery分析,参考第2,3章。获取所有的方法信息,类信息和继承信息。继承关系InheritanceMap指某个类的父类和实现的接口都有哪些
第二步是本文的核心,数据流分析确定:方法的参数和返回值之间的关系。利用第一步获得信息得到方法中的方法调用,结合InheritanceMap的继承关系,将所有方法进行拓扑逆排序(参考8.7)实现最先调用的方法在最前端。然后利用PassthroughDiscovery得到每个方法的参数和返回值之间的关系,也就是返回值能够被哪些参数污染
而PassthroughDiscovery的底层是TaintTrakingMethodVisitor,这个类是该项目的核心,参考第5节,他模拟了JVM Stack Frame中的Operand Stack和Local Variables Array让代码“动”起来,进而根据方法调用流程拿到具体的结果passthroughDataflow,参考第6,7和8节。这个结果从一开始是最底层的调用,所以他的第一步结果可以被第二步分析使用
后续利用上文模拟的机制,生成调用图(CallGraph)后结合漏洞触发入口(readObject等)得到discoveredSources,主要保存了方法入口和污染参数信息。在最后一步和之前所有信息合并
1.3 加载
主要是区分了SpringBoot Fat Jar,Jar Lib,War三种方式:
- SpringBoot Fat Jar:一个大List,加入解压后的BOOT-INF/classes路径,并加入BOOT-INF/lib下所有Jar Lib的路径,最终构造一个URLClassLoader用于获取所有字节码文件,JVM停止后自动删除解压路径
- Jar Lib:直接将所有输入的Jar Lib加入JarClassLoader(该类继承自URLClassLoader)
- War:一个大List,加入解压后的WEB-INF/classes路径,并加入WEB-INF/lib下所有Jar Lib的路径,最终构造一个URLClassLoader用于获取所有字节码文件,JVM停止后自动删除解压路径
加载字节码的核心方法来自guava库的ClassPath.from(classLoader).getAllClasses()
最终获得的都是ClassLoader对象,然后统一获得所有字节码文件
for (ClassPath.ClassInfo classInfo : ClassPath.from(classLoader).getAllClasses()) { result.add(new ClassLoaderClassResource(classLoader, classInfo.getResourceName())); }
加载JDK的rj.jar代码如下,利用String类拿到rt.jar的路径,构造URLClassLoader然后加载
// java.lang.String类在rt.jar中,JDK不只是rt.jar,个别类并不属于rt.jar URL stringClassUrl = Object.class.getResource("String.class"); URLConnection connection = stringClassUrl.openConnection(); Collection<ClassResource> result = new ArrayList<>(); if (connection instanceof JarURLConnection) { URL runtimeUrl = ((JarURLConnection) connection).getJarFileURL(); URLClassLoader classLoader = new URLClassLoader(new URL[]{runtimeUrl}); // 类似的操作 for (ClassPath.ClassInfo classInfo : ClassPath.from(classLoader).getAllClasses()) { result.add(new ClassLoaderClassResource(classLoader, classInfo.getResourceName())); } }
经过测试,ClassPath.from方式拿到Jar的class文件是包含rt.jar的,大概在三万多。如果jar数量多,会出现大量的重复,造成不小的性能问题,是否存在一种方式可以直接拿到rt.jar和输入jar的所有class文件的inputStream并且不重复?(已实现,后续开源)
当然,也可能是笔者本地调试的问题,由于一些特殊原因导致出现大量的重复,这点不是文章的重点,顺便提到而已
1.4 基础
基础主要是ASM技术的一些基础,需要大致明白ASM如何解析字节码
这里给出ClassVisit和MethodVisit的顺序,以便于后续的理解
ClassVisit:大体来看visit->visitAnno->visitField或visitMethod->visitEnd
visit [visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass] ( visitAnnotation | visitTypeAnnotation | visitAttribute )* ( visitNestMember | visitInnerClass | visitRecordComponent | visitField | visitMethod )* visitEnd
MethodVisit:大体来看visitParam->visitAnno->visitCode->visitFrame或visitXxxInsn->visitMax->visitEnd
(visitParameter)* [visitAnnotationDefault] (visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)* [ visitCode ( visitFrame | visitXxxInsn | visitLabel | visitInsnAnnotation | visitTryCatchBlock | visitTryCatchAnnotation | visitLocalVariable | visitLocalVariableAnnotation | visitLineNumber )* visitMaxs ] visitEnd
GadgetInspector模拟了JVM Stack中Frame的Operand Stack和Local Variables Array,这一步是基础将在5.1中介绍
1.5 杂项
一些细节,比如ClassReference.Handle重写equal
可以看到判断两个类名对象Handle是否相等是根据字符串name做的,因此hashcode只需要根据name做
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Handle handle = (Handle) o; return name != null ? name.equals(handle.name) : handle.name == null; } @Override public int hashCode() { return name != null ? name.hashCode() : 0; }
处理jar包内的class文件,比较巧妙。可以看到创建了一个临时目录,比如windows在C:\\User\\AppData\\Local\\Temp中。添加一个shutdownHook会在JVM退出的时候调用,在这里面删除这个临时目录。而临时目录中保存的是jar中的所有class文件,用于创建输入流,然后交给ClassReader做分析
final Path tmpDir = Files.createTempDirectory("exploded-jar"); // Delete the temp directory at shutdown Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { deleteDirectory(tmpDir); } catch (IOException e) { LOGGER.error("Error cleaning up temp directory " + tmpDir.toString(), e); } }));
还有一些小问题,这里就不继续写了,回到重点
2 MethodDiscoveryClassVisitor
继承自ASM的ClassVisitor,主要作用是对所有字节码中的类进行观察,下文将根据ASM定义的visit顺序进行分析
2.1 visit
// 观察到某一个类的时候会先调用visit方法 public void visit ( int version, int access, String name, String signature, String superName, String[]interfaces){ // 给一些全局变量赋值 this.name = name; this.superName = superName; this.interfaces = interfaces; // 接下来分析 this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0; this.members = new ArrayList<>(); // 类名 this.classHandle = new ClassReference.Handle(name); // 注解 annotations = new HashSet<>(); // 完成自己的逻辑后需要调用super.visit继续 // 类似中间人攻击,不阻断正常流程,且可以在中间做事情 super.visit(version, access, name, signature, superName, interfaces); }
注意到其中的(access & Opcodes.ACC_INTERFACE) != 0为什么这样可以判断是否为接口,因为Opcode中定义如下,发现每一种标识恰好二进制某一位为1,如果按位与,只要不包含该表示,那么得出结果一定是0
// 0000 0000 0000 0001 int ACC_PUBLIC = 0x0001; // class, field, method // 0000 0000 0000 0010 int ACC_PRIVATE = 0x0002; // class, field, method // 0000 0000 0000 0100 int ACC_PROTECTED = 0x0004; // class, field, method // 0000 0010 0000 0000 int ACC_INTERFACE = 0x0200; // class
2.2 visitAnnotation
注解在整个流程中没有什么实际的意义
// 调用完visit后会到达visitAnnotation public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { annotations.add(descriptor); // 不阻断 return super.visitAnnotation(descriptor, visible); }
2.3 visitField
// visitField和visitMethod调用优先级一样 // 对类属性进行观察 public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { // 该属性非STATIC if ((access & Opcodes.ACC_STATIC) == 0) { Type type = Type.getType(desc); String typeName; if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) { // 属性类型非引用调用getInternalName得到名称 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); }
2.4 visitMethod
// 对类里的方法进行观察 public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { // 是否是STATIC boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; //找到一个方法,添加到缓存 discoveredMethods.add(new MethodReference( classHandle, name, desc, isStatic)); // 传递 return super.visitMethod(access, name, desc, signature, exceptions); }
2.5 visitEnd
// 最后调用的一定是visitEnd方法 public void visitEnd() { ClassReference classReference = new ClassReference( name, superName, interfaces, isInterface, //把所有找到的字段(属性)封装 members.toArray(new ClassReference.Member[members.size()]), annotations); //添加类到缓存 discoveredClasses.add(classReference); super.visitEnd(); }
2.6 作用
得到所有类和方法信息后,进行分析获取进一步的信息,并保存供后续步骤操作
3 MethodCallDiscoveryClassVisitor
3.1 visit
@Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); if (this.name != null) { throw new IllegalStateException("ClassVisitor already visited a class!"); } // 记录当前visit的类 this.name = name; }
3.2 visitMethod
在Java7以前的版本会用到jsr指令,本质原因是为了程序的兼容性,兼容Jar包和JDK一些老类
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { // 先进行正常的方法观察 MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); // 跟入4 MethodCallDiscoveryMethodVisitor modelGeneratorMethodVisitor = new MethodCallDiscoveryMethodVisitor( api, mv, this.name, name, desc); // 等价于return new MethodCallDiscoveryMethodVisitor(...); return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions); }
3.3 作用
与MethodCallDiscoveryMethodVisitor一起记录所有方法内的所有方法调用
public String Test(){ A a = new A(); a.test1(); // static B.test2(); }
例如这里的test1和test2就是方法内的方法调用
4 MethodCallDiscoveryMethodVisitor
4.1 构造
// 内部类构造方法 public MethodCallDiscoveryMethodVisitor(final int api, final MethodVisitor mv, final String owner, String name, String desc) { super(api, mv); // 记录当前方法中的所有方法调用 this.calledMethods = new HashSet<>(); // 将当前visit的method添加到全局变量 methodCalls.put(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc), calledMethods); }
4.2 visitMethodInsn
方法中的方法相关指令
@Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { // visit的方法中如果存在方法调用都加入全局变量(无论static,interface还是普通调用) calledMethods.add(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc)); super.visitMethodInsn(opcode, owner, name, desc, itf); }
4.3 作用
与MethodCallDiscoveryClassVisitor一起记录方法内的方法调用,参考3.3
5 TaintTrackingMethodVisitor
5.1 JVM Frame
分析该类离不开JVM的原理。JVM Stack在每个线程被创建时被创建,用来存放一组栈帧(Frame)
每次方法调用均会创建一个对应的Frame,方法执行完毕或者异常终止,Frame被销毁
而每个Frame的结构如下,主要由本地变量数组(local variables)和操作栈(operand stack)组成
局部变量表所需的容量大小是在编译期确定下来的,表中的变量只在当前方法调用中有效
JVM把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中
例如:方法调用会从当前的Stack里弹出参数,而弹出的参数就到了新的局部变量表里,执行完返回的时候就得把返回值PUSH回Stack。比如5.4中的visitCode做的事就是将参数放到局部变量表
之所以介绍JVM Frame,是因为代码模拟了比较完善的Operand Stack和Local Varialbles交互,例如方法调用会从Stack中弹出参数,方法返回值会压入栈中。根据这样的规则,进而执行数据流的分析
5.2 SavedVariableState
在5.1中介绍stack和local variables因为在TaintTrackingMethodVisitor中自行实现了这样的结构
注意到这里保存的Set集合,实际上代码中要么是空Set和Null做占位,要么保存的是实际有意义的值,也就是污染点
污染点的含义是参数索引,进而分析影响返回值的参数是什么。那为什么要用Set不会数组或List呢?因为Set自带去重,分析代码中会往Stack中设置多次污染信息(见后文分析)
private static class SavedVariableState<T> { // [local variables] List<Set<T>> localVars; // [operand stack] List<Set<T>> stackVars; // 新建构造 public SavedVariableState() { localVars = new ArrayList<>(); stackVars = new ArrayList<>(); } // 复制构造 public SavedVariableState(SavedVariableState<T> copy) { this.localVars = new ArrayList<>(copy.localVars.size()); this.stackVars = new ArrayList<>(copy.stackVars.size()); for (Set<T> original : copy.localVars) { this.localVars.add(new HashSet<>(original)); } for (Set<T> original : copy.stackVars) { this.stackVars.add(new HashSet<>(original)); } } // 根据传入值合并 public void combine(SavedVariableState<T> copy) { for (int i = 0; i < copy.localVars.size(); i++) { while (i >= this.localVars.size()) { this.localVars.add(new HashSet<T>()); } this.localVars.get(i).addAll(copy.localVars.get(i)); } for (int i = 0; i < copy.stackVars.size(); i++) { while (i >= this.stackVars.size()) { this.stackVars.add(new HashSet<T>()); } this.stackVars.get(i).addAll(copy.stackVars.get(i)); } } }
5.3 构造
有一些变量将在后文分析
// 根据已有类信息分析生成的[子类->[祖先类,祖先的子类...父类]] private final InheritanceMap inheritanceMap; // 暂不分析 private final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow; // 一个工具类,暂不分析 private final AnalyzerAdapter analyzerAdapter; // public/private... private final int access; // 方法名 private final String name; // void(int a) -> (I)V private final String desc; // 泛型,这里没用 private final String signature; // 异常,这里没用 private final String[] exceptions; public TaintTrackingMethodVisitor(...) { super(api, new AnalyzerAdapter(owner, access, name, desc, mv)); // 一系列赋值 this.inheritanceMap = inheritanceMap; ...... } // stack和local variables private SavedVariableState<T> savedVariableState = new SavedVariableState<T>(); // 处理goto问题,暂不分析 private Map<Label, SavedVariableState<T>> gotoStates = new HashMap<Label, SavedVariableState<T>>(); // 处理异常问题,暂不分析 private Set<Label> exceptionHandlerLabels = new HashSet<Label>();
5.4 visitCode
最先调用visitCode,在进入方法体的时候
这是数据流动的起始位置,注意到根据实际情况在局部变量表里设置参数,这是模拟JVM真实的情况,以便于后续数据流分析
// 首先执行的方法 public void visitCode() { super.visitCode(); // 清空stack和local variables savedVariableState.localVars.clear(); savedVariableState.stackVars.clear(); if ((this.access & Opcodes.ACC_STATIC) == 0) { // 如果方法非static那么local variables[0]=this // 这里没有给出实际的值,而是直接占了一位 savedVariableState.localVars.add(new HashSet<T>()); } // 方法参数类型 for (Type argType : Type.getArgumentTypes(desc)) { // size只会是1或2 for (int i = 0; i < argType.getSize(); i++) { // 根据size将所有参数都占位 savedVariableState.localVars.add(new HashSet<T>()); } } }
5.5 push & pop & get
模拟Stack的push和pop操作
// 模拟stack的push private void push(T ... possibleValues) { Set<T> vars = new HashSet<>(); for (T s : possibleValues) { vars.add(s); } savedVariableState.stackVars.add(vars); } // 模拟stack的push,直接设置Set private void push(Set<T> possibleValues) { savedVariableState.stackVars.add(possibleValues); } // 模拟stack的pop private Set<T> pop() { // 注意stack后入先出,所以是最后一个 return savedVariableState.stackVars.remove(savedVariableState.stackVars.size()-1); } private Set<T> get(int stackIndex) { // 假设stack大小为10,传入index是3,那么get的索引为6,是第7个 return savedVariableState.stackVars.get(savedVariableState.stackVars.size()-1-stackIndex); }
5.6 visitFrame
visitFrame在visitCode后调用
主要作用是根据ASM给出“正确”的Frame计算方法同步当前模拟的Stack和局部变量表,确保不出现问题
第一步判断的F_NEW原因可以参考ASM源码:Must be Opcodes.F_NEW for expanded frames
几个参数的意义参考ASM文档:
- type:the type of this stack map frame
- nLocal:the number of local variables in the visited frame
- local:the local variable types in this frame.This array must not be modified
- nStack:the number of operand stack elements in the visited frame
- stack:the operand stack types in this frame.This array must not be modified
public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { if (type != Opcodes.F_NEW) { throw new IllegalStateException("Compressed frame encountered; class reader should use accept() with EXPANDED_FRAMES option."); } int stackSize = 0; for (int i = 0; i < nStack; i++) { Object typ = stack[i]; int objectSize = 1; // long和double的大小为2 if (typ.equals(Opcodes.LONG) || typ.equals(Opcodes.DOUBLE)) { objectSize = 2; } for (int j = savedVariableState.stackVars.size(); j < stackSize+objectSize; j++) { // 根据size在模拟stack中占位 savedVariableState.stackVars.add(new HashSet<T>()); } // 统计总stack大小 stackSize += objectSize; } int localSize = 0; for (int i = 0; i < nLocal; i++) { Object typ = local[i]; int objectSize = 1; // 类似 if (typ.equals(Opcodes.LONG) || typ.equals(Opcodes.DOUBLE)) { objectSize = 2; } // 类似,占位 for (int j = savedVariableState.localVars.size(); j < localSize+objectSize; j++) { savedVariableState.localVars.add(new HashSet<T>()); } // 统计 localSize += objectSize; } // 根据统计出的真实size进行缩容,达到一致 for (int i = savedVariableState.stackVars.size() - stackSize; i > 0; i--) { savedVariableState.stackVars.remove(savedVariableState.stackVars.size()-1); } // 根据统计出的真实size进行缩容,达到一致 for (int i = savedVariableState.localVars.size() - localSize; i > 0; i--) { savedVariableState.localVars.remove(savedVariableState.localVars.size()-1); } // 传递 super.visitFrame(type, nLocal, local, nStack, stack); // 验证 sanityCheck(); } private void sanityCheck() { // 利用analyzerAdapter计算和验证stack的size if (analyzerAdapter.stack != null && savedVariableState.stackVars.size() != analyzerAdapter.stack.size()) { throw new IllegalStateException("Bad stack size."); } }