首页
论坛
专栏
课程

[推荐]看雪2018峰会回顾 | 被“幽灵”所困扰的浏览器

2018-7-31 18:02 1641

[推荐]看雪2018峰会回顾 | 被“幽灵”所困扰的浏览器

2018-7-31 18:02
1641

2018 注定会在芯片发展史上留下浓墨重彩的一笔。从年初曝光 CPU 漏洞以来,随后的几个月又相继出现了多个漏洞变种,传统芯片厂商无一例外纷纷中招。而出于安全等因素考虑,更多的互联网科技巨头开始争先入局芯片的自主研发,推出了各类机器人芯片、AI 语音芯片、自动驾驶芯片等。除了资本市场的火热,各国政府也是加大力度扶持相关产业的安全研究,这些无疑都给我们敲响了芯片安全的警钟。此次议题将由腾讯安全玄武实验室的宋凯带我们回顾点燃这场硝烟的“幽灵”漏洞,在详尽介绍原理的基础上演讲者给出了在浏览器中实现稳定利用的可行方案,本文值得我们安全人员细细研读。


—— 看雪『Pwn』版主 BDomne



宋凯

宋凯(@exp-sky) ,腾讯安全玄武实验室的高级安全研究员。对于软件漏洞挖掘与利用有着丰富的研究经验,主要关注浏览器及操作系统相关的安全研究。在Fuzzing与其它漏洞挖掘技术上也有着丰富的经验。曾代表腾讯安全玄武实验室赢得Pwn2Own 2017 Edge 浏览器项目。曾连续三年入选微软 MSRC 全球 Top 100 贡献者榜单,最高排名第12位。赢得2016年微软 Mitigation Bypass Bounty 项目。分别赢得2015年和2016年 EdgeBounty 项目。曾在AsiaSecWest、HITCON、中国互联网安全大会、XKungFoo等安全会议发表演讲。


以下内容为宋凯在看雪2018安全开发者峰会上的演讲实录:


我是腾讯安全玄武实验室的宋凯,很荣幸跟大家分享这个研究。2018年初出现了系列性CPU相关的漏洞,它们能造成非常大的危害,比如泄露当前用户进程空间以及内核空间的隐私数据。如果我们所使用加密算法相关的密钥被泄露的话,会造成非常严重的后果。


这个漏洞在2018年被公开时,在业内引起非常大的轰动,随即我们实验室进行了针对性的研究。常见CPU漏洞要想实现利用,大多数情况下需要本机代码的配合,编译成本机代码,然后在你的设备上执行,才能造成一定危害。而如果能在浏览器端通过脚本实现针对CPU的漏洞利用,它所造成的影响会远大于常见的攻击方式。这也是我今天跟大家分享的议题的主要内容。我们是如何在浏览器中实现一个CPU漏洞的利用,造成更大危害的。


经过我们的一段时间研究推出了在线检测工具,为全球很多用户检查他们的设备是否存在漏洞。同时通过用户的反馈发现了很多,我们最开始都没想到的设备竟然也存在漏洞。竟然也可以被我们所写的浏览器端漏洞检测程序所发现。



我工作在腾讯安全玄武实验室,主要研究方向是浏览器的相关安全,对各平台内核以及虚拟化产品也在做一些相关研究。

今天的议题主要是三部分:



第一,  跟大家简单介绍幽灵漏洞的原理,以及它到底是怎样的一个漏洞,能造成怎样的危害。

第二,  跟大家介绍幽灵漏洞如何在浏览器中实现利用,如何有效的探测这个漏洞的存在。

第三,  最后在浏览器中通过这个漏洞如何造成实际的危害。


浏览器中写针对CPU的漏洞利用,面临一些比较复杂的问题需要解决,需要话花大量的时间进行研究。这里面我们会介绍研究过程中所有的技巧以及方法,最终可以实现浏览器中快速稳定探测CPU漏洞的技术,并且可以通过利用这个CPU漏洞产生真正的威胁。



一、首先介绍一下Spectre漏洞



2018年3月这个CPU系列漏洞首次在互联网上被公开出来,包括Google和论文的作者都各自研究出了相关的技术,并在互联网上进行公开。在2017年的7月份他们也已经报告给了相关的设备厂商。


这个漏洞是可以影响先今世面上大部分CPU的一个漏洞,它能造成的危害是让程序可以访问当前进程,以及其它进程内非预期的数据。这个就能造成一个比较严重的数据泄露问题,它所利用的一个缺陷是,现在处理器为了解决CPU到访问内存数据时间比较长的问题,所推出的推测执行技术的实现上的缺陷。




