赛博空间,攻防之战,此起彼伏
矛与盾的对决,攻与守的碰撞
在这里,我们一起来看
道德黑客使用了哪些杀手锏
正义蓝军如何见招拆招
-
IDEA上的datalog插件:https://github.com/LFrobeen/intellij-datalog ;
-
soot-fact-generator:https://github.com/BytecodeDL/soot-fact-generator ;
-
souffle:一款常用的datalog引擎。
soot-fact-generator
soot-fact-generator
是从doop项目中抠出来的,作用是使用soot生成各种程序分析所需要的fact;
soot的帮助信息如下:
P1n93r@p1n93r-2 soot % java -jar soot-fact-generator.jar
Usage: soot-fact-generator [options] file...
Options:
--main <class> Specify the name of the main class.
--ssa Generate SSA facts, enabling flow-sensitive analysis.
--full Generate facts by full transitive resolution.
--allow-phantom Allow phantom classes.
-d <directory> Specify where to generate output fact files.
-i <archive> Find classes in <archive>.
-l <archive> Find library classes in <archive>.
-ld <archive> Find dependency classes in <archive>.
-lsystem Find classes in default system classes.
--facts-subset <subset> Produce facts only for a subset of the given classes [APP, APP_N_DEPS, PLATFORM].
--ignore-factgen-errors Continue with the analysis even if fact generation fails.
--legacy-android-processing Enable legacy Android XML processing.
--no-facts Don't generate facts (just empty files -- used for debugging).
--ignore-wrong-staticness Ignore "wrong static-ness" errors in Soot.
--lowMem Consume less memory.
--failOnMissingClasses <file> Terminate if classes are missing (and record them to <file>).
--also-resolve <class> Force resolution of class that may not be found automatically.
--debug Enable debug mode (verbose output).
--log-dir <dir> Write logs in directory <dir>.
--args-file <file> Read command-line arguments from <file> (one per line).
--write-artifacts-map Write artifacts map.
Jimple/Shimple generation:
--generate-jimple Generate Jimple/Shimple files in addition to other facts.
--stdout Write Jimple/Shimple to stdout.
Android options:
--android-jars <archive> The main Android library JAR (for Android APK inputs). The same jar should be provided in the -l option.
--decode-apk Decompress APK input in facts directory.
--scan-native-code Scan native code found in JAR/APK inputs.
--R-out-dir <directory> Specify where to generate R code (when linking AAR inputs).
Supported input archive formats: AAR, APK, JAR, ZIP
# 如果不使用--facts-subset,soot只会加载-i指定的class;使用了--facts-subset,则生成的fact就会根据--facts-subset的值来选择性生成
java -Dfile.encoding=UTF-8 -Xmx32G -jar soot-fact-generator.jar -i classbean.jar -l ./rt.jar --full -d out_dir --allow-phantom --generate-jimple --facts-subset APP --ignore-wrong-staticness --debug
# 如果想使用当前java环境下的rt.jar/jce.jar/jsse.jar,可以直接使用-lsystem选项代替-l ./rt.jar;--ssa就是生成SSA格式的IR,但是速度会很慢,并且很可能会因内存溢出而程序崩溃;
java -Dfile.encoding=UTF-8 -Xmx32G -XX:-UseGCOverheadLimit -jar soot-fact-generator.jar -i classbean.jar -i classes.jar -lsystem --full -d out_dir --allow-phantom --generate-jimple --facts-subset APP --ignore-wrong-staticness --ssa --lowMem --debug
# 禁用GC开销检查,如果使用--ssa选项,可以开启这个选项
-XX:-UseGCOverheadLimit
--application-regex 设置Class过滤规则,可以设置固定的ClassName,也可以设置ClassName全限定名的前缀、包前缀;例如:java.lang.String.class/java.lang.*/java.lang.St.**/**,如果设置了**,则代表所有ClassName都可以通过;
-idir 批量添加指定目录下的jar文件,也就是批量指定-i选项;
--facts-subset 共三个可选项:APP、APP_N_DEPS、PLATFORM,如果不指定,则soot只会加载-i指定的jar包中的class;APP选项soot会加载-i和-l指定的;APP_N_DEPS选项soot会加载-i、-l和-ld指定的;PLATFORM选项soot会加载-l指定的;但是最后soot生成的fact范围,是遵从选项的字面意思的;
--also-resolve 这个选项也很重要,相当于soot.Scene#addBasicClass(extraClass, SootClass.BODIES),如果有一些class无法被soot分析,可以把他加入到基本类中;
--allow-phantom 是否支持Java中的幻象引用;
--ignore-wrong-staticness 是否忽略fact创建过程中的 wrong static-ness 错误;
此外,soot-fact-generator在macos上运行时,如果jar包中存在XML文件可能会卡死,这应该是rt.jar中XML解析的bug,暂未发现具体原因。
ByteCodeDL
现在有了soot-fact-generator,我们不必关心fact的生成,它会生成我们静态程序分析所有可能会用到的fact;
我们只需要关心程序分析分析算法的实现即可;这里可以直接看ByteCodeDL这个项目的算法实现。
inputDeclaration.dl
首先需要看到 logic/inputDeclaration.dl
,这个dl文件主要是声明一些基础谓词(关系),加载soot-fact-generator生成的各种fact,后续的程序分析需要用到这个dl中声明的谓词。
要想进行后续的分析,必须先熟悉这个dl中的谓词以及谓词所代表的fact的具体形式和含义。
#pragma once
// symbol包含所有的字符串,它的内部实现是一个ordinal number
// 我们可以使用 .type <new-type> <: <other-type> 自定义一个类型
.type Insn <: symbol
.type Var <: symbol
.type Heap <: symbol
.type Field <: symbol
.type Method <: symbol
.type Class <: symbol
// load data from facts
// class section
.decl ClassModifier(mod:symbol, class:Class)
.input ClassModifier
.decl ClassType(class:Class)
.input ClassType
.decl InterfaceType(interface:Class)
.input InterfaceType
.decl ApplicationClass(class:Class)
.input ApplicationClass
.decl DirectSuperclass(child:Class, parent:Class)
.input DirectSuperclass
.decl DirectSuperinterface(child:Class, parent:Class)
.input DirectSuperinterface
// method section
// 这个arity指的是方法的参数个数
.decl MethodInfo(method:Method, simplename:symbol, param:symbol, class:Class, return:Class, jvmDescriptor:symbol, arity:number)
.input MethodInfo(IO=file, filename="Method.facts", delimiter="t")
.decl MethodModifier(mod:symbol, method:Method)
.input MethodModifier(IO=file, filename="Method-Modifier.facts", delimiter="t")
// fact格式:<com.xx.XXX: java.util.Map getParams()> <com.xx.XXX: java.util.Map getParams()>/@this
// 实际上,soot-fact-genertator会遍历每一个方法,然后生成 method method/@this 格式的ThisVar.fact
.decl ThisVar(method:Method, this:Var)
.input ThisVar
// 这个谓词代表方法的形参,number代表第几个形参,从0开始,fact格式:0 <com.xx.XXX: void setParams(java.util.Map)> <com.xx.XXX: void setParams(java.util.Map)>/@parameter0
.decl FormalParam(n:number, method:Method, param:Var)
.input FormalParam
// insn格式:method/指令/当前指令是当前方法内相同类型指令的第几个(从0开始);index代表当前指令在方法内的唯一索引;var代表生成IR时方法内的局部变量;method代表是哪个方法内的指令;
// fact的格式为:<com.xx.XXX: java.util.Map getParams()>/return/0 2 <com.xx.XXX: java.util.Map getParams()>/$stack1 <com.xx.XXX: java.util.Map getParams()>
.decl Return(insn:Insn, index:number, var:Var, method:Method)
.input Return
// insn格式:InvokeExpr/insnIndex,例如:<xx.xxx.XX: java.util.Map execute(xx.xxx.AA)>/java.util.Map.get/0,index就是指令在方法内的唯一索引;recerver就是生成IR时方法内的局部变量;
.decl VirtualMethodInvocation(insn:Insn, index:number, callee:Method, receiver:Var, caller:Method)
.input VirtualMethodInvocation
.decl StaticMethodInvocation(insn:Insn, index:number, callee:Method, caller:Method)
.input StaticMethodInvocation
.decl SpecialMethodInvocation(insn:Insn, index:number, callee:Method, receiver:Var, caller:Method)
.input SpecialMethodInvocation
// 这里就是代表方法实参传入方法调用,insn就是invokeExpr,var就是传入方法调用的实参(实际是IR生成的局部变量)
.decl ActualParam(n:number, insn:Insn, var:Var)
.input ActualParam
// 这里就是函数返回值赋值给局部变量,var就是IR中的局部变量,insn就是InvokeExpr;
.decl AssignReturnValue(insn:Insn, var:Var)
.input AssignReturnValue
// Field section
.decl FieldInfo(field:Field, declaringType:Class, simplename:symbol, type:Class)
.input FieldInfo(IO=file, filename="Field.facts", delimiter="t")
.decl FieldModifier(modifier:symbol, field:Field)
.input FieldModifier(IO=file, filename="Field-Modifier.facts", delimiter="t")
// 这里是加载实例的属性,insn格式:<xx.xxx.XXX: java.util.Map getParams()>/read-field-params/0;base就是IR中的局部变量,从这个局部变量中加载属性;var就是加载的属性赋值给了var;inMethod就是发生在哪个方法中;
.decl LoadInstanceField(insn:Insn, index:number, var:Var, base:Var, field:Field, inMethod:Method)
.input LoadInstanceField
.decl StoreInstanceField(insn:Insn, index:number, var:Var, base:Var, field:Field, inMethod:Method)
.input StoreInstanceField
.decl LoadStaticField(insn:Insn, index:number, var:Var, field:Field, inMethod:Method)
.input LoadStaticField
.decl StoreStaticField(insn:Insn, index:number, var:Var, field:Field, inMethod:Method)
.input StoreStaticField
// Array section
// insn格式:<xx.xxx.XXX: void init(java.lang.String)>/read-array-idx/0;array就是代表数组的局部变量;to就是取出来的元素赋值给to;
.decl LoadArrayIndex(insn:Insn, index:number, to:Var, array:Var, inMethod:Method)
.input LoadArrayIndex
.decl StoreArrayIndex(insn:Insn, index:number, from:Var, array:Var, inMethod:Method)
.input StoreArrayIndex
// 这里其实就是字面意思,arrayType就是数组类型,componentType就是数组元素类型;
.decl ComponentType(arrayType:Class, componentType:Class)
.input ComponentType
// others section
// 字面意思,变量的类型,var就是IR中的局部变量,格式为:<xx.xxx.XXX: java.util.Map getParams()>/l0#_0;class就是变量的类型
.decl VarType(var:Var, class:Class)
.input VarType(IO=file, filename="Var-Type.facts", delimiter="t")
// 字面意思,对应赋值指令
.decl AssignLocal(insn:Insn, index:number, from:Var, to:Var, inMethod: Method)
.input AssignLocal
.decl AssignCast(insn:Insn, index:number, from:Var, to:Var, type:Class, inMethod:Method)
.input AssignCast(IO=file, filename="AssignCast.facts", delimiter="t")
// 堆分配,heap代表new指令产生的对象,格式为:<xx.xxx.XXX: void <init>()>/new xx.xxx.AAA/0;var就是new出来的对象赋值给var;linenumber就是指令所在的代码行,是实际class中的代码行,而不是IR中的;
// 此外,堆对象不一定是new指令产生,可能是直接的字符字面量等等
.decl AssignHeapAllocation(insn:Insn, index:number, heap:Heap, var:Var, inMethod:Method, linenumber:number)
.input AssignHeapAllocation
// 这里指的是new指令产生的对象及对象的类型,格式为:<xx.xxx.XXX: void <init>()>/new xx.xxx.AAA/0 xx.xxx.AAA
.decl NormalHeap(value:Heap, class:Class)
.input NormalHeap
// 这里指的是new指令产生的对象及对象的类型,格式为:<xx.xxx.XXX: void <init>()>/new xx.xxx.AAA/0 xx.xxx.AAA
.decl NormalHeap(value:Heap, class:Class)
.input NormalHeap
utils.dl
这个dl中,主要是声明了SubClass以及SubEqClass,并实现了VirtualCall的Dispatch谓词。
#pragma once
#include "inputDeclaration.dl"
//self define relation
// Utils
// 这里定义了SubClass和SubEqClass谓词;所谓SubClass代表存在继承关系的两个类(也包括非直接继承);SubEqClass在SubClass的基础上添加了自身类是自身类的子类这个事实;
.decl SubClass(subclass:Class, class:Class)
.decl SubEqClass(subeqclass:Class, class:Class)
SubEqClass("byte", "byte").
SubEqClass("byte[]", "byte[]").
SubEqClass(subclass, class) :- SubClass(subclass, class).
SubEqClass(class, class) :- ClassType(class).
SubClass(subclass, class) :- DirectSuperclass(subclass, class).
SubClass(subclass, class) :- DirectSuperinterface(subclass, class).
// 这里主要是实现非直接继承关系的父子类约束
SubClass(subclass, class) :-
(
DirectSuperclass(subclass, tmp);
DirectSuperinterface(subclass, tmp)
),
SubClass(tmp, class).
// 定义AbstractType谓词代表抽象类
.decl AbstractType(type:Class)
AbstractType(type) :-
ClassModifier("abstract", type).
// 这个Dispatch谓词就代表了寻找VirtualCall的真实目标callee
// simpleName、descriptor、class分别代表VirtualCall中的函数名、函数描述、以及receiver的类型
// method就代表运行时可能会调用的真实方法
.decl Dispatch(simplename:symbol, descriptor:symbol, class:Class, method:Method)
// 如果class中本身就存在符合条件的方法,则直接调用的就是class中的这个非抽象方法
Dispatch(simplename, descriptor, class, method) :-
MethodInfo(method, simplename, _, class, _, descriptor, _),
!MethodModifier("abstract", method).
// 如果class中没有符合条件的方法,则去父类中进行Dispatch寻找目标方法
Dispatch(simplename, descriptor, class, method) :-
// 子类没重写这个非抽象方法
!MethodInfo(_, simplename, _, class, _, descriptor, _),
DirectSuperclass(class, superclass),
Dispatch(simplename, descriptor, superclass, method),
!MethodModifier("abstract", method).
这里只不过是把这个函数式的算法换成了声明式而已,逻辑都是一样的。
到这里我们可以做个小实验,先热个身,比如存在如下三个Java类:One.java、Two.java、Three.java,Two类继承于One类,而Three类继承于One类,我们只在One类中声明了一个实例方法test(),我们将尝试计算这个三个类的test方法的Dispatch:
// One.java
public class One {
public void test()throws Exception{
System.out.println("this is one....");
}
}
// Two.java
public class Two extends One{
}
// Three.java
public class Three extends Two{
}
把这个示例程序打包成jar后,我们使用soot-fact-generator来对这个程序生成IR和fact:
java -Dfile.encoding=UTF-8 -Xmx32G -XX:-UseGCOverheadLimit -jar soot-fact-generator.jar -i target/basicJava-1.0-SNAPSHOT.jar -lsystem --full -d out_dir --allow-phantom --generate-jimple --facts-subset APP --ignore-wrong-staticness --ssa --lowMem --debug
然后我们编写如下测试dl(dispatch_test.dl),就可以把Dispatch的fact输出到文件了:
#include "../utils.dl"
.output Dispatch
查看扫描Dispatch.csv文件,发现Dispatch是符合预期的,Two类和Three类的test方法都应该Dispatch到One类的test方法:
cha.dl
前面已经讲过CHA算法了,这里主要是准备基于datalog,通过实现前面说过的CHA算法来构造Call Graph;
前面也说过,Java中存在三种类型的Invoke:special invoke、static invoke、virtual invoke;对于static invoke和special invoke,可以直接确定其具体的callee;但是对于virtual invoke,因为存在多态,我们只能通过dispatch得到,也就是前面实现的Dispatch谓词;也就是如下算法(函数式):
既然我们可以利用上面这个算法确定callee了,那么也就能构造call graph了,也是之前提过的算法(函数式):
#pragma once
#include "utils.dl"
// 入口点,也就是CHA分析的入口点
.decl EntryPoint(simplename:symbol, descriptor:symbol, class:Class)
// 从入口点开始,能调用到的方法
.decl Reachable(method:Method, step:number)
// 想要寻找的sink方法
.decl SinkDesc(simplename:symbol, class:Class)
.decl SinkMethod(method:Method)
// 入口函数,和入口点对应
.decl EntryMethod(method:Method)
// 这个地方主要是定义一个调用链黑名单节点,如果调用链到了这个节点,则断开
.decl BanCaller(method:Method)
// 声明call-graph谓词
.decl CallGraph(insn:Insn, caller:Method, callee:Method)
// SinkMethod谓词推导,SinkMethod不能为抽象方法,可以是SinkDesc中指定的class的子类中的方法
SinkMethod(method) :-
SinkDesc(simplename, class),
SubEqClass(subeqclass, class),
!MethodModifier("abstract", method),
MethodInfo(method, simplename, _, subeqclass, _, _, _).
// Dispatch谓词表明了class能调用到的实际方法(即class中不存在方法,则会调用从class父类继承而来的方法)
// 这里就是初始化入口方法,且经过Dispatch谓词限定,表明入口方法不是抽象方法
// 同时入口方法也需要加入到Reachable中,并且设置其深度为0(这个深度是为了限制call-graph的深度过大)
EntryMethod(method),
Reachable(method, 0) :-
EntryPoint(simplename, descriptor, class),
Dispatch(simplename, descriptor, class, method).
// SpecialMethodInvocation可以直接确定为一个call-graph,当然,需要能Reachable到对应的caller
Reachable(callee, n+1),
CallGraph(insn, caller, callee) :-
Reachable(caller, n),
!BanCaller(caller),
n < MAXSTEP,
SpecialMethodInvocation(insn, _, callee, _, caller).
// StaticMethodInvocation可以直接确定为一个call-graph,当然,需要能Reachable到对应的caller
Reachable(callee, n+1),
CallGraph(insn, caller, callee) :-
Reachable(caller, n),
!BanCaller(caller),
n < MAXSTEP,
StaticMethodInvocation(insn, _, callee, caller).
// CHA算法的关键,计算多态(Virtual Call)的callee
Reachable(callee, n+1),
CallGraph(insn, caller, callee) :-
Reachable(caller, n),
!BanCaller(caller),
n < MAXSTEP,
// 根据caller拿到对应的callee对应的VirtualInvoke
VirtualMethodInvocation(insn, _, method, receiver, caller),
// 根据method(也就是callee)得到callee的simplename和descriptor
MethodInfo(method, simplename, _, _, _, descriptor, _),
// 拿到receiver的声明类型
VarType(receiver, class),
// 拿到receiver的声明类型的子类(包含自身)
SubEqClass(subeqclass, class),
// 必须是非抽象类
!ClassModifier("abstract", subeqclass),
// 找到真正的运行时callee
Dispatch(simplename, descriptor, subeqclass, callee).
// 这里是手动把java.security.AccessController#doPrivileged的call-graph连起来了
// 也就是callee就是java.security.AccessController#doPrivileged方法的第0个参数的run()方法
Reachable(callee, n+1),
CallGraph(insn, caller, callee) :-
Reachable(caller, n),
!BanCaller(caller),
n < MAXSTEP,
StaticMethodInvocation(insn, _, method, caller),
MethodInfo(method, "doPrivileged", _, "java.security.AccessController", _, _, _),
ActualParam(0, insn, param),
VarType(param, class),
MethodInfo(callee, "run", _, class, _, _, 0).
// 如果定义了CHAO宏,并且大于0,才会启动下面的谓词推导
// CHAO is CHA OPTIMIZATION LEVEL
#if CHAO > 0
// SinkReachable就是把callee是sink的Call-Graph链形成一个叫做SinkReachable的谓词
// 代表method经过step步长后能到达sink方法
.decl SinkReachable(method:Method, sink:Method, step:number)
SinkReachable(sink, sink, 0) :-
SinkMethod(sink).
SinkReachable(caller, sink, n+1) :-
n < MAXSTEP,
SinkReachable(callee, sink, n),
CallGraph(_, caller, callee).
#endif
// 如果定义了CHAO宏,并且大于1,才会启动下面的谓词推导
// CHAO is CHA OPTIMIZATION LEVEL
#if CHAO > 1
// 这个谓词代表了能到达sink,且步长最短的caller
.decl ShortestPathToSink(caller:Method, sink:Method, step:number)
// 代表从EntryMethod到sink的最短路径
ShortestPathToSink(entry, sink, n) :-
// SinkReachable(entry, sink, step)代表了从entry到sink所需要step步长
// 而 min step : {SinkReachable(entry, sink, step)} 则代表了所有的这些组合中,步长最短的组合
n = min step : {SinkReachable(entry, sink, step)},
SinkMethod(sink),
EntryMethod(entry).
// 找到从EntryMethod到sink的最短路径后,从EntryMethod开始往下找污点链路上的callee(也就是callee经过n-1步长后可达sink),形成一个新的ShortestPathToSink
// 前面我们计算的SinkReachable,代表的是从entry到达sink的最短路径
// 现在我们要在这条最短路径上,把路径上的各个节点到sink的路径也加入到SinkReachable
// 比如entry到sink最短需要3步,这条最短路径为:entry->a->b->sink
// 那么我们就可以得到SinkReachable(entry, sink, 3)
// 然后经过下面的推导,我们就可以继续得到:SinkReachable(a, sink, 2)、SinkReachable(b, sink, 1)
ShortestPathToSink(callee, sink, n-1) :-
n < MAXSTEP + 1,
ShortestPathToSink(caller, sink, n),
SinkReachable(callee, sink, n-1),
CallGraph(_, caller, callee).
#endif
.decl RefinedReachable(method:Method)
#ifdef CHAO
#if CHAO == 1
RefinedReachable(method) :-
// SinkReachable(method, _, _).代表method能到达sink,但是这个method不一定是最短的路径
SinkReachable(method, _, _).
#endif
#if CHAO == 2
RefinedReachable(method) :-
// ShortestPathToSink(method, _, _).代表method能到达sink,且路径最短
ShortestPathToSink(method, _, _).
#endif
#else
RefinedReachable(method) :-
// Reachable(method, _).代表了能从entry访问到method,这个method不一定是污点路径上的节点
Reachable(method, _).
#endif
.decl CallNode(node:Method, label:symbol)
.output CallNode
// 下面就是为RefinedReachable(node)中的节点打标签
CallNode(node, "method") :-
!EntryMethod(node),
!SinkMethod(node),
RefinedReachable(node).
CallNode(node, "sink") :-
RefinedReachable(node),
SinkMethod(node).
CallNode(node, "entry") :-
RefinedReachable(node),
EntryMethod(node).
.decl CallEdge(caller:Method, callee:Method)
.output CallEdge
// CallEdge(caller, callee)中的caller和callee都需要在RefinedReachable(node)这个关系中
// RefinedReachable(node)具体的fact需要根据CHAO的值来确定
// 如果CHAO=1,则RefinedReachable(node)代表能到达sink的所有节点(不一定是最短路径上的节点)
// 如果CHAO=2,则RefinedReachable(node)代表能到达sink的所有节点,且这些节点都是最短路径上的节点
// 如果CHAO等于其他,则RefinedReachable(node)代表从Entry可达的所有节点,其中包含了能到达sink的节点,也包含了不能到达sink的节点,数量巨多,极其不推荐使用这种方式
CallEdge(caller, callee) :-
RefinedReachable(caller),
RefinedReachable(callee),
CallGraph(_, caller, callee).
RefinedReachable(node)
所代表的节点):如果 CHAO=2 ,会进行如下谓词推导:
ShortestPathToSink(entry, sink, n) :-
n = min step : {SinkReachable(entry, sink, step)},
SinkMethod(sink),
EntryMethod(entry).
然后继续下面的这个谓词推导过程,就是把上面那条最短路径上的两个蓝色的节点也加入到ShortestPathToSink关系中,于是可以得到SinkReachable(a, sink, 2)、SinkReachable(b, sink, 1) :
ShortestPathToSink(callee, sink, n-1) :-
n < MAXSTEP + 1,
ShortestPathToSink(caller, sink, n),
SinkReachable(callee, sink, n-1),
CallGraph(_, caller, callee).
图示如下:
所以,如果我们定义CHAO=2,就只会得到上面标黄的节点,如果我们定义CHAO=1,就会得到上图标黄以及标蓝的节点;如果CHAO定义为其他,则会得到上图所有节点(即使有很多无法到达sink的节点);
这里插一句,如果CHAO定义为其他,会进行全图搜索,速度超级无敌慢,建议不用使用这种方式进行搜索;
对于漏洞挖掘而言,为了防止漏报,推荐设置CHAO=1,即得到标黄及标蓝的节点,这些节点都是可以到达sink的节点;如果是设置CHAO=2,仅能得到标黄的节点,但是污点在这条路线上流动,经过控制流不一定真的能到达sink;
既然通过CHA算法定义了Call Graph,具体如何使用呢?举个例子,先使用soot-fact-generator对所需要分析的程序进行fact生成,然后创建一个cha-test.dl文件,内容如下:
#define MAXSTEP 23
// 这里推荐使用CHAO 1,这个选项可以只输出source->sink链路上的节点,否则neo4j查询会很慢
#define CHAO 1
#include "../logic/cha.dl"
// 加载方法注解fact
// Method-Annotation.facts这个fact文件中包含了所有方法以及方法对应的注解
.decl MethodAnnotation(method:Method, annotation:symbol)
.input MethodAnnotation(IO=file, filename="Method-Annotation.facts", delimiter="t")
// init SinkDesc
// common sink
SinkDesc("exec", "java.lang.Runtime").
SinkDesc("<init>", "java.lang.ProcessBuilder").
SinkDesc("start", "java.lang.ProcessImpl").
SinkDesc("loadClass", "java.lang.ClassLoader").
SinkDesc("defineClass", "java.lang.ClassLoader").
SinkDesc("readObject", "java.io.ObjectInputStream").
SinkDesc("readExternal", "java.io.ObjectInputStream").
// SSTI sink
SinkDesc("evaluateString", "org.mozilla.javascript.Context").
// init entrypoint
// entry节点即为存在@javax.ws.rs.Path注解的方法
EntryPoint(simplename, descriptor, class) :-
MethodAnnotation(method,"javax.ws.rs.Path"),
MethodInfo(method, simplename, _, class, _, descriptor, _).
接着使用souffle执行dl:
# soot-fact-generator生成的fact文件在out_dir中,souffle推导生成的fact在out_test目录
souffle -F /Volumes/SSD/windows10_workspace/out_dir/ -D ../out_test ./cha-test.dl
然后执行如下bash脚本(脚本需要适配你的环境进行修改),把生成的fact导入到neo4j中(需要提前安装好neo4j):
#!/bin/bash
dbname=$1$(date "+%m%d%H%M")
neo4j-admin import --relationships=Call="../neo4j/CallEdgeHeader.csv,../out_test/.*CallEdge.csv" --nodes="../neo4j/CallNodeHeader.csv,../out_test/.*CallNode.csv" --database=$dbname --delimiter="t"
if grep -q "dbms.default_database=" /Volumes/SSD/programs/neo4j/neo4j-community-4.4.12/conf/neo4j.conf; then
sed -i -E "s/dbms.default_database=w+/dbms.default_database=$dbname/g" /Volumes/SSD/programs/neo4j/neo4j-community-4.4.12/conf/neo4j.conf
echo "[!] replaced dbms.default_database=$dbname line in neo4j.conf"
else
echo "dbms.default_database=$dbname" >> /Volumes/SSD/programs/neo4j/neo4j-community-4.4.12/conf/neo4j.conf
echo "[!] appended dbms.default_database=$dbname to neo4j.conf"
fi
echo "[!] the target neo4j db is : $dbname"
启动neo4j:
neo4j console
然后执行如下cypher语句进行查询:
match p=(firstNode:entry)-[:Call*]->(lastNode:sink) return p
如下所示是一个source到sink的完整调用链(当然,只是call-graph上成立,还得人工排查数据流和控制流):
后续想删除某个不用了的数据库,直接到如下目录删除对应的文件夹即可:
/Volumes/SSD/programs/neo4j/neo4j-community-4.4.12/data/databases
rta.dl
RTA是CHA的改进版本,RTA调用图算法认为,receiver的运行时类型不仅要满足是声明类型的子类,而且这个子类还要已经创建过实例化。
新增一个新的谓词:InstantiatedClass(insn:Insn, class:Class)
,表示Reachable的方法中,实例化了一个对象,class就是对象的类型,insn就是创建对象的指令相关信息,里面携带对应的Reachable方法信息和实例化指令类型:
InstantiatedClass(insn, class) :-
Reachable(method, _),
AssignHeapAllocation(insn, _, heap, _, method, _),
NormalHeap(heap, class).
然后就是计算多态运行时方法时,需要限定receiver的subeqclass已经实例化过:
Reachable(callee, n+1),
CallGraph(insn, caller, callee) :-
Reachable(caller, n),
!BanCaller(caller),
n < MAXSTEP,
VirtualMethodInvocation(insn, _, method, receiver, caller),
MethodInfo(method, simplename, _, _, _, descriptor, _),
VarType(receiver, class),
SubEqClass(subeqclass, class),
!ClassModifier("abstract", subeqclass),
// 限制subeqclass已创建实例的类型
InstantiatedClass(_, subeqclass),
Dispatch(simplename, descriptor, subeqclass, callee).
这个算法不推荐应用于实际挖洞,很多对象是依赖注入实例化的,不会显式调用new指令,会造成严重漏报(当然,也可以优化下算法,把依赖注入这种实例化情况也加进来)。
pt-noctx.dl
注意:使用指针分析,一定不能使用 –facts-subset APP 选项,否则生成的fact文件只有app的,没有JDK等的fact,会导致很多指针相关的算法失效(事实上,只有使用CHA分析的时候,才推荐使用 –facts-subset APP );
同时,如果算法是流不敏感的,可能会产生一些虚假的结果,可以通过指定 —ssa 来提高精度;
BytecodeDL主要实现了上下文不敏感、流不敏感、数组不敏感的指针分析;然后基于指针分析和来计算VirtualCall,从而提高精度;
但是其算法是基于 Allocation-Site
来构建堆抽象,对于Java而言就是每一个 new
指令构造一个抽象堆对象,但是对于spring这种应用而言,很多堆对象是通过依赖注入构建的,那么对于这种情况,就会造成严重漏报;后续有空可以在其基础上优化下,添加spring的依赖注入注解的堆对象构建;
官方作者解释已经足够清楚了,直接看官方解释即可:
// 建立一个Component,主要是为了解决命名冲突(这个是souffle支持的语法)
.comp ContextInsensitivePt{
// 表示var变量指向heap这个对象
.decl VarPointsTo(heap:Heap, var:Var)
// 表示baseHeap这个对象的field指向heap这个对象
.decl InstanceFieldPointsTo(heap:Heap, baseHeap:Heap, field:Field)
// 表示静态field指向heap这个对象
.decl StaticFieldPointsTo(heap:Heap, field:Field)
// 表示baseHeap数组中,包含了heap对象
.decl ArrayIndexPointsTo(heap:Heap, baseHeap:Heap)
// 表示在insn指令中caller调用了callee
.decl CallGraph(insn:Insn, caller:Method, callee:Method)
// 表示方法可访问到
.decl Reachable(method:Method)
// new
// 如果method方法可访问
// 且 在method中,将创建好的heap对象赋值给了var变量
// 那么能够推导出var变量指向heap对象
VarPointsTo(heap, var) :-
Reachable(method),
AssignHeapAllocation(_, _, heap, var, method, _).
// assign
// 如果method方法可访问
// 且form变量 指向heap对象
// 且在method中,将from变量赋值给了to,即to=form
// 那么能够推到出to变量也指向heap对象
VarPointsTo(heap, to) :-
Reachable(method),
VarPointsTo(heap, from),
AssignLocal(_, _, from, to, method).
// cast
// 如果method 方法可访问
// 且form变量 指向heap对象
// 且在method中,将from变量类型转换后赋值给了to,即to=(T)from
// 那么能够推到出to变量也指向heap对象
VarPointsTo(heap, to) :-
Reachable(method),
AssignCast(_, _, from, to, _, method),
VarPointsTo(heap, from).
// load field
// 如果method方法可访问
// 且在method中,将base变量的field取出赋值给了to,即to=base.field
// 且base指向baseHeap对象
// 且baseHeap对象的field指向heap对象
// 那么能够推导出to也指向heap对象
VarPointsTo(heap, to) :-
Reachable(method),
LoadInstanceField(_, _, to, base, field, method),
VarPointsTo(baseHeap, base),
InstanceFieldPointsTo(heap, baseHeap, field).
// store field
// 如果method方法可访问
// 且在method中,将from存到了变量base的field,即base.field=from
// 且from指向heap对象
// 且base指向baseHeap对象
// 那么能够推导出baseHeap对象的field也指向heap对象
InstanceFieldPointsTo(heap, baseHeap, field) :-
Reachable(method),
StoreInstanceField(_, _, from, base, field, method),
VarPointsTo(heap, from),
VarPointsTo(baseHeap, base).
// load staic field
// 如果method 方法可访问
// 且在method中,将静态field取出赋值给了to,即to = T.field
// 且field 指向heap对象
// 那么可以推导出to也指向heap对象
VarPointsTo(heap, to) :-
Reachable(method),
LoadStaticField(_, _, to, field, method),
StaticFieldPointsTo(heap, field).
// store static field
// 如果method方法可访问
// 且在method中,将from存入静态field,即T.field = from
// 且from指向heap对象
// 那么可以推导出静态field指向heap对象
StaticFieldPointsTo(heap, field) :-
Reachable(method),
StoreStaticField(_, _, from, field, method),
VarPointsTo(heap, from).
// load from array
// 如果method可访问
// 且从base数组中取出元素到to,即to = base[i]
// 且base指向baseHeap数组对象
// 且baseHeap数组对象中 包含 heap 对象
// 那么to可能指向heap
// 这里的实现未区分取第几个索引
VarPointsTo(heap, to) :-
Reachable(method),
LoadArrayIndex(_, _, to, base, method),
VarPointsTo(baseHeap, base),
ArrayIndexPointsTo(heap, baseHeap).
// store into array
// 如果method可访问
// 将form存到base数组中,即 base[i] = from
// from指向heap对象
// base指向baseHeap数组对象
// 那么能推导出baseHeap数组对象,包含heap对象
ArrayIndexPointsTo(heap, baseHeap) :-
Reachable(method),
StoreArrayIndex(_, _, from, base, method),
VarPointsTo(heap, from),
VarPointsTo(baseHeap, base).
// 下面开始涉及到过程间的指针分析
// 先构造调用图
// Special和Static和CHA处理方式一样,编译时callee就确定,不需要再进行解析
Reachable(callee),
CallGraph(insn, caller, callee) :-
Reachable(caller),
SpecialMethodInvocation(insn, _, callee, _, caller).
Reachable(callee),
CallGraph(insn, caller, callee) :-
Reachable(caller),
StaticMethodInvocation(insn, _, callee, caller).
// Virtual Call 需要根据base指向对象的类型进行dispatch
// caller要可达
// 在caller中virtual call了method
// 调用时base指向baseHeap对象
// baseHeap对象的类型为class
// 根据method解析出被调函数的签名
// 通过函数签名和实际类型解析出真正的被调函数callee
Reachable(callee),
CallGraph(insn, caller, callee) :-
Reachable(caller),
VirtualMethodInvocation(insn, _, method, base, caller),
VarPointsTo(baseHeap, base),
NormalHeap(baseHeap, class),
MethodInfo(method, simplename, _, _, _, descriptor, _),
Dispatch(simplename, descriptor, class, callee).
// param
// 调用图中存在调用insn
// 调用时第n个实际参数传的是变量arg
// 被调函数callee的第n个形式参数是param
// 如果arg指向了heap对象
// 那么param也指向heap对象
VarPointsTo(heap, param) :-
CallGraph(insn, _, callee),
ActualParam(n, insn, arg),
FormalParam(n, callee, param),
VarPointsTo(heap, arg),
// 这里需要注意,只传播正常的对象
// 后续污点分析中,会继承本dl文件,污点分析中,形参->实参的传播不直接使用指针分析中的规则
// 所以这里加上下面的这个限定,就只传播正常对象,不传播污点对象,污点分析中自己实现这部分的传播
NormalHeap(heap, _).
// return
// 调用图中存在调用insn
// 如果在callee中,返回语句返回的是var变量
// 调用后的返回值赋值给了return变量
// var变量指向heap对象
// 那么return也指向heap对象
VarPointsTo(heap, return) :-
CallGraph(insn, _, callee),
Return(_, _, var, callee),
AssignReturnValue(insn, return),
VarPointsTo(heap, var).
// this
// 调用图中存在调用insn
// 调用时base指向heap
// 那么调用时callee的this变量也指向heap对象
VarPointsTo(heap, this) :-
CallGraph(insn, _, callee),
(
VirtualMethodInvocation(insn, _, _, base, _);
SpecialMethodInvocation(insn, _, _, base, _)
),
ThisVar(callee, this),
VarPointsTo(heap, base).
}
public class App {
public void test(String str)throws Exception{
System.out.println(str);
}
public static void main(String[] args) throws Exception{
App app1 = new App();
App app2 = new App();
String str1 = "this is app1";
String str2 = "this is app2";
app1.test(str1);
app2.test(str2);
}
}
这个程序中,在不同位置存在相同函数调用:
app1.test(str1);
app2.test(str2);
此时再看到我们的指针分析算法中的如下推导规则:
// param
// 调用图中存在调用insn
// 调用时第n个实际参数传的是变量arg
// 被调函数callee的第n个形式参数是param
// 如果arg指向了heap对象
// 那么 param 也指向heap 对象
VarPointsTo(heap, param) :-
CallGraph(insn, _, callee),
ActualParam(n, insn, arg),
FormalParam(n, callee, param),
VarPointsTo(heap, arg).
这个地方就是体现上下文不敏感的关键点,函数的形参可能会指向不同位置的CallSite的实参的堆对象;上面示例程序中,存在两个CallGraph,它们的callee相同,唯一不同的就是调用位置不同:
<org.example.model.App: void test(java.lang.String)>/@parameter0
:ptaint.dl
这部分的算法主要是根据ptaint这篇论文实现的:https://yanniss.github.io/ptaint-oopsla17-prelim.pdf
论文的演讲:https://www.youtube.com/watch?v=IA08d-kiCy8
指针分析是计算指针在运行过程中可能指向哪些对象,也可以理解为创建之后的对象,会传播到哪些指针。污点分析是计算sink函数的参数是否是污点,也可以理解为污点源会传播到哪些指针;
两者可以统一成,值在指针之间的传播,也就是在PFG(Pointer Flow Graph)中传播,但是污点分析比指针分析还多了一些东西,比如污点转移,以及污点消除(净化函数);
污点转移其实很常见,无论是CodeQL,还是GadgetInspector等中,都能见到它的身影,它代表污点经过某个函数调用后,强制将污点转移到某个地方(可能是函数的返回值,可能是receiver等);
污点消除或者说净化函数(sanitizer)也能在CodeQL中见到,它代表污点经过某个函数调用,污点传播将被阻断(比如系统中存在ESAPI对污点进行了过滤,我们就可以认为ESAPI的这个函数是净化函数,它阻断了污点的传播);
比较传统的污点分析实现(比如GadgetInspector),都是给source打上一个污点标记,观察打了标记的source经过PFG能否到达sink;
但是Ptaint论文中将污点视为独立的对象,而不是给传统的对象打上污点标记,会创建新的污点对象,和传统的对象分开各自独立沿着相同的PFG传播;
所以Ptaint中,污点分析的实现就是在指针分析的基础上,添加转换函数和净化函数的传播处理;具体实现如下,直接继承指针分析,添加转换函数和净化函数部分:
#pragma once
#include "pt-noctx.dl"
.comp PTaint : ContextInsensitivePt{
// 表示通过insn指令,创建的新的污点对象heap,包括污点源的生成,以及污点转移时的生成
.decl TaintHeap(insn:Insn, heap:Heap)
// 表示source函数,其返回值表示污点源
.decl SourceMethod(method:Method)
// 表示sink函数,其第n个实际参数如果指向污点对象,则表示可能存在安全风险
.decl SinkMethod(method:Method, n:number)
// 表示sanitize函数(净化函数),经过其处理的污点,将不再是污点,也就是说污点无法通过sanitize传播;
// 后面的净化函数阻断,是在实际参数向形式参数传播时阻断的
.decl SanitizeMethod(method:Method)
// 污点转移相关
// 这个代表receiver是污点,则强制把污点转移到返回值
.decl BaseToRetTransfer(method:Method)
// 这个代表methodCall的实参是污点,则强制把污点转移到返回值
.decl ArgToRetTransfer(method:Method, n:number)
// 将上面两个合并成一个,或者将污点转移抽象成from变量污染了to变量
.decl IsTaintedFrom(insn:Insn, from:Var, to:Var)
// heap对象污染了newHeap对象
.decl TransferTaint(heap:Heap, newHeap:Heap)
// taint arg to param
// 指针分析里,形参->实参的对象传播中,存在NormalHeap(heap, _)限定,意思就是只传播正常对象,不传播污点对象
// 所以污点分析中,我们需要额外多写一个形参->实参的传播规则,这里主要是添加了净化函数的阻断处理
// 如果污点实参流入了净化函数对应的形参,则污点传播被阻断
VarPointsTo(heap, param) :-
CallGraph(insn, _, callee),
ActualParam(n, insn, arg),
FormalParam(n, callee, param),
VarPointsTo(heap, arg),
TaintHeap(_, heap),
!SanitizeMethod(callee).
// sourceMethod的返回值作为污点源
TaintHeap(insn, heap),
VarPointsTo(heap, to) :-
SourceMethod(callee),
CallGraph(insn, _, callee),
AssignReturnValue(insn, to),
heap = cat("NewTainted::", insn).
IsTaintedFrom(insn, base, ret) :-
CallGraph(insn, _, callee),
BaseToRetTransfer(callee),
(
VirtualMethodInvocation(insn, _, _, base, _);
SpecialMethodInvocation(insn, _, _, base, _)
),
AssignReturnValue(insn, ret).
IsTaintedFrom(insn, arg, ret) :-
CallGraph(insn, _, callee),
ArgToRetTransfer(callee, n),
ActualParam(n, insn, arg),
AssignReturnValue(insn, ret).
// 污点转移
// from指向了污点对象heap
// 且from能污染to
// 那么to也是污点对象,也要指向一个污点对象
// 这里没有直接让to指向新创建的污点对象
// 而是先找到to指向的正常对象oldHeap,oldHeap第一个流向的指针var,然后让newHeap也流向指针var,即var指向newHeap
// 由于oldHeap流向var之后,通过PFG可以流到to,那么newHeap也能流到to,这样也把和var alias的指针一并污染了
TaintHeap(insn, newHeap),
TransferTaint(heap, newHeap),
VarPointsTo(newHeap, var) :-
IsTaintedFrom(insn, from, to),
VarPointsTo(heap, from),
TaintHeap(_, heap),
newHeap = cat("TransferTaint::", insn),
VarPointsTo(oldHeap, to),
AssignHeapAllocation(_, _, oldHeap, var, _, _).
}
这里很多人不明白最后的推导过程中,为什么不直接让to指向新创建的污点对象(newHeap),而是选择让var指向newHeap,让var指向的newHeap经过PFG流动后再得出to也指向newHeap,也就是不明白为什么需要污染var的alias,而不直接污染to就完事了;
这里我一开始也有疑惑,甚至认为作者写错了,经过后续仔细的探讨和思考,才发现这么做是正确且有必要的;
假设现在存在如下代码场景:
package org.example;
public class App {
private String bak;
public static void main( String[] args )throws Exception{
App app = new App();
// source, new taint
String exp = app.source();
// taint arg -> ret
exp = app.addSuffix(exp);
// transform taint arg to ret
exp = app.transform(exp);
app.sink(exp);
app.sink(app.bak);
}
private String addSuffix(String original){
return original + ";";
}
/**
* 当我们制定了一个fact: ArgToRetTransfer("<org.example.App: java.lang.String transform(java.lang.String)>", 0).
* 如果直接让ret指向newHeap,则不会处理过程内的污点传播了
* 例如如下函数中,this.bak就不会指向newHeap,就少了一条污点传播路径
*/
private String transform(String original){
String tmp = new String(original.getBytes());
this.bak = tmp;
return tmp;
}
private void sink(String exp)throws Exception{
Runtime.getRuntime().exec(new String[]{"/bin/sh","-c",exp});
}
private String source(){
return "open -a calculator";
}
}
--facts-subset APP
选项,这个选项会忽略JDK等依赖内的soot-facat生成,所以指针分析的时候,如果使用这个选择,你得到的fact将不包含JDK等依赖包的MethodInvoke,当然,如果你是用 -i
选项指定的依赖包,即使使用了 --facts-subset APP
,还是会生成依赖包里的fact,因为 -i
代表的是APP的输入):public class org.example.App extends java.lang.Object
{
private java.lang.String bak;
public void <init>()
{
org.example.App this#_0;
this#_0 := @this: org.example.App;
specialinvoke this#_0.<java.lang.Object: void <init>()>();
return;
}
public static void main(java.lang.String[]) throws java.lang.Exception
{
org.example.App $stack3, app#_8;
java.lang.String $stack7, exp#_10, exp_$$A_1#_12, exp_$$A_2#_15;
java.lang.String[] args#_0;
args#_0 := @parameter0: java.lang.String[];
$stack3 = new org.example.App;
specialinvoke $stack3.<org.example.App: void <init>()>();
app#_8 = $stack3;
exp#_10 = specialinvoke app#_8.<org.example.App: java.lang.String source()>();
exp_$$A_1#_12 = specialinvoke app#_8.<org.example.App: java.lang.String addSuffix(java.lang.String)>(exp#_10);
exp_$$A_2#_15 = specialinvoke app#_8.<org.example.App: java.lang.String transform(java.lang.String)>(exp_$$A_1#_12);
specialinvoke app#_8.<org.example.App: void sink(java.lang.String)>(exp_$$A_2#_15);
$stack7 = app#_8.<org.example.App: java.lang.String bak>;
specialinvoke app#_8.<org.example.App: void sink(java.lang.String)>($stack7);
return;
}
private java.lang.String addSuffix(java.lang.String)
{
java.lang.StringBuilder $stack2, $stack3, $stack4;
java.lang.String original#_0, $stack5;
org.example.App this#_0;
this#_0 := @this: org.example.App;
original#_0 := @parameter0: java.lang.String;
$stack2 = new java.lang.StringBuilder;
specialinvoke $stack2.<java.lang.StringBuilder: void <init>()>();
$stack3 = virtualinvoke $stack2.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(original#_0);
$stack4 = virtualinvoke $stack3.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(";");
$stack5 = virtualinvoke $stack4.<java.lang.StringBuilder: java.lang.String toString()>();
return $stack5;
}
private java.lang.String transform(java.lang.String)
{
java.lang.String $stack3, original#_0, tmp#_31;
byte[] $stack4;
org.example.App this#_0;
this#_0 := @this: org.example.App;
original#_0 := @parameter0: java.lang.String;
$stack3 = new java.lang.String;
$stack4 = virtualinvoke original#_0.<java.lang.String: byte[] getBytes()>();
specialinvoke $stack3.<java.lang.String: void <init>(byte[])>($stack4);
tmp#_31 = $stack3;
this#_0.<org.example.App: java.lang.String bak> = tmp#_31;
return tmp#_31;
}
private void sink(java.lang.String) throws java.lang.Exception
{
java.lang.Runtime $stack2;
java.lang.String[] $stack3;
java.lang.String exp#_0;
org.example.App this#_0;
this#_0 := @this: org.example.App;
exp#_0 := @parameter0: java.lang.String;
$stack2 = staticinvoke <java.lang.Runtime: java.lang.Runtime getRuntime()>();
$stack3 = newarray (java.lang.String)[3];
$stack3[0] = "/bin/sh";
$stack3[1] = "-c";
$stack3[2] = exp#_0;
virtualinvoke $stack2.<java.lang.Runtime: java.lang.Process exec(java.lang.String[])>($stack3);
return;
}
private java.lang.String source()
{
org.example.App this#_0;
this#_0 := @this: org.example.App;
return "open -a calculator";
}
}
#include "../ptaint.dl"
.init ptaint = PTaint
// 定义入口函数
ptaint.Reachable("<org.example.App: void main(java.lang.String[])>").
// 定义污点源
ptaint.SourceMethod("<org.example.App: java.lang.String source()>").
// 定义sink函数
ptaint.SinkMethod("<org.example.App: void sink(java.lang.String)>", 0).
// 我们认为污点实参进入到transform函数的形参,一定能污染transform函数的返回值,所以定义如下ArgToRetTransfer
ptaint.ArgToRetTransfer("<org.example.App: java.lang.String transform(java.lang.String)>", 0).
//
ptaint.BaseToRetTransfer("<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>").
ptaint.BaseToRetTransfer("<java.lang.StringBuilder: java.lang.String toString()>").
ptaint.ArgToRetTransfer("<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>", 0).
.output ptaint.SinkTaint
.output ptaint.TaintHeap
.output ptaint.TransferTaint
.output ptaint.VarPointsTo
.output ptaint.IsTaintedFrom
我这里多添加了一个SinkTaint谓词,用于表示source到达sink的链路上的最后一个节点;是在ptaint.dl中定义的,谓词声明及推导如下:
.decl SinkTaint(sinkCaller:Method, sinkMethod:Method, actualParam:Var)
SinkTaint(sinkCaller, sinkMethod, actualParam) :-
SinkMethod(sinkMethod,n),
CallGraph(insn, sinkCaller, sinkMethod),
ActualParam(n,insn,actualParam),
VarPointsTo(heap,actualParam),
TaintHeap(_,heap).
如果不污染var及其alias,即ptaint.dl中的TransferTaint的推导应该这么写:
TaintHeap(insn, newHeap),
TransferTaint(heap, newHeap),
VarPointsTo(newHeap, to) :-
IsTaintedFrom(insn, from, to),
VarPointsTo(heap, from),
TaintHeap(_, heap),
newHeap = cat("TransferTaint::", insn).
此时我们进行污点分析,发现得出的结果只有一条:
但是如果考虑污染var及其alias,即ptaint.dl中的TransferTaint的推导应该这么写:
TaintHeap(insn, newHeap),
TransferTaint(heap, newHeap),
VarPointsTo(newHeap, var) :-
IsTaintedFrom(insn, from, to),
VarPointsTo(heap, from),
TaintHeap(_, heap),
newHeap = cat("TransferTaint::", insn),
VarPointsTo(oldHeap, to),
AssignHeapAllocation(_, _, oldHeap, var, _, _).
此时发现能得到完整的两条结果:
相信看到这个结果的对比,有的小伙伴以及反应过来了;那么为啥直接污染to会少一条结果?详细解释如下:
我们提前知道污点进入 <org.example.App: java.lang.String transform(java.lang.String)>
函数的形参,污点在这个过程(函数)内一定会断开,所以我们会手动添加ptaint.ArgToRetTransfer
(<org.example.App:java.lang.Stringtransform(java.lang.String)>"0
)
,让这个函数的实参可以直接污染函数的返回值(有点类似GadgetInspector中手动添加passthrough);
此时看下函数内的污点流动情况如下:
污点实参–>污点形参–>函数内各种数据流(会意外断开)–x–>…–>var–>…–>函数内将要返回的变量–>函数外接收返回值的变量。
这里函数内各种数据流(会意外断开)–x–>…导致污点数据流断开,所以如果我们不手动指定ptaint.ArgToRetTransfer("<org.example.App: java.lang.String transform(java.lang.String)>", 0).
,那么污点传播到这个函数内( org.example.App#transform
)就断了;
虽然通过手动指定ArgToRetTransfer,函数外部污点传播通了, 但是这个函数内部的污点数据流却没接通,从函数内意外断开的位置开始,后续的污点传播也就断了;所以这个示例代码内, this.bak = tmp;
这条污点数据流就也断了(因为这条statement在函数内数据流断开点之后);
如果此时处理var及其alias,就会尽可能的把函数内断开的污点数据流连起来,即:
污点实参–>污点形参–>函数内各种数据流(会意外断开)–x–>…–>var(被指向污点,从此处开始,污点数据流被接通)–>…–>函数内将要返回的变量–>函数外接收返回值的变量。
所以对于上面的示例代码,如果不污染var及其alias,那么因为函数内污点数据流缺少处理,整个扫描结果就少一条。
GadgetInsepctor
前面讲的ByteCodeDL是基于soot和datalog来实现的,是学术界比较流行的静态程序分析方法;
在工业界比较常听说的还是Black Hat USA 2018会议上lan Haken提出的GadgetInspector(至少野路子进安全圈的我是先听说GadgetInspector的=-=’’’),作者原版PPT:https://i.blackhat.com/us-18/Thu-August-9/us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains.pdf
这个工具原本是用来自动化挖反序列化Gadget的,但是稍作修改就可以用来挖普通漏洞;而且它的实现方式天然的支持流敏感、上下文敏感的指针分析,精度还不错;唯一的缺点就是速度慢(精度上的缺点可以二开解决);下面就是主要介绍GadgetInspector的实现,领会另外一种神奇的静态程序分析实现方法;
我基于作者的仓库修改了一版,可能更加利于阅读,也添加了一些我挖洞会用到的东西:https://github.com/p1n93r/GadgetInspector ,后续的讲解都是基于我这个版本的GI;
JVM栈祯
在了解GI之前,先需要简单了解JVM栈祯,因为GI就是通过模拟JVM的栈祯来实现污点分析的;
每个线程在创建时都会创建一个虚拟机栈,其内部保存着一个个的栈帧,每个栈帧对应着每个方法的调用情况;
栈祯由一下几个部分组成:
-
局部变量表;
-
操作数栈;
-
动态链接;
-
方法返回地址;
-
一些附加信息。
图示如下:
进行静态程序分析时,我们不必关心所有的statement,我们只关心会影响数据流、影响指针的statements;所以我们只需要了解局部变量表和操作数栈即可。
局部变量表
-
局部变量表也称之为本地变量表,用于存储方法参数和定义在方法体内的局部变量,这些变量的类型包括基本数据类型、对象引用类型以及返回值类型;
-
局部变量表中的变量只在当前方法调用中有效;当方法调用结束后,随着栈帧的销毁,局部变量表也会随着销毁;
-
局部变量表的基本存储单元为Slot(变量槽),32位以内的数据类型只占用一个slot(包括returnAddress类型),64位的数据类型(long和double)占用两个连续的slot;
-
如果当前栈帧是有构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处;
-
静态方法的局部变量表不会存储this引用。
操作数栈
-
在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈;
-
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间;
-
如果被调用的方法带有返回值,其返回值也会被压入当前栈帧的操作数栈中;
-
栈中可以存储java的任意类型,32bit的类型占用一个栈单位深度,64bit的类型占两个栈单位深度。
举个栗子
假设存在如下函数:
/**
* @author P1n93r
*/
public class Demo {
public String append(String one,String two){
String three = "";
three = one + two;
return three;
}
public static void main(String[] args){
Demo demo = new Demo();
String ret = demo.append("Hello","Hacker");
}
}
现在模拟一下调用上面的 org.example.Demo#main
方法时,函数的本地变量表和操作数栈的流动情况;首先看到main方法的字节码指令:
0 new #7 <org/example/Demo>
3 dup
4 invokespecial #8 <org/example/Demo.<init> : ()V>
7 astore_1
8 aload_1
9 ldc #9 <Hello>
11 ldc #10 <Hacker>
13 invokevirtual #11 <org/example/Demo.append : (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;>
16 astore_2
17 return
在程序计数器运行到4之前,都是属于main函数的栈祯,它的字节码运行流程如下图所示;
(0)初始态
进入main函数还没执行main函数内的字节码指令时,局部变量表就已经存在一个args对象了,就是main函数的形参:
(1)第一步
当程序计数器执行到0时,会使用new指令实例化一个对象到操作数栈中:
(2)第二步
当程序计数器执行到3时,会使用dup指令复制栈顶元素到栈中:
(3)第三步
当程序计数器执行到4时,会调用Demo的构造方法,进入 Demo#<init>
方法的栈祯,invokespecial指令会根据callee的形参表来对caller的操作数栈进行pop,存储到callee的本地变量表中:
(4)第四步
现在已经进入了 Demo#<init>
的栈祯,执行 Demo#<init>
的栈祯内的0位置的指令,aload_0指令会从局部变量表的第0位置加载一个objectref到操作数栈中:
(5)第五步
然后又出现了invoke指令,调用了 Object#<init>
方法,和进入 Demo#<init>
栈祯类似,invokespecial指令会对 Demo#<init>
栈祯内的操作数栈进行pop,取出thisRef、形参(当然,这里不用取形参):
(6)第六步
然后 Object#<init>
方法内,只有一条return指令,这条指令代表返回空,并且会清空当前栈祯的操作数栈,然后返回到calller(也就是 Demo#<init>
方法的栈祯):
(7)第七步
当前栈祯继续回到 Demo#<init>
里,此时也将执行 Demo#<init>
里的return指令,清空 Demo#<init>
里的操作数栈,回到 Demo#main
栈祯,即将执行astore_1指令:
(8)第八步
在 Demo#main
栈祯内,执行astore_1指令,对操作数栈进行pop,存储到本地变量表的第1位置:
(9)第九步
这个地方很经典,接下来是准备调用带参的实例函数了,第8、9、11位置的指令,都是为了接下来的函数调用准备实参;
接下来运行aload_1指令,从本地变量表的第1位置加载一个ObjectRef压栈:
(10)第十步
继续压入实参,调用ldc压入字符常量“Hello”:
(11)第十一步
继续压入实参,调用ldc压入字符常量“Hacker”:
(12)第十二步
此时又准备进行函数调用了,这个函数有形参,会对caller( Demo#main
栈祯内的操作数栈进行pop,pop的次数以及pop的长度根据callee的函数形参来确定,这里callee为 Demo#append
,有两个形参,所以会pop 3次),ObjectRef和函数实参进入 Demo#append
栈帧的本地变量表,其中ObjectRef在第0位置,第一个实参在本地变量表的第1位置,依此类推;
(13)第十三步
接下来就进入了 Demo#append
函数的栈帧,接下来就不演示 Demo#append
函数内的invoke了,会直接步过invoke指令;
进入 Demo#append
函数栈帧,先执行0位置的指令,也就是 ldc #2
,会从常量池中加载一个字符串常量(这里是一个空字符串)到操作数栈中:
(14)第十四步
然后执行astore_3,操作数出栈,存储到本地变量表的第3位置:
(15)第十五步
程序计数器执行到第3位置,会创建一个StringBuilder对象放在操作数栈内:
(16)第十六步
使用dup指令复制栈顶元素压入栈顶:
(17)第十七步
接着使用invokespecial指令调用 StringBuilder#<init>
方法,会对 Demo#append
栈帧内的操作数进行出栈,而且构造函数没有返回值,所以步过 invokespecial #4 <java/lang/StringBuilder.<init> : ()V>
后, Demo#append
栈帧内,操作数栈就是pop了一个栈顶元素:
(18)第十八步
接着调用aload_1指令,把操作数栈中第1位置的元素压入栈中:
(19)第十九步
接着使用invokevirtual进行函数调用,此时 Demo#append
栈帧的操作数栈需要pop两次,并且函数调用后,会返回一个ObjectRef压入 Demo#append
栈帧内的操作数栈:
(20)第二十步
接着调用aload_2,进行参数入栈,准备下一个invokevirtual的实参:
(21)第二十一步
继续使用invokevirtual调用 StringBuilder#append
,会对caller操作数栈进行pop,pop两次,最后会返回一个ObjectRef压入caller的操作数栈:
(22)第二十二步
继续使用invokevirtual指令调用 StringBuilder#toString
,此时会从caller操作数栈内pop一次,函数调用完毕,会返回一个ObjectRef:
(23)第二十三步
接下来执行astroe_3指令,会对操作数栈进行pop,存储到局部变量表的第3位置:
(24)第二十四步
接着执行aload_3指令,从局部变量表中的第3位置拿一个元素入操作数栈:
(25)第二十五步
最后执行 Demo#append
函数栈帧内的areturn指令,将返回到 Demo#main
函数栈帧,并且把 Demo#append
函数栈帧内操作数栈栈顶的元素pop到 Demo#main
函数栈帧的操作数栈内:
(26)第二十六步
最后回到 Demo#main
的栈帧,执行astore_2指令,将在操作数栈内pop一个元素,存储到本地变量表中的第2位置:
最后执行return指令,结束 Demo#main
函数调用;
GadgetInspector基本原理
Demo#source
的返回值即为污点源source,Runtime#exec
即为sink函数:/**
* @author P1n93r
*/
public class Demo {
public String append(String one,String two){
String three = "";
three = one + two;
return three;
}
public String source(){
return "calc";
}
public static void main(String[] args)throws Exception{
Demo demo = new Demo();
String source = demo.source();
String ret = demo.append(source,"");
Runtime.getRuntime().exec(ret);
}
}
Demo#append
函数时,污点处理过程如下:Demo#main
中使用astore_3指令把含有污点标记的String对象“calc”弹出到本地变量表中的第3位置:GadgetInspector具体实现
我把GI的代码结构改成了我认为比较好阅读的方式,所以接下来的一些代码截图稍微和原作者有一些出入;不过原理是一样的;GI的步骤大致可以分为如下几个步骤:
-
初始化 URLClassLoader
加载需要分析的classPath以及jarUrl;
-
使用 MethodDiscovery
得到所有的classes、methods、inheritance信息;
-
如果开启了污点分析,则使用 PassthroughDiscovery
进行过程内的污点分析(污点可以从函数的第n个形参流出到函数返回值);
-
使用 CallGraphDiscovery
创建CallGraph
,如果开启了污点分析,则创建CallGraph
时会进行污点传播判断;
-
使用 SourceDiscovery
得到所有的source; -
最后使用 GadgetChainDiscovery
,根据source和sink遍历CallGraph
,找到能从source到sink的CallGraph
链。
初始化ClassLoader
/**
* 对于WEB应用的处理无非就是加载class和jar
* 这里可以手动指定class的位置和jar的位置
*/
public static ClassLoader getCustomClassLoader(Path classPath,Path jarPath) throws IOException {
final List<URL> classPathUrls = new ArrayList<>();
if (classPath!=null && Files.exists(classPath)) {
classPathUrls.add(classPath.toUri().toURL());
}
if(jarPath!=null && Files.exists(jarPath)){
Files.list(jarPath).forEach(p -> {
try {
classPathUrls.add(p.toUri().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
});
}
if( (classPath==null || !Files.exists(classPath)) && (jarPath==null || !Files.exists(jarPath)) ){
throw new RuntimeException("your custom classPath or jarPath is none or wrong.");
}
return new URLClassLoader(classPathUrls.toArray(new URL[0]));
}
/**
* 专门处理spring-boot jar
* 获取jar包中BOOT-INF/classes以及BOOT-INF/lib 下的class
*/
public static ClassLoader getJarAndLibClassLoader(Path jarPath) throws IOException {
//创建临时文件夹,在jvm shutdown自动删除
final Path tmpDir = Files.createTempDirectory("exploded-jar");
// Delete the temp directory at shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> FileUtil.removeDir(tmpDir.toFile())));
// Extract to war to the temp directory
try (JarInputStream jarInputStream = new JarInputStream(Files.newInputStream(jarPath))) {
JarEntry jarEntry;
while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
Path fullPath = tmpDir.resolve(jarEntry.getName());
if (!jarEntry.isDirectory()) {
Path dirName = fullPath.getParent();
if (dirName == null) {
throw new IllegalStateException("Parent of item is outside temp directory.");
}
if (!Files.exists(dirName)) {
Files.createDirectories(dirName);
}
try (OutputStream outputStream = Files.newOutputStream(fullPath)) {
FileUtil.copy(jarInputStream, outputStream);
}
}
}
}
final List<URL> classPathUrls = new ArrayList<>();
// spring-boot
if (Files.exists(tmpDir.resolve("BOOT-INF"))) {
classPathUrls.add(tmpDir.resolve("BOOT-INF/classes").toUri().toURL());
Files.list(tmpDir.resolve("BOOT-INF/lib")).forEach(p -> {
try {
classPathUrls.add(p.toUri().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
});
}else {
// shadow jar / common lib jar
classPathUrls.add(tmpDir.toUri().toURL());
}
return new URLClassLoader(classPathUrls.toArray(new URL[0]));
}
BOOT-INF
换成 WEB-INF
;后续就是通过这个UrlClassLoader,把其中的所有class全部遍历一次,添加到一个 List<ClassFile>
中,方便后续进行分析。MethodDiscovery
ClassFile
,使用一个叫做 MethodDiscoveryClassVisitor
的java asm visitor(Class Visitor)去解析对应的class文件。public void discover(final List<ClassFile> classFileList) throws Exception {
for (ClassFile classFile : classFileList) {
try (InputStream in = classFile.getInputStream()) {
ClassReader cr = new ClassReader(in);
try {
// 使用asm的ClassVisitor、MethodVisitor,利用观察模式去扫描所有的class和method并记录
cr.accept(new MethodDiscoveryClassVisitor(discoveredClasses, discoveredMethods), ClassReader.EXPAND_FRAMES);
} catch (Exception e) {
LOGGER.error("Exception analyzing: " + classFile.getResourceName(), e);
}
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
}
}
MethodDiscoveryClassVisitor
继承于 org.objectweb.asm.ClassVisitor
, org.objectweb.asm.ClassVisitor
是一个java asm框架中的 Visitor 类,可以访问java的字节码;我们可以通过继承 org.objectweb.asm.ClassVisitor
类来实现class文件的解析,比如想获取class文件中的类的注解:// 获取class内的类注解
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
annotations.add(descriptor);
return super.visitAnnotation(descriptor, visible);
}
// 获取class内的类名称、父类、接口等等信息
@Override
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(version, access, name, signature, superName, interfaces);
}
// 获取class内的方法信息
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
// 使用AnnotationMethodVisitor进行方法访问,获取注解信息等
return new AnnotationMethodVisitor(Opcodes.ASM6, methodVisitor, discoveredMethods, classHandle, name, desc, isStatic);
}
visitMethod
中又使用了一个 AnnotationMethodVisitor
类来访问方法信息,这个 AnnotationMethodVisitor
继承于 org.objectweb.asm.MethodVisitor
,也是Java asm框架中的一个Visitor类,通过继承它可以实现对方法的访问,例如得到方法的名称、方法注解信息、形参注解信息等等:// 获取方法注解
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
this.methodAnnotation += descriptor;
return super.visitAnnotation(descriptor, visible);
}
// 获取形参注解
@Override
public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) {
paramAnnotation += parameter + ":" + descriptor;
return super.visitParameterAnnotation(parameter, descriptor, visible);
}
MethodDiscovery
中把获取的类信息保存在 classes.dat
文件中,把获取的方法信息保存在 methods.dat
文件中:// classes.dat数据格式:
// 类名(例:java/lang/String) 父类 接口A,接口B,接口C 是否接口 字段1!字段1access!字段1类型!字段2!字段2access!字段1类型
DataLoader.saveData(Paths.get("classes.dat"), new ClassReference.Factory(), discoveredClasses);
// methods.dat数据格式:
// 类名 方法名 方法描述 是否静态方法 方法注解 方法形参注解
DataLoader.saveData(Paths.get("methods.dat"), new MethodReference.Factory(), discoveredMethods);
子类→所有父类
的Map),存储在 inheritanceMap.dat
文件中:// inheritanceMap.dat数据格式:
// 类名 父类或接口类1 父类或接口类2 父类或接口类3 ...
DataLoader.saveData(Paths.get("inheritanceMap.dat"), new InheritanceMapFactory(), inheritanceMap.entrySet());
PassthroughDiscovery
PassthroughDataflowClassVisitor
类进行 ClassVisit ,最终的结果存储到 passthroughDataFlow
中:// 记录被分析的方法,第几个参数可以影响返回值
final Map<MethodReference.Handle, Set<Integer>> passthroughDataFlow = new HashMap<>();
// 遍历所有方法,然后asm观察所属类,经过前面DFS的排序,调用链最末端的方法在最前面
// 调用链最末端的方法,其方法体内没有call-site
for (MethodReference.Handle method : sortedMethods) {
// 跳过static静态初始化代码,静态初始化块代码无法进行污点传播
if (method.getName().equals("<clinit>")) {
continue;
}
// 获取所属类进行观察
ClassFile classResource = classResourceByName.get(method.getClassReference().getName());
try (InputStream inputStream = classResource.getInputStream()) {
ClassReader cr = new ClassReader(inputStream);
try {
PassthroughDataflowClassVisitor cv = new PassthroughDataflowClassVisitor(classMap, inheritanceMap, passthroughDataFlow, serializableDecider, Opcodes.ASM6, method);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
// 方法的哪个形参可以污染返回值
passthroughDataFlow.put(method, cv.getReturnTaint());
} catch (Exception e) {
LOGGER.error("Exception analyzing " + method.getClassReference().getName() + ", analyzing method: " + method.getName()+method.getDesc(), e);
}
} catch (IOException e) {
LOGGER.error("Unable to analyze " + method.getClassReference().getName(), e);
}
}
PassthroughDataflowClassVisitor
中,如果发现了待观察的方法,则使用 PassthroughDataflowMethodVisitor
进行方法观察:@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// 只观察选定的method
if (!name.equals(methodToVisit.getName()) || !desc.equals(methodToVisit.getDesc())) {
return null;
}
if (passthroughDataflowMethodVisitor != null) {
throw new IllegalStateException("Constructing passthroughDataflowMethodVisitor twice!");
}
// 对目标method进行观察
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);
}
PassthroughDataflowMethodVisitor
类继承于 TaintTrackingMethodVisitor
类,GI中最核心的类就是 TaintTrackingMethodVisitor
,它实际上是一个 MethodVisitor
,但是它模拟了JVM中函数执行过程中的本地变量表和操作数栈的数据流动。下面简单分析下 TaintTrackingMethodVisitor
类的实现。首先看到 TaintTrackingMethodVisitor
类中的内部类:SavedVariableState
,这个内部类就是模拟本地变量表和操作数栈:/**
* 维护本地变量表和操作数栈
*/
private static class SavedVariableState<T> {
/**
* 本地变量表
*/
List<Set<T>> localVars;
/**
* 操作数栈
*/
List<Set<T>> stackVars;
// 省略其他......
}
@Override
public void visitCode() {
super.visitCode();
// 刚进入方法,需要清空本地变量表和操作数栈
savedVariableState.localVars.clear();
savedVariableState.stackVars.clear();
// 如果方法是public、private、protected修饰的非static方法
if ((this.access & Opcodes.ACC_STATIC) == 0) {
// 非static方法,进入方法时,本地变量表首先添加的就是this,这里用一个空来Set代表
savedVariableState.localVars.add(new HashSet<T>());
}
// 现在就是将形参加入本地变量表
for (Type argType : Type.getArgumentTypes(desc)) {
for (int i = 0; i < argType.getSize(); i++) {
savedVariableState.localVars.add(new HashSet<T>());
}
}
}
@Override
public void visitLdcInsn(Object cst) {
if (cst instanceof Long || cst instanceof Double) {
push();
push();
} else {
push();
}
super.visitLdcInsn(cst);
sanityCheck();
}
@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>());
}
// 变量操作,var为操作的本地变量索引
Set<T> saved0;
switch(opcode) {
// 省略部分代码......
// 省略部分代码......
case Opcodes.ASTORE:
// 从栈中取出数据存到本地变量表,这个数据可能是被污染的(主要还是得看调用的方法,返回值是否可被污染)
saved0 = pop();
savedVariableState.localVars.set(var, saved0);
break;
// 省略部分代码......
// 省略部分代码......
default:
throw new IllegalStateException("Unsupported opcode: " + opcode);
}
super.visitVarInsn(opcode, var);
sanityCheck();
}
TaintTrackingMethodVisitor
类对JVM的几百个指令都进行了模拟,实现了对应指令下的操作数栈和本地变量表的数据处理;此外值的注意的是, TaintTrackingMethodVisitor
类还对 if…else
这种控制流结构也进行模拟;visitMethodInsn
的处理,这部分的处理代表当前visit的函数内出现了invoke指令,前面说过对于invoke指令,caller栈祯内需要pop出实参到callee的本地变量表,如果callee有返回值,则会把返回值push到caller的操作数栈种;接下来详细介绍下 visitMethodInsn
的处理;argTypes
的第0个元素是receiver的类型:Type[] argTypes = Type.getArgumentTypes(desc);
if (opcode != Opcodes.INVOKESTATIC) {
Type[] extendedArgTypes = new Type[argTypes.length+1];
System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
// argType[0] 为被调用方法的实例对象的Type
extendedArgTypes[0] = Type.getObjectType(owner);
argTypes = extendedArgTypes;
}
final Type returnType = Type.getReturnType(desc);
final int retSize = returnType.getSize();
// argTaint就是模拟的callee的本地变量表,也可以说是callee的形参列表(第0个元素是receiver)
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++) {
// caller pop
pop();
}
// long和double的参数长度为两个slot,所以污点放在第一个slot中,所以前面需要根据参数的长度先pop掉一个
// 这里pop出来的才可能存在污点
argTaint.set(argTypes.length - 1 - i, pop());
}
}
resultTaint
的变量(Set类型),Set内的元素n用于表示callee的第n个形参可以污染返回值(如果是0,代表receiver本身就可以污染返回值),这个 resultTaint
也可以看做callee的返回值;首先判断当前call-site的callee是否为构造方法,如果是,则将receiver中的污点直接传播到callee返回值中: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<>();
}
java/io/ObjectInputStream#defaultReadObject()
,则把callee的receiver中的污点直接传播到caller的this当中;// If calling defaultReadObject on a tainted ObjectInputStream, that taint passes to "this"
if (owner.equals("java/io/ObjectInputStream") && name.equals("defaultReadObject") && desc.equals("()V")) {
savedVariableState.localVars.get(0).addAll(argTaint.get(0));
}
PASSTHROUGH_DATAFLOW
列表代表哪些callee的第几个形参可以污染返回值(也就是一个硬编码的transfer列表),然后根据这个 PASSTHROUGH_DATAFLOW
列表直接把来自形参或者receiver中的污点传播到callee的返回值中:// 污染例外关联,不通过参数关联污点
// 在名单内的方法的调用,已预置哪个参数可以污染返回值
// 污染名单,固定哪个参数可以污染下去
for (Object[] passthrough : PASSTHROUGH_DATAFLOW) {
if (passthrough[0].equals(owner) && passthrough[1].equals(name) && passthrough[2].equals(desc)) {
// 从index=3开始,后面定义的数字都是代表taintArg
for (int i = 3; i < passthrough.length; i++) {
resultTaint.addAll(argTaint.get((Integer)passthrough[i]));
}
}
}
// Heuristic; if the object implements java.util.Collection or java.util.Map, assume any method accepting an object
// taints the collection. Assume that any method returning an object returns the taint of the collection.
if (opcode != Opcodes.INVOKESTATIC && argTypes[0].getSort() == Type.OBJECT) {
// 获取被调用函数的所有父类
Set<ClassReference.Handle> parents = inheritanceMap.getSuperClasses(new ClassReference.Handle(argTypes[0].getClassName().replace('.', '/')));
if (parents != null && (parents.contains(new ClassReference.Handle("java/util/Collection")) ||
parents.contains(new ClassReference.Handle("java/util/Map")))) {
// 如果该类为集合类,callee的所有形参都是污点,把污点标记加入到this中
for (int i = 1; i < argTaint.size(); i++) {
argTaint.get(0).addAll(argTaint.get(i));
}
if (returnType.getSort() == Type.OBJECT || returnType.getSort() == Type.ARRAY) {
resultTaint.addAll(argTaint.get(0));
}
}
}
if (retSize > 0) {
// 传播污点,污点放在retSize对应的第0个push
push(resultTaint);
for (int i = 1; i < retSize; i++) {
push();
}
}
visitMethodInsn
的处理,就可以知道GI中的 PASSTHROUGH_DATAFLOW
列表其实就是前面我们用soot分析中的Transfer fact
;这个 PASSTHROUGH_DATAFLOW
列表需要覆盖足够全才能保证污点数据流不会意外断开;PASSTHROUGH_DATAFLOW
列表中指定了某个无参构造函数可以污染返回值,污点传播也一定断开了;PassthroughDataflowMethodVisitor
,这个类继承于 TaintTrackingMethodVisitor
,现在我们已经分析完了 TaintTrackingMethodVisitor
,那么 PassthroughDataflowMethodVisitor
重写了哪些部分?几乎没动,只是在 visitCode
中重写了函数调用时,形参入本地变量表时,给形参打上污点标记(污点标记为参数的索引,0代表this,1代表第一个形参):/**
* 这部分主要是方法开始前的本地变量表初始化
* 也就是形参入本地变量表,如果是实例方法,则本地变量表的第0个元素代表的就是this,如果是静态方法,则第0个元素代表的就是实际方法参数
*/
@Override
public void visitCode() {
// 需要注意super的调用,这里调用super.visitCode()就是为了初始化元素为空的本地变量表空间
super.visitCode();
int localIndex = 0;
int argIndex = 0;
if ((this.access & Opcodes.ACC_STATIC) == 0) {
// 非静态方法,第一个局部变量应该为对象实例this
// 注意,这里存储的是本地变量表,不是操作数栈
setLocalTaint(localIndex, argIndex);
localIndex += 1;
argIndex += 1;
}
for (Type argType : Type.getArgumentTypes(desc)) {
// 判断参数类型,得出变量占用空间大小,然后存储
// 注意,这里存储的是本地变量表,不是操作数栈
setLocalTaint(localIndex, argIndex);
localIndex += argType.getSize();
argIndex += 1;
}
}
visitInsn
,对RETURN指令进行了特殊处理,主要作用是获取函数的返回值,加入到 returnTaint
这个Set中:/**
* 访问无操作数的指令,例如NOP、ACONST_NULL等等
*/
@Override
public void visitInsn(int opcode) {
// 这里只针对return指令进行特殊处理: 获取操作数栈栈顶元素,加入到returnTaint中
// 注意,这里只是获取,并没有pop,pop操作在super.visitInsn(opcode)中进行
switch(opcode) {
// 从当前方法返回int
case Opcodes.IRETURN:
// 从当前方法返回float
case Opcodes.FRETURN:
// 从当前方法返回对象引用
case Opcodes.ARETURN:
// 从操作数栈的栈顶取元素
returnTaint.addAll(getStackTaint(0));
break;
// 从当前方法返回long
case Opcodes.LRETURN:
// 从当前方法返回double
case Opcodes.DRETURN:
// long和double占两个slot,所以index为1
returnTaint.addAll(getStackTaint(1));
break;
// 从当前方法返回void
case Opcodes.RETURN:
break;
default:
break;
}
// 这里才是进行真正的指令处理
super.visitInsn(opcode);
}
visitFieldInsn
主要是对反序列化Gadget的挖掘进行了特殊处理,普通漏洞挖掘无需考虑这块;visitMethodInsn
,但是逻辑几乎和父类中的一样,唯独不一样的就是这里不是用硬编码的 PASSTHROUGH_DATAFLOW
,而是用后续动态推导出来的 passthroughDataFlow
(也就是新生成的Transfer规则)进行Transfer判断;PassthroughDiscovery
中使用 PassthroughDataflowClassVisitor
分析完毕后,获取函数的返回值,如果返回值中携带污点标记(污点标记是函数形参的索引),就是推导出了一个新的Transfer规则(例如返回值中携带的污点标记为1,则代表函数的第1个形参可以传播污点到函数的返回值):PassthroughDataflowClassVisitor cv = new PassthroughDataflowClassVisitor(classMap, inheritanceMap, passthroughDataFlow, serializableDecider, Opcodes.ASM6, method);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
// 方法的哪个形参可以污染返回值
passthroughDataFlow.put(method, cv.getReturnTaint());
CallGraphDiscovery
/**
* 注意: 这个是在Method内的call-site时发生
*/
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
// 获取被调用method的参数和类型,非静态方法需要把实例类型放在第一个元素
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:
if (!Command.enableTaintTrack) {
// 不进行污点分析,无脑记录调用关系,创建callgraph
this.discoveredCalls.add(new CallGraph(
new MethodReference.Handle(new ClassReference.Handle(this.owner), this.name, this.desc),
new MethodReference.Handle(new ClassReference.Handle(owner), name, desc),
0,
"",
0));
break;
}
// 进行污点分析
int stackIndex = 0;
for (int i = 0; i < argTypes.length; i++) {
int argIndex = argTypes.length-1-i;
Type type = argTypes[argIndex];
// 从栈顶取操作数(也就是取callee的receiver以及形参)
Set<String> taint = getStackTaint(stackIndex);
// 如果取出来的receiver或者形参携带污点,则加入call-graph节点
if (taint.size() > 0) {
for (String argSrc : taint) {
// 取出出栈的参数,判断是否为当前方法的入参,arg前缀
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 {
// 这种不带点的,是属性携带污点的情况,此时argSrc的形式为:arg0.Username
srcArgIndex = Integer.parseInt(argSrc.substring(3, dotIndex));
srcArgPath = argSrc.substring(dotIndex+1);
}
//记录参数流动关系
//argIndex:当前方法参数索引,srcArgIndex:对应上一级方法的参数索引
this.discoveredCalls.add(new CallGraph(
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);
}
callgraph.dat
文件中。SourceDiscovery
-
类是否存在 RestController
或者Controller
注解;
-
source方法是否存在 RequestMapping
等注解。
/**
* @author : P1n93r
* @date : 2022/4/12 14:46
*/
public class SpringMVCSourceDiscovery extends HttpServletRequestSourceDiscovery {
@Override
public void discover(Map<ClassReference.Handle, ClassReference> classMap, Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap, Map<MethodReference.Handle, Set<CallGraph>> callGraphMap) {
methodMap.values().forEach(item -> {
ClassReference classReference = classMap.get(item.getClassReference());
Set<CallGraph> callGraphs = callGraphMap.get(item.getHandle());
if (callGraphs == null) {
return;
}
for (CallGraph call : callGraphs) {
if (
// 类限定
classReference != null && (classReference.getAnnotations()
.contains("Lorg/springframework/web/bind/annotation/RestController;") || classReference
.getAnnotations().contains("Lorg/springframework/stereotype/Controller;"))
) {
// 现在能保证是Controller类内的方法了,继续寻找Controller路由方法
String methodAnnotation = item.getMethodAnnotation();
// 保证存在SpringMVC的Controller相关的注解
List<String> annotations = Arrays.asList(
"Lorg/springframework/web/bind/annotation/RequestMapping;",
"Lorg/springframework/web/bind/annotation/GetMapping;",
"Lorg/springframework/web/bind/annotation/PostMapping;",
// 不太常用,但是也是攻击面
"Lorg/springframework/web/bind/annotation/DeleteMapping;",
"Lorg/springframework/web/bind/annotation/PatchMapping;",
"Lorg/springframework/web/bind/annotation/PutMapping;"
);
annotations.forEach(anno->{
if(methodAnnotation.contains(anno)){
// controller方法的每个argIndex都被添加到了source
addDiscoveredSource(new Source(item.getHandle(), call.getCallerArgIndex()));
}
});
}
}
});
super.discover(classMap, methodMap, inheritanceMap, callGraphMap);
}
}
GadgetChainDiscovery
GadgetInspector边界
NEWARRAY
没进行污点传播处理,所以污点会断开:@Override
public void visitIntInsn(int opcode, int operand) {
switch(opcode) {
case Opcodes.BIPUSH:
case Opcodes.SIPUSH:
push();
break;
case Opcodes.NEWARRAY:
// 这里就简单的pop和push,污点到达NEWARRAY将断开
// 如果认为污点进入数组,可以污染整个数组,那么应该需要 push(pop())
pop();
push();
break;
default:
throw new IllegalStateException("Unsupported opcode: " + opcode);
}
super.visitIntInsn(opcode, operand);
sanityCheck();
}
PUTFIELD
,这条指令的作用就是:pop出 value
以及 objectRef
,使得 objectRef#field=value
;但是GI中操作数栈和本地变量表中存储的元素都是一个用于模拟污点的Set,无法实际的对 objectRef#field
进行赋值操作,所以污点进入实例属性,则污点就断开了。@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
int typeSize = Type.getType(desc).getSize();
switch (opcode) {
// 省略其他代码
.......
.......
case Opcodes.PUTFIELD:
// pop objectref and value
for (int i = 0; i < typeSize; i++) {
pop();
}
// this is objectref
// GI中无法真正处理objectref,造成污点断开
pop();
break;
default:
throw new IllegalStateException("Unsupported opcode: " + opcode);
}
super.visitFieldInsn(opcode, owner, name, desc);
sanityCheck();
}
StringBuilder content = new StringBuilder();
content.append("calc");
Runtime.getRuntime().exec(content.toString());
content.append("calc")
没有把返回值进行赋值,返回值作为污点会被直接丢弃;直接看对应的JVM指令,发现返回值被直接pop掉了,所以污点就从这里被断开了:8 aload_1
9 ldc #4 <calc>
11 invokevirtual #5 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
14 pop
GadgetInspector实战效果
java -jar GadgetInspector-1.0-SNAPSHOT.jar --is-springboot false --package com.landray --class-path /Volumes/Seagate/classes --jar-path /Volumes/Seagate/code_audit/jar --type struts --use-taint true
-
GadgetInspector想要精度更高的污点分析,还需要二开,主要是想办法进行更高精度的堆抽象; -
基于soot进行静态程序分析,现在已经变成了主流,但是落地实现门槛颇高,基本都是从一些论文中才能找到一些资料; -
对于挖洞而言,推荐非污点分析和污点分析都跑一遍,防止遗漏。 推荐阅读:《自动化漏洞挖掘:静态程序分析入门【上】》
-完-
原文始发于微信公众号(青藤智库):自动化漏洞挖掘:静态程序分析入门【下】