FavoriteLoading
0

微软找到谷歌Chrome浏览器漏洞,谷歌找到微软Edge漏洞

如预期的那样,调用DataView.prototype.setUint32会触发崩溃,尝试将值0xdeadcafe写入0x00000badbeefc0de:微软找到谷歌Chrome浏览器漏洞,谷歌找到微软Edge漏洞

 

 

安全性现在是挑选正确浏览器的强大区别。我们都使用浏览器进行日常活动,例如与亲人保持联系,还可以编辑敏感的私人和公司文档,甚至管理我们的金融资产。通过网络浏览器进行的单一妥协可能会产生灾难性的后果。这并不意味着浏览器也正在成为现有的最复杂的消费者软件之一,从而增加潜在的攻击面。

我们在微软攻防安全研究(OSR)团队的工作是使计算更安全。我们通过确定开发软件的方法,并与公司其他团队合作解决方案来减轻攻击。该工作流通常涉及识别软件漏洞利用。但是,我们认为,总是会有更多的漏洞找到,所以这不是我们的主要重点。相反,我们的工作真的是要求:假设存在一个漏洞,我们该怎么办?

迄今为止,我们已经取得了成功。我们帮助提高了几个Microsoft产品的安全性,包括  Microsoft Edge。我们继续在防止远程执行代码(RCE)与缓解措施(如  控制流程保护(CFG)),导出禁止和  任意代码保护(ACG)以及隔离方面取得重大进展,特别是与特权AppContainer(LPAC)和  Windows Defender应用程序卫兵  (WDAG)。不过,我们认为,我们必须验证我们的安全策略。我们这样做的一个方法是看看其他公司在做什么,并研究他们的努力结果。

对于这个项目,我们开始研究Google的Chrome浏览器,其安全策略  显示了沙盒的重点。我们想看看Chrome是如何阻止一个RCE漏洞,并尝试回答:是否有强大的沙盒模式足以使浏览器安全?

我们的一些主要发现包括:

 

我们发现CVE-2017-5121表示可以在现代浏览器中找到可远程利用的漏洞
Chrome相对缺乏RCE缓解意味着从内存损坏错误到利用的路径可能很短
在沙箱内进行的几次安全检查导致RCE利用能够绕过同源政策(SOP),使RCE能够攻击者访问受害者的在线服务(如电子邮件,文档和银行会话)并保存凭据
Chrome的漏洞处理流程可能导致公开披露安全漏洞的细节,因为这些漏洞将被修复被推送给客户

 

查找和利用远程漏洞

为了做这个评估,我们首先需要找到某种入口点漏洞。通常,我们通过发现内存损坏错误(例如缓冲区溢出或免用后漏洞)来实现此目的。与任何网络浏览器一样,攻击面广泛,包括  V8 JavaScript解释器,  Blink  DOM引擎和  ium  PDF PDF渲染器等。对于这个项目,我们把注意力集中在  V8上

我们最终用于我们的漏洞利用的bug是通过模糊化来发现的。我们利用Windows安全保证团队的基于Azure的模糊基础架构来运行ExprGen,这是一个由Chakra后面的团队(我们自己的JavaScript引擎)编写的内部JavaScript  模糊  器。人们可能已经在V8上投掷了所有公开的模糊器  ; 另一方面,ExprGen只是针对  Chakra进行过操作,给它更多的机会导致发现新的bug。

识别错误

与手动代码审查相比,模糊的一个缺点是,并不总是立即清楚什么导致给定的测试用例触发漏洞,或者意外的行为甚至构成一个漏洞。这对我们在OSR尤其如此; 我们没有任何与V8合作的经验   ,因此对其内部工作知之甚少。在这种情况下,ExprGen产生的测试案例   可靠地坠毁了  V8,但并不总是以相同的方式,而不是以一种可以容易地受到攻击者的影响的方式。

由于模糊器通常会生成非常大和复杂的代码(在这种情况下,将近1,500行不可读JavaScript),第一步通常是最小化测试用例 – 修剪脂肪,直到我们留下一个小小的,可理解的码。这就是我们最终的结论:

 

 

上面的代码看起来很奇怪,并没有真正实现任何东西,但它是有效的JavaScript。它所做的就是创建一个奇怪的结构化对象,然后设置其一些字段。这不应该触发任何奇怪的行为,但它是。当使用D8运行此代码时  ,  V8的独立可执行版本由git标签6.1.534.32构建,我们会遇到崩溃:

 

 

看看崩溃发生在(0x000002d168004f14)的地址,我们可以告诉它不会在静态模块中发生。因此,它必须是由V8的即时(JIT)编译器动态生成的代码  。我们也看到崩溃是因为  rax  寄存器为零。

乍看起来,这看起来像一个经典的零解除引用错误,这将是一个放弃:通常不可利用的,因为现代操作系统阻止零虚拟地址被映射。我们可以查看周围的代码,以便更好地了解可能发生的情况:

 

 

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

从上面的反汇编中我们可以收集到的第二条信息是 崩溃时的寄存器rax中包含的零值  是从内存加载的。它也看起来像应该被加载的值被传递给 作为参数的  toString函数调用。我们可以告诉它正在从[rdi + 0x18]加载  。基于此,我们可以看一下那段记忆:

 

 

这不会产生非常有用的信息。我们可以看到,这些值中的大多数都是指针,但这是关于它的。但是,知道值(这是什么意思是一个指针)的位置是有用的,因为它可以帮助我们弄清楚为什么这个值首先为零。使用  WinDbg新推出的“  时间旅程调试(TTD)”  功能,我们可以在该位置放置一个内存写入断点(baw 8 0000025e`a6845dd0),然后在该函数的起始处放置一个执行断点,最后重新运行向后跟踪(g-)。

有趣的是,我们的内存写断点不会触发,这意味着这个内存插槽在这个函数中没有被初始化,或者至少不会被使用。这可能是正常的,但是如果我们玩测试用例,例如通过替换  obbc.bca.bcab = 0; 线与  obbc.bca.bcab = 0xbadc0de; ,那么我们开始注意到我们的崩溃值发生的内存区域的更改:

 

 

我们看到我们的  0xbadc0de  常数值最终在该内存区域。尽管这并不能证明任何东西,但是这似乎很可能这个内存区域被JIT编译的函数用来存储局部变量。这个想法是通过前面的代码看起来像我们试图加载的值而被传递给Object.toString  作为参数来加强  的。

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

为了证实这一点,我们可以 使用  -trace-turbo  和  -trace-turbo-graph  参数在D8中运行测试用例  。这样做会导致  D8  输出关于  TurboFan,  V8的JIT编译器关于构建和优化相关代码的信息。我们可以将它与turbolizer一起使用,   以便可视化TurboFan  用于表示和优化代码的图形  。

TurboFan  通过将各种优化阶段相继应用于图表来工作。大约通过优化管道的一半,在  卸载 优化阶段之后,这就是我们代码的流程:

 

 

这是非常简单的:优化器显然将  func0  嵌入到无限循环中,然后拉出第一个循环迭代。这些信息有助于了解块之间如何相互关联。然而,该表示省略了对应于加载函数调用参数的节点,以及局部变量的初始化,这是我们感兴趣的信息。

幸运的是,我们可以使用  turbolizer的界面来显示。关注第二个  Object.toString  调用,我们可以看到参数来自哪里,以及它被分配和初始化的位置:

注意:手动修改节点标签的可读性

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

 

 

分配存储块以存储本地对象obba(节点235),并且其字段baa和bab被初始化
分配存储器块以存储本地对象ob(节点259),并且其字段都被初始化,其中ba专门用引用前面的obba分配来初始化
分配存储块以存储本地对象o(节点303),并且其字段都被初始化
本地对象o的字段b被对象ob的引用覆盖(节点185)
本地对象字段obba.bab被加载(节点199,209和212)
调用Object.toString方法,将obba.bab作为第一个参数传递

 

在这个阶段在优化管道中编译的代码看起来不应该展示未初始化的局部变量行为,我们假设这是错误的根本原因。话虽如此,这种表示的某些方面可以证明我们的假设。看节点209和212分别加载  obba  和  obba.bab作为函数调用参数,我们可以看到偏移量  +24  和  +32  对应于崩溃代码的拆卸:

 

 

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

由于我们仍然没有解释这个错误,我们会继续寻找优化通行证,直到找到奇怪的东西。这在Escape分析  通过后发生  。此时,图形如下所示:

 

 

有两个显着的差异:

  • 代码不再经历加载o的麻烦,   然后  ob -it被优化为 直接引用  ob,可能是因为该字段的值从未更改
  • 代码不再初始化  obba ; 从图中可以看出,  湍流器  灰度出节点264,这意味着它不再生存,因此不会被内置到最终的代码中

在这个阶段查看所有的活动节点似乎确认这个字段不再被初始化。作为另一个理智检查,我们 在这个测试案例中运行  d8,使用-no-turbo-escape标志   ,以省略此优化阶段:  d8  不再崩溃,确认这是问题所在。事实上,事实证明是谷歌修复这个错误:  完全禁用v8 6.1中的逃脱分析阶段,直到新的逃生分析模块准备在v8 6.2中进行生产。

有了关于bug的根本原因的所有信息,我们需要找到方法来利用它。看起来它可能是一个非常强大的bug,但它完全取决于我们控制未初始化的内存插槽的能力,以及它如何最终被使用。

获取信息泄漏

在这一点上,了解我们可以做什么或不能做的错误的最简单的方法是简单地测试用例。例如,我们可以看到从未初始化的指针更改我们正在加载的字段的类型的效果:

 

 

结果是该字段现在直接作为float加载,而不是一个对象指针或SMI:

 

类似地,我们可以尝试向对象添加更多的字段:

 

 

运行这个,我们得到以下崩溃:

 

 

这是有趣的,因为它看起来像向对象添加字段修改从对象字段加载的偏移量。事实上,如果我们做数学,我们看到(0x67 – 0x1f)/ 8 = 9,这正是我们从obba.bab添加的字段数。这同样适用于从rbx加载的新偏移量。

使用测试用例多一点,我们可以确认,我们对未初始化指针加载的偏移量进行了广泛的控制,即使这些字段都没有被初始化。在这一点上,看看我们是否可以将任意数据放入这个内存区域是非常有用的。与0xbadc0de的早期测试似乎表明我们可以,但偏移似乎随着测试用例的每次运行而改变。通常,通过喷涂价值来解决这些问题。理由是,如果我们无法准确地将目标陷入某一特定的地点,我们可以改变目标。实际上,我们可以通过使用内联数组来尝试喷涂值:

 

 

看看崩溃转储,我们看到:

 

 

崩溃与以前基本相同,但如果我们查看未初始化数据来自的内存,我们将看到:

 

 

我们现在有一个大块的任意脚本控制值在与r11的偏移量。将这个观察结果与以前的观点结合起来,我们可以想出更好的东西:

 

 

结果是我们现在从任意地址中取消引用一个浮点值:

 

 

这当然是非常强大的:它立即导致任意读取原语,因为它可能是不切实际的。不幸的是,没有初始信息泄漏的任意读取原语并不那么有用:我们需要知道要读取哪些地址才能使用它。

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

 

 

以上代码返回(模ASLR):

 

使用WinDbg,我们可以验证这是我们的缓冲区的后备存储:

 

 

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

构建任意的读/写原语

能够以已知地址放置任意数据意味着我们可以解锁更强大的原语:创建任意JavaScript对象的能力。只需将从float读取的字段的类型更改为对象,就可以从内存中的任何位置读取一个对象指针,包括我们知道地址的缓冲区。我们可以使用WinDbg使用以下命令将受控数据放置在已知地址(与上面刚刚开发的相同的原语)中进行测试:

 

 

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

 

 

如预期,V8打印以下输出:

 

鉴于此,我们有能力创建任意对象。从那里,我们可以通过创建假的DataViewArrayBuffer对象来组合方便的任意读/写原语。我们再次使用WinDbg将我们的假对象数据放在已知位置:

 

 

然后我们用以下JavaScript测试:

 

 

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

 

 

控制数据写入或读取的地址只是修改通过WinDbg填充的obj.arraybuffer.backing_store插槽的问题。因为在真正的漏洞的情况下,内存将成为真正的ArrayBuffer对象的后备存储的一部分,这样做并不难。例如,写入原语可能如下所示:

 

有了这个,我们可以在JavaScript的Chrome渲染器流程中可靠地读取和写入任意内存位置。

实现任意代码执行

给予任意的读/写原语,在渲染器进程中实现代码执行是比较容易的。在写入时,V8将其JIT代码页分配给读写执行(RWX)权限,这意味着可以通过定位JIT代码页,覆盖它,然后调用代码来完成代码执行。在实践中,这是通过使用我们的信息泄漏来定位JavaScript函数对象的地址并读取其函数entrypoint字段来实现的。一旦我们将代码放在了这个入口点,我们可以调用JavaScript函数来执行代码。在JavaScript中,这可能是:

 

 

值得注意的是,即使V8没有使用RWX页面,由于缺乏控制流完整性检查,仍然很容易触发执行返向导向编程(ROP)链。在这种情况下,例如,我们可以覆盖JavaScript函数对象的entrypoint字段来指向所需的小工具(可能是某种堆栈枢纽),然后进行函数调用。

这些技术都不能直接适用于具有CFG和ACG功能的Microsoft Edge。在Windows 10 Creators Update中引入的ACG执行严格的数据执行保护(DEP),并将JIT编译器移动到外部进程。这样做可以保证攻击者无法覆盖可执行代码,而无需首先损害JIT进程,这将需要发现和利用其他漏洞。

另一方面,CFG保证间接调用站点只能跳转到某一组函数,这意味着它们不能用于直接启动ROP执行。创作者更新还引入了CFG导出抑制,通过从有效的目标集中删除大多数导出的功能,大大减少了一组有效的CFG间接呼叫目标。所有这些缓解和其他措施使得利用Microsoft Edge中的RCE漏洞更加复杂。

RCE的危险

Chrome是一款现代化的网络浏览器,采用多进程模式。涉及到多种流程类型:浏览器进程,GPU进程和渲染器进程。如其名称所示,GPU进程会经纪人GPU与需要使用它的所有进程之间的交互,而浏览器是经纪人访问从文件系统到网络的所有过程的全局管理器进程。

每个渲染器意在成为一个或多个选项卡背后的大脑 – 它负责解析和解释HTML,JavaScript等。沙箱模型使得这些进程只能访问其需要的功能。因此,无法从渲染器中对发现受害者的系统进行彻底的妥协,而不会发现第二个漏洞来逃脱该沙箱。

考虑到这一点,我们认为,有趣的是,检测攻击者可能没有二次错误可能实现。虽然大多数选项卡在个别进程中都是隔离开的,但并不总是如此。例如,如果您在bing.com上使用JavaScript开发者控制台(可以通过按F12打开该控制台)来运行window.open(’https://microsoft.com’),则会打开一个新的选项卡,但是它通常将与原始标签相同的过程。这可以通过使用Chrome的内部任务管理器来看到,可以通过按Shift + Escape来打开它:

 

 

这是一个有趣的观察,因为它表明渲染器进程没有被锁定到任何一个来源。这意味着在渲染器进程中实现任意代码执行可以使攻击者有能力访问其他来源。虽然攻击者以这种方式获得绕过单一起源政策(SOP)的能力似乎并不大,但后果可能很大:

 

攻击者可以通过劫持PasswordAutofillAgent界面从任何网站窃取保存的密码。
攻击者可以将任意JavaScript注入任何页面(称为通用跨站点脚本或UXSS),例如劫持blink :: ClassicScript :: RunScript方法。
攻击者可以导航到背景中的任何网站,而无需用户注意,例如,创建隐藏的弹出式窗口。这是可能的,因为许多用户交互检查发生在渲染器进程中,无法让浏览器进程验证。结果就是像ChromeContentRendererClient :: AllowPopup这样的东西可以被劫持,这样就不需要用户交互,攻击者就可以隐藏新的窗口。他们还可以在关闭时保持打开新的弹出式窗口,例如,通过挂入onbeforeunload窗口事件。

 

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

结论

这种攻击推动了我们的承诺,不断使我们的产品在各个方面都安全。使用Microsoft Edge,我们将继续改进隔离技术,并使得任意代码执行难以实现。就Google而言,Google正在开发一个网站隔离功能,一旦完成,Chrome就可以通过保证任何给定的渲染器进程只能与单一来源进行交互,使Chrome更能适应这种RCE攻击。用户可以通过chrome://标志界面启用此网站隔离功能的高度实验性版本。

我们负责任地公布了我们发现的脆弱性,并在2017年9月14日向Google发送了可靠的RCE漏洞。该漏洞被分配给CVE-2017-5121,该报告被Google授予了$ 7,500的错误奖赏。除了其他错误,我们的团队报告但没有利用,我们获得的总奖金金额为$ 15,837。Google与此相匹配,并向我们在西雅图选定的组织丹尼斯·路易教育中心捐赠了3万美元  。 本文中描述的漏洞的  错误跟踪器项目在写入时仍然是私有的。

服务安全修复程序是此过程的重要组成部分,对Google来说,他们的转折令人印象深刻:  错误修复  仅在初次报告四天后提交,固定版本在三天后发布。但是,请注意,修补程序的源代码可在Github上公开发布,然后再推送给客户。虽然这个问题的解决方案并没有立即消除潜在的漏洞,但其他情况可能不那么微妙。在这个例子中,这个安全  漏洞的跟踪器项目  当时也是保密的,但是  公共的修复 使得该漏洞显而易见,特别是随着回归测试。这可以预料到一个开放源码的项目,但是当这些漏洞在补丁可用之前,攻击者知道这些漏洞是有问题的。在这个具体情况下,Chrome的稳定渠道在这个承诺被推到了git之后,仍然很容易受到将近一个月的困扰。攻击者有足够的时间利用它。一些Microsoft Edge组件,如Chakra,也是开源的。因为我们认为,在向公众发布公开知识之前,必须先向客户运送修复程序,我们只会在修补程序发货后更新Chakra git仓库。

我们的策略可能有所不同,但我们相信在整个安防行业进行合作,以帮助保护客户。这包括通过协调漏洞披露(CVD)披露供应商的  漏洞,并在提供安全修复的整个过程中进行合作。