虽然我们常见认为CPU访问内存速度很快,但基于CPU运算频率来看,这个访问过程还是非常慢的,CPU访问内存中的数据需要比较长的等待时间,为了提高CPU的性能,它提出了分支预测、预测执行的功能。让CPU再访问内存等待数据时,依然可以进行相对有效的计算。如果这个分支跳转的目标的情况,是基于一个内存中的数据,并且这个内存中的数据又没有被缓存过的话(CPU就只能去内存中进行读取相关数据),CPU就会去尝试进行分支预测推测执行的动作。当CPU读取的内存数据回来的时候,CPU再根据这个数据的内容以及分支条件的逻辑去确认,它刚才的推测执行是否有效,如果无效则把计算结果丢弃,恢复到之前的状态。如果有效则继续执行,这大大提高了CPU的效率。


但这里存在一个问题,如果推测执行的结果被丢弃,但推测执行过程中所执行的代码仍可能会影响CPU的缓存。在CPU进行恢复状态的时,缓存不会被恢复,CPU只会把相关的寄存器的状态恢复。这个漏洞的根本原因是因为,推测执行中的代码可以影响CPU的缓存,而这个缓存的影响,又可以用一些技术手段探测出来。分支逻辑在这种实现上是不可靠的,因为缓存被修改了,它可以让攻击者经过测量,稳定推测出预测执行中所访问数据的内容,就导致了内存中数据的泄露。



我们来看一下CPU实际执行分支推测的逻辑,分支条件非常简单,一个length的判断,如果length没有被缓存中缓存过,只在内存中存在,CPU执行的时候就会尝试进行分支预测。它会首先去访问内存,因为这是一个耗时比较长的动作,所以它会并行的进行分支预测逻辑。首先保存一个检查点,将当前CPU的状态保存起来,接着预测要执行的分支,选择它认为这一次分支可能所走的路径,然后去执行相关的代码。比如这里我们去访问一个通过size访问一个数组,分支预测执行这个代码时,数组里面的内容会被分支预测执行时缓存起来。当内存中的length数据被读到后,CPU要检查一下分支的条件和预测所执行的分支,是不是正确的。这里有两种结果,要么恢复到之前保存检查点的状态,去正确的分支继续执行。要么把分支预测执行代码的计算结果提交。这两种情况都会导致我们CPU缓存,实际上已经产生了变化。


推测执行就是这样的逻辑,CPU不知道接下来的分支该怎么走,但是CPU可以先把当前的状态保存起来,然后预测这个分支可能走的路径,并沿这个路径执行。如果推测是正确的话,那么提交CPU计算的结果,这可以大大提高CPU执行的效率。否则它会恢复计算机保存的状态,并沿正确的分支来执行。但是无论怎样,缓存都可能已经被修改了。



CPU缓存是这样的结构,我们可以理解为CPU到内存间的数据缓冲区,它的速度远高于内存,但是低于寄存器。现在现代CPU在访问内存中的数据时,会把内存中的数据先放入缓存,然后再去缓存中访问。下一次当CPU访问数据时候,如果发现其在缓存中存在的话,就直接在缓存中尝试读取这个数据(命中),就会大大提高访问的效率。

基于分支条件本身以及CPU的分支预测来看,我们可能会获得四种不同的结果,其中一种可能产生安全的问题。如果我们的分支条件,所判断的是一个缓冲区的边界的话,不管是C++这样的代码,还是高级语言,缓冲区的边界判断是为了保证,我们在访问数据时是处于缓冲区之内的。而如果超过了缓冲区的范围,则会造成程序的异常行为(OOB Read/Write)。


如果分支结果是允许的,但CPU认为可能走的是false的分支,那么CPU会恢复到预测之前的状态,又需要重新执行true分支的代码,这是一个比较慢的流程。


最重要的是当我们的边界检查的分支结果是false,但是CPU的分支预测却推测为true的话,大家想一想,这是一个非常危险情况,它可能会导致安全问题。边界检查是保证我们程序访问数据的有效性的,如果边界检查返回的是false,但CPU在推测执行的时候认为它是true,我们就违背了程序代码的逻辑,我们也违背了开发者设计的约定,在执行这个代码之后,缓存里面就会保存我们刚才越界访问的数据,这种微小的错误,就造成了很严重的安全问题。


