作者:y4er
原文鏈接:https://y4er.com/posts/simple-use-of-the-java-static-analysis-framework-tai-e
前言
在做代碼審計的時候,總是遇到一些批量垃圾洞,或者是遇到需要自動化批量找調用鏈驗證的工作,一直想著解決這個問題,后來發現tabby,用了一段時間,總覺得不太舒服,配置不足oom異常加上非人的neo4j的語法,加上太多的toString、equals等無用調用關系,配合上雜亂的neo4j的圖,有點擾亂審計思路。
自己照著tabby抄了一個poop出來,發現自己的問題并沒有解決,只是熟悉了一下soot的基礎用法,會抽取類信息了而已,在此期間狠狠補了一下soot,逐字逐句翻譯啃完了英文的《soot存活指南》 https://www.brics.dk/SootGuide/sootsurvivorsguide.pdf 然后發現soot出了一個新版本的sootup,自己試了試ifds污點分析。
對于指針分析、污點分析還是一知半解,中間嘗試過bytecodedl、doop這種聲明式的分析工具,然后發現suffle語法更變態,鬼畫符,加上沒有詳細的文檔和對應的規則,自己想要進一步拓展過于困難。
思考很久,發現還是自己底子不扎實,于是學了很長一段時間的靜態軟件分析,看了很多的論文(折磨)和視頻,其中包括南京大學譚添、李樾兩位老師的課,北大熊英飛老師的課等等,今天就簡單寫一下譚添、李樾兩位老師開發的tai-e指針分析框架的簡單使用。
防噴:我只是看了課,并不代表我會了,很慚愧的是兩位老師的課我看了第二遍有些地方還是不太理解,但是每看一遍總有新收獲,所以本文有錯實屬正常不過,望讀者賜教。
配置tai-e
GitHub的wiki給了配置教程
git clone https://github.com/pascal-lab/Tai-e
cd Tai-e
git submodule update --init --recursive
idea打開 需要jdk17

gradle也需要jdk17

然后運行pascal.taie.Main,配置下主類,加一個jvm options Xmx防止oom異常

輸出
Tai-e starts ...
Writing options to output\options.yml
Usage: Options [-gh] [-ap] [--[no-]native-model] [-pp] [--pre-build-ir] [-cp=<classPath>] [-java=<javaVersion>]
[-m=<mainClass>] [--options-file=<optionsFile>] [-p=<planFile>] [-scope=<scope>]
[--world-builder=<worldBuilderClass>] [-a=<String=String>]... [--input-classes=<inputClass>[,
<inputClass>...]]...
Tai-e options
-a, --analysis=<String=String> Analyses to be executed
-ap, --allow-phantom Allow Tai-e to process phantom references, i.e., the referenced classes that are
not found in the class paths (default: false)
-cp, --class-path=<classPath> Class path. Multiple paths are split by system path separator.
-g, --gen-plan-file Merely generate analysis plan
-h, --help Display this help message
--input-classes=<inputClass>[,<inputClass>...]
The classes should be included in the World of analyzed program (the classes can
be split by ',')
-java=<javaVersion> Java version used by the program being analyzed (default: 6)
-m, --main-class=<mainClass> Main class
--[no-]native-model Enable native model (default: true)
--options-file=<optionsFile> The options file
-p, --plan-file=<planFile> The analysis plan file
-pp, --prepend-JVM Prepend class path of current JVM to Tai-e's class path (default: false)
--pre-build-ir Build IR for all available methods before starting any analysis (default: false)
-scope=<scope> Scope for method/class analyses (default: APP, valid values: APP, REACHABLE, ALL)
--world-builder=<worldBuilderClass>
Specify world builder class (default: pascal.taie.frontend.soot.SootWorldBuilder)
--------------------
Version 0.1
--------------------
運行參數
列舉幾個關鍵參數,或者直接看文檔
| 參數 | 示例 | 用途 |
|---|---|---|
-ap |
-ap |
允許虛引用,等同于soot的--allow-phantom |
-java |
-java 8 |
指定java版本為jre8 tai-e會從java-benchmarks/JREs加載對應的jdk lib |
-pp |
-pp |
將當前jvm的類路徑添加到分析類路徑中 和-java選項沖突 |
-m |
-m com.example.demo.Main |
指定主類 表示程序入口 必選參數 |
--input-classes |
--input-classes=com.example.demo.controller.TestController,javax.servlet.ServletRequestWrapper |
當main函數無法調用到TestController時,可以用這個參數把TestController強制加進來,類似于強制分析? |
-cp |
-cp E:\demo5\target\classes;.\lib\test.jar |
類路徑 和soot差不多 支持jar文件或者.java、.class文件目錄 在Windows中多個jar以;分隔,unix以:分隔 |
-g |
-g |
僅生成選項配置文件output/options.yml不執行分析 |
--options-file |
--options-file=output/options.yml |
解析配置文件作為選項配置 |
--pre-build-ir |
--pre-build-ir |
分析之前為所有的method構建IR |
-scope |
-scope=APP |
指定分析類和方法的分析范圍APP, REACHABLE, ALL |
-a |
-a <id>[=<key>:<value>;...] |
指定分析選項,-a可重復指定多個分析,選項會保存在output/tai-e-plan.yml文件中 |
-p |
-p output/tai-e-plan.yml |
用文件指定分析選項 yaml語法 |
舉一個污點分析的例子
-cp
E:\tools\code\demo5\target\classes;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-aop-5.3.23.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-beans-5.3.23.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-boot-2.7.4.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-boot-autoconfigure-2.7.4.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-boot-jarmode-layertools-2.7.4.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-context-5.3.23.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-core-5.3.23.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-expression-5.3.23.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-jcl-5.3.23.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-web-5.3.23.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\spring-webmvc-5.3.23.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\tomcat-embed-core-9.0.65.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\tomcat-embed-el-9.0.65.jar;E:\tools\code\demo5\target\demo5-0.0.1-SNAPSHOT\BOOT-INF\lib\tomcat-embed-websocket-9.0.65.jar
--input-classes=com.example.demo.controller.TestController,javax.servlet.ServletRequestWrapper,javax.servlet.ServletResponseWrapper,org.apache.catalina.connector.Request
-java
8
-m
com.example.demo.Main
-ap
-a pta=action:dump;action-file:result.txt;taint-config:src\test\resources\pta\taint\taint-config.yml
直接配置在idea的參數中就行,表示在給定的幾個jar包和class中做pta,指定了taint-config文件表示做p/taint污點分析,強制指定com.example.demo.controller.TestController控制器和幾個引用類,允許虛類,使用的污點配置文件為src\test\resources\pta\taint\taint-config.yml,結果導出到result.txt文件中。
需要重點講一下-a參數,tai-e有三大類參數
- Program options 指定程序執行的參數
- Analysis options 指定執行代碼分析時的參數
- 其他選項
-h這類
-a就是第二種,涉及到代碼分析時需要指定的參數,在src/main/resources/tai-e-analyses.yml中,tai-e作為一個插件式的框架將分析插件模塊化,對應的配置放在了這個文件中。
拿出來一段配置來看
- description: whole-program pointer analysis
analysisClass: pascal.taie.analysis.pta.PointerAnalysis
id: pta
options:
cs: ci # | k-[obj|type|call][-k'h]
only-app: false # only analyze application code
implicit-entries: true # analyze implicit entries
merge-string-constants: false
merge-string-objects: true
merge-string-builders: true
merge-exception-objects: true
handle-invokedynamic: false
advanced: null # specify advanced analysis:
# zipper | zipper-e | zipper-e=PV
# scaler | scaler=TST
# mahjong | collection
action: null # | dump | compare
action-file: null # path of file to dump/compare
reflection-log: null # path to reflection log
taint-config: null # path to config file of taint analysis,
# when this file is given, taint analysis will be enabled
plugins: [ ] # | [ pluginClass, ... ]
- description: call graph construction
analysisClass: pascal.taie.analysis.graph.callgraph.CallGraphBuilder
id: cg
requires: [ pta(algorithm=pta) ]
options:
algorithm: pta # | cha
dump: null # path of file to dump reachable methods and call edges
dump-methods: null # path of file to dump reachable methods
dump-call-edges: null # path of file to dump to call edges
id作為插件的唯一標識,當指定-a pta時表明用pascal.taie.analysis.pta.PointerAnalysis類進行分析,options指定了對應類的相關選項,其中id為cg的插件有一個選項為requires: [ pta(algorithm=pta) ]表示調用圖構造需要用到pta的分析結果,相當于依賴。
tai-e實現了很多的分析插件,列一下
- whole-program pointer analysis 全程序指針分析
- call graph construction 調用圖
- identify casts that may fail 識別可能失敗的強制類型轉換
- identify polymorphic callsites 識別多態callsites
- throw analysis 異常分析
- intraprocedural control-flow graph 過程內控制流圖
- interprocedural control-flow graph 過程間控制流圖
- live variable analysis 存活變量分析
- available expression analysis 有效表達分析
- reaching definition analysis 可達分析
- constant propagation 常量傳播
- inter-procedural constant propagation 過程間的常量傳播
- dead code detection 死代碼檢查
- process results of previously-run analyses 結果分析處理
- dump classes and Tai-e IR 導出類和IR
- null value analysis 空指針分析
- Null pointer and redundant comparison detector 空指針和冗余比較檢查
- find clone() related problems clone相關的問題
- find the method that may drop or ignore exceptions 找到可能刪除或忽略異常的方法
基本上用到的都在里面了。
使用tai-e分析javase程序
javase程序指的是命令行/桌面程序,javaee指的是web程序。
兩者區別在于程序入口點不同,在javaee中存在多個servlet/controller,需要將多個路由對應的方法加入到靜態軟件分析的入口點entrypoint,而javase只有一個main函數,整個數據流是從main函數進去然后流向其他函數最終到sink點。
tai-e中需要指定-m參數指定程序主類,對于javaee來說需要增加分析入口點。接下來先以一個簡單的javase項目為例學習tai-e的污點分析用法
新建一個maven空項目,創建主類 org.example.Main
package org.example;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Main main = new Main();
String source = main.source(args[0]);
main.sink(source);
}
public String source(String s) {
return s;
}
public String sink(String s) throws IOException {
Runtime.getRuntime().exec(s);
return "ok";
}
}
修改src/test/resources/pta/taint/taint-config.yml污點規則文件
sources:
- { method: "<org.example.Main: java.lang.String source(java.lang.String)>", type: "java.lang.String" }
sinks:
- { method: "<java.lang.Runtime: java.lang.Process exec(java.lang.String)>", index: 0 }
transfers:
- { method: "<java.lang.String: java.lang.String concat(java.lang.String)>", from: base, to: result, type: "java.lang.String" }
- { method: "<java.lang.String: java.lang.String concat(java.lang.String)>", from: 0, to: result, type: "java.lang.String" }
- { method: "<java.lang.String: char[] toCharArray()>", from: base, to: result, type: "char[]" }
- { method: "<java.lang.String: void <init>(char[])>", from: 0, to: base, type: "java.lang.String" }
- { method: "<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.String)>", from: 0, to: base, type: "java.lang.StringBuffer" }
- { method: "<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.Object)>", from: 0, to: base, type: "java.lang.StringBuffer" }
- { method: "<java.lang.StringBuffer: java.lang.String toString()>", from: base, to: result, type: "java.lang.String" }
- { method: "<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>", from: 0, to: base, type: "java.lang.StringBuilder" }
- { method: "<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.Object)>", from: 0, to: base, type: "java.lang.StringBuilder" }
- { method: "<java.lang.StringBuilder: java.lang.String toString()>", from: base, to: result, type: "java.lang.String" }
運行tai-e給定如下參數
-cp
E:\tools\code\aa\src\main\java
-java
8
-m
org.example.Main
-ap
-a
pta=action:dump;action-file:result.txt;taint-config:src\test\resources\pta\taint\taint-config.yml
指定E:\tools\code\aa\src\main\java為classpath,指定java版本為8,指定主類,允許幻象引用,啟用指針分析和污點分析,并將污點分析結果導出到result.txt文件中。
查看txt文件會發現 tai-e列出了污點的信息流
Detected 1 taint flow(s):
TaintFlow{<org.example.Main: void main(java.lang.String[])>[5@L8] temp$4 = invokevirtual main.<org.example.Main: java.lang.String source(java.lang.String)>(temp$3); -> <org.example.Main: java.lang.String sink(java.lang.String)>[1@L17] invokevirtual temp$0.<java.lang.Runtime: java.lang.Process exec(java.lang.String)>(s);/0}
分析java web
以一個java web tomcat servlet項目為例
存在如下的servlet
package com.example.demo6;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
String source = request.getParameter("source");
Runtime.getRuntime().exec(source);
}
}
在上面說到分析java web項目需要我們自己定義分析入口點,并且模擬參數對象,所以我們需要修改下污點分析的處理類pascal.taie.analysis.pta.plugin.taint.TaintAnalysis
tai-e實現了插件式編程,將分析拆成小模塊,官方wiki中提到了《如何寫一個分析插件》,除了寫分析插件以外,還可以《創建新的分析》
接下來我們將針對java web修改TaintAnalysis類,使污點分析時識別路由并將其加入到entrypoint中,即增加tai-e分析入口點。
TaintAnalysis類實現了pascal.taie.analysis.pta.plugin.Plugin接口,該接口有幾個生命周期函數

