Chrome零日漏洞(CVE-2019-5786)分析

 

1.介绍

3月1日,谷歌发布安全公告, 指出chrome浏览器的实现过程FileReader API存在一个use-after-free漏洞。根据谷歌威胁分析小组的报告,该漏洞已在野外利用,针对目标是32位的window7操作系统,漏洞利用主要分为两部分,一是在Renderer(浏览器渲染进程)进程中执行代码,二是用于完全接管破坏目标主机系统。本文是一篇技术文章,主要聚焦于该漏洞利用的第一个部分,即如何在Renderer进程中实现代码执行,并通过研究分析发现更多的技术信息。  在本文攥写时,谷歌漏洞报告尚未公开。缺省安装情况,Chrome会自动更新补丁,目前最新版本的Chrome已不受该漏洞影响。请确保您的Chrome处于安全状态,可通过chrome://version命令查询,查看Chrome版本是否已是72.0.3626.121或更高版本。

 

2. 信息收集

2.1 漏洞修复

大多数的Chrome代码库都基于Chromium开源项目。由于漏洞代码包含在开源代码中,因此我们可以更直接的查看更新补丁对FileReader API做了哪些修复动作。此外,谷歌分享了其修复版本的更新日志为我们的分析工作提供了更大的便利。

我们看到更新文件中只有一个与FileReader API相关,并带有以下消息内容:

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Picture7.png

该消息暗示对同一个底层ArrayBuffer多次引用是一件非常糟糕的事情。虽然目前尚不清楚段话意味着什么,下面的工作将致力于寻找隐藏于该条信息之下的真实细节。

首先,我们可以比较GitHub上新旧两个版本的差异。,看看其中到底发生了哪些变化。为了便于阅读,下面展示的是补丁前后两个版本差异的具体情况。

修复前老版本

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-20-at-3.28.57-PM.png

补丁后新版本

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-20-at-3.22.32-PM.png

这两个版本在GitHub上都可以找到。从图中我们可以看出,主要对ArrayBufferResult函数进行了修复。ArrayBufferResult函数在用户调用访问FileReader.result时用于响应并返回数据。

上图中,我们可以看到前后版本不同就在于对DOMArrayBuffer对象是如何处理的。在新版本中,增加了对数据尚未加载完成(finished_loading_标志)情况下对DOMArrayBuffer对象的处理。在老版本中,如果数据尚未加载完成的情况下,ArrayBufferResult函数直接调用DOMArrayBuffer::Create(raw_data_->ToArrayBuffer())函数并返回结果,在新版本中,则是调用DOMArrayBuffer::Create(ArrayBuffer::Create(raw_data_->Data(),raw_data_->ByteLength()))函数并返回结果。

我们来看补丁后的版本,因为该版本更容易理解。新版本中,DOMArrayBuffer::Create参数中包含了一个ArrayBuffer::Create函数调用。该函数包含两个参数,一个是指向数据的指针类型,一个是数据的长度(该函数在/third_party/blink/renderer/platform/wtf/typed_arrays/array_buffer.h文件中定义)。

函数定义如下图:

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-20-at-3.23.19-PM.png

函数主要创建一个新的ArrayBuffer对象,将其置于scoped_refptr<ArrayBuffer>中并将数据复制到其中。scoped_refptr在Chromium项目中用于处理引用计数,也就是跟踪一个对象被引用了多少次。当创建一个新的scoped_refptr实例,底层目标对象的引用次数会递增。当该对象退出时,该计数会递减。当引用数目为0的时候,该对象将被删除(好玩的是,当引用计数溢出后,Chrome将终止进程)。

老版本代码中则没有调用ArrayBuffer::Create,而是调用ArrayBufferBuilder::ToArrayBuffer函数并返回值。(该函数在third_party/blink/renderer/platform/wtf/typed_arrays/array_buffer_builder.cc中定义)

函数定义如下:

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-20-at-3.24.07-PM.png

我们可以看到,在这可能还存在另外一个问题。根据bytes_used_值,函数将返回buffer_自身,或者只是buffer_的一部分。(即一个较小空间的ArrayBuffer,其内包含一份数据副本)

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-20-at-3.24.12-PM.png

到目前为止,我们看到的所有修复的代码中,都是直接返回数据副本,而不是返回实际缓冲区地址。除非是在运行老版本代码,并且我们试图访问的缓冲区是处于“完全占用”的状态的情况下。

但是,在FileReaderLoader对象的实现过程,buffer_->ByteLength()获取的是预先分配的缓冲区大小,它对应于我们要加载的数据的大小(稍后将会相关)。

目前看来,在finished_loading标志被设置为true之前,且在数据已经完全加载之后,多次访问调用ArrayBufferBuilder::ToArrayBuffer()函数,将是利用该漏洞的唯一条件,这个时间点是漏洞触发的最佳交换时期。

为了总结代码检查这一部分,我们在看一下新老版本中都会调用的DOMArrayBuffer::Create函数,让我们感兴趣的是DOMArrayBuffer::Create(raw_data_->ToArrayBuffer())这个函数调用。该函数定义在third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h头文件中。

定义如下图:

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-20-at-3.24.57-PM.png

有趣的是,它调用了std::move,该函数具有所有权转换的语义。

例如,在以下代码中:

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-20-at-2.57.41-PM.png

std::move函数调用后,‘b’取得属于‘a’的所有权(‘b’现在包含的内容是“hello”),而‘a’现在处于某种未定义的状态(C++ 11规范以更精确的术语解释)。

当前情况下,有些事情令人感到困惑,具体请查看链接1链接2。ArrayBufferBuilder::ToArrayBuffer()返回的对象已经是一个scoped_refptr<ArrayBuffer>。我相信,当调用ToArrayBuffer()函数时,ArrayBuffer的引用计数将增加1,然后std::move函数又获取该引用对象实例的所有权(而非ArrayBufferBuilder拥有那个对象)。调用10次ToArrayBuffer()函数,引用计数将增加10,但所有的返回值将是有效的(与前面提到的‘a’,‘b’的例子不同,该例子中‘a’将导致无法预期的行为发生)。

如果在上面描述的最佳交换期间我们多次调用ToArrayBuffer()函数,这将会触发生成一个明显的use-after-free漏洞,ArrayBufferBuilder对象中的buffer_对象将被破坏。

2.2 FileReader API

我们也可以通过查看JavaScript中的API调用,看我们能否找到另一个方法找到我们所要寻找的漏洞触发的最佳交换时期。

Mozilla Web文档中,我们能获取所有的信息。其实,操作十分简单,我们可以在Blob对象或文件对象中调用诸如readAsXXX的函数,其后我们可以中断读取操作,此外,还有几个事件我们可以注册回调函数(比如onloadstart、onprogress,、onloadend …等)。

其中onprogress处理事件听起来是最有趣的一个,它是在数据正在加载并加载完成之前被调用。如果我们查看FileReader.cc源文件,我们可以看到该事件背后的逻辑,在收到数据时,每隔50毫秒(或更长)该事件触发一次。下面,让我们来看看在一个真实的系统中它时如何表现的…

 

3. web浏览器环境测试

3.1 准备工作

我们要做的第一件事是下载存在漏洞的代码版本。有一些非常有用的网络资源,在那里我们可以获取老的版本,而无需自己再去重新编译源码获得。

资源如下图所示。

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Picture1-2.png

值得注意的是,图中资源还包含了一个文件名称包含‘syms’字符串的zip文件,内含.pdb调试符号文件,你可以直接导入到各调试器和反汇编软件,有助于更易分析。

3.2 调试器附加

Chromium是一个复杂的、支持多进程通信的软件,对其调试比较困难。最有效的调试方法是正常启动Chromium,然后将调试器附加到你要进行漏洞测试的那个进程中。我们要调试的代码运行在renderer进程中,并且关注的函数是由chrome_child.dll导出的(这些信息通过反复实验发现,比如附加到Chrome每个进程,寻找感兴趣的函数名等)

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/olly1.png

如果要在x64dbg中导入调试符号,一个可行的方法是在Symbol栏,右键选中要导入调式符号的.dll或.exe,然后选择下载调试符号。如果没有正确设置调试符号下载服务器,可能会失败,但是它仍将在x64dbg的‘symbols’目录下创建相应的目录结构,你也可以在该目录下直接放置之前已下载的.pdb文件。

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Picture3-2.png

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Picture4-2.png