第四种情况就比较简单了,如果我们的分支结果是false,推测也是false,那就可以加速CPU执行效率。



对于CPU的这个问题,简单的两三行代码就可以触发CPU的分支预测,这是导致信息泄露的基础条件。我们简单来看一下,首先,分支条件判断里x小于array1的size,array1就是用这个size来初始化的,它是缓存区的边界。开发者为了防止我们传递的x大于array的边界也就是array_1.length,通常会写这样一个边界检查。如果检查通过就允许你访问数据,否则就拒绝掉。看上去这个代码是没有任何问题的,但如果我们进入这个条件时x会作为array1的数据来访问,然后把访问出来的值作为array2的index,一会儿再讲后面为什么需要k*4096。这样一段简单的代码完全满足了CPU分支预测,及将泄漏的数据保存到缓存中的所有条件。



CPU怎样执行这样的代码?大体逻辑接近,但有小的不同。首先,CPU发现这是分支判断,arry1的size没有在缓存里,它需要去内存读取,这是一个比较耗时的过程,所以它进行分支预测。它保存一个检查点,然后推测执行后面的代码。这个x之所以进行分支预测,就是它现在并不知道array1的size到底多大,但是它假设用x去作为array1的index来进行访问内存,后面把访问出来的值(k),作为array2的index来进行访问内存。结果就是array2是基于array1越界读出来的数据进行的内存访问,array2里被访问的数据会从内存提交给CPU缓存。当Array1的size从内存中读取出来后,CPU再进行分支条件检查,它发现这不满足条件,不能继续执行。所以它会把当前的计算结果丢弃掉,然后恢复到之前的状态,继续执行另一个分支的代码。但现在就像前面所介绍的,array2里面的数据已经被提交给CPU缓存了。


如果x是越界的并且满足了分支预测的条件,array1会通过我们传的x访问内存,注意这个x没有任何大小限制。虽然代码写得非常好进行了有效的检查,但实际上推测执行里面没有array1 size的大小,x可以为任意的值,边界检查在这里可以完全被绕过。还有一个条件是array2里面所有的数据都需要是未被缓存的,这样在array1越界读一个字节,这个字节作为array2的index的时候,它里面才会有一个页被缓存,这一个页的访问速度就有别于array2里所有其他的内存也页。


那么这里有一个问题,如何才能让CPU的分支预测,稳定的认为我们的分支条件是真的呢?我们的解决办法是,对CPU分支预测进行一定的训练。我们所实现的逻辑需要执行五轮,每轮执行6次的条件分支,前5次我们传递的x是有效的,就是小于array1的size,这样分支在这5次里都会进入。但在第六次,我们会传一个大的导致了越界的x, CPU认为这个分支我们已经执行了5次都是ture,那么第6次它也会推测为进入这个分支。这样训练一下可以让CPU的推测逻辑,非常稳定的推测进入分支内执行。



这里面还有一些关键因素,简单说一下,并且把我们刚才的逻辑也统一整理一下,因为这是整个漏洞利用中最重要的关键点。首先array1的size一定是不能被缓存的,一定要是只存储在内存中的值,因为只有这样读取它所花费的时间才比较多,CPU才会进行分支的预测。然后我们还要进行训练,让CPU的分支预测认为这个分支为ture,所以我们需要先进行几轮的x小于array1size的合法调用,然后再传一个大的x,让它产生一个越界访问。紧接着array1会通过我们传得比较大的x,去访问array越界的内存中的值k。这个k就是我们泄露内存中的敏感数据的一个字节。然后我们再用这个字节作为array2的index,乘以4096的目的是让每一个index访问的时候,CPU进行缓存时的数据长度不会大于它,这样我们可以给予4096长度来进行探测,最终探测出访问哪一个被CPU缓存的index块是最快的。最后呢CPU会发现分支预测是错误的,会恢复到分支预测执行前的状态。但是现在array2中通过我们泄露的数据k,做为index的内存,已存到我们的CPU缓存里了,这个k就是我们泄露的数据。


我们现在知道了缓存中存储了array2中一块的内存,接下来我们要做的就是如何探测出哪一块被缓存了,我们知道这个的话,基于array2的布局就知道泄露的数据内容到底是什么了。首先要测试array2中哪一块存在于缓存中,我们需要基于数据访问速度的不同,我们访问一次不在缓存中的数据所用的时间,要远远大于存在缓存中数据所用的时间。因为单次访问缓存与非缓存数据的时间差异非常小,而且CPU根据当前系统的状态不同可能有点波动,那么我们可以通过累加访问的次数,来增大它们的时间差异,就可以有效的测量出来缓存数据与非缓存数据的时间差异了。最后,我们可以通过这个缓存的地址,计算出k的值,也就是通过访问Array1的CPU分支预测所泄露出的k的值,这样就实现了完整的通过幽灵漏洞来泄露一个字节的整个流程。




