警告
本文最后更新于 2023-01-18 ,文中内容可能已过时。
在做代码审计的时候,总是遇到一些批量垃圾洞,或者是遇到需要自动化批量找调用链验证的工作,一直想着解决这个问题,后来发现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指针分析框架的简单使用。
防喷:我只是看了课,并不代表我会了,很惭愧的是两位老师的课我看了第二遍有些地方还是不太理解,但是每看一遍总有新收获,所以本文有错实属正常不过,望读者赐教。
GitHub的wiki给了配置教程
1
2
3
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异常
输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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语法
举一个污点分析的例子
1
2
3
4
5
6
7
8
9
- 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.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - beans - 5.3 . 23.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - boot - 2.7 . 4.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - boot - autoconfigure - 2.7 . 4.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - boot - jarmode - layertools - 2.7 . 4.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - context - 5.3 . 23.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - core - 5.3 . 23.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - expression - 5.3 . 23.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - jcl - 5.3 . 23.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - web - 5.3 . 23.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \spring - webmvc - 5.3 . 23.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \tomcat - embed - core - 9.0 . 65.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \tomcat - embed - el - 9.0 . 65.j ar ; E : \tools \code \demo5 \target \demo5 - 0.0 . 1 - SNAPSHOT \BOOT - INF \lib \tomcat - embed - websocket - 9.0 . 65.j ar
-- 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作为一个插件式的框架将分析插件模块化,对应的配置放在了这个文件中。
拿出来一段配置来看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- 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 找到可能删除或忽略异常的方法 基本上用到的都在里面了。
javase程序指的是命令行/桌面程序,javaee指的是web程序。
两者区别在于程序入口点不同,在javaee中存在多个servlet/controller,需要将多个路由对应的方法加入到静态软件分析的入口点entrypoint,而javase只有一个main函数,整个数据流是从main函数进去然后流向其他函数最终到sink点。
tai-e中需要指定-m
参数指定程序主类,对于javaee来说需要增加分析入口点。接下来先以一个简单的javase项目为例学习tai-e的污点分析用法
新建一个maven空项目,创建主类 org.example.Main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
污点规则文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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给定如下参数
1
2
3
4
5
6
7
8
9
-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列出了污点的信息流
1
2
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 tomcat servlet项目为例
存在如下的servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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函数
1
2
3
@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的类型系统就行
1
2
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,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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中
1
2
3
4
5
6
7
8
9
10
// 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类引入进来。
1
- 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.j ar - 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中,发现成功检测出来一条污点传播的路径来
1
2
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 文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。