从V8角度分析Spectre漏洞

lucywang 漏洞 2019年5月7日发布
Favorite收藏

导语:2018年1月3日,Google Project Zero(GPZ)团队安全研究员Jann Horn在其团队博客中爆出CPU芯片的两组漏洞,分别是Meltdown与Spectre。

2018年1月3日,Google Project Zero(GPZ)团队安全研究员Jann Horn在其团队博客中爆出CPU芯片的两组漏洞,分别是Meltdown与Spectre。

它们之所以广受重视,是因为它们根据的是体系结构的设计漏洞,而非针对某个系统或者某个程序,因此它几乎可以遍及大多数近代的CPU。

这里主要有三个漏洞: 

1.CVE-2017-5753(边界检查绕过);

2.CVE-2017-5715(分支目标注入);

3.CVE-2017-5754(恶意数据缓存载入);

Spectre主要利用前两个漏洞进行攻击,而meltdown则主要利用第三个漏洞进行攻击。

Meltdown漏洞影响几乎所有的Intel CPU和部分ARM CPU,而Spectre则影响所有的Intel CPU和AMD CPU,以及主流的ARM CPU。

这两个漏洞允许黑客窃取计算机的全部内存内容,包括移动设备、个人计算机、以及在所谓的云计算机网络中运行的服务器。

漏洞原理

这两组漏洞来源于芯片厂商为了提高CPU性能而引入的两种特性:乱序执行(Out-of-Order Execution)和推测执行(Speculative Execution)。

Meltdown漏洞

Meltdown漏洞只发生在Intel处理器上,利用了处理器对乱序(out-of-order)执行处理不当的缺陷。现代处理器为了提高各个运算单元的利用率,不是一次执行一条指令,而是综合考虑当前指令和后续几条指令,一次批量调度多条指令到空闲的运算单元并行执行,所以代码里指令的先后关系并不一定是指令执行的先后关系。

如果用户空间一条指令读取了内核的内存地址,正常情况下该指令会导致陷入(trap),从而终止该指令的执行。但由于乱序原因,该指令后续指令可能会在trap发生之前就得到执行。如果后续是精细策划的恶意代码,就能够抓住上面提到的处理器漏洞,把“一闪而过”的内核数据“藏匿”在缓存中,偷运出去。

Spectre漏洞

和Meltdown不同,Intel,AMD和ARM都有Spectre漏洞。该漏洞利用了处理器在跳转预测(branch prediction)失效处理不当的缺陷。

简而言之,就是攻击者利用CPU的推测执行机制,暂时绕过代码中的隐式和显式安全检查,以防止程序读取内存中的未授权数据。虽然处理器的推测被设计为微架构的细节,在架构层面是不可见的,但精心设计的程序可以在推测中读取未经授权的信息,并通过诸如程序片段的执行时间(黑客利用短短的时间窗口)之类的侧通道将其公开。

当JavaScript被证明可以用来安装Spectre攻击时,V8团队就开始从 V8 角度分析 Spectre 漏洞。V8使用C++开发,并在谷歌浏览器中使用。在运行JavaScript之前,相比其它的JavaScript的引擎转换成字节码或解释执行,V8将其编译成原生设备码(IA-32, x86-64, ARM, or MIPS CPUs),并且使用了如内联缓存(inline caching)等方法来提高性能。有了这些功能,JavaScript程序在V8引擎下的运行速度媲美二进制程序。

对Spectre漏洞的攻击由两部分组成:

1.将无法访问的数据泄漏到隐藏的CPU状态,所有已知的Spectre攻击都使用推测将无法访问的数据位泄漏到CPU缓存中。

2.提取隐藏状态以恢复无法访问的数据,为此,攻击者需要一个足够精确的计时器。令人惊讶的是,低精确度时钟就足够了,尤其是使用边缘阈值等技术。

由于V8的开发者不知道如何彻底解决这两个进程,因此他们只能设计并部署一些缓解措施,这些措施可以极大的减少泄漏到CPU缓存中的信息量,缓解措施使恢复隐藏状态变得非常困难。

处理器在性能增强的同时,行为越来越智能,充分挖掘潜力,优化计算资源,这本来是件好事。Intel是这方面的先行者,包括超线程在内都做得很成功。然而如今Intel面临的局面,罪魁祸首也是这种激进的优化策略。因为对于这些推测执行的指令,由于不知道最终是否会采纳,处理器是在一种“试探”的方式下运行,现在问题就出在这个“试探”机制上。