如何泄露更多字节?简单来说就是把刚才的流程循环执行就可以了。首先让CPU缓存中不存在任何array2中的数据,然后我们通过触发Spectre漏洞进行内存访问,泄露一个字节,让这个字节作为索引的array2里的内存块被缓存,接着测量出array2中的缓存块。可以通过累加访问次数的方式,来有效的测量出哪一页被缓存,最后推测出泄漏的字节的值。这个漏洞影响范围非常大,世界上大多数的CPU都受这个漏洞的影响,有英特尔平台、ARM平台,PC和移动端都受到这个漏洞的影响,这也是为什么它会引起行业关注,并且非常轰动的一个原因。




现在我们知道了什么是幽灵漏洞、它到底是怎样的逻辑所产生问题、如何稳定触发这个问题、如何实际的泄露内存中的数据。



二、这个漏洞如何在浏览器中实现?



这个漏洞如何在浏览器中实现?我们面临一些新的挑战。


首先想实现这个漏洞的利用,本身就需要解决一个问题,就是需要稳定的刷新缓存的方法,不管你CPU什么型号,不管你缓存有多大,都要能稳定的刷新它。其次如何保证在利用过程中特定的数据不出现在缓存中,这是针对在浏览器中说的。为什么呢?因为在利用过程中可能泄露很多字节,在这个过程中array的size是一定不能出现在缓存中的,这是我们刚才讲的先决条件之一。还有就是,如何获得高精度的计时器?虽然访问内存的时间远高于访问缓存的时间,但对于脚本语言来说还是一个非常微小的差距,所以依然需要一个高精度的计时器能看到这个差别,才能通过累加的方式,稳定的测量时间差。




解决以上问题,在C代码中相对比较容易,利用_mm_clflush函数可以将特定数据刷出缓存,非常稳定又快速。




在C代码中也比较容易通过__rdtscp函数获取精确的时间。但在浏览器中我们没有这样的条件,没有这样的函数来给我们使用,怎么办?我们在浏览器中如何稳定的刷行缓存,如何让特定的数据不出现在缓存中,我们只要访问一次,这个数据就一定出现在缓存中,但我又不能每次循环把所有的缓存都刷新。并且各大浏览器也降低了性能计时器的频率,让它在单次访问中看不出差别,并且还有一定的随机性,这样就导致累加起来也看不出任何的时间访问的差距,所以如何找到高精度的计时器也是一个问题。并且我们因为我们想做一个检测的工具,所以我们要支持尽可能多的设备,在不同的设备上、不同的CPU型号、不同的核心数、不同的缓存大小大小。以及缓存大小怎么探测,这也是我们需要解决的一个问题。



我们解决刷新缓存的方法是,通过访问大量其他的内存页,将我们之前保留在CPU缓存中的数据间接的刷出去,让它只保留我们新访问的内存数据,这种方式没办法做到特别快,但也可以相对稳定、有效的刷新缓存。


那我们如何保证在利用过程中,特定的数据不出现在缓存中?因为我们刚才说过要进行CPU的训练,让它认为我们执行5次有效的分支后,1预测导致越界的访问,这个过程里面是不能刷新缓存的,因为是一次一轮的攻击,那怎么办?我们想到一个办法是,把array的size保存到一个大列表里,每一个列表存储30个array的size,每一个size会间隔有比较大的空间,它即使缓存了,下一个块的size也不会缓存,我们通过这种方式来抵消掉,访问一次就把size缓存的情况。我们在内存中放了很多size,每一个又有了一定的间隔,我们通过变index的方式保证每一次缓存都不保留数据。



高精度时间计时器因为浏览器降低了其精度,所以不能用现成的API了,我们使用了Worker的方式来实现,我们用Worker在后台不断的累加SharedArrayBuffer中的数据,根据CPU的主频大小,它可以实现计数的累加。我们测试这个精度也可以,虽然远低于C代码的精度,但我们积累足够多的次数之后,它还是能够稳定的让我们看出区别。



