前言

以前研究过使用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;
}

最后生成的图片打开直接卡烂,笑死。


参考

官方Github

使用太阿(Tai-e)进行静态代码安全分析(spring-boot篇一)


Web Java 代码分析

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Java Lambda
破壳漏洞调试学习