译者:shan66
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
传送门
简介
现在人们在选择浏览器的时候,在诸多的考虑因素中,安全性无疑是首当其冲的。在我们的日常活动中,浏览器的应用几乎无处不在,例如通过它与亲人保持联系、编辑敏感的私人和公司文档,甚至管理我们的金融资产。因此,如果网络浏览器被黑客攻陷的话,可能会产生灾难性的后果。实际上,随着浏览器功能的增加,其代码也变得越来越复杂,从而增加了潜在的攻击面。
我们微软攻防安全研究(OSR)团队的工作使命,便是让计算技术变得更安全。我们每天都在通过各种方法来识别exploit软件,并与公司其他团队紧密合作,以便为缓解网络世界所面临的各种攻击提供相应的解决方案。在我们的工作过程中,通常涉及软件漏洞的识别。但是,我们认为,无论我们多么努力,总是会有更多的漏洞被发现,因此,这并不是我们关注的重点。相反,我们更关心的问题是:假设存在一个漏洞,我们该怎么办?
迄今为止,我们已经取得了不错的成绩。比如,在我们的协助下,已经提高了多款Microsoft产品的安全性,其中就包括Microsoft Edge。与此同时,我们在防止远程执行代码(RCE)方面也取得了取得重大进展,其中涉及的缓解措施包括:控制流程保护(CFG)、禁止导出、任意代码保护(ACG)和隔离,特别是Less Privileged AppContainer(LPAC)和Windows Defender Application Guard(WDAG)。不过,我们认为,还必须对自己的安全策略进行严格的验证。为此,我们经常采用的一种方法是看看其他公司在做什么,并深入学习他们的研究结果。
为此,我们开始研究Google的Chrome浏览器,因为其安全策略的重点在于沙盒上面。我们想知道Chrome是如何阻止RCE漏洞攻击的,并尝试回答:沙盒模式是否强大到足以保护浏览器的安全?
我们的主要研究发现包括:
我们发现的CVE-2017-5121漏洞表明,在现代浏览器中找到可远程利用的漏洞是完全可能的
Chrome相对缺乏的RCE缓解措施,意味着从内存损坏漏洞到exploit的路径可能会很短
在沙箱内进行的多次安全检查导致RCE exploit能够绕过同源策略(SOP),从而使RCE攻击者可以访问受害者的在线服务(如电子邮件,文档和银行会话)并保存凭据
Chrome的漏洞处理流程会导致安全漏洞的细节在相关的安全补丁推送到用户之前就已经被公开。
远程漏洞的查找和利用
为了完成这次安全评估,我们首先需要找到某些漏洞作为突破口。通常,我们首先想到的是寻找内存损坏漏洞,例如缓冲区溢出或UAF漏洞。对于所有网络浏览器来说,其攻击面都是非常广泛的,包括V8 JavaScript解释器、Blink DOM引擎和ium PDF PDF渲染器等。对于这个项目来说,我们将把注意力集中在V8上面。
利用fuzzing,我们最终为exploit找到了一个漏洞。实际上,我们是利用Windows Security Assurance小组的基于Azure的fuzzing基础架构来运行ExprGen的,这是一个由Chakra背后的团队编写的内部JavaScript fuzzer,使用的是我们自己的JavaScript引擎。迄今为止,所有公开可用的fuzzer可能都在V8上跑过了;从另一方面来说,ExprGen只在Chakra内部使用,所以我们更有可能在V8上发现新的漏洞。
寻找bug
与手动代码审查相比,fuzzing的一个缺点是,无法立即搞清楚到底是测试用例触发了一个漏洞,还是意外的行为造成了一个漏洞。这对我们的OSR来说,尤其如此;我们之前没有V8方面的使用经验,因此对其内部工作机制知之甚少。在我们的测试中,ExprGen产生的测试用例能够可靠令V8崩溃,但并不总是以相同的方式,或者说无法以一种有利于攻击者的方式让其崩溃。
由于fuzzer通常产生的代码会非常庞大和复杂(在我们的测试中,产生了将近1,500行不可读的JavaScript代码),所以,首先要做的事情就是将测试用例最小化 ——为其瘦身,直到变成一个比较小而且可理解的代码。下面就是我们最终得到的结果:
上面的代码看起来很令人费解,并且没有真正实现任何东西,但它却是合法的JavaScript代码。它所做的事情,就是创建一个奇怪的结构化对象,然后对其中的一些字段进行设置。按理说,这不应该触发任何奇怪的行为,不过事实上,的确发生了这样的情况。当使用D8运行该代码时,崩溃发生了:
我们看到,崩溃发生在(0x000002d168004f14)处,说明不是位于静态模块中。因此,它必定是由V8的即时(JIT)编译器动态生成的代码。同时,我们还可以发现崩溃是因为rax寄存器的值为零导致的。
乍看起来,这像是一个经典的空指针引用bug,这样的话到此就可以放弃了:因为它通常是无法利用的,因为现代操作系统阻止零虚拟地址被映射。我们可以考察一下周围的代码,以便更好地了解可能发生的情况:
我们可以从这段代码中提取一些有用的信息。首先,我们注意到,崩溃发生在一个函数调用之前,它看起来像一个JavaScript函数调度器存根,这次崩溃主要是由于v8::internal::Builtin_FunctionPrototypeToString的地址被加载到该调用之前的一个寄存器中引起的。通过查看位于0x000002d167e84500处的函数的代码,我们发现地址0x000002d167e8455f确实包含一个call rbx指令,这似乎证实了我们的怀疑。它调用Builtin_FunctionPrototypeToString这一事实的却很有趣,因为这是Object.toString方法的实现,而我们最小化的测试用例会调用它。这似乎表明,崩溃发生在Javascript函数func0的JIT编译版本中。
从上面的反汇编代码中,我们可以收集到的第二条信息是,发生崩溃时寄存器rax中包含的零值是从内存加载的。这个被加载的值好像应该作为参数传递给toString函数调用的。我们不难看出,它是从[rdi + 0x18]处加载的。基于此,我们可以仔细看一下相关的内存区:
这里没有发现非常有用的信息,只能看出大部分值都是指针。但是,知道这个值(它是一个指针)是从哪里加载的是非常有用的信息,因为它可以帮助我们弄清楚为什么这个值此前为零。利用WinDbg最新的Time Travel Debugging(TTD)功能,我们可以在该位置放置一个内存写入断点(baw 8 0000025e`a6845dd0),然后在该函数的起始处放置一个执行断点,最后重新运行反跟踪(g-)。
有趣的是,我们的内存写断点并没有触发,这意味着这个内存片段在这个函数中没有被初始化,或者至少没有用到。这可能是正常的,但是如果我们借助于测试用例,例如使用o.b.bc.bca.bcab = 0xbadc0de;行替换o.b.bc.bca.bcab = 0;行的话,我们就发发现,导致崩溃的值原来所在的内存区发生了变化:
我们看到,常数值0xbadc0de出现在该内存区域的末端。虽然这并不能证明任何事情,但是至少说明,这个内存区域可能是JIT编译的函数用来存储局部变量的。还记得前面代码崩溃的原因吗?导致崩溃的原因是加载了一个本来要作为参数传递给Object.toString的值引发的。这进一步印证了我们的这个看法。
结合TTD,我们可以进一步确认这段内存的确未被该函数初始化的事实,可能的原因是JIT编译器没有给出初始化指向用于访问o.b.ba.bab字段的对象成员的指针的代码。
为了证实这一点,我们可以使用-trace-turbo和-trace-turbo-graph参数在D8中运行测试用例。这样的话,D8就会输出TurboFan(V8的JIT编译器)任何构建和优化相关代码方面的信息。我们可以将它与turbolizer结合起来使用,可以显示TurboFan用于表示和优化代码的相关图。
TurboFan会依次对这些图实施各种优化。当优化过程经过一半,也就是在卸载优化阶段之后,将得到如下所示的代码的流程图:
显然,优化器将func0放入了无限循环中,然后从第一次循环迭代中把它拽了出来。这些信息对于了解各个代码块之间的相互关系非常帮助。然而,这种展示方式的缺点是,漏掉了对应于加载函数调用的参数的节点,以及局部变量的初始化对应的节点,但是这些信息正好都是我们比较感兴趣的。
幸运的是,我们可以使用turbolizer的界面来显示这些内容。下面我们考察第二个Object.toString调用,我们可以看到参数来自哪里,以及它被分配到哪里以及在哪里进行初始化:
(注意:需要手动修改节点标签以提高可读性)
在优化阶段,代码看起来非常合理:
分配内存块以存储本地对象o.b.ba(节点235),并初始化baa和bab字段
分配内存块以存储本地对象o.b(节点259),并且初始化所有字段,其中需要特别指出的是,ba初始化为指向前面的o.b.ba的引用
分配内存块以存储本地对象o(节点303),并且初始化所有字段
本地对象o的字段b被对对象o.b(节点185)的引用覆盖,
本地对象字段o.b.ba.bab被加载(节点199、209和212)
调用Object.toString方法,将o.b.ba.bab传递为第一个参数
在这个优化阶段中,编译的代码看起来没有表现出未初始化的局部变量行为(我们猜测这种行为是该bug的根本原因)。话虽如此,该图的某些方面可以证明我们的猜测。请看一下节点209和212,分别加载o.b.ba和o.b.ba.bab,用于函数调用参数,我们可以看到,偏移量+24和+32与崩溃代码的反汇编结果是相对应的:
0x17和0x1f的十进制值分别为23和31。考虑到V8标签的值是如何区分实际对象与内联整数(SMI)的,这一切就顺理成章了:如果一个JavaScript变量的值的最低有效位为1,则被视为指向对象的指针,否则被视为SMI。因此,V8代码在解引用的时候,作为一种优化措施,会从JavaScript对象偏移中减1。
由于我们仍然无法解释这个bug,所以还需继续考察这个优化过程。在经过Escape分析之后,图形变成下面的样子:
这里有两个显着的差异:
代码不再是加载o和o.b了——它被优化为直接引用o.b,可能是因为该字段的值从未发生变化的缘故
代码不再初始化o.b.ba;从图中可以看出,turbolizer将节点264显示未灰色,这意味着它不再活动,因此不会被内置到最终的代码中
查看这个阶段所有的活动节点,可以发现这个字段确实没有被初始化。同时,我们在这个测试用例上运行d8,并且使用–no-turbo-escape选项,以省略这个优化阶段:d8不再崩溃,从而确认这就是问题所在。这个结果表明,谷歌对这个bug的修补方法为:完全禁用v8 6.1中的escape分析,直到v8 6.2版本中的新escape分析模块就绪为止。
现在,我们已经清楚了这个bug的根本原因,下面我们要做的就是寻找相应的利用方法。虽然这个bug看上去非常强大,但实际上它完全取决于我们对未初始化的内存片段的控制能力,以及最终的利用方式。