我們增加程序分析入口點肯定是在onStart函數中,所以在TaintAnalysis類重寫onStart函數
@Override
public void onStart() {
}
那么如何添加entrypoint呢?我搜了issue,發現有人有和我同樣的問題,官方也給出了解決方案
添加entrypoint需要調用pascal.taie.analysis.pta.core.solver.Solver#addEntryPoint函數,該函數需要一個EntryPoint對象,EntryPoint構造函數中需要兩個參數JMethod method, ParamProvider paramProvider,分別對應了入口點函數的JMethod對象,和入口點函數的參數處理器。其中ParamProvider接口有幾個實現類

分別對應不同情況下的參數提供器。其中MainEntryPointParamProvider就是對應的Main函數的參數處理器

其中getParamObjs調用getMainArgs拿到模擬的main函數的參數String[] args,模擬參數用了heapModel.getMockObj()。
官方的代碼中ThreadHandler的onStart函數是一個非常好的參數模擬并添加入口點的參考例子

參考這個我們來照貓畫虎,首先我們需要拿到com.example.demo6.HelloServlet的JMethod對象,很簡單,直接用tai-e的類型系統就行
JClass controller = World.get().getClassHierarchy().getClass("com.example.demo6.HelloServlet");
JMethod method = controller.getDeclaredMethod("doGet");
然后我們需要模擬參數對象,doGet函數有兩個參數HttpServletRequest request, HttpServletResponse response,HttpServletRequest和HttpServletResponse都是一個接口類,我們模擬對象必須是模擬具體的類,所以這里用HttpServletRequest和HttpServletResponse的實現類HttpServletRequestWrapper和HttpServletResponseWrapper,代碼如下
// mock obj
JClass requestWrapper = World.get().getClassHierarchy().getClass("javax.servlet.http.HttpServletRequestWrapper");
JClass responseWrapper = World.get().getClassHierarchy().getClass("javax.servlet.http.HttpServletResponseWrapper");
HeapModel heapModel = solver.getHeapModel();
Obj mockRequest = heapModel.getMockObj("EntryPointObj", "<http-request-wrapper>", requestWrapper.getType(), method);
Obj mockResponse = heapModel.getMockObj("EntryPointObj", "<http-response-wrapper>", responseWrapper.getType(), method);
Obj mockServlet = heapModel.getMockObj("EntryPointObj", "<http-controller>", servlet.getType());
// mock param
SpecifiedParamProvider paramProvider = new SpecifiedParamProvider.Builder(method)
.addThisObj(mockServlet)
.addParamObj(0, mockRequest)
.addParamObj(1, mockResponse)
.build();
solver.addEntryPoint(new EntryPoint(method, paramProvider));
很簡單,到這我們就把doGet加入到了entrypoint中,然后我們將javax.servlet.ServletRequest#getParameter加入到sink中
// add source request.getParameter(java.lang.String)
JMethod getParameter = requestWrapper.getDeclaredMethod("getParameter");
if (getParameter == null) {
getParameter = requestWrapper.getSuperClass().getDeclaredMethod("getParameter");
}
sources.put(getParameter, getParameter.getReturnType());
// print sources
sources.forEach((k, v) -> System.out.println(k.getMethodSource() + "\t" + v.getName()));
System.out.println();
運行一下試試,修改idea參數,加上javax.servlet-api-4.0.1.jar的lib包,并且要強制指定--input-classes=com.example.demo6.HelloServlet,javax.servlet.http.HttpServletRequestWrapper,javax.servlet.http.HttpServletResponseWrapper把兩個warpper類引入進來。
-cp E:\tools\code\demo6\target\classes;C:\Users\xxx\.m2\repository\javax\servlet\javax.servlet-api\4.0.1\javax.servlet-api-4.0.1.jar -java 8 --input-classes=com.example.demo6.HelloServlet,javax.servlet.http.HttpServletRequestWrapper,javax.servlet.http.HttpServletResponseWrapper -m com.example.demo6.Main -ap -a pta=action:dump;action-file:result.txt;taint-config:src\test\resources\pta\taint\taint-config.yml
查看result.txt中,發現成功檢測出來一條污點傳播的路徑來
Detected 1 taint flow(s):
TaintFlow{<com.example.demo6.HelloServlet: void doGet(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)>[1@L12] $r1 = invokeinterface request.<javax.servlet.http.HttpServletRequest: java.lang.String getParameter(java.lang.String)>(%stringconst0); -> <com.example.demo6.HelloServlet: void doGet(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)>[3@L13] invokevirtual $r2.<java.lang.Runtime: java.lang.Process exec(java.lang.String)>($r1);/0}
文末
對我而言,靜態軟件分析是一門高深的學問,涉及到的算法以及理論知識都比較晦澀,好在有tai-e這種開箱即用的框架。
回顧來看,tai-e在指針分析的能力和算法設計上毋庸置疑是很優秀的,而且整個框架的設計和插件式編程等設計思維遠超soot這種單例模式一把梭的框架。但是其架構對我們做審計java web自動化并不友好:
- 需要指定--input-classes參數強制引入對應的路由
- main函數限制
- 指定entrypoint入口點和參數mock比較麻煩
參考
- 南京大學譚添、李樾兩位老師的課 https://tai-e.pascal-lab.net/
- 北大熊英飛老師的課 https://liveclass.org.cn/cloudCourse/#/courseDetail/8mI06L2eRqk8GcsW
- tai-e Github開源地址 https://github.com/pascal-lab/Tai-e
- 兩位老師的pascal課題組開源的一些指針分析的代碼 https://pascal-group.bitbucket.io/code.html
- 《soot存活指南》 https://www.brics.dk/SootGuide/sootsurvivorsguide.pdf
- fynch3r師傅的Soot知識點整理
- How to analyze java web or spring project with tai-e
文筆垃圾,措辭輕浮,內容淺顯,操作生疏。不足之處歡迎大師傅們指點和糾正,感激不盡。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/3040/
暫無評論