执行这些指令时,处理器没有做严格的权限验证。例如入侵者从用户空间读取内核地址。推测执行时攻击者首先把数据从内核里读出来,然后再检查这个读操作是否合法的危险方式。这就好比你发现有人闯入密室,慌忙打开灯,结果强盗把你的宝藏看得清清楚楚,你发现有强盗马上又把灯关上。在这些推测执行的指令里,攻击者竟然能够在处理器察觉到越权之前的短暂一刻获得这些内核数据。这个时间窗口很短暂,只有纳秒数量级(大约十亿分之一秒),但足够执行数十条指令,发起一次攻击。

缓解措施

降低计时器的精度

为了精准地抓住这个时差,攻击者程序需要高精度计时器。 糟糕的是,CPU就提供了这样的计时器,但Web平台不会公开它们。Web平台最精确的计时器performance.now(),这个时间戳实际上并不是高精度的。为了降低像Spectre这样的安全威胁,各类浏览器对该类型的值做了不同程度上的四舍五入处理。(Firefox从Firefox 59开始四舍五入到2毫秒精度)一些浏览器还可能对这个值作稍微的随机化处理,这个值的精度在未来的版本中可能会再次改善。

和JavaScript中其他可用的时间类函数(比如Date.now)不同的是:1.window.performance.now()返回的时间戳没有被限制在一毫秒的精确度内,而它使用了一个浮点数来达到微秒级别的精确度。

2.另外一个不同点是,window.performance.now()是以一个恒定的速率慢慢增加的,它不会受到系统时间的影响(可能被其他程序调整)。另外,performance.timing.navigationStart + performance.now() 约等于 Date.now()。

另外在Chrome中,计时器的精度从5微秒范围降低到100微秒范围,并引入随机均匀抖动来防止恢复精确度。在与V8的所有供应商协商之后,他们决定一起采取前所未有的措施,立即在所有浏览器上禁用SharedArrayBuffer API,以防止任何人构建一个纳秒级计时器,用于Spectre攻击。

提高计时器的精度

与上面的方法正好相反,那就是处理器的计时器精度比捕获漏洞的时间还短。

最初提高计时器的精度这样的方法被认为是无法用于缓解漏洞的,然而两年前,一个专门研究微架构攻击的学术研究团队发表了一篇论文,研究了网络平台中计时器在缓解中的可用性。他们得出结论,并发可变共享内存和各种精确度恢复技术可以构建甚至更高精确度的计时器,精确度可达纳秒级。这样的计时器足够精确,可以检测单个L1高速缓存命中和未命中的情况,Spectre gadget通常就是这样泄漏信息的。

比如V1攻击可以用于绕过内存访问的边界检查,核心是利用了推测执行可以执行条件分支语句之后的指令这一性质。攻击者可以利用V1攻击来执行特定的代码片段(gadget),获取其无权限获取的内存空间的内容。

译者注:L1高速缓存也就是V8团队经常说的一级缓存。在CPU里面内置了高速缓存可以提高CPU的运行效率,这也正是Pentium II比Celeron快的原因。内置的L1高速缓存的容量和结构对CPU的性能影响较大,容量越大,性能也相对会提高不少,所以这也正是一些公司力争加大L1级高速缓冲存储器容量的原因。

放大(Amplification)技术

在V8团队的进攻性研究中,他们清楚地发现,仅仅依靠以上所说的计时器的缓解措施是远远不够的。一个原因是攻击者可能只是重复执行他们的gadget,这样累积的时间差异就远大于单个缓存命中或未命中。目前V8团队能够设计出可靠的gadget,一次使用多个缓存行,以便进行缓存容量,产生的时间差异高达600微秒。后来,他们又发现了不受缓存容量限制的任意放大技术,这种放大技术依赖于多次读取机密数据的尝试。

JIT缓解措施

为了使用Spectre读取无法访问的数据,攻击者欺骗CPU以推测性方式执行代码,该代码通常读取无法访问的数据并将其编码到缓存中。这种攻击可以通过以下两种方式得到缓解:

1.防止推测执行代码的出现;

2.防止推测执行读取无法访问的数据;

V8团队通过在每个关键条件分支上插入推荐的推测障碍指令(例如英特尔的LFENCE)并使用retpolines作为间接分支来对上述的第一种措施进行试验。所谓的“retpolines” ,就是用一个不触发间接分支预测器的指令序列替换间接分支。显然,Linux社区建议所有编译器和汇编编写者避免在用户空间中的所有间接分支。这意味着,例如,V8团队应该更新rr的手写汇编以避免间接分支。不幸的是,这种缓解措施的条件太过苛刻,以至于大大降低了设备的运行性能(通过Octane基准测试,发现减速2-3倍)。所以最后,V8团队选择了上述的第二种缓解方法,插入缓解序列,防止因错误推测而读取秘密数据。让V8团队用以下代码片段来说明这个缓解技术:

