导语:前言 Google安全团队Project Zero曾多次揭露微软产品的安全漏洞,有几次更在微软修补前即公布,惹来微软的不满。双方曾为此隔空交战,微软在10月18号也揭露了Chrome浏览器漏洞,并指责其沙箱安全性不足,修补政策无法有效防止黑客攻击。

timg.jpg

前言

Google安全团队Project Zero曾多次揭露微软产品的安全漏洞,有几次更在微软修补前即公布,惹来微软的不满。双方曾为此隔空交战,微软在10月18号也揭露了Chrome浏览器漏洞,并指责其沙箱安全性不足,修补政策无法有效防止黑客攻击。

下面就让我具体分析一下此次事件,看看微软说的有没有道理。让我先从Chrome开始,看看Chrome是如何阻止一个RCE漏洞的,然后进一步探讨是否有强大的沙箱模式足以使浏览器达到最安全的状态。

通过取证分析,我发现了Chrome所包含的以下漏洞:

1.CVE-2017-5121漏洞表明在Chrome中有RCE漏洞;

2.Chrome缺乏预防RCE漏洞的技术,这意味着从发现漏洞到使用漏洞的时间会很短;

3.在Chrome沙箱内进行的几次安全检查表明,RCE能够绕过同源策略(SOP),让攻击者访问受害者的在线服务(如电子邮件、文档和银行会话)并保存登录凭据;

4.由于Chrome的漏洞处理流程是先向用户公开披露漏洞细节,所以很可能使得漏洞在修复之前被滥用。

发现并利用一个RCE漏洞

要发现漏洞,就要首先找到一个异常现象。通常,我们都会找到内存漏洞,如缓冲区溢出或UAF漏洞来来实现此目的。对浏览器的攻击方法多种多样,包括V8 JavaScript引擎编译、Blink DOM引擎和pdfium PDF渲染器等,其中V8编译是本文的重点。

本文所使用的CVE-2017-5121漏洞就是微软使用基于Azure的漏洞检查工具ExprGen发现的。ExprGen是通过监测Chrome浏览器所使用的V8 JavaScript引擎,从而发现的CVE-2017-5121。

译者注:Google在2014将其V8JavaScript 解析引擎移到Github地址是:https://github.com/v8/v8V8是Google发布的开源 JavaScript引擎,采用C++ 编写,在Google的Chrome浏览器中被使用。V8 引擎可以独立运行,也可以用来嵌入到C++应用程序中执行。

识别漏洞

与手动审查代码相比,基于Azure的自动化模糊检测的缺点就是不能很快地分析出漏洞发生的原因。

由于模糊检测技术经常产生大量复杂的代码片段,所以在使用ExprGen检测时,首先要将测试样本的代码进行精简,只留下一些能够识别的代码。

1.png

当使用D8运行此代码时,V8的独立可执行版本由git标签6.1.534.32构建,我们会遇到崩溃:

这段代码的任务就是创建一个奇怪的结构化对象,然后设置一些字段。虽然从理论上讲,这不应该引发任何奇怪的行为,但实际上却出现了一些运行结果。当使用D8运行这段代码运行时,发现V8的独立可执行版本是由git标签6.1.534.32构建,此时会出现一次崩溃(如下图)。

译者注:如果你想了解如何使用D8分析javascript被V8引擎优化,请点此链接

2.png

由上图可以看出,崩溃发生的地址是0x000002d168004f14,因此我可以断定该崩溃是不会发生在静态模块中的。因此,它必须是由V8的just-in-time(JIT)编译器动态生成的。还能看到,崩溃发生是因为rax寄存器是零。

乍一看,这看起来像空引用异常(null dereference bug),如果是这样的话就没有什么可讲的了,因为这些异常通常不是可利用的,因为目前的操作系统会禁止零虚拟地址(zero virtual address)的映射。

3.png

我可以从上图的代码中发现两个有用的线索:

首先,可以注意到崩溃发生在一个函数调用之前,该函数看起来像一个JavaScript函数调度器存根,这主要是由于v8::internal::Builtin_FunctionPrototypeToString的地址被加载到该调用之前的一个寄存器中。看看位于0x000002d167e84500的函数代码,我发现地址0x000002d167e8455f确实包含一个调用rbx指令,这似乎证实了我的怀疑。

调用Builtin_FunctionPrototypeToString很有趣,因为这是Object.toString方法的实现。这似乎表明崩溃发生在func0 Javascript函数的JIT编译器中。

其次,在崩溃时寄存器rax中包含的零值是从内存中加载的。这个过程看起来像是原本应该被加载的值被传递给作为参数的toString函数调用,并且是从[rdi + 0x18]加载的,以下就是内存加载的部分代码。

4.png

以上代码并没有包含什么有价值的信息,因为大多数值都是指针。但是,分析指针的来源却很有用,因为它可以帮助我弄清楚为什么这个值一开始就为零。使用WinDbg最新公布的“时间旅程调试(TTD)” 功能,我可以在该位置上放置一个内存写入断点(baw 8 0000025e ' a6845dd0),然后在函数的起始处放置一个执行断点,最后重新反过来进行跟踪(g -)。

有趣的是,放置的内存写入断点并不会被触发,这意味着这个内存数据不会在这个函数中被初始化,或者至少在使用之前不会被初始化。但如果我对测试样本进行一些处理,例如把o.b.bc.bca.bcab = 0;替换成o.b.bc.bca.bcab = 0xbadc0de;,然后就可以发现崩溃值源自于内存区域的变化:

5.png

由上图可以看到, 0xbadc0de常数值最终出现在内存区域中。尽管这并没有证明什么,但却似乎表明,这个内存区域被jit编译的函数使用过,用来存储局部变量,因为开始处的代码看起来像是前面被传递给Object.toString作为参数的加载值。

结合TTD确认该内存槽未被该功能初始化的事实,可能的解释是JIT编译器无法发出将初始化表示用于访问o.b.ba.bab字段的对象成员的指针代码。

为了证实我的推断,我可以用-trace-turbo和-trace-turbo-graph参数在D8中运行测试样本。这样,D8就会输出有关TurboFan,V8的JIT编译器如何构建和优化相关代码的信息。Javascript 引擎 V8采用的是新的引擎:TurboFan 和 Ignition,其中 Turbofan 是新的优化编译器,而 Ignition 则是新的解释器。

TurboFan的工作原理是将各个阶段的优化信息都转化成对象映射图,如下图所示。

6.png

可以看出优化器将func0嵌入到无限循环中,然后拉出第一个循环进行迭代。这些信息有助于了解这些功能块之间如何相互关联。然而,用映射图进行表示的缺点就是无法表示对应于加载函数调用参数的节点,以及局部变量的初始化。

幸运的是,我们可以使用turbolizer来显示出这些内容。关注第二个Object.toString调用,我可以看到参数的来源,以及它被分配和初始化的位置:

7.png

在优化流程的阶段,代码看起来完全合理:

1.分配存储块以存储局部对象o.b.ba(节点235),并且其字段baa和bab被初始化;

2.分配存储块以存储局部对象o.b(节点259),并且其字段都被初始化,其中ba专门用对前一个o.b.ba分配的引用来初始化;

3.分配存储块以存储局部对象o(节点303),并且其字段都被初始化;

4.局部对象o的字段b被具有对象o.b(节点185)的引用覆盖;

5.局部对象字段o.b.ba.bab被加载(节点199,209和212);

6.调用Object.toString方法,将o.b.ba.bab作为第一个参数传递。

不过,优化过程中编译的代码不应该显示未初始化的局部变量行为,所以我可以假设这chrome漏洞发生的根本原因。话虽如此,这只是我的一个假设。查看分别被加载到o.b.ba和o.b.ba.bab的节点209和212,它们被用作函数调用参数,如下图,我可以在偏移量+ 24和+ 32处看到被反汇编过的崩溃代码。

8.png

0x17和0x1f分别为23和31,考虑到V8标签的值可以区分实际对象与内联整数(SMI),可以进行这样的推测:如果一个表示JavaScript变量的值具有最低有效位设置,则被视为指向对象的指针,否则就是SMI。因此,在取消引用(dereferencing)之前,可以使用V8代码进行优化,减去一个JavaScript对象的偏移量。

不过到目前我还是没有找出chrome漏洞的原因,所以我会继续进行优化以找到原因。之后我进行了逃逸分析(Escape Analysis) ,分析试图如下所示。

9.png

可以看出,有两个显著的不同:

1.代码不需要从o加载到o.b,被优化后直接引用o.b,我想可能是因为该字段的值从未更改过;

2.代码不再初始化o.b.ba,从上图中可以看出,turbolizer输出了节点264,这意味着它不再是实时的,因此不会被嵌入到最终的代码中。

此时,查看所有的活动节点似乎都确认这个字段不会再被初始化了。不过为了保险起见,我还是在这个带有–no-turbo-escape标志的测试样本上运行d8,以跳过此优化阶段。结果我发现d8不再崩溃,所以可以确定这就是漏洞的真正原因。其实, Google已在v8 6.1中完全禁用了逃逸分析阶段,只有在v8 6.2中才会发生逃逸分析。

现在我已经掌握了该漏洞的所有信息,不过为了深度验证此漏洞,我还要想办法利用它,看看它是不是一个攻击力很大的漏洞,不过这完全取决于我控制未初始化的内存插槽的能力。 

获取泄漏信息

此时,利用漏洞的最简单的方法是对样本进行简单地处理。例如,可以从未初始化的指针中更改正在加载的字段类型,看看会出现什么反应:

10.png

其结果是,该字段现在直接被加载为一个FLOAT 数据类型,而不是一个对象指针或SMI:

11.png

同样,我可以尝试在对象中添加更多字段:

12.png

运行以上的代码段,可以得到以下崩溃:

13.png

这很有趣,因为它看起来像是将字段添加到经过对象修改的偏移量中,其中该偏移量的位置位于被加载的对象字段中。如果你感兴趣,可以做个数学运算,你会看到(0x67 – 0x1f)/ 8 = 9,这恰好是我从o.b.ba.bab中添加的字段数量。这同样适用于从rbx加载的新偏移量。

经过对测试样本进行更多的操作后,我可以确认,即使这些字段没有被初始化,我还是能对未初始化的指针加载的偏移量进行控制。现在,就让我检测一下,看看是否可以将任意的数据放到这个内存区域中发生反应。使用0xbadc0de进行的早期测试似乎肯定了我的检测,不过每个测试样本的每次运行所检测出的偏移量都不一样。通常情况下,我会利用喷洒数值(spray value)的方式来解决这些问题。其原理是这样的:如果我不能准确地将目标锁定到给定的位置,那我就可以让目标范围扩大。在实践中,我经常使用内嵌数组(inline array)来扩大数值范围。

14.png

发生的故障转储(crash dump)如下所示:

15.png

可以看出,崩溃与以前基本相同,但如果我查看来自内存的未初始化数据,你会发现如下的情况。

browser-security-sandbox-16.png

在r11的偏移位置处,我可以看到一大块由任意脚本控制的值。将这个发现与之前的偏移量相结合进行分析,我可以得出更靠谱的结论。

browser-security-sandbox-17.png

由上图可以看出,我取消了任意地址的float值:

browser-security-sandbox-18.png

当然,这是非常强大的,因为它会立即导致任意读取原语。但不幸的是,没有初始信息泄漏的任意读取原语在实践中并不是很有用的,因为我需要知道要读取哪些地址才能使用它。

变量v可以替换成任何我想要的东西,比如,我可以用一个对象替换它,然后读取它的内部字段。再比如,我可以使用自定义的回调来替换对Object.toString()的调用,并将V替换为DataView对象,从而读取该对象的备份存储地址,这就为我提供了一种找到完全由脚本控制的数据的方法。

browser-security-sandbox-19.png

以上就是返回的代码(以ASLR为模型):

browser-security-sandbox-20.png

使用WinDbg,就可以验证这是我的缓冲区的后备存储器。

browser-security-sandbox-21.png

由于这是一个非常强大的原语,我可以使用它来泄漏任何来自对象的字段,以及任何JavaScript对象的地址,因为这些对象有时会存储为其他对象中的字段。

构建任意的读/写原语

能够在已知地址处放置任意数据,就意味着我可以构建一个更强大的原语,因为此时我已经具备了创建任意JavaScript对象的能力。只需改变从float读取到的对象的字段类型,我就可以从内存中的任何地方读取对象指针,比如已知地址的缓冲区。我可以使用WinDbg在已知的地址(上面发现的原语)中使用以下命令来进行测试:

browser-security-sandbox-22.png

这将在我的任意对象指针的加载位置放置一个表示整数0xbadc0de的SMI,由于我没有设置最低有效位,它将被V8解释为内联整数:

browser-security-sandbox-23.png

正常情况下,V8会打印以下输出:

browser-security-sandbox-24.png

鉴于此,我就可以创建任意对象,比如,我可以通过创建假的DataView和ArrayBuffer对象来组合一个我使用起来很方便的任意读/写原语。现在,我会再次使用WinDbg将我的假对象数据放在已知位置。

browser-security-sandbox-25.png

然后我会用以下JavaScript进行测试:

browser-security-sandbox-26.png

如预期的那样,调用DataView.prototype.setUint32会触发崩溃,因为调用会尝试将0xdeadcafe值写入0x00000badbeefc0de地址。

browser-security-sandbox-27.png

可以看出,数据写入或读取的地址被控制了,该问题只是修改通过WinDbg填充的obj.arraybuffer.backing_store插槽所出现的问题之一。因为在实际的漏洞中,内存将成为真正的ArrayBuffer对象的后备存储器的一部分,要实现这点很容易,例如,写入如下所示的原语。

browser-security-sandbox-28.png

这样,我就可以在JavaScript的Chrome渲染程序中可准确地读写任意内存位置。

实现任意代码执行

只要有任意读/写原语,就可以很容易地在Chrome渲染程序中实现代码执行。由于V8会将其JIT代码页分配给读写执行(RWX)权限,这意味着可以通过定位一个JIT代码页,然后覆盖该页面,最后通过调用来完成代码执行。在实际操作过程中,可以通过使用我的信息泄漏来定位JavaScript函数对象的地址并读取其函数entrypoint字段来实现。一旦我将代码放在了入口点,就可以调用JavaScript函数来执行代码。

browser-security-sandbox-29.png

值得注意的是,即使V8没有使用RWX页面,由于缺乏对控制流程的完整性检查,仍然很容易触发返向导向编程(ROP)链的执行。如果发生这种情况,可以通过重写JavaScript函数对象的entrypoint字段来指向所需的gadget,然后进行函数调用。

不过,这些技术都不能直接用于具有CFG和ACG功能的Microsoft Edge。在Windows 10 Creators Update中引入的ACG会执行严格的数据执行保护(DEP),并将JIT编译器移动到外部进程。这就形成了一个强大的防御,即攻击者在不破坏JIT进程的情况下,是不能重写可执行代码的,这就需要发现和开发其它的漏洞。

其次,CFG保证了间接调用站点只能跳转到某一组函数,这意味着该函授不能用于直接启动ROP执行。另外, win10 creators update还引入了CFG导出抑制,通过将大部分导出的函数从有效的目标集合中删除,从而大大减少了有效的CFG间接调用目标集,所有这些缓解措施都使得黑客很难利用Microsoft Edge中的RCE漏洞。

RCE漏洞的危险

Chrome采用多进程模式,涉及到多种流程类型,比如浏览器进程,GPU进程和渲染器进程。

每个渲染器都会负责解析和解释对应网页的的HTML,JavaScript等。沙箱模型使得这些进程只能访问其需要的功能。因此,我无法通过渲染器中对受害者的系统进行攻击。

如此一来,我就要考虑攻击者有没有可能使用了其它的漏洞完成攻击。虽然大多数选项卡的进程中都是进行单独隔离的,但还是有些特殊情况,例如,如果在bing.com上使用JavaScript开发者控制台(可以通过按F12打开该控制台)来运行window.open('https://microsoft.com'),则会打开一个新的选项卡,但新的选项卡通常会与原始标签的进程完全相同。这可以通过使用Chrome的内部任务管理器来看到,你可以通过按Shift + Esc来打开它。

browser-security-sandbox-30.png

这是一个有趣的观察,因为它表明渲染器进程没有被锁定到任何一个来源。这意味着在渲染器进程中实现任意代码执行就可以使攻击者访问其他来源的进程。虽然攻击者以这种方式绕过同源策略(SOP)的可能性并不大,但如果一旦发生,则后果不堪设想:

1.攻击者可以通过劫持PasswordAutofillAgent界面从任何网站窃取用户保存的密码;

2.攻击者可以将任意JavaScript注入任何页面,例如通过劫持 blink::ClassicScript::RunScript 的方法;

3.攻击者可以在用户没有注意的情况下导航到后台的任何网站,例如,通过创建隐藏的弹出式窗口。因为许多用户交互检查是发生在渲染器进程中的,无法让浏览器进行进程验证。这样,类似ChromeContentRendererClient:AllowPopup的进程就会被劫持。这样在不需要用户交互的情况下, 攻击者就可以隐藏新的窗口。他们还可以在关闭的时候继续打开新的弹出窗口,例如,通过连接到onbeforeunload窗口事件。

这种攻击更好地研究并实施了渲染器和浏览器进程如何相互通信并直接模拟相关消息,但这表明这种攻击可以在有限的努力下实现。虽然双重身份验证的民主化减轻了密码盗窃的危险,但是由于可以让攻击者在已经登录的网站上欺骗用户的身份,所以不可认为双重认证就是绝对可靠的。

这表明只要在渲染器和浏览器进程的通信中做点文章,即使用户采用了双因素身份验证,攻击者还是能够达到盗取账号的目的的。

总结

客观地讲,Google与微软互怼的结果,会让普通的用户受益,因为每个浏览器都有其长板和短板,互相竞争的结果迫使它们不得不努力改善自己以吸引用户。就Microsoft Edge而言,目前它正在继续改进隔离技术,并使得任意代码执行难以实现。就Google而言,Google正在开发一个网站隔离功能,一旦该功能实现,Chrome就可以通过保证任何给定的渲染器进程只能与单一来源进行交互,这样Chrome就能轻松应对RCE攻击。

源链接

Hacking more

...