前言 以前研究过使用Java-Parser和ASM框架自己搞底层分析的原始工作方式,现在学习一下底层使用tai-e工具,自己只需要实现上层分析规则的分析方式。
环境搭建 在官方Github 里可以找到两种安装方式,一种是像参考文章一样,自行下载并编译整个tai-e项目,将自己的代码写入到tai-e里面去,这里采用的是第二种方法,使用maven将tai-e作为工具导入:
1 2 3 4 5 6 7 <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:
1 --options -file =D:\Java1.8 \Tai-e\src\main\resources\options .yml
不带参数启动时tai-e会在output目录下写入一个默认的options.yml配置文件,我们自己的options.yml配置文件可以从这里复制过来,改改就能用了。
测试用例是参考文章给出的java-sec-code:
1 2 git clone https://github.com/JoyChou93/java-sec-code.git cd java-sec-code && mvn clean package
简单看一下代码,可以知道这是个简单的测试各种漏洞的项目,污染从控制器方法的输入参数流入到漏洞函数中。
改一下options.yml配置:
1 2 3 4 5 appClassPath: E:\Code\java-sec-code\target\classes ... analyses: pta: taint-config:taint-config.yml ...
运行发现报错:
1 2 3 4 5 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并不完整,缺少了某些工具或者依赖,根据提示补充一下:
1 2 git clone https://github.com/pascal-lab/Tai-e.git cd Tai-e/ && git submodule update --init --recursive
看到目录下多了一个java-benchmarks目录,再回去看一下报错地点研究一下导入方式:
1 2 3 4 5 6 7 8 9 10 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定义为:
1 protected static final String JREs = "java-benchmarks/JREs" ;
简单来说就是按路径加载jar,将整个目录拖到我们项目里面就行了。
然后又遇到了下一个问题:
1 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配置项看起来跟类加载有关:
1 2 3 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:
1 2 boolean onlySignatures = sc.isPhantom() || (no_bodies_for_excluded && scene.isExcluded(sc) && !scene.isBasicClass(sc.getName()));
因此在options.yml中修改allowPhantom为true再运行,还有问题:
1 Exception in thread "main" pascal.taie .config .ConfigException : taint-config.yml is neither a file nor a directory
路径配置有问题,修改一下就行,运行成功,不过没有检测到污染路径。
代码分析 添加entryPoint 众所周知,一次污染分析由程序入口entryPoints、污染源sources、污染终点sinks和污染传播规则transfers组成。而分析不出污染路径的原因可以回到代码里面去找,按照官方所说,污点分析是从属于指针分析的一个插件:
1 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接口的插件:
1 public class TaintAnalysis implements Plugin
来到指针分析类PointerAnalysis中,可以看到setPlugin函数根据配置项taint-config加载了污点分析插件:
1 2 3 if (options.getString("taint-config" ) != null ) { plugin.addPlugin(new TaintAnalysis ()); }
此外还可以加载我们配置的插件:
1 addPlugins(plugin, (List<String>) options.get("plugins" ));
除此之外还有一些默认加载的插件:
1 2 3 4 5 6 7 8 9 10 11 12 CompositePlugin plugin = new CompositePlugin (); ... plugin.addPlugin( new AnalysisTimer (), new EntryPointHandler (), new ClassInitializer (), new ThreadHandler (), new NativeModeller (), new ExceptionAnalysis () ); ... solver.setPlugin(plugin);
可以看到跟污点分析强相关的插件EntryPointHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void onStart () { JMethod main = World.get().getMainMethod(); if (main != null ) { solver.addEntryPoint(new EntryPoint (main, new DeclaredParamProvider (main, solver.getHeapModel(), 1 ))); } if (solver.getOptions().getBoolean("implicit-entries" )) { for (JMethod entry : World.get().getImplicitEntries()) { solver.addEntryPoint(new EntryPoint (entry, EmptyParamProvider.get())); } } }
所有被集成到CompositePlugin里面的插件,都会在加载完插件的后续流程中被逐个调用onStart函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 setPlugin(solver, options); solver.solve();@Override public void solve () { initialize(); analyze(); }private void initialize () { ... plugin.onStart(); }@Override public void onStart () { allPlugins.forEach(Plugin::onStart); }
显而易见,将项目中的main函数作为程序入口添加到了污点分析流程中,那么污点分析没有结果的原因也明白了,Spring项目的程序入口与一般程序不同,应该将控制器里面的方法作为程序入口来分析。
题外话,这里加载的入口点除了main函数,还有依据implicit-entries配置项加载的implicit entries,这是一些看起来与污点分析不怎么相关的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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类,但是在规则编写中又有很重要的作用。主要有两种方法,或者修改包名,或者使用反射,这里我采用反射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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) { } }
然后在onStart函数中添加entryPoint,直接使用参考文章给出的代码就行,将所有注明了Mapping注解的函数都视为程序入口。
再次运行,发现还是没有结果,测试后发现问题在于taint-config中的call-site-mode配置,定位到SinkHandler函数,调试得知在该配置为false的情况下结果数量为0,此时进入这一段代码:
1 2 3 4 5 6 sinks.forEach(sink -> { int i = sink.index(); result.getCallGraph() .edgesInTo(sink.method()) ... });
经过调试,发现原因在于构造出的调用图中没有executeQuery的被调用边,原因不明。
尝试在Plugin的onNewCallEdge函数下断点调试,结果发现数量太多了根本看不过来,而且前面的都是Static函数。于是换个做法,在自己写的Plugin里面过滤好CallSite再调试:
1 2 3 4 5 6 @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函数相关的调用边:
1 2 3 4 5 6 7 8 9 10 11 12 [] :<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配置来访问:
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 if (callSiteMode) { MultiMap<JMethod, Sink> sinkMap = sinks.stream() .collect(MultiMapCollector.get(Sink::method, s -> s)); 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函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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:
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 33 @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) { } return taint; }
最后生成的图片打开直接卡烂,笑死。
参考 官方Github
使用太阿(Tai-e)进行静态代码安全分析(spring-boot篇一)