3.3 寻找漏洞触发点

现在,我们已经下载了一个尚未补丁的Chromium版本,并且已经知道如何用调试器去附加调试它。接下来编写一段JavaScript代码,来看看是否能够到达我们所关注的代码位置。

代码如下:

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-20-at-3.25.36-PM.png

为了总结这将发生的事情,上述代码中我们创建了一个Blog对象用于传递给FileReader。此外,我们还注册了progress事件的一个回调函数onProgress,并当该事件触发的时候,我们还试图多次访问FileReader返回值。在之前的文中,我们已经知道,数据需要被完全加载(这就是我们检查缓冲区大小的原因),并且如果我们使用同一个ArrayBuffer获得多次DOMArrayBuffer, JavaScript中他们看起来应该是多个分离对象(相等测试)。最后,为确认我们已经有两个指向同一个缓冲区的不同对象,我们创建了两个视图对象并修改数据,结果验证了如果修改一个对象,另一个对象也随之改变。

在此,我们没有预见到,发生了一个令人非常遗憾的问题:progress事件其实并不是经常性的会被调用,导致我们必须加载一个非常大的数组,用以强制进程花销一些时间并多次触发progress事件。也许,会存在比上述做法更好的技术方法(可能谷歌的漏洞报告中会揭露一个好方法)。但是,所有创建慢速加载的对象的尝试都失败了(比如使用Proxy,扩展Blob类等…)。或者我们可以把数据加载与一个Mojo管道绑定,因此使用MojoJS看起来是一个拥有更多控制的好方法,但是在实际的攻击场景中却似乎不切实际。有关该方法的示例,可查看链接

3.4 导致崩溃

到此,既然我们已清楚了如何进入存在漏洞的代码路径,那么我们又该如何来利用这个漏洞呢?这绝对是最难回答的问题,本段旨在分享找到该问题答案的过程。

我们已经看到底层的ArrayBuffer会被引用计数,所以如果只是通过从已获得的一些DOMArrayBuffer中进行垃圾内存搜集的方法,我们无法对其进行释放。使引用计数溢出,这听起来是一个非常有趣的想法。但是,如果我们通过手动修改引用计数值为接近其最大值(比如通过x64dbg),再来看看会发生什么…。好吧,进程崩溃了。最终,我们无法对这些ArrayBuffer做更多的事情;我们能修改它们的内容,却不能修改它们的大小,除非我们能手动释放它们…

如果对这些代码库不是很熟悉的话,最好的方法就是去查阅各种提及了use-after-free、ArrayBuffer等关键词的漏洞报告。去看看别人都做了什么,谈论了什么。必须假设在某个地方一定存在一个拥有底层内存的DOMArrayBuffer对象,这也是一个我们知道的并努力实现的假设。

经过一些网络搜索,我们发现了一些很有趣的评论,比如链接1链接2。这两个链接讨论了DOMArrayBuffer被外部化(externalized)、被转移(transferred)以及被阉割(neutered)的各种情况。对上述这些术语我们不是很清楚,但是从上下文结合来看,当上述情况发生时,内存的所属权就转移到了其他人身上。这听起来非常完美,因为我们希望底层缓冲区能被释放(就像我们急切的在寻找一个use-after-free漏洞一样)

WebAudio中存在的use-after-free漏洞向我们展示了如何让我们的ArrayBuffer发生“转移”,所以让我们试试吧!

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Screen-Shot-2019-03-20-at-3.26.03-PM.png

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Picture5-1.png

在调试器下可以看到:

https://securingtomorrow.mcafee.com/wp-content/uploads/2019/03/Picture6-1.png

图中我们可以看到,被解除的内存引用的地址保存在ECX中(我们看到EAX=0,这是因为我们正在该视图对象中寻找第一个选项)。该地址看起来有效,但是事实却并非如此。ECX指向数组缓冲区的原始数据(AAAAA…)的地址,但是由于其被释放,系统取消了保存它的页面映射,从而导致了内存访问冲突(我们试图访问一个未映射的内存地址)。因此,我们找到了一个一直在寻找的use-after-free漏洞。

 