还有动态遍历缓存的大小,因为不同设备的缓存大小不一样,基于我们前面讨论的方法需要把缓存全部刷新,那么这个缓存大小就很重要。我们现在使用的方法比较粗暴,会从8M一直到128M进行遍历,哪一次可以稳定触发漏洞了,我们就认为在当前的缓存大小中,可以检测出这个漏洞了。为什么不直接选择相对较大的?因为移动端设备上CPU的能力有限,刷新越大的缓存需要CPU执行的时间越长、手机越热,并且过度刷新缓存,稳定性也不太好。所以我们从8M开始遍历,一直到128M。


总结一下,浏览器中利用Spectre漏洞需要解决的问题,首先需要访问大量地址不同的内存来强制刷新缓存,实现稳定的缓存刷新功能。通过将变量放在不同的内存中,再对其进行遍历的访问方式,保证每一次遍历的变量都不在缓存中。通过Worker+SharedArraryBuffer可以做到对内存访问时间的精确计时器,通过动态遍历缓存大小的方式用来适配不同的设备。当我们发布Spectre在线检测工具之后,Google和各个浏览器厂商都强制推出了把SharedArraryBuffer对象禁用的更新,默认浏览器中在那个版本之后不能为我们提供高精度的计时器了,这也是在之后的浏览器中,这个漏洞不太容易写出比较稳定利用的一个问题所在。



这是我们发布的检测工具在Chrome浏览器中进行检测的情况。在64M的时候才可以检查出CPU Spectre漏洞。检测中如果CPU缓存比较大,刷新的缓存比较小,缓存中会有很多残留数据,最终导致大大影响漏洞检测。当时我们是全球首发针对这个漏洞的在线检测工具,为数以成千上万的用户检查出了其设备中存在的漏洞。同时比较出乎意料的是,因为我们最开始的测试环境有限,只在一些Windows机器的Chrome浏览器中进行了相关的漏洞检测,发布之后发现竟然很多意想不到的设备都会收到影响,比如surfacePro、macOS、iPhoneX、pixel2等。



三、如何利用这个漏洞产生实际的攻击



第三部分简单跟大家介绍一下如何利用这个漏洞产生实际的攻击。


我们的在线检测工具是创造一个大的内存,内存中保留我们想要泄露的数据,生成代码来做边界检查,这个设计对于探测漏洞是有效的,但它并不能造成实际的危害。如何造成实际的危害呢?我们选择泄露进程的敏感数据或者对象的虚函数表的指针。


基于前面的研究,这一步相对简单,泄露进程中的敏感数据最重要的是如何布局,要把泄露的数据放在可以泄露内存的array后面,再利用前面所介绍的分支预测的条件,泄露出这一个字节,然后通过一个字节作为array2的index,然后进行访问,最后探测它的缓存到底是在哪一个Block,然后推测出泄露的字节到底是什么。

我们的这个Demo是可以泄露浏览器进程中的用户ID、英文密钥、存储的数据、字符串等等敏感数据。在浏览器漏洞过程中比较重要的是模块的地址,如泄漏虚函数表的地址,我们可以通过内存布局的方式,把我们需要泄露的对象放在可以泄露数据的array后面,可以有效泄露出对象的虚函数表的指针,这就可以为后续漏洞做好准备,包括找到一些模块内的有用逻辑等等。


四、总结


首先,推测执行可以导致CPU中缓存上存在非法数据,通过访问数据的时间差,可以探测泄露的数据内容。通过JavaScript想实现漏洞利用,需要解决很多问题,如何稳定刷行缓存、确保特定的数据不出现在缓存中、高精度时间计时器、动态探测缓存等技巧。想要造成实际的危害,主要需要解决的问题是稳定的内存布局。



整个研究是由我们团队成员共同完成。

Reference:

https://meltdownattack.com/

https://spectreattack.com/spectre.pdf


PPT下载:https://pan.baidu.com/s/1gavoyQyRbuo0oD9vFyPn1A

*转载请注明来自看雪社区




[推荐]看雪企服平台,提供安全分析、定制项目开发、APP等级保护、渗透测试等安全服务!

最后于 2018-7-31 18:10 被kanxue编辑 ,原因:
上一主题 下一主题
打赏 + 1.00
打赏次数 1 金额 + 1.00
收起 
赞赏  junkboy   +1.00 2018/07/31
最新回复 (1)
petersonhz 2019-5-16 00:45
2
0
i5不受影响么?
游客
登录 | 注册 方可回帖
返回