前言
以前研究过使用Java-Parser和ASM框架自己搞底层分析的原始工作方式,现在学习一下底层使用tai-e工具,自己只需要实现上层分析规则的分析方式。
环境搭建
在官方Github里可以找到两种安装方式,一种是像参考文章一样,自行下载并编译整个tai-e项目,将自己的代码写入到tai-e里面去,这里采用的是第二种方法,使用maven将tai-e作为工具导入:
<dependencies>
<dependency>
<groupId>net.pascal-lab</groupId>
<artifactId>tai-e</artifactId>
<version>0.2.2</version>
</dependency>
</dependencies>
发现tai-e需要java17,从Oracle下载后发现我的IDEA 2020已经退版本了,不支持java17,只能又弄一个IDEA 2023,也算是跟上时代了。
然后引入依赖,通过Main.mian调用就能使用tai-e开始代码分析了。
加载配置
新建一个目录放两个配置文件options.yml和taint-config.yml,taint-config.yml配置由options.yml负责引入,taint-config文件里面的污点分析规则可以借用官方和参考文章的。然后在程序启动参数中加上options.yml:
--options-file=D:\Java1.8\Tai-e\src\main\resources\options.yml
不带参数启动时tai-e会在output目录下写入一个默认的options.yml配置文件,我们自己的options.yml配置文件可以从这里复制过来,改改就能用了。
测试用例是参考文章给出的java-sec-code:
git clone https://github.com/JoyChou93/java-sec-code.git
cd java-sec-code && mvn clean package
简单看一下代码,可以知道这是个简单的测试各种漏洞的项目,污染从控制器方法的输入参数流入到漏洞函数中。
改一下options.yml配置:
appClassPath: E:\Code\java-sec-code\target\classes
...
analyses:
pta: taint-config:taint-config.yml
...
运行发现报错:
Exception in thread "main" java.lang.RuntimeException: Failed to locate Java library.
Please clone submodule 'java-benchmarks' by command:
'git submodule update --init --recursive' (if you are running Tai-e)
or 'git clone https://github.com/pascal-lab/java-benchmarks' (if you are using Tai-e as a dependency),
then put it in Tai-e's working directory.
看来是maven引入的tai-e并不完整,缺少了某些工具或者依赖,根据提示补充一下:
git clone https://github.com/pascal-lab/Tai-e.git
cd Tai-e/ && git submodule update --init --recursive
看到目录下多了一个java-benchmarks目录,再回去看一下报错地点研究一下导入方式:
// check existence of JREs
File jreDir = new File(JREs);
if (!jreDir.exists()) {
throw new RuntimeException("""
Failed to locate Java library.
Please clone submodule 'java-benchmarks' by command:
'git submodule update --init --recursive' (if you are running Tai-e)
or 'git clone https://github.com/pascal-lab/java-benchmarks' (if you are using Tai-e as a dependency),
then put it in Tai-e's working directory.""");
}
JREs定义为:
protected static final String JREs = "java-benchmarks/JREs";
简单来说就是按路径加载jar,将整个目录拖到我们项目里面就行了。
然后又遇到了下一个问题:
Exception in thread "main" java.lang.RuntimeException: couldn't find class: org.springframework.boot.web.support.SpringBootServletInitializer (are your class path and class name given properly?)
看起来是待分析目录classes里面没有spring相关代码导致的,此时也有两种解法,第一种就是把spring等用到的依赖都加入到待分析目录中,但是由于我们的代码分析仅限于待分析项目本身,因此这样做会导致大量的无效分析。
第二种方法就是找一找配置项,或许可以找到无视ClassNotFound异常的配置,在Options类中找到–allow-phantom配置项看起来跟类加载有关:
description = "Allow Tai-e to process phantom references, i.e.," +
" the referenced classes that are not found in the class paths" +
" (default: ${DEFAULT-VALUE})",
简单调试一下,看到分析过程中加载类时,会根据该配置项决定是否只加载类的标识,否则则会加载body:
boolean onlySignatures
= sc.isPhantom() || (no_bodies_for_excluded && scene.isExcluded(sc) && !scene.isBasicClass(sc.getName()));
因此在options.yml中修改allowPhantom为true再运行,还有问题:
Exception in thread "main" pascal.taie.config.ConfigException: taint-config.yml is neither a file nor a directory
路径配置有问题,修改一下就行,运行成功,不过没有检测到污染路径。
代码分析
添加entryPoint
众所周知,一次污染分析由程序入口entryPoints、污染源sources、污染终点sinks和污染传播规则transfers组成。而分析不出污染路径的原因可以回到代码里面去找,按照官方所说,污点分析是从属于指针分析的一个插件:
In Tai-e, taint analysis is designed and implemented as a plugin of pointer analysis framework. To enable taint analysis, simply start pointer analysis with option taint-config
在tai-e源码中也可以找到污点分析类的定义,这是一个继承了Plugin接口的插件:
public class TaintAnalysis implements Plugin
来到指针分析类PointerAnalysis中,可以看到setPlugin函数根据配置项taint-config加载了污点分析插件:
if (options.getString("taint-config") != null) {
plugin.addPlugin(new TaintAnalysis());
}
此外还可以加载我们配置的插件:
addPlugins(plugin, (List<String>) options.get("plugins"));
除此之外还有一些默认加载的插件:
CompositePlugin plugin = new CompositePlugin();
...
plugin.addPlugin(
new AnalysisTimer(),
new EntryPointHandler(),
new ClassInitializer(),
new ThreadHandler(),
new NativeModeller(),
new ExceptionAnalysis()
);
...
solver.setPlugin(plugin);
可以看到跟污点分析强相关的插件EntryPointHandler:
@Override
public void onStart() {
// process program main method
JMethod main = World.get().getMainMethod();
if (main != null) {
solver.addEntryPoint(new EntryPoint(main,
new DeclaredParamProvider(main, solver.getHeapModel(), 1)));
}
// process implicit entries
if (solver.getOptions().getBoolean("implicit-entries")) {
for (JMethod entry : World.get().getImplicitEntries()) {
solver.addEntryPoint(new EntryPoint(entry, EmptyParamProvider.get()));
}
}
}
所有被集成到CompositePlugin里面的插件,都会在加载完插件的后续流程中被逐个调用onStart函数:
// PointerAnalysis.runAnalysis()
setPlugin(solver, options);
solver.solve();
// DefaultSolver
@Override
public void solve() {
initialize();
analyze();
}
private void initialize() {
...
plugin.onStart();
}
// CompositePlugin
@Override
public void onStart() {
allPlugins.forEach(Plugin::onStart);
}
显而易见,将项目中的main函数作为程序入口添加到了污点分析流程中,那么污点分析没有结果的原因也明白了,Spring项目的程序入口与一般程序不同,应该将控制器里面的方法作为程序入口来分析。
题外话,这里加载的入口点除了main函数,还有依据implicit-entries配置项加载的implicit entries,这是一些看起来与污点分析不怎么相关的函数:
protected static final List<String> implicitEntries = List.of(
"<java.lang.System: void initializeSystemClass()>",
"<java.lang.Thread: void <init>(java.lang.ThreadGroup,java.lang.Runnable)>",
"<java.lang.Thread: void <init>(java.lang.ThreadGroup,java.lang.String)>",
"<java.lang.Thread: void exit()>",
"<java.lang.ThreadGroup: void <init>()>",
"<java.lang.ThreadGroup: void <init>(java.lang.ThreadGroup,java.lang.String)>",
"<java.lang.ThreadGroup: void uncaughtException(java.lang.Thread,java.lang.Throwable)>",
"<java.lang.ClassLoader: void <init>()>",
"<java.lang.ClassLoader: java.lang.Class loadClassInternal(java.lang.String)>",
"<java.lang.ClassLoader: void checkPackageAccess(java.lang.Class,java.security.ProtectionDomain)>",
"<java.lang.ClassLoader: void addClass(java.lang.Class)>",
"<java.lang.ClassLoader: long findNative(java.lang.ClassLoader,java.lang.String)>",
"<java.security.PrivilegedActionException: void <init>(java.lang.Exception)>"
);
总而言之,参考TaintAnalysis的写法,我们可以自定义一个Plugin,在onStart函数中向污点分析里加入新的程序入口,然后通过配置将我们的Plugin添加到污点分析流程里面去。
有个问题是TaintAnalysis中有一个manager成员,其类型TaintAnalysis类不是一个public类,但是在规则编写中又有很重要的作用。主要有两种方法,或者修改包名,或者使用反射,这里我采用反射:
private Solver solver;
private Object manager;
@Override
public void setSolver(Solver solver) {
this.solver = solver;
initManager(solver);
}
private void initManager(Solver solver) {
try {
Constructor<?> ctr = Class.forName("pascal.taie.analysis.pta.plugin.taint.TaintManager").
getDeclaredConstructors()[0];
ctr.setAccessible(true);
this.manager = ctr.newInstance(solver.getHeapModel());
}catch (Exception e) {
// pass
}
}
然后在onStart函数中添加entryPoint,直接使用参考文章给出的代码就行,将所有注明了Mapping注解的函数都视为程序入口。
再次运行,发现还是没有结果,测试后发现问题在于taint-config中的call-site-mode配置,定位到SinkHandler函数,调试得知在该配置为false的情况下结果数量为0,此时进入这一段代码:
sinks.forEach(sink -> {
int i = sink.index();
result.getCallGraph()
.edgesInTo(sink.method())
...
});
经过调试,发现原因在于构造出的调用图中没有executeQuery的被调用边,原因不明。
尝试在Plugin的onNewCallEdge函数下断点调试,结果发现数量太多了根本看不过来,而且前面的都是Static函数。于是换个做法,在自己写的Plugin里面过滤好CallSite再调试:
@Override
public void onNewCallEdge(Edge<CSCallSite, CSMethod> edge) {
if (edge.getCallSite().toString().contains("jdbc_ps_vuln")) {
System.out.println(edge.getCallee().toString());
}
}
发现流程中确实没有出现executeQuery的被调用边,还需要再往上。
观察分析流程,看到待分析实体都放在workList队列中,然后找到该队列的CallEdgeEntry类型实体由DefaultSolver类的addCallEdge函数添加,可以看到该函数只有两处调用,一处是Static函数调用,另一处再往上找到processCall函数和analyze函数,发现还是没有结果。
观察了一下与jdbc_ps_vuln函数相关的调用边:
[]:<java.lang.Class: java.lang.Class forName(java.lang.String)>
[]:<java.sql.DriverManager: java.sql.Connection getConnection(java.lang.String,java.lang.String,java.lang.String)>
[]:<java.lang.String: java.lang.String format(java.lang.String,java.lang.Object[])>
[]:<java.lang.StringBuilder: void <init>()>
[]:<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>
[]:<java.lang.StringBuilder: java.lang.String toString()>
[]:<java.lang.StringBuilder: void <init>()>
[]:<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>
[]:<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>
[]:<java.lang.Throwable: java.lang.String toString()>
[]:<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>
[]:<java.lang.StringBuilder: java.lang.String toString()>
不是静态函数就是JDK自带的一些类函数,不知道底层字节码分析是怎么写的,以后再看。其他的函数调用没有形成边,而是被放到了reachableMethods里面,通过callSiteMode配置来访问:
if (callSiteMode) {
MultiMap<JMethod, Sink> sinkMap = sinks.stream()
.collect(MultiMapCollector.get(Sink::method, s -> s));
// scan all reachable call sites to search sink calls
result.getCallGraph()
.reachableMethods()
.filter(m -> !m.isAbstract())
.flatMap(m -> m.getIR().invokes(false))
.forEach(callSite -> {
JMethod callee = callSite.getMethodRef().resolveNullable();
if (callee == null) {
return;
}
for (Sink sink : sinkMap.get(callee)) {
int i = sink.index();
Var arg = InvokeUtils.getVar(callSite, i);
SinkPoint sinkPoint = new SinkPoint(callSite, i);
result.getPointsToSet(arg)
.stream()
.filter(manager::isTaint)
.map(manager::getSourcePoint)
.map(sourcePoint -> new TaintFlow(sourcePoint, sinkPoint))
.forEach(taintFlows::add);
}
});
}
使用Graphviz处理生成文件,就可以看到图形化的污染流。
添加sources
总结一下发现,MyPlugin中按照Mapping注解添加了大量的entryPoint,但还没有为这些方法的输入参数配置为对应的污染源source,在taint-config中手动写这么多sources过于繁琐,可以考虑用跟添加entryPoint类似的方式,在Plugin里设置sources。
因为我们要添加的sources为方法参数类型,所以可以参考SourceHandler中的handleParamSource函数:
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 -> {
int index = source.index();
Var param = ir.getParam(index);
SourcePoint sourcePoint = new ParamSourcePoint(method, index);
Type type = source.type();
Obj taint = manager.makeTaint(sourcePoint, type);
solver.addVarPointsTo(context, param, taint);
});
}
}
大抄特抄参考文章和SourceHandler:
@Override
public void onNewCSMethod(CSMethod csMethod) {
JMethod method = csMethod.getMethod();
boolean isMappingMethod = !method.getAnnotations()
.stream().filter(
annotation -> annotation.getType().matches("org.springframework.web.bind.annotation.\\w+Mapping")
).toList().isEmpty();
if(!isMappingMethod){
return;
}
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, i);
Obj taint = makeTaint(param, sourcePoint);
solver.addVarPointsTo(context, param, taint);
}
}
private Obj makeTaint(Var param, SourcePoint sourcePoint) {
Obj taint = null;
try {
Type type = param.getType();
Method makeTaint = manager.getClass().getDeclaredMethod("makeTaint",
SourcePoint.class, Type.class);
makeTaint.setAccessible(true);
taint = (Obj)makeTaint.invoke(manager, sourcePoint, type);
}catch (Exception e) {
// pass
}
return taint;
}
最后生成的图片打开直接卡烂,笑死。