Java 静态分析框架 Tai-e 的简单使用
source link: https://paper.seebug.org/3040/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Java 静态分析框架 Tai-e 的简单使用
2023年09月21日2023年09月21日安全工具&安全开发
作者: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比较麻烦
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3040/
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK