大佬可否加个V交流下,邮箱springkill@163.com
author: keanu、lcark
概述
Tai-e是由南京大学的老师(李樾、谭添)开发的针对java的静态程序分析框架,支持包括指针分析、数据流分析、污点分析在内的诸多静态程序分析。由于Tai-e并非专门用来做静态代码安全分析,所以并非开箱即用,在实际安全分析中使用有许多问题。准备通过大致如下多篇文章,逐渐将Tai-e改造为开箱即用的静态代码安全分析框架。
- 分析SpringBoot应用,支持控制翻转、依赖注入、面向切面编程等特性
- 分析基于Mybatis框架的sql注入漏洞
- 优化输出结果
- Java Api提取,更偏向于支持企业Api安全。
- Pointer Analysis And Taint Analysis Flow Analysis
由于spring-boot实现了控制反转与面向切面编程的设计思想,使得程序并非顺序执行,因此很难通过程序入口来顺序分析所有代码。本篇文章旨在从0开始,利用Tai-e来分析spring-boot程序,解决控制反转的问题。
从0开始配置tai-e并加载java-sec-code
下载tai-e代码
git clone https://github.com/pascal-lab/Tai-e.git
git submodule update --init --recursive(this repo contains the Java libraries used by the analysis; it is large and may take a while to clone)
IDEA配置tai-e
其实就是按照官方文档进行配置IDEA
在打开tai-e程序后设置
File->Project Structure
编译java-sec-code
下载java-sec-code源码
git clone https://github.com/JoyChou93/java-sec-code.git
进入该下载路径执行
mvn clean package -X
看到Build success 代表编译成功
用idea看是target路径
配置tai-e加载java-sec-code
首先创建common目录和options和taint-config文件。作为我们自己的配置
options文件内容,注意需要修改appClassPath也就是我们刚才编译的java-sec-code的target。
optionsFile: null
printHelp: false
classPath: []
appClassPath:
- /Users/Documents/codeqljava/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的结果。
taint-flow 结果分析
主要的问题是tai-e的taint-analysis是基于pointer-anysis的。而pointer-analysis的分析是基于worklist,这个worklist如果是java se程序初始化就是main函数,可以通过main函数进行分析,进行函数调用处理,从而reach到需要分析的函数,这个tai-e可以支持。由于我们的是springboot程序包含依赖注入和控制反转等。所以从springboot的入口tai-e没办法分析到我们的 controller函数。所以pointer-anlaysis的结果也就是空的,导致taint-anlaysis结果也是空的。
经过分析发现tai-e分析SpringBoot项目存在2个问题。
第一个为缺少entrypoint,因为JavaWeb项目从入口点进行pointerAnalysis分析没办法reach到我们需要分析的controller 方法。
第二个问题为缺少source,我们暂时没有办法通过yml将所有Mapping注解的parameters加入 sources,我们上边yml仅加入了一个source。
需要解决以上两个问题,才能进行基础的分析。
关键源码分析及实现
PointerAnaysis分析及入口点添加
由于tai-e的插件是集成在PointerAnalysis,包括污点分析也是,也就是说PointerAnalysis就是这些分析的核心。所以在写如何编写插件前我们先大概分析一下PointerAnlaysis的执行流程。
PointerAnaysis 插件执行流程
DefalutSolver
实现指针分析以及插件的调用,并返回指针分析的结果(core)
Plugin
即一个插件,可以通过编写插件的方式,在进行指针分析的过程中执行我们想要的操作(污点分析也是类似的操作)。
我们先看PointerAnalysis
的runAnalysis
方法,这个其实不算是入口点,analyze
才是,但是analyze
仅根据配置然后调用runAnalysis
,所以我们直接分析这个就好。
创建DefaultSolver对象并没有特别的操作对于插件,接下来看一下setPlugin方法
分析DefaultSolver的solve
方法
会首先调用 initialize
方法
● 根据初始化调用图、指针流向图 、worklist(可以理解为指针分析的工作集)、reachableMethods(也就是分析能到达的方法)、initializedClasses(可以理解为内置的类,会进行指针分析,不需要我们添加)、stmtProcessor(这个类是指针分析处理语句的核心,根据访问者模式处理对行的语句(new、load、invoke、array等))
● 然后会执行插件的onStart()方法
analyze
方法
- 通过消费worklist中的Entry,对Entry进行指针分析的处理包括,然后调用Plugin的
onNewPointsToSet
方法(本篇文章用不到该方法,该方法是处理field和array source会用到) - 在进行worklist后调用Plugin.Finish()的方法
插件方法执行流程
loadPlugin(pointerAnalysis加载插件)->setSolver->onStart->指针分析以及一些plugin方法(由于不影响本篇顺序故没有写)->onFinish
从上边我们可以理解编写Plugin的流程 - 编写插件实现plugin的方法,选择我们需要的方法比如 onStart 、onFinish
- 在PointerAnalysis加入编写的插件。
分析EntryPointerHandler
通过对PointerAnalysis执行流程的分析应该能大概了解 Plugin的执行顺序了。
看一下Plugin的interface ,有一系列plugin的方法,对应我们上边分析的时序。
看一下tai-e自带的EntryPointHandler
它就是在onStart处判断加入口点。
添加EntryPoint
这样加入我们自己入口点的方法也有了,在onStart的时候把所有 有Mapping注解的方法都加入入口点,这样pointeranalysis就能分析到我们的source点了,taintanalysis也会有结果。
下边的代码就是获取所有的class 然后获取class的method 看注解是不是Mapping 是的话就加入entrypoint
public class AddControllerEntryPointHandler implements Plugin {
private Solver solver;
@Override
public void setSolver(Solver solver) {
this.solver=solver;
}
@Override
public void onStart() {
//add all hsd mapping annotation methods to entrypoint
List<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转换为svg
dot -Tsvg -o taint-flow-graph.svg taint-flow-graph.dot
由于上边我们只加入了一个sources 和sql的sink点,所以只能检测出来一个taintflow
SourceHandler 分析及springboot source 添加
污点分析的核心包括如下三个集成到指针分析的核心插件:
SourceHandler
添加代表污点的特殊堆抽象加入到符合source点规则的指针的指向集中
TransferHandler
将符合污点传播规则的边加入指针分析
SinkHandler
收集污点流,输出source->sink的污点流的集合
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点由调用点生成,若callsite语句调用了特定方法,那么返回值就是source点。
paramater sources
对于特定方法,例如入口方法,没有特定的调用点。但其形参依然可能是source点。在spring-boot,通过注解来声明请求的处理入口,没有明确调用controller方法,在这种情况下,paramater sources非常有用。
field sources
对于字段类型的指针,其依然可能是source。
流程分析
从配置中获取source点的配置
当指针分析中产生新的语句或新的调用边时,若语句符合source的规则,则代表指针即为source点
当指针分析产生新的方法时,会处理其中的语句,也会在这个地方处理paramater sources
若我们想添加自定义的source点,便可以在handleParamSource handleFieldSource handleCallSource中添加。
添加source
我们可以看到上边的一条传播路径,但是sink点为executeQuery的漏洞不止一个,因为我们的source仅加入了一个,但是一条一条加source也不太现实,没办法实现通用型。我们可以根据加entrypoint点的地方同样加入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。
结果展示
由于为了降低耦合性,结果展示我们选择了另一种方法进行添加sources点。
更改的相关代码以及配置文件已经上传至github,见https://github.com/lcark/Tai-e-demo/tree/main/spring-boot-1
具体食用方法如下:
下载代码,并移动至spring-boot-1目录下
git clone https://github.com/lcark/Tai-e-demo
cd Tai-e-demo/spring-boot-1
git submodule update --init
将SpringBootHandler.java移动至Tai-e源码的src/main/java/pascal/taie/analysis/pta/plugin/taint/目录下,并重新编译打包
使用如下命令运行tai-e便可以成功获取到扫描结果
java -cp ~/Downloads/Tai-e/build/tai-e-all-0.5.1-SNAPSHOT.jar pascal.taie.Main --options-file=options.yml
如下图所示,总共扫出24个漏洞
对应DOT图片段如下
参考
[0]. https://tai-e.pascal-lab.net/en/lectures.html
[1]. https://github.com/pascal-lab/Tai-e
[2]. https://tai-e.pascal-lab.net/docs/current/reference/en/index.html