if (condition) {
  return a[i];
}

为了方便讲解,V8团队假设条件是0或1。如果CPU在i超出界限(访问通常不可访问的数据)时从[i]推测性地读取数据,那么上面的代码很容易受到攻击。V8团队发现,在这种情况下,当条件为0时,推测会尝试读取[i]。此时,第二种缓解会重写此程序,使其行为与原始程序完全相同,但不会泄漏任何推测加载的数据。

在测试时,V8团队预留了一个CPU寄存器,V8团队称之为poison,以跟踪代码是否在错误预测的分支中执行。poison寄存器在所有分支中维护并调用生成的代码,因此任何错误预测的分支都会导致poison寄存器变为0。然后V8团队会检测所有内存访问,以便它们无条件地用poison寄存器的当前值屏蔽所有加载的结果。不过这么做的目的并不是妨碍处理器预测(或错误预测)分支,而是用错误预测的分支破坏(潜在的越界)加载值的信息。测试代码如下所示(假设a是数字数组):

let poison = 1;
// …
if (condition) {
poison *= condition;
return a[i] * poison;
}

附加代码对程序的正常(架构定义)行为没有任何影响,它只影响在推测cpu上运行时的微架构状态。如果程序是在源级别进行检测的,那么现代编译器中的高级优化可能会删除此类检测。在V8中,V8团队阻止编译器通过在编译的最后阶段插入缓冲区来删除缓解。

另外,V8团队还使用poison技术来防止在解释器的字节码分派循环和JavaScript函数调用序列中错误推测的间接分支造成的泄漏。在解释器中,如果字节码处理程序(即解释单个字节码的设备代码序列)与当前字节码不匹配,V8团队就将poison设置为0。对于JavaScript调用,V8团队将目标函数作为参数传递(在寄存器中),如果传入的目标函数与当前函数不匹配,V8团队会在每个函数的开头将poison设置为0。随着poison缓解措施的不断改善,V8团队发现在进行Octane基准测试时,设备的性能减速不到20%。

而WebAssembly的缓解措施更简单,因为主要的安全检查是确保内存访问在一定的范围内进行。对于32位平台,除了正常的边界检查之外,V8团队将所有内存填充到下一个次方,并无条件地屏蔽用户提供的内存索引的任何高位。而64位平台则不需要这样的缓解,因为它已经实现了使用虚拟内存保护进行边界检查。现在,V8团队正在尝试将switch/case语句编译为二进制搜索代码,而不是使用潜在易受攻击的间接分支,但这在某些场景中就不好用了,因为有的间接调用受到retpolines的保护。

缓解措施终究不是一个长久之计

事实上,V8团队所进行的攻击性研究比防御性研究进行地更加顺利,这意味着,再好的缓解措施都是权宜之计,是不可能从根子上缓解Spectre攻击的。让缓解措施失效的原因很多,首先,比Spectre攻击厉害的攻击多得是,V8团队面临许多其他更严重的安全威胁,包括由于常规错误导致的直接越界读取(比Spectre更快更直接),越界写入和潜在的远程代码执行。

其次,由于V8本身的日益复杂的缓解措施,实际上可能会增加攻击面并降低设备的性能。第三,测试和维护微架构泄漏的缓解甚至比设计gadget更棘手,因为很难确定缓解措施是否能按着设计进行工作。如上所述,重要的缓解就被后来的编译器优化功能给删除了。

第四,V8团队发现, Spectre的变体出现的速度非常快,这让既定的缓解方法疲于应付。

终极解决办法:网站隔离

最终V8团队得出的结论是,本质上,是不受信任的代码可以使用Spectre和边信道读取进程的整个地址空间。虽然程序缓解降低了许多潜在gadget的有效性,但效率不高或不全面。唯一有效的缓解措施是将敏感数据移出进程的地址空间。值得庆幸的是,Chrome多年来一直在努力将网站分成不同的进程,以减少传统漏洞造成的攻击面。目前,这项付出终究得到了回报,V8团队在2018年5月之前已尽可能多的为平台部署了网站隔离。

本文翻译自:https://v8.dev/blog/spectre如若转载,请注明原文地址: https://www.4hou.com/vulnerable/17809.html
点赞 4
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论