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漏洞产生真正的威胁。
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的值,这样就实现了完整的通过幽灵漏洞来泄露一个字节的整个流程。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2018-7-31 18:10
被kanxue编辑
,原因: