Pwn2Own 2017: JSC::CachedCall中的UAF漏洞 (WebKit)
2017年5月4日
作者:niklasb, saelo
作为简要介绍,我们是Samuel Groß(又名saelo)和Niklas Baumstark,都是卡尔斯鲁厄理工学院的学生,在决定组队参加今年的Pwn2Own之前,我们已经一起玩CTF有一段时间了。今天我们将讨论Safari 10.0.3中的一个use-after-free漏洞,该漏洞可用于在浏览器渲染进程中实现远程代码执行。本文是我们计划撰写的一系列漏洞和利用分析文章的一部分,这些漏洞和利用用于攻破Safari并在最新的MacBook Pro上提升到root权限。
两个WebKit漏洞的悲伤故事
我们在Pwn2Own上的演示有点不寻常,因为我们使用了一个1-day漏洞来获取Safari渲染器内的RCE。这是由于一些不幸的时间安排:
大约在二月初,saelo在CachedCall类中发现了一个漏洞,当我们第一次查看时,它似乎几乎无法利用。然后我们决定在大约两周后尝试它,并最终得到了一个可用的利用。在那个时候,saelo在推特上发布了一个简单PoC的SHA-256,即文件poc-cachedcall-uaf.js。仅仅7小时后(!),一名苹果员工就关于此问题开了一个bug报告,粉碎了我们用这个漏洞构建完整0-day利用链的希望。我们不认为开发人员将其识别为安全问题,因为该漏洞没有隐藏在跟踪器中。后来我们得知,一个内部fuzzer触发了该漏洞,迫使他们推送修复。
距离比赛大约还有一个月,我们决定专注于即将在Safari 10.1中引入的新WebKit代码。我们成功在那里发现并利用了一个漏洞,但再次有点不幸,因为苹果决定在比赛前不发布Safari 10.1(作为macOS 10.12.4的一部分)。我们也报告了这第二个漏洞,并将在修复后撰写分析文章,因为它影响当前的Safari 10.1。
概述
我们在Pwn2Own使用的WebKit漏洞是CVE-2017-2491 / ZDI-17-231,这是JavaScriptCore中JSString对象的use-after-free。通过触发它,我们可以在JavaScript回调中获得一个指向JSString对象的悬空指针。起初,特定场景似乎非常难以利用,但我们发现了一种相当通用的技术,仍然可以从中获得可靠的读/写原语,尽管它需要非常大的(约28 GiB)堆喷。由于macOS中的页面压缩机制,即使在只有8 GB RAM的MacBook上也是可能的。
以下文章结构如下:
- 漏洞
- 利用
- 触发漏洞
- 从fakeobj/addrof到任意R/W
- 在完全损坏的堆中生存
完整的带注释的利用可以在文件cachedcall-uaf.html中找到。
漏洞
当使用RegExp对象作为第一个参数调用String.prototype.replace时,会在WebKit的JavaScript引擎JavaScriptCore(JSC)中调用以下原生函数:
|
|
在[[ 0 ]],创建了一个CachedCall实例,稍后用于调用回调函数。
在用于Safari 10.0.3的WebKit分支中,CachedCall类如下所示:
|
|
如我们所见,使用WTF::Vector来保存参数,在上述算法期间,这些是jsSubstring创建的对象的唯一引用。
在JavaScriptCore中,所有由垃圾收集器管理生命周期的对象都继承自JSCell。在标记清除算法的标记步骤中,会扫描多个位置以查找对JSCells的引用并递归标记。这些包括:
- 当前调用栈
- 全局JavaScript执行上下文,包括所谓的全局对象
- 特殊缓冲区,如MarkedArgumentBuffer
- 可能还有其他一些
所有无法通过从这些位置遍历指针到达的对象都有资格在GC算法的清除步骤中被释放。这意味着如果对JSCell的唯一引用位于不透明的堆缓冲区(如WTF::Vector)中,垃圾收集器无法正确找到和标记它们,如果发生主要的垃圾收集周期,它们将被清除和释放。
在String.prototype.replace的情况下,主要的GC可以在[[ 1 ]]处分配新的参数字符串时发生。在这种情况下,先前的参数(JSString实例)将被收集和释放。当稍后在[[ 2 ]]处执行调用时,回调函数将接收到指向已释放JSCells的指针作为参数。注意,GC需要在回调之前发生,否则调用栈上会有对子字符串对象的引用。
文件poc-cachedcall-uaf.js演示了这个问题,并在Safari 10.0.3中工作:在脚本结束时,i_am_free是一个指向已释放JSString的指针。除了检查其类型外,对其做任何操作都可能导致浏览器崩溃,因为其JSCell头已被自由列表指针覆盖(稍后详述)。相关源代码如下:
|
|
该漏洞通过在CachedCall类中用MarkedArgumentBuffer替换Vector来修复。
利用
通常,浏览器中的use-after-free漏洞可以通过在释放的位置分配新对象来转化为类型混淆。然而,在JSC的上下文中情况不同:它操作于包含自己类型信息的JSCell对象。因此,如果释放的位置被不同的、新分配的JSCell占据,指向JSCell的悬空指针不能直接利用。如果释放的对象保持其他垃圾收集对象存活,或者如果悬空指针可以通过错位指向另一个JSCell的内部,利用可能仍然可能。前一种情况从著名的Pegasus利用中已知,其中使用了一个已释放(但仍然完整)的JSArray,在其支持缓冲区已被释放和替换之后。
这两种情况在这里都不存在:由jsSubstring创建的JSString对象与调用replace()的原始JSString共享其内容,后者继续存活。此外,JSString对象分配在一个堆块中,其中只分配其他大致相同大小(24或32字节)的JSCells,并且所有分配都将对齐到32字节。
我们在这里应用常见技术的唯一想法是释放包含竞技场的整个堆块,并在其位置分配负责不同类型的竞技场。我们从未进一步调查这一点。相反,我们最终使用了一种不同的方法,这种方法非常通用,并且在Pwn2Own中效果很好。
JSCell自由列表指针类型混淆
当JSCell被收集和清除时,其前8个字节被替换为指向同一堆块中下一个自由单元的指针。其他字段保持不变。因此,如果我们尝试使用悬空指针而不首先在释放的JSString对象上分配某些东西,我们会崩溃。
使用的JSCell的前8个字节包含以下字段:
|
|
此时,Safari非常弱的堆地址布局随机化派上了用场:在macOS 10.12.3上,Safari中的堆地址从大约0x110000000到0x120000000开始并向上增长。如果此范围内的指针与JSCell头重叠,自由列表指针的低32位将与结构ID重叠,位32-39将成为索引类型,而其他三个字段为零。
我们可以通过喷洒多个千兆字节的内存(使用数组缓冲区)来构造一个可用的JSObject,使得索引类型变为8。我们需要喷洒大约7次4 GiB的内存,因为我们需要一个在0x800000000到0x8ffffffff范围内的地址。由于macOS的页面压缩机制,喷洒这么多内存是很容易可能的,但在Pwn2Own使用的目标机器上(2016年13.3英寸MacBook Pro,16GB RAM)大约需要50秒。
索引类型8对应于JSValues的快速连续存储(ContiguousShape)。对此对象的索引访问将直接咨询对象的butterfly,而不是执行完整的属性查找。butterfly的工作原理在saelo的phrack论文的第1.2节中有描述。butterfly指针是JSObject的第二个四字,恰好与释放的JSString实例的旧字符串长度和类型标志(都是32位整数)重叠。在我们的利用中,这些将始终产生指针0x200000001,这方便地指向我们的堆喷内部。以下图形说明了JSString和JSObject之间的重叠,发生在JSCell头被形式为0x8xxxxxxxx的堆指针覆盖之后:
原始JSString:
|
|
在头被指针0x8xxxxxxxx覆盖后,我们得到一个JSObject:
|
|
此时,我们访问了一个具有快速路径索引的假JSObject,其butterfly与作为我们堆喷一部分的ArrayBuffer重叠。这可以很容易地转化为fakeobj和addrof原语,如上面链接的phrack论文第4节所述,通过写入假JSObject并从相应的ArrayBuffer读取,以及反之亦然。从那里我们照常进行:我们构造一个任意读/写原语,用我们自己的shellcode覆盖JavaScript函数的JIT代码,并调用该函数。
一些操作仍然访问我们假JSObject的结构,因此我们需要有一个有效的结构ID。结构通过使用结构ID作为索引的表查找检索。该表包含指向Structure实例的指针。由于我们无法控制结构ID本身(它与自由列表指针重叠),查找将访问表之外的内存和我们的喷洒区域。因此,我们必须在堆喷中创建假结构表条目。自由列表指针将是16字节对齐的,这是JSC分配器的粒度。此外,当通过结构表访问结构实例时,索引乘以8(指针大小)。因此,我们只需要在喷洒中每128字节有一个结构指针。在我们的利用中,我们将所有假表条目设置为固定指针0x150000008,并在喷洒数据的每个页面的开头创建一个假结构实例。
触发漏洞
上述方法起初看起来很简单:
- 喷洒28 GiB内存以将堆推入0x8xxxxxxxx区域。
- 触发漏洞。
然而,第二步实际上并不那么容易。虽然上面的简单PoC代码在Safari 10.0.3中几乎立即触发,但这只是因为堆在那个时候非常小,所以GC发生得非常频繁。有了28 GiB的映射内存,分配仍然触发GC由于JSC分配器中的启发式方法是非常不可能的。
然而,好的一点是,至少在用于Safari 10.0.3的JSC版本中,垃圾收集器是完全确定性的,尽管WebKit HEAD中的新并发垃圾收集器不再是。所以我们只是玩弄直到我们找到了堆喷、正则表达式和输入字符串的组合,可靠地在0x8xxxxxxxx区域触发漏洞。在我们实际的利用中,我们只喷洒14 GiB的数组缓冲区,然后开始循环调用String.prototype.replace。恰好这可靠地导致我们需要的情况,其中释放的JSString的索引类型最终被覆盖为8。
我们有一些想法如何使其在WebKit HEAD中工作,但然后苹果修复了漏洞,所以我们停止了工作。然而,这意味着我们的利用在堆设置方面非常脆弱。如果我们改变利用的分配模式太多,漏洞不再在正确的时间触发,利用失败。
从fakeobj/addrof到任意R/W
saelo的phrack论文使用以下方法从能够伪造JSC对象到任意读/写:
- 喷洒大量不同的Float64Array结构,以便我们可以可靠地猜测其中一个结构ID。
- 在JSObject的内联属性中伪造一个Float64Array fakearray,其结构ID为猜测的。其数据指针指向一个称为hax的Uint8Array。
- 对于读或写,设置fakearray[2] = <目标地址>,然后从/向hax读/写。
设置如下:
|
|
我们首先尝试在我们的Pwn2Own利用中使用相同的方法,但步骤1搞乱了我们的堆布局太多。为了简单起见,我们再次使用我们新学到的技巧:
- 在另一个对象的内联属性中伪造一个称为fakearray的JSObject,索引类型为8,结构ID为0。使其butterfly指向一个称为hax的Uint8Array。
- 创建一个额外的Uint8Array称为hax2。
- 设置fakearray[2] = hax2,从而将hax的支持缓冲区更改为hax2的地址。
- 对于读/写,将目标地址写入hax的偏移16,从而将hax2的支持缓冲区更改为目标。然后从/向hax2读/写。
这是因为在Safari 10.0.3中总是存在ID为0的结构。这里的设置如下:
|
|