自动化漏洞挖掘:静态程序分析入门【下】

渗透技巧 1年前 (2022) admin
734 0 0

自动化漏洞挖掘:静态程序分析入门【下】

自动化漏洞挖掘:静态程序分析入门【下】

赛博空间,攻防之战,此起彼伏

矛与盾的对决,攻与守的碰撞

在这里,我们一起来看

道德黑客使用了哪些杀手锏

正义蓝军如何见招拆招




编者按:在漏洞挖掘时,有很多现成的漏扫工具可用,那为啥还要学习晦涩难懂的静态程序分析,岂不是自讨苦吃?其实不然,在挖洞过程中,很多时候需要自己挖Gadget,现有的很多工具是远远满足不了滴!继《自动化漏洞挖掘:静态程序分析入门【上】》之后,本文继续带领大家一起探索静态程序分析。


文章目录(上下滑动查看)



前言

请参见《自动化漏洞挖掘:静态程序分析入门【上】》

推荐学习路线
静态程序分析相关概念快速了解
IR(Intermediate Representation)
SSA(Static Single Assignment)
CFG(Control Flow Graph)
Data Flow Analysis
Input and Output States
Transfer Function’s Constraints
Control Flow’s Constraints
Interprocedural Analysis
Caller & Callee & Receiver
Call Graph
Method Calls(Invocations) in Java
Method Dispatch of Virtual Calls
CHA(Class Hierarchy Analysis)
Call Graph Construction
Pointer Analysis
Pointer Analysis and Alias Analysis
Key Factors in Pointer Analysis
Heap Abstraction
Allocation-Site Abstraction
Context Sensitivity
Flow Sensitivity
Analysis Scope
Concerned Statement
Datalog
Predicates
Atoms
Datalog Rules
Souffle简单教程
注释和预处理器
关系声明
I/O指令
语法糖
原始类型
算术表达式
数字编码
简单例子
辅助工具
soot-fact-generator
ByteCodeDL
inputDeclaration.dl
utils.dl
cha.dl
rta.dl
pt-noctx.dl
ptaint.dl
GadgetInsepctor
JVM栈祯
局部变量表
操作数栈
举个栗子
GadgetInspector基本原理
GadgetInspector具体实现
初始化ClassLoader
MethodDiscovery
PassthroughDiscovery
CallGraphDiscovery
SourceDiscovery
GadgetChainDiscovery
GadgetInspector边界
GadgetInspector实战效果
总结
//

辅助工具

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
自动化漏洞挖掘:静态程序分析入门【下】
生成的fact在out_dir目录中:
自动化漏洞挖掘:静态程序分析入门【下】
一些其他的有用参数:
--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).

这里需要注意的主要是Dispatch谓词的推导,之前我们讲利用CHA算法计算VirtualCall的真实被调函数的算法如下:
自动化漏洞挖掘:静态程序分析入门【下】

这里只不过是把这个函数式的算法换成了声明式而已,逻辑都是一样的。

到这里我们可以做个小实验,先热个身,比如存在如下三个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了,也是之前提过的算法(函数式):

自动化漏洞挖掘:静态程序分析入门【下】
把它们翻译成声明式的datalog就是下面的内容:
#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).

假设存在如下的调用图(也就是CHAO不等于1或者2的时候, RefinedReachable(node)所代表的节点):
自动化漏洞挖掘:静态程序分析入门【下】
如果 CHAO=1 ,根据前面的算法,就会进行SinkReachable谓词的推导,它从Sink节点往上推导,找到所有能到达Sink的节点,并且记录节点到达Sink的步长,经过推导,可以得到SinkReachable谓词所代表的节点就是下标蓝的节点:
自动化漏洞挖掘:静态程序分析入门【下】

如果 CHAO=2 ,会进行如下谓词推导:

ShortestPathToSink(entry, sink, n) :-
   n = min step : {SinkReachable(entry, sink, step)},
   SinkMethod(sink),
   EntryMethod(entry).
它的含义前面也解释过了,就是找到从entry到sink路径最短的组合,很明显,就是下图中红色虚线标注的路线,于是可以得到 ShortestPathToSink(entry, sink, 3) 这个fact:
自动化漏洞挖掘:静态程序分析入门【下】

然后继续下面的这个谓词推导过程,就是把上面那条最短路径上的两个蓝色的节点也加入到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).
}

其实这个算法主要是围绕下图来进行具体实现的,但是多了对于数组的处理以及类型转换的处理。
自动化漏洞挖掘:静态程序分析入门【下】
这个地方可能很多人不理解到底什么是上下文不敏感,前面提到过,上下文不敏感可以用这个图来理解:
自动化漏洞挖掘:静态程序分析入门【下】
有点抽象,那么我们来看一个具体的例子,假设我们现在存在一个Java程序,代码如下:
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相同,唯一不同的就是调用位置不同:

自动化漏洞挖掘:静态程序分析入门【下】

再来看到这个callee的形参,callee的形参是固定的 <org.example.model.App: void test(java.lang.String)>/@parameter0
自动化漏洞挖掘:静态程序分析入门【下】
但是我们再来看到这两个不同调用点传递的实参,是传递了两个不同的实参的,它们分别指向不同的堆对象:
自动化漏洞挖掘:静态程序分析入门【下】
所以最终,callee的形参就指向了两个不同调用点的实参的堆对象:
自动化漏洞挖掘:静态程序分析入门【下】
这正对应了下面这个图,不同调用点的callee,callee的形参指向了多个调用点的实参的堆对象:

自动化漏洞挖掘:静态程序分析入门【下】

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";
   }

}
使用soot得到Jimple如下(这里还是要再强调一次,使用指针分析,一定不能使用 --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";
   }
}
我们可以编写如下dl来进行污点分析:
#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基本原理

前面我们分析了函数调用过程中,JVM对本地变量表和操作数栈的处理,可以知道,进行函数调用的时候,会先将函数的实例对象以及实参入栈,进入callee的栈帧后,会对刚刚calller入栈的实例对象以及实参进行pop出栈,存储到callee的本地变量表中。
自动化漏洞挖掘:静态程序分析入门【下】
如果我们进行函数调用的时候,从caller的操作数栈pop到callee的本地变量表中时,先给参数打上污点标签,callee内再调用各种JVM指令把含有污点的参数进行各种转移,最终函数返回时,如果栈顶元素含有污点,则完成了:污点形参→污点返回值的污点传播过程;
现在我们假设我们存在如下代码,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 函数时,污点处理过程如下:
自动化漏洞挖掘:静态程序分析入门【下】
最后append函数返回时,图示如下:
自动化漏洞挖掘:静态程序分析入门【下】
接下来就是 Demo#main 中使用astore_3指令把含有污点标记的String对象“calc”弹出到本地变量表中的第3位置:
自动化漏洞挖掘:静态程序分析入门【下】
最终运行到sink函数(Runtime#exec)的时候,此时判断污点流入了sink的形参,则整个污点分析完成:
自动化漏洞挖掘:静态程序分析入门【下】

GadgetInspector具体实现

我把GI的代码结构改成了我认为比较好阅读的方式,所以接下来的一些代码截图稍微和原作者有一些出入;不过原理是一样的;GI的步骤大致可以分为如下几个步骤:

  1. 初始化 URLClassLoader 加载需要分析的classPath以及jarUrl;
  1. 使用 MethodDiscovery 得到所有的classes、methods、inheritance信息;
  1. 如果开启了污点分析,则使用 PassthroughDiscovery 进行过程内的污点分析(污点可以从函数的第n个形参流出到函数返回值);
  1. 使用 CallGraphDiscovery 创建 CallGraph ,如果开启了污点分析,则创建 CallGraph 时会进行污点传播判断;
  1. 使用 SourceDiscovery 得到所有的source;
  2. 最后使用 GadgetChainDiscovery ,根据source和sink遍历 CallGraph ,找到能从source到sink的 CallGraph 链。

初始化ClassLoader

首先使用一个URLClassLoader加载用户指定的classPath以及jarUrl;当然,可以根据不同类型的项目(例如war包、springboot-fat-jar等等)分别进行处理;如下是普通方式,手动指定需要分析的classPath以及jarUrl:
/**
* 对于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]));
}
如下是处理springboot-fat-jar:
/**
* 专门处理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]));
}
处理war包也是类似的方式,只不过就是把 BOOT-INF 换成 WEB-INF后续就是通过这个UrlClassLoader,把其中的所有class全部遍历一次,添加到一个 List<ClassFile> 中,方便后续进行分析。

MethodDiscovery

这个discovery的主要逻辑就是遍历所有的 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.ClassVisitororg.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文件中的类的名字、父类名、实现的接口、是否为接口等信息:
// 获取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文件中的方法信息:
// 获取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);
}
于是借助Java Asm框架,就可以完成class文件的解析,同时也能得到我们想要的类名、方法名、类注解、方法注解等信息;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

这个Discovery主要是根据前面已有的class信息、method信息,进行过程内污点分析,具体得到的结果就是:污点可以从函数的第n个参数流出到函数返回值 ;
进行计算之前,先对所有的method逆拓扑排序;所谓逆拓扑排序,就是把调用链最深的函数排到最前面;例如存在如下函数调用链:
自动化漏洞挖掘:静态程序分析入门【下】
如果我们想要知道one函数的形参arg是否能传播到返回值中,就必须知道two函数的arg形参能否传播污点到返回值中,同样函数two要想知道形参arg是否能污染返回值,就必须要知道three函数的形参是否能污染返回值;
也就是,我们需要先知道调用链最底端的函数中,其形参是否能污染返回值,才能知道调用链高层的函数中其形参能否污染返回值;所以在进行过程内污点分析之前,我们需要先对所有的函数进行逆拓扑排序;
排序完毕,就对排序后的方法进行过程内污点分析,先获取当前分析的方法所在的类,使用 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;

// 省略其他......
}
在栈祯进入callee中时,首先需要清空当前栈祯的本地变量表和操作数栈,然后将this引用和函数形参添加到本地变量表中:
@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>());
       }
   }
}
这个过程就对应下面的这个图:
自动化漏洞挖掘:静态程序分析入门【下】
接下来就是解析字节码中的JVM指令,根据指令的含义模拟操作数栈的push和pop以及对局部变量表的load和strore;
例如执行ldc指令,就对应如下visit处理,往操作数栈中push一个元素(ldc指令操作的是long或者double类型,则需要push两次,占用两个槽):
@Override
public void visitLdcInsn(Object cst) {
   if (cst instanceof Long || cst instanceof Double) {
       push();
       push();
   } else {
       push();
   }

   super.visitLdcInsn(cst);

   sanityCheck();
}
此时图示如下:
自动化漏洞挖掘:静态程序分析入门【下】
再例如执行astore_3指令,把刚添加到操作数栈中的数据存储到本地变量表中(省略了部分代码):
@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 的处理;
首先获取callee的形参类型以及callee的返回值大小,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();
然后根据形参的类型,模拟初始化callee的本地变量表,本地变量表先暂时都置空:
// 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);
}
接着从caller的操作数栈pop出元素到callee的本地变量表中:
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<>();
}
接着再进行判断,如果callee是 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));
}
然后就是最关键的地方了,GI不会对callee再进行过程内分析,而是使用一个硬编码的 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]));
       }
   }
}
最后推集合类以及Map类进行特殊传播处理,认为污点只要进入了集合类以及Map类的形参(或者集合类以及Map类本身携带污点),则污点一定能传播到callee的返回值中:
// 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));
       }
   }
}
callee的污点传播到callee的返回值后,最后需要将callee的返回值压入caller的操作数栈中了:
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 列表需要覆盖足够全才能保证污点数据流不会意外断开;
这里再提一点,GI中对于NEW指令生成的对象,是没做污点标记的,所以即便 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());
于此就可以得到所有函数的Transfer规则,也就是可以知道所有函数的第几个形参可以污染返回值。

CallGraphDiscovery

这个Discovery也和之前的操作差不多,也是使用ClassVisitor和MethodVisitor进行方法visit,如果当前访问的方法内存在invoke指令(invoke指令代表方法调用),就可以形成一个CallGraph;如果开启了污点分析,则需要判断callee的形参或者receiver是否含有污点,如果没有,则不创建callgraph:
/**
* 注意: 这个是在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);
}
最终得到的call-graph信息保存在 callgraph.dat 文件中。

SourceDiscovery

这个就是根据用户自定义的source规则以及前面解析出来的class、method、annotation等信息创建souce对象。例如对于SpringMVC项目,判断是否为source的依据为:
  • 类是否存在 RestController 或者 Controller 注解;
  • source方法是否存在 RequestMapping 等注解。
所以可以编写如下SpringMVC的SourceDiscovery:
/**
* @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

这个类比较简单,就是从source出发,从source方法开始广度优先搜索callgraph,判断callgraph的callee是否为sink,是sink则查到GadgetChain,否则将callee添加到待处理列表准备对callee也进行callgraph搜索。

GadgetInspector边界

目前GI还是存在一些边界问题没解决,例如污点进入数组,GI中由于对 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();
}
再例如污点进入实例属性,对应的JVM指令就是 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实战效果

例如使用如下命令扫描蓝陵EKP:
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进行静态程序分析,现在已经变成了主流,但是落地实现门槛颇高,基本都是从一些论文中才能找到一些资料;
  • 对于挖洞而言,推荐非污点分析和污点分析都跑一遍,防止遗漏。
    推荐阅读:自动化漏洞挖掘:静态程序分析入门【上】



关于作者:
p1n93r:吴钩实验室安全研究员,主要研究方向为攻防技术、自动化漏洞挖掘。

-完-

自动化漏洞挖掘:静态程序分析入门【下】

自动化漏洞挖掘:静态程序分析入门【下】


自动化漏洞挖掘:静态程序分析入门【下】

原文始发于微信公众号(青藤智库):自动化漏洞挖掘:静态程序分析入门【下】

版权声明:admin 发表于 2022年12月1日 下午7:02。
转载请注明:自动化漏洞挖掘:静态程序分析入门【下】 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...