Pwn2Own 2017: JSC::CachedCall 中的 UAF (WebKit) - phoenhex 团队
概述
我们在 Pwn2Own 2017 中使用的 WebKit 漏洞是 CVE-2017-2491 / ZDI-17-231,这是 JavaScriptCore 中 JSString 对象的 use-after-free 漏洞。通过触发该漏洞,我们可以在 JavaScript 回调中获得一个指向 JSString 对象的悬空指针。虽然初始场景看起来很难利用,但我们找到了一种通用技术,通过大规模堆喷(约 28 GiB)获得可靠的读写原语。得益于 macOS 的页面压缩机制,即使在只有 8 GB RAM 的 MacBook 上也能实现。
漏洞分析
当 String.prototype.replace
使用 RegExp 对象作为第一个参数调用时,WebKit 的 JavaScript 引擎 JavaScriptCore (JSC) 会调用以下本地函数:
|
|
在 [[ 0 ]] 处创建了一个 CachedCall 实例,稍后用于调用回调函数。在 Safari 10.0.3 使用的 WebKit 分支中,CachedCall 类如下:
|
|
可以看到,使用 WTF::Vector 来保存参数,这些是 jsSubstring
创建的对象唯一的引用。在 JavaScriptCore 中,所有由垃圾收集器管理生命周期的对象都继承自 JSCell。在标记清除算法的标记步骤中,会扫描多个位置以查找对 JSCell 的引用并递归标记。这些位置包括:
- 当前调用栈
- 全局 JavaScript 执行上下文,包括全局对象
- 特殊缓冲区,如 MarkedArgumentBuffer
- 可能还有其他一些
如果对 JSCell 的唯一引用位于不透明的堆缓冲区(如 WTF::Vector)中,垃圾收集器无法正确找到和标记它们,如果在主垃圾收集周期中发生,它们将被清除和释放。
在 String.prototype.replace
的情况下,主 GC 可能在 [[ 1 ]] 处分配新参数字符串时发生。此时,之前的参数(JSString 实例)将被收集和释放。当稍后在 [[ 2 ]] 处进行调用时,回调函数将接收到指向已释放 JSCell 的指针作为参数。
漏洞利用
通常,浏览器中的 use-after-free 漏洞可以通过在释放的位置分配新对象来转化为类型混淆。但在 JSC 的上下文中情况不同:它操作的是包含自身类型信息的 JSCell 对象。因此,如果释放的位置被不同的、新分配的 JSCell 占用,指向 JSCell 的悬空指针不能直接利用。
我们的方法是利用 JSCell 空闲列表指针类型混淆。当 JSCell 被收集和清除时,其前 8 字节被替换为指向同一堆块中下一个空闲单元的指针。其他字段保持不变。因此,如果我们尝试使用悬空指针而不先在释放的 JSString 对象上分配内容,就会导致崩溃。
通过喷洒大量内存(使用数组缓冲区),我们可以构造一个可用的 JSObject,使其索引类型变为 8。我们需要喷洒大约 7 次 4 GiB 的内存,因为我们需要一个在 0x800000000 到 0x8ffffffff 范围内的地址。得益于 macOS 的页面压缩机制,即使是在只有 8 GB RAM 的 MacBook 上也能实现,但在 Pwn2Own 使用的目标机器(2016 年 13.3 英寸 MacBook Pro,16GB RAM)上大约需要 50 秒。
索引类型 8 对应于 JSValues 的快速连续存储(ContiguousShape)。对此对象的索引访问将直接咨询对象的 butterfly,而不是执行完整的属性查找。通过写入假 JSObject 并从相应的 ArrayBuffer 读取,以及相反的操作,我们可以轻松实现 fakeobj 和 addrof 原语。
触发漏洞
虽然上面的方法看起来简单:
- 喷洒 28 GiB 内存以将堆推入 0x8xxxxxxxx 区域
- 触发漏洞
但第二步实际上并不容易。虽然在 Safari 10.0.3 中,简单的 PoC 代码几乎立即触发,但这只是因为此时堆很小,所以 GC 发生非常频繁。在有 28 GiB 映射内存的情况下,由于 JSC 分配器中的启发式算法,分配仍然触发 GC 的可能性非常小。
幸运的是,至少在用于 Safari 10.0.3 的 JSC 版本中,垃圾收集器是完全确定性的。我们只是尝试直到找到了一种堆喷洒、正则表达式和输入字符串的组合,可以在 0x8xxxxxxxx 区域可靠地触发漏洞。
从 fakeobj/addrof 到任意 R/W
我们使用以下方法从能够伪造 JSC 对象到实现任意读写:
- 伪造一个名为 fakearray 的 JSObject,索引类型为 8,结构 ID 为 0,在其内联属性中。使其 butterfly 指向一个名为 hax 的 Uint8Array。
- 创建一个额外的 Uint8Array 名为 hax2。
- 设置 fakearray[2] = hax2,从而将 hax 的支持缓冲区更改为 hax2 的地址。
- 对于读/写操作,将目标地址写入 hax 的偏移量 16 处,从而将 hax2 的支持缓冲区更改为目标地址。然后从 hax2 读/写。
在完全破坏的堆中生存
通过在释放的 JSString 上分配 JSCell,我们实际上破坏了 JSString 所在堆块的自由列表。这完全破坏了分配器,因此我们需要确保在利用过程中不进行大小为 24 或 32 的分配。虽然我们可以通过不创建任何对象来轻松避免手动分配,但调用 JavaScript 函数或执行超过 16 次迭代的循环会在内部触发某些 JIT 编译任务,这些任务会执行有问题大小的分配并立即崩溃 JSC。
我们可以修复损坏的自由列表并恢复合理的堆状态,但为了 Pwn2Own 的目的,我们决定采用非常丑陋但可靠的利用代码,避免任何循环、函数调用/定义或其他危险操作。在我们的第二阶段中,我们立即为 SIGSEGV、SIGBUS 和 SIGALRM 注册信号处理程序,使故障线程无限睡眠。这样,任何并发运行的线程都不能在我们的沙箱逃逸运行时崩溃进程。
完整的带注释的利用代码可以在文件 cachedcall-uaf.html
中找到。