查看原文
其他

看我如何利用教科书级别的释放后使用漏洞(CVE-2020-6449)

Man Yue Mo 代码卫士 2022-05-23
 聚焦源代码安全,网罗国内外最新资讯!
编译:奇安信代码卫士团队

GitHub 研究员 Man Yue Mo详述了自己在2020年3月份上报的一个严重的释放后使用漏洞(CVE-2020-6449)。如他所说,这是一个“教科书级别”的漏洞利用。如下是他发布的文章原文,我们一睹为快。


2020年3月,我报告了 Chrome WebAudio 模块中的释放后使用 (UAF) 漏洞 (CVE-2020-6449)。该漏洞存在于非 GC (non-garbage-collected,非垃圾回收) 对象中,由 PartitionAlloc 内存分配器分配。在 blink (包含 WebAudio) 中,堆对象根据类型的不同获得不同内存分配器的分配。例如,多数GC 对象由 Oilpan 分配,而非GC对象由 PartitionAlloc 分配(ArrayBuffer 和 String 的后备存储器除外,它们由 PartitionAlloc 分配,即使对象本身是 GC)。

2019年,Chrome 中两个“已遭在野利用的” 漏洞就是 blink 中 PartitionAlloc 对象中的 UAF 漏洞。其中一个是 CVE-2019-5786,由谷歌 TAG 团队发现;另外一个是 CVE-2019-13720即 WizardOpium,由卡巴斯基实验室的研究员发现并报告。

利用这类漏洞的困难之处很多在于 PartitionAlloc,它会区分原语容器(String、Vectors、ArrayBuffers 等)和普通的“可执行的”对象:原语容器被分配在 Buffer 和 ArrayBuffer 分区,而普通的对象则被分配在 Fast 分区。首先,这种区分使得我们难以通过利用 ArrayBuffer 或 Buffer 分区中的内存损坏漏洞劫持控制流。其次,它使我们难以通过利用 Fast 分区中的内存损坏漏洞以 ArrayBuffer 或 Buffer 分区中收集的数据创建虚假数据,因为写原语可能非常有限。在之前提过的两个漏洞中,UAF 存在于 ArrayBuffer 分区中,挑战在于跳出 ArrayBuffer 分区。这两个漏洞的利用详情已披露。

而现在我们面临的是相反的挑战:利用 Fast 分区中的 UAF 实现 RCE。虽然在本文中我会通过 CVE-2020-6449 举例说明,但技术本身是通用的,它适用于其它 Fast 分区对象产生 UAF 的情况。


漏洞简介