4. 漏洞利用和下一步工作思考

4.1 漏洞利用

本文重点不是展示如何通过use-after-free漏洞获取完整代码执行权限(事实上,在本文发布的同时,Exodus已发布了一篇博客文章及一个可用的漏洞利用代码)。

根据我们触发该use-after-free漏洞的方法,我们最终获得了一个非常大的未分配的内存缓冲区。use-after-free漏洞通常利用方法是在释放区域之上分配一个新对象从而产生某种混淆。文中,我们释放了用于备份ArrayBuffer对象数据的原始内存。很好的是,我们可以读取/写入一个大内存区域。但是,这种方法也存在问题,就是由于该内存区域确实太大了,导致没有一个对象可以符合该要求。假如我们有一个小一点的内存区域,我们就能创建大量特定大小的对象,希望可能有一个对象正好在该区域被分配。不过这个更难,我们需要去等待,一直要到堆为不相关的对象回收内存。在64位windows 10系统,由于内存随机分配方式以及随机地址可用等机制导致很难做到这一点。在32的windows 7系统中,由于地址空间要小的多,因此相对而言堆的分配更具确定性。分配10K左右的对象可能足以让我们控制一些地址空间中的元数据。

另外有趣的是,由于要取消引用一个未映射的内存区域。如果上面提到的10K分配方法无法在我们控制的那个区域中分配至少一个对象,那么我们未免就太不走运了。我们将由于内存访问冲突而导致进程崩溃。此外,也有一些方法可以使得这一步更稳定,比如有些文章描述的利用 iframe的方法,以及Javascript对象元数据被破坏的示例

4.2 下一步工作

即使攻击者在浏览器渲染进程中获得代码执行权限,但是依然受到沙箱机制的限制。本漏洞发现的野外利用,攻击者使用了另外一个零日漏洞用于躲避沙箱机制。最近360CoreSec发布了一篇描述该野外漏洞利用的技术文章

5. 结论

通过研究提交的漏洞修复方式,查找提示和类似的修复,我们有可能恢复漏洞利用路径。再一次的,我们可以看到,在windows新近操作系统版本中引入的安全防护机制使得攻击者的日子愈发困难,从防守方的角度来说,我们应该对此表示庆祝。此外,谷歌在漏洞修补策略方面非常高效、积极,其大部分用户群的Chrome浏览器已经及时更新到最新版本。

 

Links

[1] https://chromereleases.googleblog.com/2019/03/stable-channel-update-for-desktop.html
[2] https://security.googleblog.com/2019/03/disclosing-vulnerabilities-to-protect.html
[2b] https://bugs.chromium.org/p/chromium/issues/detail?id=936448
[3] https://chromium.googlesource.com/chromium/src/+log/72.0.3626.119..72.0.3626.121?pretty=fuller
[3b] https://github.com/chromium/chromium/commit/ba9748e78ec7e9c0d594e7edf7b2c07ea2a90449
[4a] https://github.com/chromium/chromium/blob/17cc212565230c962c1f5d036bab27fe800909f9/third_party/blink/renderer/core/fileapi/file_reader_loader.cc
[4b] https://github.com/chromium/chromium/blob/75ab588a6055a19d23564ef27532349797ad454d/third_party/blink/renderer/core/fileapi/file_reader_loader.cc
[5] https://www.chromium.org/developers/smart-pointer-guidelines
[6a] https://chromium.googlesource.com/chromium/src/+/lkgr/styleguide/c++/c++.md#object-ownership-and-calling-conventions
[6b] https://www.chromium.org/rvalue-references
[7] https://developer.mozilla.org/en-US/docs/Web/API/FileReader
[8] https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/612439/
[9] https://www.exploit-db.com/exploits/46475
[10a] https://bugs.chromium.org/p/v8/issues/detail?id=2802
[10b] https://bugs.chromium.org/p/chromium/issues/detail?id=761801
[11] https://blog.exodusintel.com/2019/01/22/exploiting-the-magellan-bug-on-64-bit-chrome-desktop/
[12] https://halbecaf.com/2017/05/24/exploiting-a-v8-oob-write/
[13] http://blogs.360.cn/post/RootCause_CVE-2019-0808_EN.html

(完)