【论文阅读】ARCUS: Symbolic Root Cause Analysis of Exploits in Production Systems

Yagemann C, Pruett M, Chung S P, et al. {ARCUS}: Symbolic Root Cause Analysis of Exploits in Production Systems[C]//30th USENIX Security Symposium (USENIX Security 21). 2021: 1989-2006.

一、问题与已有的解决方案

问题

存在很多监控程序的手段(比如 CFI,系统调用监视等)可以监控程序的异常。然而当监控系统发现程序异常时,往往已经距离崩溃的根因非常远了。这使得崩溃的根因分析非常困难。

已有的解决方案与缺陷

  • 典型的手段,例如分析 crashdump 文件;(不好用)

  • 具体的程序输入引发隐私问题,可能对开发者也没帮助(我理解为单个具体的输入可能不能有效展示漏洞的全貌)

二、本文创新方案与难点

创新方案

本文提出了一个崩溃根因分析方法,它不断考虑 “假设” (what if) 问题,以测试特定输入对漏洞状态可满足性的影响,从而定位程序崩溃的根因。基于这个理论,本文设计了系统 Arcus。

Arcus 会在监控系统(CFI,系统调用监控)发现一个程序的异常执行时触发,但不会关心它为什么被触发了。

举栗说明

Untitled

这段代码中存在栈溢出漏洞:name 仅有 256 字节大小,如果 hname 超过了 256 字节,且没有包含 ] 时,就会导致 name 溢出。

根据符号执行的思想,如果 hname 字符串被符号化,那么覆盖返回地址之后,就会导致程序计数器 PC 的内容被符号化,从而监测到栈溢出漏洞。

Arcus 的目的是:当发现了某个指针被符号化的时候(PC 也算是个指针),向上回推,找到是哪个状态让这个指针变成符号化的。(对应到这个例子就是第九行代码)

Arcus 通过分析控制流依赖,自动识别了 guardian basic block,即决定了什么时候退出相关循环的基本块(就是第八行的退出条件)。此时,Arcus 考虑假设条件 (what if) :如果循环提前退出,会发生什么。答案是:提前退出就不会有符号化的 PC 了。通过对比这两种结果对应的约束的不一致,就能找到问题所在(root cause)。

读到这里有以下几个问题:

  1. 如何用符号执行找到这个符号化 PC 的状态?

  2. 如何分析控制流约束,令循环提前返回?(我有猜想,但想知道作者的具体细节)

威胁模型

假设攻击仅来自用户态,硬件层面和操作系统内核层面是可信的。

Arcus 针对无源码、无符号的二进制程序分析。

三、框架概述

Untitled

Arcus 的工作流程是:Arcus 内核模块采用 Intel PT 对用户程序进行监控。当监控器发现异常时,监控数据被发送到分析环境,该环境负责重建符号变量信息,定位漏洞。

因此系统可以分为两个部分:

  • 记录部分:记录被监控程序执行的初始状态,并通过 intel PT 收集其执行过程中的控制流。

  • 分析部分:分析部分和记录部分可以分属不同的机器。采用符号执行来重建单一执行路径的可能的数据流,这使得系统能够辨别出存在漏洞的条件,并考虑“假设” (what if) 条件来自动识别导致漏洞的约束。

四、技术点描述

这里解释系统里的关键技术。

通过记录的路径实现符号执行

前面提到通过 intel PT 收集程序实际执行的控制流,那么如何实现符号执行?

Arcus 仅考虑某条特定路径下的所有数据流(这句话是华而不实的,符号执行本身就考虑了各种数据的情况,这句话等价于 Arcus 只分析单路径)。方法是:将所有的程序输入都替换为符号变量(例如命令行参数,环境变量,文件,sockets,标准输入/输出流等),且仅构建被追踪路径等约束。

文章 3.4 节更详细地介绍了 intel PT 记录路径和符号执行。根据文章描述,intel PT 只有在处理分支路径的指令才会产生跟踪数据包。这里提到了使用 intel PT 的一个问题:例如 scas/stos 指令本身就会循环,但是 intel PT 不会记录次数(因为在 PT 的视角下是单个指令,而不是分支)。如果符号执行恰好在这里传入符号变量,那么就会丢失信息。

“假设” (what if) 问题

仍然用刚才的那个例子解释:

Untitled

左边的 PT Trace 是 intel PT 记录的路径,猜测是以基本块为单位,第六行代码对应的基本块执行了 1 次,第八行代码对应的基本块执行了 312 次。

Arcus 首先把攻击者能控制的数据设置为符号变量。对应在这个例子中,hname 是来自命令行参数的,因此符号化的命令行参数传递给 hname,从而使 hname 符号化。随后开始符号执行,并严格按照 intel PT 记录的路径探索,即:在第六行执行分支,并在 for 循环中循环 312 次。执行到十四行时,PC 就被符号化了。

当符号执行找到了某个状态包含符号化指针时,接下来就该考虑“假设”问题了:Arcus 能够发现存在一个别的可能的路径,使得循环更早退出。循环退出的条件是:cp <= hname + 257 && hname[256] == ']'

“假设”循环更早退出会发生什么?通过添加一个约束实现这一点。具体的细节在下一节(分析模块)中解释。

分析模块

本文对漏洞分类,随后分类讨论。对每个类型的漏洞实现了一个插件模块来处理。

Stack & Heap Overflow 类型:仅针对控制流劫持类型(即符号化 PC),流程如下。

  1. 监测到符号化的 PC 对应的状态;

  2. 识别出是哪个基本块向 PC 写入了符号化变量;对已经执行过的基本块转化为 VEX IR,然后采用回退污点分析 (backwards tainting)。我的理解就是对 VEX IR 逆向执行。首先把 PC 数据标记为污点,然后逐语句逆向执行,例如 PUT(eip) = t3 ,由于 eip 有污点,因此执行该语句时污点传递到临时变量 t3。总之就是不断向上逆推,找到哪里写入了这个指针。(具体细节见论文 3.7 节,我还没有很仔细研究这个算法)。

  3. 找到控制这个写操作执行的基本块;从前面恢复出来的状态开始,采用顺序执行(我理解就是借助 angr),生成控制依赖图。如果这个漏洞状态有控制块 (guardians),则选择路径最短的那个。首先这里完全没有说清楚从哪个地方开始符号执行,也并没有解释清楚满足什么条件的 block 就是 guardians。也许后面会解释?使用 angr 符号执行,必须要有一个起始状态 (论文 3.5 节)。本文的做法是:内核模块监视程序的入口点,每当进程创建时保存一份快照,快照内容包括:环境变量、命令行参数和当前程序 brk 指针。所以我理解的是,符号执行会从被分析程序的入口点开始,沿着 PT 保存的 trace 执行。

  4. 测试额外的约束能否使程序偏离漏洞状态。“额外的约束”指的就是找到了 guardian 之后,使其执行发生偏离(执行另一条分支)而添加的约束。

如果让我自己来解决这个问题的话,对于第二步和第三步我暂时没有想到解决方案。符号执行应该没有提供表达式的溯源功能,所以应该只能通过作者自己做插桩捕获写入事件,自己记录写入关系?
至于第三步“找到控制它的基本块”,也许可以暴力地把写操作所在的基本块前面最近的分支作为“控制它的基本块”?只是这样也许太粗糙了?

其他类型的处理方法待续。

五、实验与总结

待续。但是直到目前为止我有以下疑问:

  1. guardian 是如何识别的?(什么样的 block 会被识别为 guardian?)

  2. 额外的约束是如何添加的?