该漏洞的确切详情和根因分析不再赘诉,这里重点概述该漏洞的情况,并假设读者已经了解我之前提到的 WebAudio 详情(https://securitylab.github.com/research/garbage-collection-uaf-chrome_gc)。

该漏洞出现在 DeferredTaskHandler::BreakConnections 函数中:

void DeferredTaskHandler::BreakConnections() { ... wtf_size_t size = finished_source_handlers_.size(); if (size > 0) { for (auto* finished : finished_source_handlers_) { // Break connection first and then remove from the list because that can // cause the handler to be deleted. finished->BreakConnectionWithLock(); active_source_handlers_.erase(finished); } finished_source_handlers_.clear(); }}


在一般情况下,active_source_handlers_ 负责维持 finished_source_handlers_ 中的原始指针处于活动状态。由于在使用后,才会从 active_source_handlers_ 中删除 finished,因此在一般情况下没有问题。然而,如果我们在不清除 finished_source_handlers_ 的情况下就设法清空 active_source_handlers_,那么 finished 可能就会在上述函数中释放,从而造成 UAF。


漏洞触发


要理解如何触发该 bug,我们需要先来仔细分析下我在报告 CVE-2020-6449 之前报告的另外一个相关漏洞。

void DeferredTaskHandler::BreakConnections() { ... wtf_size_t size = finished_source_handlers_.size(); if (size > 0) { for (auto* finished : finished_source_handlers_) { active_source_handlers_.erase(finished); //<-- finished is now free'd finished->BreakConnectionWithLock(); //<-- UaF } finished_source_handlers_.clear(); }}


从如上代码片段中可知,由于在使用 active_source_handlers_ 之前就清除了 finished,因此要触发该 bug,我们需要确保 active_source_handlers_ 是当前使 finished_source_handlers_ 保持活跃的唯一句柄。然而要触发 CVE-2020-6449,我们还需要提前清空 active_source_handlers_

如在issue 中所述(https://bugs.chromium.org/p/chromium/issues/detail?id=1057593),active_source_handlers_ finished_source_handlers_ 与 AudioScheduleSourceNode 有关。后者具有两个子类:ConstantSourceNode 和 OscillatorNode。当 AudioScheduleSourceNode 的 start 方法被调用时,其 AudioHandler 也会被添加到 active_source_handlers_中。因此,例如在工单 1057593 的 PoC 中,如下代码将 src 的 AudioHandler 添加到 active_source_handlers_ 中。

let src = audioCtx.createConstantSource(); src.start();


此时,srcactive_source_handlers_ 节点负责确保 AudioHandler 是活跃状态。在 src 调用 stop 时,在0点时会安排一个 stop 事件。该事件实际上会由函数 HandleStoppableSourceNode 处理,该函数将事件添加到 finished_source_handlers_ 中。此时,我们可以暂停音频并通过处理 promise 来运行一些 javascript:

audioCtx.suspend((3 * 128)/3072.0).then(()=>{ gc(); audioCtx.resume(); });

现在,由于停止了所创建的 constantSource(ConstantSourceNode 的 javascript 句柄),因此使其保持活跃的句柄不存在,而调用 GC 会收集并毁掉它。之后,active_source_handlers_ 负责维护 finished_source_handlers_ 保持活跃。audioCtx.resume 的调用将触及 BreakConnection 并触发 UAF。

现在我们理解了如何触发 CVE-2020-6449 的简单版本后,所需要做的就是多走一步,在触及 BreakConnections 之前清空 active_source_handlers_。这里的主要区别在于清空 active_source_handlers_ 的唯一途径是破坏 javascript 执行上下文,也就是将所有一切置于 iframe 中,之后将其破坏。新的 PoC 中的主要文件位于 iframe 中,并在音频图表中增加了多个新节点,例如,onLoad 方法目前创建了2000 个 PannerNode。

function onLoad() { startStop().then((audioCtx) => { audioCtx.suspend((3 * 128)/3072.0).then(()=>{ //======new====== let dest = audioCtx.createConstantSource(); dest.start(); for (let i = 1; i < 2000; i++) { dest = dest.connect(audioCtx.createPanner()); } dest.connect(audioCtx.destination); //=====new end====== .... }); audioCtx.startRendering(); });}


startStop 方法还增加了一个 AudioWorkletNode。创建 AudioWorkletNode 和 PannerNode 的主要原因是控制音频线程即 BreakConnection 运行的地方和主线程 active_source_handlers_ 被清空的地方之间的时间。它们的目的是引发音频线程中的延迟,以便在触发漏洞前有足够的时间清空 active_source_handlers_


漏洞利用


为利用该漏洞,我需要首先用大小类似的对象替换它并寄望于 BreakConnectionWithLock 会在被替换对象的上下文中做一些“有用的”事情。大体来讲,我需要如下方面:

1、当在被替换对象的上下文中调用 BreakConnectionWithLock 时,无需要能够推断出堆指针的位置以及函数或 vtable 等的地址。后者将使我能够找到一些已加载库的地址,从而找到其中rop gadgets 的地址。例如,如果一个指针的地址由被替换对象中的 BreakConnectionWithLock 写入一个整数字段,那么我就能通过读取该整数字段来获取一些指针地址。

2、知道这些地址后,我希望能够创建一个对象,伪造其 vtable 并将其指向一个 rop gadget 的地址(从1开始)。之后当我调用这个虚拟函数时,它会执行我所选择的 gadget。这是可以实现的,例如,通过 ArrayBuffer 或类似的数据结构创建一个虚假的对象,以便伪造带有数组条目的 vtable。

所以,我们先来看下 BreakConnectionWithLock 实际上做的事情:

void AudioHandler::BreakConnectionWithLock() { deferred_task_handler_->AssertGraphOwner(); //<---- No effect in release build connection_ref_count_--;
#if DEBUG_AUDIONODE_REFERENCES fprintf(stderr, "[%16p]: %16p: %2d: AudioHandler::BreakConnectionWitLock %3d [%3d] " "@%.15g\n", Context(), this, GetNodeType(), connection_ref_count_, node_count_[GetNodeType()], Context()->currentTime());#endif
if (!connection_ref_count_) DisableOutputsIfNecessary(); //<--- calls virtual function}

运气不错。第一行仅仅为一个调试 build 编译,因此我得以避免一个指针引用,它本可引发难以避免的崩溃。第二行也不错。它减少了计数器,因此我得到了不太可能崩溃的有限的写原语。之后,它检查 connection_ref_count_ 并选择性地调用了 DisableOutputslfNecessary,后者最终将进行虚拟函数调用。目前,我想要避免调用虚拟函数的路径,因为在不了解堆布局的情况下,它可能最终会导致崩溃。

总结一下,我们现在拥有:

1、一个 UAF,释放和使用之间的时间可轻易控制

2、一个受限的写原语,它将释放对象的特定偏移量处的字段减一。

3、调用虚拟函数的可能性。

之后你会发现,第一点和第二点正是我利用该 bug 所需的条件。


1对象替换


UAF 利用的第一步通常是用大小相似的不同对象替换被释放的对象,引发类型混淆,之后尝试从中引发信息泄漏。之后我们就可再次触发该漏洞并用另外一个虚假对象替换被释放的对象。我们可从另外一个虚假对象处伪造一个 vtable 并将其指向某些 rop gadgets 等。

如之前所述,用于分配这些对象的内存分配器是 PartitionAlloc。从exploit 开发的角度来看,PartitionAlloc 的最重要方面是:

1、它是一个存储桶分配器,负责为每个存储桶维护已释放和已分配对象的列表。对象被释放后,它就会称为其存储桶中的释放列表的头,而之前的头称为下一个空闲chunk。存储桶中大小相同的被分配对象占据这个最新被释放对象的位置。在容器中,所有chunk都连续分配。

2、 它具有四个不同的分区,将多数数据容器(ArrayBuffer、Vector、String 等后备存储桶)与“普通“对象分开。

目前,我们只需要关注第1点。之后我会详细讨论第2点。

这里被释放的对象是 AudioScheduledSourceHandler 的子类,或者是 ConstantSourceHandler 或者是 OscillatorHandler。Linux 中 80.0.3978.137版本(漏洞存在的最后一个版本)中这些对象的大小分别是240和312,它和 bin 的大小 (225-240) 以及 (289-320) 是相对应的。通过 CodeQL,可以很容易地找到这些 bin 中的类型:

from Type twhere (t.getSize() <= 240) and (t.getSize() > 225)select t


遍历 bin 中的多种类型后,类 BiquadDSPKernel 看起来更有希望。我们可以使用函数 AudioContext::createBiquadFilter() 通过 javascript 创建该类,而我们可以在 BreakConnectionWithLock 中减少的 ConstantSourceHandler 中的字段 connection_ref_count_ 和其字段 biquad_ biquad_.a1_.allocation 一致,后者是 AudioDoubleArray 中的一个指针字段。

然而,allocation_ 仅用于创建 aligned_data_,之后不会再用。这意味着如果我们要用 BiquadDSPKernel 替换被释放的 ConstantSourceHandler,那么被 BreakConnectionWithLock 修改过的 biquad_.a1_.allocation_ 的值不会被使用。因此,我们进入了僵局 …… 但或许有转机?


2一步一步损坏释放列表


上面提到, 当我们将allocation_ 的值修改为 connection_ref_count_ 后,就不会使用它的值,但实际上并非如此,因为当 AudioArray 被破坏后,它会被释放。

意思就是,当 AudioArray 被释放时,allocation_ 成为释放列表的头。然而,随着我们将它的值减一,当前该指针将和之前的内存chunk会重叠,而它可能还在使用状态。虽然chunk之间存在一个字节的重叠可能用处不是那么大,但如果我们能反复触发该漏洞并将 biquad_.a1_.allocation_ 置于相同的位置,那么我们就能反复减小该指针并在两个chunk之间创建一个足够大的重叠,引发另外的类型混淆。实际上只需要触发该漏洞并每次用 BiquadDSPKernel 替换它就会实现这个目的。由于 PartionAlloc 将再次复用这些相同的chunk(这里的 allocate_ 位于大小为 8*128=1024的bin中,不会经常使用),我们每次都会最终修改相同的 allocation_ 指针。因此例如,如果我想要将 allocation_ 指针的值减小 n,那么我需要做的就是修改 remove 函数:

function remove() { let frame = document.getElementById("ifrm"); frame.parentNode.removeChild(frame);
if (counter < n) { //Trigger bug to move chunk backwards let biquad = audioCtx.createBiquadFilter(); counter++; delete biquad; sleep(700); createIframe(); }}


这里的 sleep 仅为了确保对象获得 GC。在实际的 exploit 中,我必须触发62次,它才能获得几分钟的运行时间。

截至目前,我已设法在大小为1024的bin 中损坏了该释放列表,触发对象之间的重叠。现在我将用它制造信息泄漏,从而获取 libchrome 和堆指针的地址和,从而能够在一个已知的地址创建一些受控数据。


3构建信息泄露


为了构建信息泄漏,我通过 HRTFPanner 类(大小为1152,和 AudioArray 的字段 allocate_ 位于同样的 bin中)。在理想的情况下,需要将 HRTFPanner 分配到受损的 allocation_ 指针的位置,以便该 HRTFPanner 的开头和损坏此前chunk的另一个对象的末尾重叠,如下所示:


当我分配 HRTFPanner 时,其 vtable和共享指针字段 database_loader_ 将会映射到损坏此前chunk的对象末尾,因此如果我能找到一个字段且它的字段可在 javascript 中轻易读取以损坏该chunk,那么就会很方便。遍历大小不同的对象后,仍然未找到之前 chunk 的候选。

此外,如上所述,PartitionAlloc 将数据容器和一般对象的分配区分开来。虽然对象如 HRTFPanner 等被分配在 Fast 分区,但数据容器被分配在缓冲区分区或数组缓冲区分区,因此我无法仅仅分配一个大小动态变化的对象如 javascript 中的 ArrayBuffer,使其和 HRTFPanner 重叠并读取vtable 等。

回顾下我是如何走到这一步的。我首先使用 AudioArray 的 allocation_ 字段损坏释放列表,它是位于fast 分区中的一个对象,大小易控且其内容可能也可从 javascript 中读取。虽然它似乎是占据“之前chunk”的好的候选,但也存在问题。AudioArray 仅在内部用作存储临时音频数据的缓冲区且并不会直接和 javascript 交互。更糟糕的是,几乎在所有的用例中,它被用作一个临时缓冲区,可在从 javascript 读取数据时将数据覆写。因此,即使我可以创建一个 AudioArray 并通过重叠的 HRTFPanner 对象以 vtable 和堆指针覆写其缓冲区,这些数据也很可能在我读取之前被覆写。

不过只是几乎所有时候。有一个位于 AudioDelayDSPKernel::Process 中的用例,AudioFloatArray 字段 buffer_ 可能不会在返回用户之前完全被覆写,使得我可以创建一个特别构造的 DelayNode,一旦释放列表遭损坏,则其 buffer_ 会被 HRTFPanner 对象会被覆写,之后从音频输出中可检索到:

//Create a DelayDSPKernel whose buffer_ has the right size, which will be used to leak data. delay_leak = audioCtx.createDelay(0.0908); //3/3072 = 1./1024, need to divide by power of 2 to avoid rounding error when converting to double delay_leak.delayTime.value = 3 * 0.0009765625;

通过这个延迟节点渲染音频表使我之后可以读取输出中的 HRTFPanner 对象 database_loader_ 字段的 vtable 和堆指针。

一旦我拥有这些信息,剩下的就相对容易了。我只需使用和 ConstantSourceHandler 位于同一个 bin 中的另外一个 AudioArray 创建一个虚假的对象(vtable 指向 rop gadget),之后再次以任意参数调用任意函数触发该 bug 即可。

另外一种容易的方法是仅破坏 delay_leak 并分配另外一个 AudioArray,覆写 HRTFPanner 的 vtable,之后使用其虚拟结构器运行代码。这实际上正是我最后做的事情。这样,我甚至不需要原始 UAF bug 就能调用任何虚拟函数。

最后,我使用的是如下的 gadget:

//mov rax,QWORD PTR [rdi + 0x20]; <-- function call //mov rsi,QWORD PTR [rdi + 0x98]; <-- arg0 //mov rdx,QWORD PTR [rdi + 0xa0]; <-- arg1 //add rdi, 0x28 <--- arg2


它大概位于该符号的地址(它只是含有三个参数的其中一个回调):

base::internal::Invoker<base::internal::BindState<void (*)(blink::KURL const&, base::WaitableEvent*, std::__1::unique_ptr<blink::WebGraphicsContext3DProvider, std::__1::default_delete<blink::WebGraphicsContext3DProvider> >*), blink::KURL, WTF::CrossThreadUnretainedWrapper<base::WaitableEvent>, WTF::CrossThreadUnretainedWrapper<std::__1::unique_ptr<blink::WebGraphicsContext3DProvider, std::__1::default_delete<blink::WebGraphicsContext3DProvider> > > >, void ()>::RunOnce(base::internal::BindStateBase*)

Libchrome 中存在很多这类回调,它基本上可使gadget 能够通过任意参数调用任意函数,尽管找出类型正确的参数比较麻烦。

通过这个 gadget 调用 OS::SetPermissions 使我能够将受控数据的页面权限覆写到 rwx,从而能够在渲染器中运行任意 shell 代码。


结论


本文详述了利用 CVE-2020-6449 的过程以及在 exploit 过程中涉及的一些常用策略和技术。我们也看到内存分配器中的缓解措施如何使这个 bug 更难以被 exploit。最后,我仅通过减少被替代对象(非常有限的原语)中的指针字段就可以利用该 bug。这表明即使有限的(或许并没有这么罕见)原语也可造成巨大破坏。幸运的是,Chrome 拥有沙箱架构,要攻陷 Chrome 必须利用另外一个沙箱逃逸 bug。这说明从多个层面(沙箱+bug快速修复+bug查找)确保安全对于提升 Chrome 安全性起着多么重要的作用。

完整的 exploit 可见如下链接,其中含有一些设置备注,我已经在 Ubuntu 上测试了 80.0.3987.137 的符号版本:

https://github.com/github/securitylab/tree/main/SecurityExploits/Chrome/blink/CVE-2020-6449



推荐阅读
【缺陷周话】第 11期 :释放后使用
Apache Shiro权限绕过漏洞 (CVE-2020-11989) 挖掘分析和复现
CVE-2020-15999:Chrome FreeType 字体库堆溢出原理分析奇安信代码安全实验室帮助微软修复两个“重要”漏洞,获官方致谢




原文链接

https://securitylab.github.com/research/CVE-2020-6449-exploit-chrome-uaf

题图:Pixabay License


本文由奇安信代码卫士编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。


奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的

产品线。

    觉得不错,就点个 “在看” 或 "” 吧~




您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存