參考:使用太阿(Tai-e)進行靜態代碼安全分析(spring-boot篇一) - 先知社區
----------------------------------------------------------------------
?由于spring-boot實現了控制反轉與面向切面編程的設計思想,使得程序并非順序執行,因此很難通過程序入口來順序分析所有代碼。本篇文章旨在從0開始,利用Tai-e來分析spring-boot程序,解決控制反轉的問題。
1. 從0開始配置tai-e并加載java-sec-code
1.1 下載tai-e代碼
創建文件夾并進入
git clone https://github.com/pascal-lab/Tai-e.git
git init
git submodule update --init --recursive
1.2 IDEA配置tai-e
其實就是按照官方文檔進行配置IDEA
在打開tai-e程序后等待一會
Tai-e加載完之后
設置
File->Project Structure,將SDK與Language level設置為17
File->Settings
然后將Build and run using與Run tests using改為idea
1.3 編譯java-sec-code
下載java-sec-code源碼
git clone https://github.com/JoyChou93/java-sec-code.git
cd java-sec-code
mvn clean package -X
注意:若出現'mvn'不是內部或外部命令,也不是可運行的程序或批處理文件。
則首先確認自己是否安裝了maven
若已經安裝,則
添加Maven到環境變量:
- 打開系統的環境變量設置,找到
Path
變量,確保Maven的bin
目錄路徑已經添加到Path
變量中。重新打開命令行執行
等待
看到Build success代表編譯成功
1.4 配置tai-e加載java-sec-code
首先創建common目錄和options和taint-config文件。作為我們自己的配置
options文件內容,注意需要修改appClassPath也就是我們剛才編譯的java-sec-code的target。
optionsFile: null
printHelp: false
classPath: []
appClassPath:- D:\software\java-sec-code\java-sec-code\target\classes
mainClass:
inputClasses: []
javaVersion: 8
prependJVM: false
allowPhantom: true
worldBuilderClass: pascal.taie.frontend.soot.SootWorldBuilder
outputDir: output
preBuildIR: false
worldCacheMode: true
scope: REACHABLE
nativeModel: true
planFile: null
analyses:# ir-dumper: ;pta: cs:ci;implicit-entries:true;distinguish-string-constants:null;reflection-inference:solar;merge-string-objects:false;merge-string-builders:false;merge-exception-objects:false;taint-config:config/common/taint-config.yml;
onlyGenPlan: false
keepResult:- $KEEP-ALL
taint-config文件內容,這個文件暫時不需要修改,就是加了一個sources和sinks點以及transfers。
sources:
# - { kind: param, method: "<org.joychou.controller.SQLI: java.lang.String jdbc_sqli_sec(java.lang.String)>", index: 0}- { kind: param, method: "<org.joychou.controller.SQLI: java.lang.String jdbc_sqli_vul(java.lang.String)>", index: 0}
# - { kind: param, method: "<org.joychou.controller.SpEL: void main(java.lang.String[])>", index: 0}
# - {kind: param, method: "<org.joychou.controller.Rce: java.lang.String CommandExec(java.lang.String)>",index: 0}
sinks:## SQLI- { vuln: "SQL Injection", level: 4, method: "<java.sql.Statement: java.sql.ResultSet executeQuery(java.lang.String)>", index: 0 }- { vuln: "SQL Injection", level: 4, method: "<java.sql.Connection: java.sql.PreparedStatement prepareStatement(java.lang.String)>", index: 0 }
transfers:- { method: "<java.lang.String: java.lang.String concat(java.lang.String)>", from: base, to: result }- { method: "<java.lang.String: java.lang.String concat(java.lang.String)>", from: 0, to: result }- { method: "<java.lang.String: char[] toCharArray()>", from: base, to: result }- { method: "<java.lang.String: void <init>(char[])>", from: 0, to: base }- { method: "<java.lang.String: void getChars(int,int,char[],int)>", from: base, to: 2 }- { method: "<java.lang.String: java.lang.String format(java.lang.String,java.lang.Object[])>", from: "1[*]", to: result }- { method: "<java.lang.StringBuffer: void <init>(java.lang.String)>", from: 0, to: base }- { method: "<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.String)>", from: 0, to: base }- { method: "<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.String)>", from: 0, to: result }- { method: "<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.String)>", from: base, to: result }- { method: "<java.lang.StringBuffer: java.lang.String toString()>", from: base, to: result }- { method: "<java.lang.StringBuilder: void <init>(java.lang.String)>", from: 0, to: base }- { method: "<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>", from: 0, to: base }- { method: "<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>", from: 0, to: result }- { method: "<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>", from: base, to: result }- { method: "<java.lang.StringBuilder: java.lang.String toString()>", from: base, to: result }call-site-mode: true
配置Main函數參數
現在可以運行了,但是你會發現沒有taint-flow的結果。
主要的問題是tai-e的taint-analysis是基于pointer-anysis的。而pointer-analysis的分析是基于worklist,這個worklist如果是java se程序初始化就是main函數,可以通過main函數進行分析,進行函數調用處理,從而reach到需要分析的函數。由于我們的是springboot程序包含依賴注入和控制反轉等,所以從springboot的入口tai-e沒辦法分析到我們的controller函數,所以pointer-anlaysis的結果也就是空的,導致taint-anlaysis結果也是空的。
經過分析發現tai-e分析SpringBoot項目存在2個問題。
1. 缺少entrypoint,因為pointerAnalysis分析沒辦法reach到我們需要分析的controller方法。
2. 缺少source,我們暫時沒有辦法通過yml將所有Mapping注解的parameters加入sources,我們上邊yml僅加入了一個source。
需要解決以上兩個問題,才能進行基礎的分析。
2. 關鍵源碼分析及實現
2.1 PointerAnaysis分析及入口點添加
由于tai-e的插件是集成在PointerAnalysis,包括污點分析也是,也就是說PointerAnalysis就是這些分析的核心。所以在寫如何編寫插件前我們先大概分析一下PointerAnlaysis的執行流程。
2.1.1 PointerAnaysis執行流程
我們先看PointerAnalysis
的runAnalysis
方法,這個其實不算是入口點,analyze
才是,但是analyze
僅根據配置然后調用runAnalysis
,所以我們直接分析這個就好。
DefalutSolver
實現指針分析以及插件的調用,并返回指針分析的結果(core)。
Plugin
即一個插件,可以通過編寫插件的方式,在進行指針分析的過程中執行我們想要的操作(污點分析也是類似的操作)。
接下來看一下setPlugin方法,目前先不用修改它。
接下來分析DefaultSolver的solve
方法。
solve
方法會首先調用 initialize
方法,然后執行analyze()方法
initialize方法:
● 初始化調用圖、指針流向圖 、worklist(可以理解為指針分析的工作集)、reachableMethods(也就是分析能到達的方法)、initializedClasses(可以理解為內置的類,會進行指針分析,不需要我們添加)、stmtProcessor(這個類是指針分析處理語句的核心,根據訪問者模式處理對行的語句(new、load、invoke、array等))
● 然后會執行插件的onStart()方法
analyze
方法
- 通過消費worklist中的Entry,對Entry進行指針分析的處理,然后調用Plugin的
onNewPointsToSet
方法(本篇文章用不到該方法,該方法在處理field和array source時會用到) - 在處理worklist后調用Plugin.onFinish()方法
2.1.2 分析EntryPointerHandler
通過對PointerAnalysis執行流程的分析應該能大概了解Plugin的執行順序了。
看一下Plugin的interface ,有一系列plugin的方法。
- 插件方法執行流程
loadPlugin(pointerAnalysis加載插件)->setSolver->onStart->指針分析以及一些plugin方法->onFinish - 從上邊我們可以理解編寫Plugin的流程,選擇我們需要的方法比如onStart 、onFinish
- 在PointerAnalysis加入編寫的插件。
看一下tai-e自帶的EntryPointHandler
它就是在onStart處判斷加入口點。
2.1.3 添加EntryPoint
這樣加入我們自己入口點的方法也有了,在onStart的時候把所有 有Mapping注解的方法都加入 入口點,這樣pointeranalysis就能分析到我們的source點了,taintanalysis也會有結果。
下邊的代碼就是獲取所有的class,然后獲取class的method,若注解是Mapping,則加入entrypoint
public class AddControllerEntryPointHandler implements Plugin {private Solver solver;@Overridepublic void setSolver(Solver solver) {this.solver=solver;}@Overridepublic void onStart() {//add all hsd mapping annotation methods to entrypointList<JClass> list = solver.getHierarchy().applicationClasses().toList();for (JClass jClass : list) {jClass.getDeclaredMethods().forEach(jMethod->{if (!jMethod.getAnnotations().stream().filter(annotation -> annotation.getType().matches("org.springframework.web.bind.annotation.\\w+Mapping")).toList().isEmpty()) {solver.addEntryPoint(new EntryPoint(jMethod, EmptyParamProvider.get()));}});}}
}
然后在pointer Analysis直接加入該插件,然后執行程序
然后執行程序將dot轉換為svgdot -Tsvg -o taint-flow-graph.svg taint-flow-graph.dot
由于上邊我們只加入了一個sources和sql的sink點,所以只能檢測出來一個taintflow
2.2 SourceHandler 分析及springboot source 添加
污點分析的核心包括如下三個集成到指針分析的核心插件:SourceHandler
:添加代表污點的特殊堆抽象加入到符合source點規則的指針的指向集中TransferHandler
:將符合污點傳播規則的邊加入指針分析SinkHandler
:收集污點流,輸出source->sink的污點流的集合
2.2.1 source類型
tai-e的污點分析支以下持三種source
sources:- { kind: call, method: "<javax.servlet.ServletRequestWrapper: java.lang.String getParameter(java.lang.String)>", index: result }- { kind: param, method: "<com.example.Controller: java.lang.String index(javax.servlet.http.HttpServletRequest)>", index: 0 }- { kind: field, field: "<SourceSink: java.lang.String info>" }
call sources
source點由調用點生成,若call site語句調用了特定方法,那么返回值就是source點。
paramater sources
對于特定方法,例如入口方法,沒有特定的調用點。但其形參依然可能是source點。在spring-boot,通過注解來聲明請求的處理入口,沒有明確調用controller方法,在這種情況下,paramater sources非常有用。
field sources
對于字段類型的指針,其依然可能是source。
2.2.2 流程分析
從配置中獲取source點的配置
當指針分析中產生新的語句或新的調用邊時,若語句符合source的規則,則代表指針即為source點
當指針分析產生新的方法時,會處理其中的語句,也會在這個地方處理paramater sources
若我們想添加自定義的source點,便可以在handleParamSource、handleFieldSource、handleCallSource中添加。
2.2.3 添加source
我們的source僅加入了一個,但是一條一條加source也不太現實,沒辦法實現通用型。
通過分析找到2個比較適合加source點的地方,一個是處理source的開始,一個是處理source結束的地方。
1. SourceHandler處理ParamSource的地方
2. 在tai-e讀取taint-config文件解析后添加source
我們這里就看第一種方法吧
代碼如下
private void handleParamSource(CSMethod csMethod) {JMethod method = csMethod.getMethod();if (paramSources.containsKey(method)) {Context context = csMethod.getContext();IR ir = method.getIR();paramSources.get(method).forEach(source -> {IndexRef indexRef = source.indexRef();Var param = ir.getParam(indexRef.index());SourcePoint sourcePoint = new ParamSourcePoint(method, indexRef);Obj taint = manager.makeTaint(sourcePoint, source.type());switch (indexRef.kind()) {case VAR -> solver.addVarPointsTo(context, param, taint);case ARRAY, FIELD -> sourceInfos.put(param, new SourceInfo(indexRef, taint));}});}else {if (!method.getAnnotations().stream().filter(annotation -> annotation.getType().matches("org.springframework.web.bind.annotation.\\w+Mapping")).toList().isEmpty()) {Context context = csMethod.getContext();IR ir = method.getIR();for (int i = 0; i < ir.getParams().size(); i++) {Var param = ir.getParam(i);SourcePoint sourcePoint = new ParamSourcePoint(method, new IndexRef(IndexRef.Kind.VAR, i, null));Obj taint = manager.makeTaint(sourcePoint, param.getType());solver.addVarPointsTo(context, param, taint);}}}}
然后執行程序,查看結果,可以看到source都添加成功了,并且新增了一個taintflow。
?3. 結果展示
由lcark、keanu大佬實現更改的相關代碼以及配置文件已經上傳至github
具體食用方法如下:
1. 下載代碼,并移動至spring-boot-1目錄下
git clone https://github.com/lcark/Tai-e-demo
cd Tai-e-demo/spring-boot-1
git submodule update --init
2. 將java-sec-code文件夾移至與Tai-e-demo文件夾相同目錄下
3. 將SpringBootHandler.java移動至Tai-e源碼的src/main/java/pascal/taie/analysis/pta/plugin/taint/目錄下,并重新編譯打包
4. 使用如下命令運行tai-e便可以成功獲取到掃描結果java -cp xxx\tai-e-all-0.5.1-SNAPSHOT.jar pascal.taie.Main --options-file=options.yml