翻译: Spectre Attacks - 幽灵攻击

翻译
Spectre Attacks: Exploiting Speculative Execution 处理器安全方面的经典论文

幽灵攻击: 利⽤推测执⾏

Spectre Attacks: Exploiting Speculative Execution
Paul Kocher 1 ,Jann Horn 2 ,Anders Fogh 3 ,Daniel Genkin 4 , Daniel Gruss 5 ,Werner Haas 6 ,Mike Hamburg 7 ,Moritz Lipp 5 , Stefan Mangard 5 ,Thomas Prescher 6 ,Michael Schwarz 5 ,Yuval Yarom 8 1 Independent (www.paulkocher.com),2 Google Project Zero, 5 G DATA Advanced Analytics, 4 University of Pennsylvania and University of Maryland, 5 Graz University of Technology, 6 Cyberus Technology, 7 Rambus, Cryptography Research Division,8 University of Adelaide and Data61

2019 https://spectreattack.com/spectre.pdf

摘要

现代处理器使用分支预测(branch prediction)和推测执行(speculative execution)来让性能最大化。例如,如果分支的目标取决于正在读取的内存值,CPU将会尝试预测分支目标地址,并提前执行。当内存加载完成时,CPU将放弃或提交推测计算的结果。推测逻辑的执行方式并不安全,可以访问受害者的内存和寄存器,并会执行一些有侧信道副作用的操作。

幽灵(Spectre)攻击涉及诱使受害者会推测执行在正常的程序执行期间不会执行的操作,并且通过侧信道将受害者的机密信息泄露给攻击者。本文描述了实际的攻击,这些攻击结合了来自侧信道攻击、故障攻击和面向返回的编程的方法,可以从受害者的进程中读取任意内存。此外,本文表明,推测执行的实现违反了支撑众多软件安全机制的安全假设基础,包括操作系统进程分离、容器化、即时(JIT)编译,以及针对缓存时间和侧信道攻击的对策。这些攻击对实际系统构成了严重威胁,因为易受攻击的推测执行能力在Intel,AMD和ARM的特定处理器中被验证有效,这些微处理器已用于数十亿台设备。

虽然在某些情况下,临时的针对特定处理器的对策是可行的,但是合理的解决方案需要对处理器设计以及指令集架构(ISAs)进行更新,以便让硬件架构师和软件开发人员对当前实现的CPU计算状态是否允许(或不允许)泄漏达成共识。

I. 介绍

物理设备执行的计算通常会在计算的标准输出之外留下可观察到的副作用。侧信道攻击集中于利用这些副作用来提取不可访问的秘密信息。自90年代末被提出[43]后,许多物理效应比如功耗[41,42]、电磁辐射[58]、或噪声[20]已被用来提取加密密钥及其他秘密。

物理侧信道攻击还可用于从计算机和移动终端等复杂设备中提取秘密信息[21,22]。然而由于这些设备经常执行来自潜在未知来源的代码,因此它们面临着基于软件的攻击形式的额外威胁,且其不需要外部测量设备。有些攻击则利用软件漏洞(如缓冲区溢出[5]或双自由错误[12]),有些软件攻击利用硬件漏洞泄露敏感信息。后者的攻击包括利用缓存时间[8,30,48,52,55,69,74],分支预测历史[1,2],分支目标缓冲区[14,44]或开放DRAM行[56]的微架构攻击。基于软件的技术也被用来发动故障攻击,从而改变物理内存[39]或CPU内部值[65]。

在过去的几十年里,几种微架构设计技术促进了处理器速度的提高。其中一个技术是推测执行,它被广泛用于提高性能,涉及让CPU猜测未来可能的执行方向,并在这些路径上提前执行指令。更具体地,考虑一个示例,其中程序的控制依赖于位于外部物理内存中的未被缓存的值。由于该内存比CPU慢得多,因此通常需要几百个时钟周期才能知道该值。CPU不会通过空闲状态浪费这些周期,而是尝试猜测控制流的方向,保存其寄存器状态的检查点,并在猜测的路径上推测性地执行程序。当值最终从内存中到达时,CPU将检查其最初猜测的正确性。如果猜测是错误的,CPU会通过将寄存器状态恢复到存储的检查点来丢弃错误的推测执行,导致的性能与空闲状态相近。然而如果猜测正确,则提交推测性执行结果,从而在延迟期间完成了有用的工作,产生显著的性能增益。

从安全角度来看,推测执行涉及以可能不正确的方式执行程序。然而由于CPU被设计为通过将错误的推测性执行的结果恢复到其先前的状态来保持功能的正确性,因此这些错误以前被认为是安全的。

A. 我们的结果

在本文中,我们分析了这种不正确的推测执行的安全隐患。我们提出了一类微架构攻击,我们称之为幽灵攻击。在高层次上,幽灵攻击诱使处理器推测性地执行在正确的程序执行下不应该执行的指令序列。因为这些指令对标称CPU状态的影响会被恢复,我们称它们为瞬时指令。通过影响哪些瞬时指令推测性地执行,我们能够从受害者的存储器地址空间内泄漏信息。

我们通过实验证明了幽灵攻击的可行性,即利用瞬时指令序列从非特权的本机代码和可移植的JavaScript代码中泄漏跨安全域的信息。

使用本机代码进行攻击。作为概念验证,我们创建了一个简单的受害者程序,在其内存地址空间中包含秘密数据。接下来,我们搜索编译后的受害者二进制文件和操作系统的共享库,寻找可用于泄漏受害者地址空间信息的指令序列。最后,我们编写了一个攻击者程序,该程序利用CPU的推测执行功能,将之前发现的序列作为瞬时指令执行。使用这种技术,我们能够从受害者的地址空间读取内存,包括存储在其中的秘密。

使用JavaScript和eBPF进行攻击。除了使用本机代码突破进程隔离边界之外,幽灵攻击还可用于突破沙箱隔离,例如,通过可移植的JavaScript代码装载它们。为从经验上证明这一点,我们展示了一个JavaScript程序,它成功地从运行它的浏览器进程的地址空间读取数据。此外,我们还演示了在Linux中利用eBPF解释器和JIT的攻击。

B. 我们的技术

在高层次上,幽灵攻击通过微架构隐蔽信道将推测执行与数据泄漏相结合,从而违反内存隔离边界。更具体地,为了发起幽灵攻击,攻击者通过在进程地址空间内定位或引入指令序列来开始,该指令序列在执行时充当泄漏受害者的存储器或寄存器内容的隐蔽信道发送器。然后,攻击者欺骗CPU以推测方式错误地执行此指令序列,从而通过隐蔽信道泄漏受害者的信息。最后,攻击者通过隐蔽信道检索受害者的信息。虽然由该错误的推测执行导致的对标称CPU状态的改变最终被恢复,但是先前泄漏的信息或对CPU的其他微架构状态(例如缓存内容)的改变可以在标称状态恢复之后继续存在。

上述对幽灵攻击的介绍是一般性的,需要通过一种引发错误推测执行的方法以及一个微架构隐蔽信道来具体的实例化。隐蔽信道组件有许多选择,本文中描述的实现使用基于缓存的隐蔽信道[64],即Flush+Reload[74]和Evict+Reload[25,45].

我们现在继续介绍我们诱导和影响错误推测执行的技术。

变体1:利用条件分支。在幽灵攻击的这种变体中,攻击者误训练CPU的分支预测器,使其预测错误的分支方向,导致CPU暂时违反程序语义去执行原本不会执行的代码。正如我们所展示的,这种不正确的推测执行允许攻击者读取存储在程序地址空间中的秘密信息。实际上,考虑以下代码示例:

if (x < array1_size)
    y = array2[array1[x] * 4096];

在上面的示例中,假设变量x包含攻击者控制的数据。为了确保对array1的内存访问的有效性,上面的代码包含一个if语句,其目的是验证x的值是否在合法范围内。我们将展示攻击者如何绕过此if语句,从进程的地址空间读取潜在的秘密数据。

首先,在最初的误训练(mistrain)阶段,攻击者使用有效输入调用上述代码,从而训练分支预测器以预期if将为真。接下来,在攻击阶段,攻击者在array1的边界之外调用值为x的代码。CPU没有等待分支结果的确定,而是猜测边界检查将为真,并且已经推测性地执行使用恶意x来计算指令array2[array1[x]*4096]。请注意,从array2加载到缓存的地址取决于使用恶意值xarray1[x],通过放大使其访问不同的缓存行并避免硬件预取效应。

当边界检查的结果最终确定时,CPU会发现其错误,并恢复对其标称微架构状态所做的任何更改。但是对缓存状态所做的更改不会被还原,因此攻击者可以分析缓存内容,从而找到在受害者内存被越界读取时获取的可能的秘密字节的值。

变体2:利用间接分支。借鉴面向返回的程序设计(ROP)[63],在该变体中,攻击者从受害者的地址空间中选择一个 gadget ,并影响受害者推测性地执行该 gadget 。与ROP不同,攻击者不依赖于受害者代码中的漏洞。相反,攻击者训练分支目标缓冲区(BTB)错误预测从间接分支指令到 gadget 地址的分支,从而导致 gadget 的推测执行。如前所述,虽然不正确的推测执行对CPU标称状态的影响最终会恢复,但它们对缓存的影响不会恢复,从而允许 gadget 通过缓存侧信道泄漏敏感信息。我们从经验上证明了这一点,并展示了如何通过仔细选择 gadget 来允许该方法从受害者那里读取任意内存。

为了误训练BTB,攻击者在受害者的地址空间中找到 gadget 的虚拟地址,然后对该地址执行间接分支。这种训练是从攻击者的地址空间完成的。在攻击者的地址空间中, gadget 地址上驻留的内容并不重要;所有这一切要求攻击者在训练期间的虚拟地址与受害者的虚拟地址匹配(或别名)。事实上只要攻击者处理了异常,即使攻击者的地址空间中没有映射到 gadget 虚拟地址的代码,攻击也能奏效。

其他变体。进一步的攻击可以通过改变实现推测执行的方法和用于泄漏信息的方法来设计。示例包括误训练返回指令、通过时序变化泄漏信息,以及算术单元上的争用。

C. 目标硬件和当前状态

硬件。我们已经通过实验验证了多款英特尔处理器在幽灵攻击下的脆弱性,包括Ivy Bridge、Haswell、Broadwell、Sky lake和Kaby Lake处理器。我们还验证了该攻击对AMD Ryzen CPUs的适用。最后,我们还成功地对流行手机中的几款基于ARM的三星和高通处理器发起了幽灵攻击。

当前状态。使用负责任的披露实践,本文中不相交的作者团队向部分重叠的CPU供应商组和其他受影响的公司提供了我们的初版结果。在与业界的协调下,作者们还参与了对结果的封锁。幽灵系列攻击被记录在CVE-2017-5753和CVE-2017-5715中。

D. 熔断攻击

熔断攻击(Meltdown Attack)[47]是一种相关的微架构攻击,它利用乱序执行来泄漏内核内存。熔断攻击在两个主要方面不同于幽灵攻击。首先与幽灵攻击不同,熔断攻击不使用分支预测。相反,它依赖于这样一种观察,即当一条指令导致陷阱时,后续指令在被终止之前会被乱序执行。其次,熔断攻击利用了许多Intel和某些ARM处理器特有的漏洞,允许某些推测执行的指令绕过内存保护。结合这些问题,熔断攻击从用户空间访问内核内存。这种访问会导致陷阱,但在发出陷阱之前,访问之后的指令会通过缓存隐蔽信道泄漏被访问内存的内容。

相比之下,幽灵攻击适用于更广泛的处理器,包括大多数AMD和ARM处理器。此外,KAISER机制[29]已被广泛用作熔断攻击的缓解措施,但它无法抵御幽灵攻击。

II. 背景

在本节中,我们将介绍现代高速处理器的一些微架构组成,它们如何提高性能,以及如何从运行的程序中泄漏信息。我们还介绍了面向返回的编程(ROP)和 gadget

A. 乱序执行

乱序执行机制通过允许程序的指令流中更下游的指令与前面的指令并行执行,有时在之前执行,从而提高处理器组件的利用率。

现代处理器在内部使用微指令,模拟体系结构的指令集,即将指令解码为微指令[15]。一旦与一条指令对应的所有微指令以及前面的所有指令都完成,这些指令就可以失效,提交对寄存器和其他体系结构状态的更改,并释放重新排序的缓冲空间。因此,指令按程序执行顺序失效。

B. 推测执行

处理器通常不知道程序的未来指令流。例如,当向外执行到达其方向依赖于其执行尚未完成的在前指令的条件分支指令时,就会发生这种情况。在这种情况下,处理器可以保留其当前寄存器状态,对程序将遵循的路径进行预测,并沿着该路径推测性地执行指令。如果预测被证明是正确的,则提交推测执行的结果(即保存),从而在等待期间产生优于空闲的性能优势。否则,当处理器确定它遵循错误的路径时,它通过恢复其寄存器状态并沿着正确的路径恢复来放弃它推测地执行的工作。

我们将被错误执行(即作为错误预测的结果)但可能留下微架构痕迹的指令称为瞬时指令。尽管推测性执行会保存程序的体系结构状态,就像执行遵循正确的路径一样,但微架构元素可能处于与瞬时执行之前不同(但有效)的状态。

现代CPU上的推测执行可以提前运行数百条指令。该限制通常由CPU中的重新排序缓冲区的大小决定。例如,在Haswell微架构上,重新排序缓冲区有足够的空间容纳192个微指令[15]。由于微指令数量与指令数量之间不存在一一对应的关系,因此限制取决于所使用的指令。

C. 分支预测

在推测执行期间,处理器对分支指令的可能结果进行预测。更好的预测通过增加可以成功提交的推测执行操作的数量来提高性能。

现代Intel处理器(例如,Haswell Xeon处理器)的分支预测器具有用于直接和间接分支的多个预测机制。间接分支指令可以跳转到运行时计算的任意目标地址。例如,x86指令可以跳转到寄存器、内存位置或堆栈上的地址,例如jmp eaxjmp [eax]ret。 ARM(例如,MOV pc, r14)、MIPS(例如,jr $ra)、RISC-V(例如,jalr x0,x1,0)和其他处理器也支持间接分支。为了补偿与直接分支相比的额外灵活性,使用至少两种不同的预测机制来优化间接跳转和调用[35]。

英特尔[35]对处理器预测介绍如下:

  • 以静态或单调的方式“直接调用和跳转”,
  • “间接调用和跳转”可以是单调的,也可以是变化的,这取决于最近的程序行为
  • “条件分支”分支目标以及是否将执行分支。

因此,使用若干处理器组件来预测分支的结果。分支目标缓冲器(BTB)保存从最近执行的分支指令的地址到目的地址的映射[44]。处理器甚至可以在解码分支指令之前使用BTB预测未来的代码地址。Evtyushkin等人[14]分析了Intel Haswell处理器的BTB,并得出结论,只有分支地址中最不重要的31位用于索引BTB。

对于条件分支,记录目标地址对于预测分支的结果不是必需的,因为在运行时确定条件时,目标通常在指令中编码。为了改进预测,处理器维护分支结果的记录,包括最近的直接分支和间接分支。Bhattacharya等人[9]分析了近期英特尔处理器中分支历史预测的结构。

尽管返回指令是一种间接分支,但在现代CPU中,通常使用一种单独的机制来预测目标地址。返回堆栈缓冲区(RSB)维护调用堆栈最近使用部分的副本[15]。如果RSB中没有可用的数据,不同的处理器将暂停执行或将BTB用作备用[15]。

分支预测逻辑,例如BTB和RSB,通常不会在物理核心之间共享[19]。因此,处理器只能从同一内核上执行的先前分支中学习。

D. 内存层次结构

为了弥补速度较快的处理器和较慢的内存之间的速度差距,处理器使用依次较小但较快的高速缓存的层次结构。高速缓存将内存划分为固定大小的块,称为行,典型的行大小为64或128字节。当处理器需要来自内存的数据时,它首先检查位于层次结构顶部的L1缓存是否包含副本。在高速缓存命中的情况下,即在高速缓存中找到数据的情况下,从L1高速缓存中检索数据并使用。否则,在高速缓存未命中的情况下,重复该过程以尝试从下一高速缓存级并最终从外部存储器检索数据。一旦读取完成,数据通常被存储在缓存中(并且先前缓存的值被逐出以腾出空间),以防在不久的将来再次需要它。现代英特尔处理器通常具有三个高速缓存级别,每个内核具有专用的L1和L2高速缓存,并且所有内核共享公共的L3高速缓存,也称为末级高速缓存(LLC)。

处理器必须使用缓存一致性协议(通常基于MESI协议)确保每个内核的L1和L2高速缓存是一致的[35]。特别地,MESI协议或其一些变体的使用意味着一个核上的存储器写入操作将导致其他核的L1和L2高速缓存中,相同数据的副本被标记为无效,这意味着其他核上对该数据的未来访问将不能快速地从L1或L2高速缓存加载数据[53,68].当这种情况在特定的内存位置重复发生时,这被非正式地称为缓存行反弹。由于内存是以线粒度缓存的,因此即使两个内核访问映射到同一缓存行的不同邻近内存位置,也会发生这种情况。这种行为称为虚假共享,是众所周知的性能问题的根源[33]。高速缓存一致性协议的这些特性有时会被滥用,以代替使用clflush指令或回收模式的高速缓存回收[27]。这种行为先前被探索为促进Rowhammer攻击的潜在机制[16]。

E. 微架构侧信道攻击

我们上面讨论的所有微架构组件都通过预测未来的程序行为来提高处理器性能。为此,它们维护依赖于过去程序行为的状态,并假设将来的行为与过去的行为相似或相关。

当多个程序同时或通过分时在同一硬件上执行时,由一个程序的行为引起的微架构状态的变化可能会影响其他程序。反过来,这可能导致从一个程序到另一个程序的意外信息泄漏[19]。

最初的微架构侧信道攻击利用了时间可变性[43]和L1数据高速缓存的泄漏,来从密码原语中提取密钥[52,55,69]。多年来,信道已在多个微架构组件(包括指令缓存)被演示,包括指令缓存[3]、低级缓存[30,38,48,74]、BTB[14,44]和分支历史[1,2]。攻击目标已经扩大到包括共定位检测[59]、破坏ASLR[14,26,72]、击键监控[25]、网站文件打印[51]和基因组处理[10]。最近的结果包括跨核心和跨CPU攻击[37,75],基于云的攻击[32,76],对可信执行环境的攻击[10,44,61],来自移动代码的攻击[23,46,51],以及其他新攻击技术[11,28,44]。

在这项工作中,我们使用Flush+Reload技术[30,74],及其变体Evict+Reload[25]来泄露敏感信息。使用这些技术,攻击者首先从与受害者共享的缓存中清除缓存行。受害者执行一段时间后,攻击者会测量在与被逐出的缓存行对应的地址执行内存读取所需的时间。如果受害者访问了受监控的缓存行,则数据将在缓存中,并且访问将速度要快。否则,如果受害者尚未访问该行,则读取速度将会很慢。因此,通过测量访问时间,攻击者可以了解受害者是否在逐出和探测步骤之间访问了受监控的高速缓存行。

这两种技术之间的主要区别在于用于从缓存中逐出受监控缓存行的机制。在Flush+Reload技术中,攻击者使用专用机器指令(例如x86的clflush)来逐出该行。使用Evict+Reload,通过对存储行的高速缓存组强制争用来实现逐出,例如,通过访问被加载到高速缓存中的其他存储器位置,并且(由于高速缓存的有限大小)使处理器丢弃(逐出)随后被探测的行。

F. 面向返回的程序设计

面向返回的编程(ROP)[63]是一种技术,它允许劫持控制流的攻击者通过将易受攻击的受害者的代码中发现的机器代码片段(称为 gadget )链接在一起,使受害者执行复杂的操作。更具体地说,攻击者首先在受害者二进制文件中找到可用的 gadget 。每个 gadget 在执行返回指令之前执行一些计算。可以修改堆栈指针(例如,指向写入外部可写缓冲区的返回地址)或覆盖堆栈内容(例如,使用缓冲区溢出)的攻击者可以使堆栈指针指向一系列恶意选择的 gadget 地址的开头。当执行时,每个返回指令从堆栈跳转到目的地地址。因为攻击者控制了这一系列地址,所以每次返回都会有效地跳转到链中的下一个 gadget

III. 攻击概述

幽灵攻击诱使受害者推测性地执行在程序指令被严格序列化有序执行期间不会发生的操作,并且通过隐蔽信道将受害者的秘密信息泄露给攻击者。我们首先介绍利用条件分支误预测的变体(节IV),然后介绍利用间接分支使目标预测失误的变体(节V)。

在大多数情况下,攻击从设置阶段开始,在该阶段,攻击者执行使处理器出错的操作,以便稍后做出可利用的错误推测预测。此外,设置阶段通常包括有助于引发推测执行的步骤,例如操纵高速缓存状态以移除处理器确定实际控制流所需的数据。在设置阶段期间,攻击者还可以准备将用于提取受害者的信息的隐蔽信道,例如,通过执行Flush+ReloadEvict+Reload攻击的冲洗或驱逐部分。

在第二阶段期间,处理器推测性地执行将机密信息从受害者上下文传送到微架构隐蔽信道中的指令。这可以通过让攻击者请求受害者执行动作来触发,例如通过系统调用、套接字或文件。在其他情况下,攻击者可能会利用投机(错误)执行自己的代码以从同一进程获取敏感信息。例如,由解释器、即时编译器或“安全”语言进行沙箱化的攻击代码可能希望读取其不应访问的内存。推测执行可能会通过各种隐蔽信道暴露敏感数据,给出的示例会导致推测执行首先读取攻击者选择的地址上的内存值,然后执行内存操作,以暴露该值的方式修改缓存状态。

在最后阶段,敏感数据被恢复。对于使用Flush+ReloadEvict+Reload的幽灵攻击,恢复过程包括对正在监视的缓存行中的内存地址的访问进行计时。

幽灵攻击仅假设推测执行的指令可以从受害者进程可以正常访问的存储器中读取,例如,不会触发页面错误或异常。因此,幽灵攻击与熔断攻击[47]正交,它利用了某些CPU允许乱序执行用户指令,以读取内核内存的场景。因此,即使处理器阻止用户进程中指令的推测执行访问内核内存,幽灵攻击仍然有效[17]。

IV. 变体1:利用条件分支预测失误

在本节中,我们将演示攻击者如何利用条件分支预测失误从另一个上下文(例如,另一个进程)读取任意内存。

考虑以下情况:例1是从不可信的源接收无符号整数x的函数(例如,系统调用或库)的一部分。运行代码的进程可以访问大小为array1_size的无符号字节数组array1和大小为1MB的第二个字节数组array2

if(x < array1_size)
    y = array2[array1[x] * 4096];

例1:条件分支示例

代码片段以对x的边界检查开始,这对安全性至关重要。特别是,此检查可防止处理器读取array1外部的敏感内存。否则,超出界限的输入x可能触发异常,或者可能导致处理器通过提供x = (要读取的秘密字节的地址)+(array1的基址)来访问敏感存储器。

图片见原文

图1:在边界检查的正确结果已知之前,分支预测器继续最可能的分支目标,如果结果被正确预测,则导致整体执行加速。但是,如果边界检查被错误地预测为真,则攻击者可以在某些情况下泄漏机密信息。

图1结合推测执行说明了边界检查的四种情况。在边界检查的结果已知之前,CPU通过预测比较的最可能结果来推测地执行遵循该条件的代码。边界检查的结果不能立即知道的原因有很多,例如,边界检查之前或期间的高速缓存未命中、边界检查所需的执行单元的拥塞、复杂的算术依赖性或嵌套推测执行。然而,如图所示,在这些情况下,条件的正确预测导致更快的整体执行。

不幸的是,在推测执行期间,边界检查的条件分支可能会遵循不正确的路径。在此示例中,假设攻击者使其代码运行,使得:

  • x的值被恶意选择(超出边界),使得array1[x]解析为受害者存储器中某处的秘密字节k
  • array1_sizearray2未缓存,但缓存了k
  • 先前的操作接收到有效x值,导致分支预测器假if可能为真。

该缓存配置可以自然地发生,或者可以由攻击者创建,例如,通过触发逐出(eviction)array1_sizearray2,使内核在合法操作中使用密钥。

当上面编译的代码运行时,处理器开始将恶意值xarray1_size进行比较。读取array1_size会导致缓存未命中,并且处理器将面临相当长的延迟,直到其值可从DRAM中获得。特别是,如果分支条件或分支之前某处的指令等待未缓存的参数,则可能需要一些时间才能确定分支结果。同时,分支预测器假设IF为真。因此,推测执行逻辑将x加到array1的基地址,并从存储器子系统请求结果地址处的数据。该读取是高速缓存命中,并且快速返回秘密字节k的值。然后,推测执行逻辑使用k来计算array2[k*4096]的地址。然后,它发送从内存读取此地址的请求(导致缓存未命中)。当来自阵列2的读取已经在进行中时,可以最终确定分支结果。处理器意识到其推测性执行是错误的,并倒回其寄存器状态。然而,来自array2的推测性读取以地址特定的方式影响高速缓存状态,其中地址取决于k

为了完成攻击,攻击者测量array2中的哪个位置被带入缓存,例如,通过Flush+ReloadPrime+Probe。这揭示了k的值,因为受害者的推测执行缓存了array2[k*4096]。类似的,攻击者也可以使用Evict+Time,即立即使用界内值x再次调用目标函数,并测量该第二次调用花费多长时间。如果array1[x]等于k,则在array2中访问的位置在高速缓存中,并且操作趋于更快。

使用此变体,许多不同的场景都可能导致可利用的泄漏。例如,错误预测的条件分支可以检查先前计算的安全结果或对象类型,而不是执行边界检查。类似地,推测执行的代码可以采取其他形式,例如将比较结果泄漏到固定的存储器位置中,或者可以分布在更大数量的指令上。上述高速缓存状态也比可能需要的更具限制性。例如,在一些场景中,即使array1_size被高速缓存,如即使比较中涉及的值是已知的,如果在推测执行期间应用分支预测结果,攻击也会起作用。根据处理器的不同,推测执行也可能在各种情况下启动。更多的变体将在第VI节中讨论。

A. 实验结果

我们在多个x86处理器架构上进行了实验,包括Intel Ivy Bridge(i7-3630QM)Intel Haswell(i7-4650U)Intel Broadwell(i7-5650U)Intel Sky lake(GoogleCloud上 未指 定的 Xeon, I5-6200U, i7-6600U, i76700K)Intel Kaby Lake(i7.7660U)AMD Ryzen。在所有这些CPU上都发现了幽灵漏洞。在32位和64位模式以及Linux和Windows上都观察到了类似的结果。一些基于ARM架构的处理器也支持推测执行[7],我们在高通Snap dragon 835SoC(配备高通Kyro280CPU)和三星Exynos 7420O ctaSoC(配备Cortex-A57Cortex-A53 CPU)上进行的初步测试证实,这些ARM处理器受到了影响。我们还观察到,推测执行可以远远领先于指令指针。在Haswell i7-4650U上,代码附录C(参见第IV-B节)在 “if”语句和访问array1/array2的行之间的源代码中插入了多达188条简单指令,这些指令刚好位于该处理器的重新排序缓冲区中的192个微指令之下(参见第II-B节)。

B. C中的示例实现

附录C包括一个用于x86处理器1的概念验证代码,该代码与第IV节中的描述密切相关。未优化的实现可以在i7-4650U上以10KB/s读取的数据,错误率较低(<0.01%)。

C. JavaScript中的示例实现

我们在JavaScript中开发了一个概念验证,并在Google Chrome版本62.0.3202中对其进行了测试,该版本允许网站从其运行的进程中读取私有内存。代码如例2所示。

在分支预测器偏移通过时,将索引设置(通过位操作)为范围内的值。在最后一次迭代中,将索引设置为SimpleBytearray中的越界地址。我们使用变量localJunk来确保操作不会被优化。根据ECMAScript 5.1第11.10节[13],“|0”操作将值转换为32位整数,作为JavaScript解释器的优化提示。与其他优化的JavaScript引擎一样,V8执行即时编译(just-in-time),将JavaScript转换为机器语言。虚拟操作被放置在代码周围例2将SimpleBytearray.length存储在本地内存中,以便在攻击期间可以将其从缓存中删除。看到例3用于从D8得到的反汇编输出。

由于无法从JavaScript访问clflush指令,因此我们改为使用缓存逐出[27,51],即我们以这样的方式访问其他存储器位置,使得目标存储器位置随后被逐出。泄漏的结果被传达通过probeTable[n * 4096]的缓存状态$n \in 0..255$,因此攻击者必须逐出这256条缓存行。 长度参数(JavaScript代码中的simpleByteArray.length和反汇编中的[ebp-0xe0])也需要逐出。JavaScript不提供对rdtscp指令的访问,并且Chrome故意降低其高分辨率计时器的准确性,以阻止使用performance.now()[62]的计时攻击。然而,HTML5的Web Workers特性使得创建一个单独的线程变得简单,该线程可以反复递减一个在共享内存位置中的值[24,60]。该方法能产生一个提供足够精确的高精度定时器。

D. 利用eBPF的示例实现

作为利用条件分支的第三个示例,我们开发了一个可靠的概念验证,它通过滥用eBPF(扩展BPF)接口,在没有针对幽灵的补丁的情况下,从未经修改的Linux内核中泄漏内核内存。eBPF是基于Berkeley数据包过滤器(BPF)的Linux内核接口[49]且可用于各种目的,包括根据数据包的内容对其进行过滤。eBPF允许非特权用户在内核上下文中触发用户提供的、内核验证的eBPF字节码的解释或JIT编译以及后续执行。攻击的基本概念类似于针对JavaScript的攻击的概念。

在此攻击中,我们仅对推测执行的代码使用eBPF代码。我们使用用户空间中的本地代码来获取隐信道信息。这与上面的JavaScript示例不同,在上面的示例中,两个函数都是用脚本语言实现的。为了推测性地访问用户空间内存中依赖于秘密的位置,我们执行对内核内存中的数组的推测性越界内存访问,其索引足够大,可以访问用户空间内存。概念验证假设目标处理器不支持管理员模式访问保护(SMAP)。然而,没有这种假设的攻击也是可能的。它在Intel Xeon Haswell E5-1650v3上进行了测试,它可以在eBPF的默认解释模式和非默认JIT编译模式下工作。在高度优化的实施中,我们能够在此设置中泄漏高达2000B/s。它还在AMD Pro A8-9600 R7处理器上进行了测试,在该处理器上,它只能在非默认的JIT编译模式下工作。我们将对这一原因的调查留待今后的工作。

eBPF子系统管理存储在内核存储器中的数据结构。用户可以请求创建这些数据结构,然后可以从eBPF字节码访问这些数据结构。为了加强这些操作的内存安全性,内核存储与每个这样的数据结构相关联的一些元数据,并对这些元数据执行检查。特别地,元数据包括数据结构的大小(当数据结构被创建并用于防止越界访问时被设置一次)和来自被加载到内核中的eBPF程序的引用的数量。引用计数跟踪有多少引用该数据结构的eBPF程序正在运行,从而确保在加载的eBPF程序引用该数据结构时不会释放属于该数据结构的内存。

我们通过滥用假共享来增加针对eBPF管理的数组的长度的边界检查的延迟。内核将数组长度和引用计数存储在同一缓存行中,允许攻击者将包含数组长度的缓存行移动到另一个处于修改状态的物理CPU内核上(参见[16,53]).这 是通过加载和丢弃引用另一物理核心上的eBPF阵列的eBPF程序来完成的,这使得内核递增和递减另一物理核心上的阵列的引用计数器。该攻击在Haswell CPU上实现了大约5000B/s的泄漏率。

E. 恢复数据的准确性

幽灵攻击可以高精度地揭示数据,但由于多种原因可能会出现错误。发现内存位置是否被缓存的测试通常使用计时测量,其准确性可能受到限制(例如在JavaScript或许多ARM平台中)。因此,可能需要多次攻击迭代才能做出可靠的判断。如果array2元素意外地被高速缓存,例如,由于硬件损坏、操作系统活动或访问存储器的其他进程(例如,如果array2对应于其他进程正在使用的共享库中的存储器),则也可能发生错误。攻击者可以重做导致array2中没有元素或2+个元素被缓存的攻击传递。在英特尔Sky lake和Kaby Lake处理器上,使用这种简单的重复标准(但没有其他错误纠正)和精确的基于RDTSCP的时序的测试产生了大约0.005%的错误率。

1 if (index < simpleBytearray.length){ 
2     index = simpleBytearray[index| 0];
3     index = (((index * 4096)|0) & (32 * 1024 * 1024 - 1))|0;
4     localJunk ˆ= probeTable[index|0]|0;
5 }

例2:通过JavaScript实现推测执行的利用。

1 cmpl r15,[rbp-0xe0]           ; Compare index (r15) against simpleBytearray.length
2 jnc 0x24dd099bb870            ; If index >= length, branch to instruction after movq below
3 rex.w leaq rsi,[r12+rdx*1]    ; Set rsi = r12 + rdx = addr of first byte in simpleBytearray
4 movzxbl rsi,[rsi+r15*1]       ; Read byte from address rsi+r15 (= base address + index)
5 shll rsi,12                   ; Multiply rsi by 4096 by shifting left 12 bits
6 andl rsi,0x1ffffff            ; AND reassures JIT that next operation is in-bounds
7 movzxbl rsi,[rsi+r8*1]        ; Read from probeTable
8 xorl rsi,rdi                  ; XOR the read result onto localJunk
9 rex.w movq rdi,rsi            ; Copy localJunk into rdi

例3:例2中JavaScript示例的反汇编。

图片见原文

图2:分支预测器在攻击者中(错误)训练受控上下文A。在上下文B中,分支预测器基于来自上下文A的训练数据进行预测,导致在攻击者选择的地址处进行推测执行,该地址对应于受害者地址空间中的幽灵 gadget 的位置。

V. 变体2:毒害间接分支

在本节中,我们将演示攻击者如何毒害间接分支,以及如何利用间接分支的错误预测从另一个上下文(例如,另一个进程)读取任意内存。间接分支通常用于所有体系结构的程序中(参见第II-C节)。如果间接分支的目的地址的确定被延迟,例如由于高速缓存未命中,则推测执行通常将在从先前代码执行预测的位置继续。

在幽灵变体2中,敌手用恶意目的地扰乱分支预测器,使得推测执行在敌手选择的位置继续。中对此进行了说明参见图2,其中分支预测器在一个上下文中被(误)训练,并且在不同的上下文中应用该预测。更具体地说,攻击者可以将推测执行错误地指向在合法程序执行期间永远不会发生的位置。由于推测执行留下了可测量的副作用,这是一个极端的攻击者的强大手段,例如,即使在没有可利用的条件分支错误预测的情况下,也会暴露受害者内存(参见部分IV)。

对于一个简单的攻击示例,我们考虑攻击者试图读取受害者的内存,当发生间接分支时,受害者可以控制两个寄存器。这通常发生在现实世界的二进制文件中,因为当寄存器包含攻击者控制的值时,操作外部接收的数据的函数通常会进行函数调用。经常是这些被调用的函数会忽略这些值,而只是在函数序言中将它们压入堆栈,并在函数尾声中恢复它们。

攻击者还需要找到一个“幽灵 gadget ”,即一个代码片段,其推测执行将把受害者的敏感信息转移到隐蔽信道中。对于该示例,简单而有效的 gadget 将由两个指令(不一定需要相邻)形成,其中第一个指令是加法(或异或、减法等)。由攻击者控制的寄存器R1寻址到攻击者控制的寄存器R2上的内存位置,后面是访问R2中该地址的内存的任何指令。在这种情况下, gadget 为攻击者提供了(通过R1)对泄漏地址的控制,以及(通过R2)对泄漏内存如何映射到由第二条指令读取的地址的控制。在我们测试的CPU上, gadget 必须驻留在内存中,由受害者执行,以便CPU执行推测执行。然而,由于有几兆字节的共享库映射到大多数进程中,[25],攻击者有足够的空间来搜索 gadget ,甚 至不必搜索受害者自己的代码。

许多其他攻击是可能的,这取决于攻击者已知或控制的状态、攻击者所寻求的信息驻留的位置(例如,寄存器、堆栈、存储器等)、攻击者控制推测执行的能力、可用于形成 gadget 的指令序列以及可从哪些信道泄漏信息投机活动。例如,如果攻击者能够简单地在一条指令上诱导推测执行,从而将寄存器中指定地址的内存带入缓存,那么在寄存器中返回秘密值的加密函数可能会被利用。同样,尽管上面的示例假设攻击者控制两个寄存器,但对于某些 gadget 来说,攻击者对单个寄存器、堆栈上的值或内存值的控制是足够的。

在许多方面,利用漏洞与面向返回的编程(ROP)类似,只是正确编写的软件易受攻击, gadget 的持续时间有限,但不需要完全终止(因为CPU最终会识别推测错误), gadget 必须通过侧信道而不是显式地过滤数据。不过,推测执行可以执行复杂的指令序列,包括从堆栈读取、执行算术、分支(包括多次)和读取内存。

在x86处理器上错误训练分支预测器。攻击者根据自己的上下文对分支预测器进行错误训练,以诱使处理器在运行受害者代码时推测性地执行 gadget 。我们的攻击过程模仿受害者的分支模式,导致分支被误导。

请注意,不同CPU的历史误训练要求各不相同。例如,在Haswell i7-4650U上,使用了大约29个先前目标地址的低20位,尽管观察到了对这些地址的进一步散列。在AMD Ryzen上,只使用大约前9个分支的低12位。附录A中提供了用于更新英特尔至强Haswell E5-1650v3上分支历史缓冲区的逆向工程伪代码。

此外,我们在攻击者和受害者进程中的同一虚拟地址处设置了误训练跳转。请注意,这可能不是必需的,例如,如果CPU仅根据跳转地址的低位对预测进行索引。当错误训练分支预测器时,我们只需要模拟虚拟地址;物理地址、时间和进程ID似乎无关紧要。由于分支预测不受其他内核上操作的影响(参见第II-C节),因此必须在同一CPU内核上进行任何误训练。

我们还观察到分支预测器从跳转到非法目的地的过程中学习。尽管在攻击者的过程中触发了异常,但这很容易被捕获,例如,在Linux上使用信号处理程序或在Windows上使用结构化异常处理。与前一种情况一样,分支预测器随后将做出预测,将其他进程发送到同一目标地址,但在受害者的虚拟地址空间(即 gadget 所在的地址空间)中。

A. 实验结果

与我们关于条件分支预测失误(参见第IV-A节)的结果类似,我们观察到多个x86处理器架构上的间接分支毒化,包括英特尔常春藤桥(i7-3630QM)、英特尔哈斯韦尔(i7-4650U)、英特尔布罗德韦尔(i7-5650U)、英特尔天湖(谷歌云上未指定的至强、i5-6200U、i7-6600U、i7-6700K),英特尔 Kaby Lake(i7-7660U)、AMD Ryzen以及一些ARM处理器。我们能够在32位和64位模式以及不同的操作系统和虚拟机监控程序上观察到类似的结果。

为了衡量分支毒化的有效性,我们实施了一个测试受害者程序,该程序重复执行32个间接跳转的固定模式,使用clflush刷新最终跳转的目标地址,并在探测内存位置上使用Flush+Reload。受害者程序还包括一个测试 gadget ,可以读取探针位置,并且永远不会被合法执行。我们还实施了一个攻击程序,该程序重复执行31次间接跳转,其目的地与受害者序列中的前31次跳转相匹配,然后间接跳转到受害者 gadget 的虚拟地址(但在攻击过程中,该地址的指令会将控制流返回到第一次跳转)。

在Haswell(i7-4650U)处理器上,受害者进程每秒执行270万次迭代,攻击成功毒害了99.7%的最终跳跃。在Kaby Lake(i7-7660U)处理器上,受害者每秒执行310万次迭代,毒化率为98.6%。当攻击进程停止或在另一个内核上执行时,在探测位置未观察到虚假缓存命中。因此,我们得出结论,间接分支毒化非常有效,包括速度远远高于攻击者试图毒化的典型受害者程序执行给定间接跳转的速度。

B. Windows上间接分支毒化的概念验证

作为概念验证,我们构建了一个简单的目标应用程序,该应用程序提供计算密钥和输入消息的SHA1散列的服务。该实现由一个程序组成,该程序连续运行一个循环,该循环调用Sleep(0),从文件加载输入,调用Windows加密函数来计算散列,并在输入发生变化时打印散列。我们发现Sleep()调用是使用寄存器ebx、edi和攻击者已知的edx值中输入文件的数据完成的,即两个寄存器的内容由攻击者控制。这是本节开头介绍的幽灵gadget类型的输入标准。

搜索受害者进程的可执行内存区域,我们在ntdll中识别出一个字节序列。dll(在Windows8和Windows10上),它形成以下(可能未对齐)指令序列,用作幽灵攻击 gadget

adc edi,dword ptr [ebx+edx+13BE13BDh]
adc dl,byte ptr [edi]

利用攻击者控制的ebxedi推测性地执行此 gadget ,可以让攻击者读取受害者的内存。攻击者将edi设置为探测器阵列的基址,例如共享库中的内存区域,并将ebx设置为m−0x13BE13BD−edx。因此,第一条指令从地址m读取32位值,并将其添加到edi中。然后,第二条指令将探测数组中的索引m提取到缓存中。类似的 gadget 也可以在第一条指令的字节读取中找到。

对于间接分支毒化,我们的目标是Sleep()函数的第一条指令,因为ASLR,每次重新启动时,跳转目标的位置和目标本身都会发生变化。为了让受害者推测性地执行 gadget ,包含跳转的内存位置从缓存中溢出,分支预测器错误地将推测执行发送到幽灵 gadget 中。由于包含跳转目的地的内存页被映射为写时复制,我们可以通过修改Sleep()函数的攻击者副本、将跳转目的地更改为gadget地址并在那里放置ret指令来错误训练分支预测器。然后通过多次从多个线程跳转到gadget地址来完成误训练。

Win32上的代码ASLR只会更改几个地址位,因此只需尝试几个组合即可找到对受害者有效的训练序列。使用一个由指令sbbeax[esp+ebx]组成的指令 gadget 来定位堆栈。

在攻击过程中,使用了一个单独的线程来错误训练分支预测器。该线程与受害者运行在同一个内核上(例如,通过超线程),因此共享分支预测器状态。因为分支预测器使用之前的跳转历史进行预测,所以每次误训练迭代都会模仿受害者在跳转之前的分支历史来重定向。尽管误训练可能与受害者的确切虚拟地址和指令类型完全匹配,但这并不是必需的。相反,每个误训练迭代使用一系列ret指令,其目标地址与受害者跳转历史的低20位相匹配(映射到一个1MB(220字节)可执行数组中的地址,该数组中填充了ret指令)。在模拟历史之后,误训练线程执行跳转到重定向(被修改为跳转到 gadget )。

然后,攻击者可以通过选择ebx(调整要读取的内存地址)和edi(调整读取结果映射到探测器阵列的方式)的值来泄漏内存。然后,攻击者使用Flush+Reload从受害者进程中推断出值。在例1中,读取值分布在缓存行上,因此可以很容易地推断出来。然而,在上面的示例中,该值的最不重要的6位不会分布在缓存行上,因此,落入同一缓存行的值无法通过基本的Flush+Reload攻击进行区分。为了区分这些值,探测器阵列的基址可以按字节移位,以确定访问值落入连续缓存行的阈值。通过重复攻击,攻击者可以从受害者进程中读取任意内存。在Intel Haswell(i7-4650U)上的一个未优化概念验证实现,攻击者使用文件影响放置在RAM驱动器上的受害者寄存器,读取速度为41B/s,包括回溯和纠正错误的开销(约占尝试次数的2%)。

C. 逆向工程

现在,我们将介绍对Intel Haswell branch predictor内部进行逆向工程的基本方法,为针对KVM的攻击做准备。这种逆向工程有助于优化分支预测器误训练或描述处理器的漏洞,尽管在实践中,在不完全了解分支预测器的情况下,往往可以实现误训练。

第V-D节介绍了对KVM的攻击。

对于逆向工程,我们从公共来源获得的信息开始。英特尔的公开文档包含一些关于其处理器中分支预测实现的基本但权威的信息[35]。AgnerFog[15]介绍了英特尔Haswell处理器分支预测背后的基本思想。最后,我们使用了之前研究中的信息,这些信息对英特尔处理器上预测直接跳跃的方式进行了逆向工程[14]。

分支历史缓冲区(BHB)的结构是[15]中模式历史的逻辑扩展。BHB有助于根据指令历史进行预测,同时保持简单性和提供滚动哈希的特性。这自然会产生一个包含重叠数据、XOR组合(混合两段数据的最简单方式)的历史缓冲区,并且在历史缓冲区内没有额外的向前或向后传播(以简单的方式保留滚动哈希属性)。

为了确定分支预测器使用的精确函数,利用了预测器冲突。我们设置了两个超线程,它们运行相同的代码,导致具有不同目标的highlatency间接分支。超读A中的进程配置为执行跳转到目标地址1,而超读B中的进程配置为执行跳转到目标地址2。此外,代码被放在目标地址2的超线程中,该地址加载缓存行以进行Flush+Reload。然后,我们测量了缓存行在超线程中加载的频率;这是预测失误率。高预测失误率表示处理器无法区分这两个分支,而低预测失误率表示处理器可以区分它们。在其中一个线程中应用了各种更改,例如在地址中一次跳转一个或两个位。然后,预测失误率就像一个二进制甲骨文,显示给定的位是否会影响分支预测(单位预测),或者两位是否异或在一起(两位预测在单独预测时会导致高低预测失误率,但在同时预测时会导致低预测失误率)。

结合这些知识可以得到图3所示的概述。

D. 对KVM的攻击

我们实施了一种攻击(使用Intel Xeon Haswell E5-1650v3,运行Linux内核包Linux-image-4.9.03-amd64,版本为4.9.30-2+deb9u2),该攻击会从来宾虚拟机内部泄漏主机内存,前提是攻击者可以访问来宾环0(即完全控制虚拟机内部运行的操作系统)。

图片见原文

图3:多种机制影响直接、间接和条件分支的预测。

攻击的第一阶段确定有关环境的信息。它通过分析分支历史缓冲区和分支目标缓冲区泄漏来确定虚拟机监控程序ASLR的位置[14,72]。它还使用通过分支目标注入执行的幽灵gadget查找三级缓存集关联信息[48],以及物理内存映射位置信息。此初始化步骤需要10到30分钟,具体取决于处理器。然后,它通过使用间接分支毒化(又称分支目标注入)在虚拟机监控程序内存中作为幽灵 gadget 执行eBPF解释器,以间接分支的主要预测机制为目标,从Attackercosen地址泄漏虚拟机监控程序内存。我们能够泄漏1809B/s,其中1.7%的字节错误/不可读。

VI. 变种

在推测执行期间发生的缓存状态。未来处理器(或具有不同微码的现有处理器)的行为可能会有所不同,例如,如果采取措施防止推测执行的代码修改缓存状态。在本节中,我们将研究攻击的潜在变体,包括推测执行如何影响其他微架构组件的状态。一般来说,幽灵攻击可以与其他微架构攻击相结合。在本节中,我们将探讨潜在的组合,并得出结论,几乎任何可观察到的投机性执行代码的影响都可能导致敏感信息泄漏。尽管测试的处理器不需要以下技术(而且尚未实施),但在设计或评估缓解措施时,了解潜在的变化是至关重要的。

幽灵变种4。幽灵变种4使用存储中的推测来加载转发逻辑[31]。处理器推测负载不依赖于之前的存储[73]。开采机制类似于我们在本文中详细讨论的变体1和变体2。

驱逐+时间。逐出+时间(Evict+Time)攻击[52]的工作原理是根据缓存的状态测量操作的时间。

如下所示该技术同样可适用于幽灵攻击。考虑以下代码:

if(false but mis predicts as true) 
    read array1 [R1]
read [R2]

假设寄存器R1包含一个秘密值。如果array1[R1]的推测执行的内存读取是缓存命中,那么内存总线上不会发生任何事情,从[R2]的读取将快速启动。如果对array1[R1]的读取是缓存未命中,则第二次读取可能需要更长时间,从而导致受害线程的计时不同。此外,系统中可以访问内存的其他组件(例如其他处理器)可能能够感知内存总线上存在的活动或内存读取的其他影响,例如,更改DRAM行地址选择[56]。我们注意到,与我们已经实现的攻击不同,即使推测执行不会修改缓存的内容,这种攻击也会起作用。所需的只是缓存的状态会影响推测执行的代码或某些其他属性的时间,这些属性最终会对攻击者可见。

指令定时。幽灵漏洞不一定需要涉及缓存。定时取决于操作数值的指令可能会泄漏操作数的信息[6]。在下面的示例中,乘法器被乘法R1、R2的推测执行占用。乘法器可用于乘法R3、R4的时间(用于乱序执行或识别预测失误后)可能会受到第一次乘法的时间的影响,从而显示有关R1和R2的信息。

if(false but mis predicts ast rue)
    multiply R1,R2
multiply R3,R4

寄存器文件上的争用。假设CPU有一个寄存器文件,其中有一定数量的寄存器可用于存储检查点,以便推测执行。在下面的示例中,如果第二个“if”中R1上的条件为真,则将创建一个比R1上的条件为假更多的推测执行检查点。如果攻击者可以检测到该检查点,例如,如果超线程中的代码推测执行由于存储不足而减少,则会显示有关R1的信息。

if(false but mis predicts as true) 
    if(condition on R1) 
        if(condition)

投机处决的变体。即使是不包含条件分支的代码也可能存在风险。例如,考虑攻击者希望确定R1是否包含攻击者选择的值X或某个其他值的情况。做出这种判断的能力足以破坏某些加密实现。攻击者错误地训练了分支预测器,因此在中断发生后,中断会返回对读取内存[R1]的指令的预测失误。然后,攻击者选择X对应于适合Flush+Reload的内存地址,显示R1是否=X。当iret指令在英特尔CPU上序列化时,其他处理器可能会应用分支预测。

利用任意可观察的效果。实际上,推测执行的代码的任何可观察效果都可以用来创建泄漏敏感信息的隐蔽信道。例如,考虑例1中的示例运行在一个处理器上,其中推测读取不能修改缓存。在这种情况下,array2中的推测查找仍会发生,其时间将受到进入推测执行的缓存状态的影响。这一时机反过来会影响后续投机操作的深度和时机。因此,通过在投机性执行之前操纵缓存的状态,攻击者可以潜在地利用投机性执行的几乎任何可观察到的效果。

if(x < array1_size){
    y = array2[array1[x]* 4096];
    // do something detectable when
    // speculatively executed
}

最终可观测操作可能涉及几乎任何侧信道或隐蔽信道,包括对资源(总线、算术单元等)的争夺和常规侧信道发射(如电磁辐射或功耗)。

更普遍的形式是:

if(x < array1_size){
    y = array1[x];
    // do some thing using y that is 
    // observable when speculatively 
    // executed
}

VII. 建议

针对幽灵攻击提出了几种对策。每种方法都解决了攻击所依赖的一个或多个功能。我们现在讨论这些对策及其适用性、有效性和成本。

A. 阻止推测执行

幽灵攻击需要推测执行。确保指令只有在确定了通向指令的控制流时才能执行,这将防止推测执行,并由此防止幽灵攻击。虽然是一种有效的对策,但防止推测执行会导致处理器性能显著下降。

虽然目前的处理器似乎没有能允许软件层面禁用推测执行的方法,但这种模式可以在未来的处理器中添加,或者在某些情况下可能通过微代码更改引入。或者,一些硬件产品(如嵌入式系统)可以切换到不实现推测执行的替代处理器型号。不过,这种解决方案不太可能立即解决这个问题。

或者,可以对软件进行修改,以使用序列化或推测阻止指令,确保后续指令不会被推测执行。英特尔和AMD建议使用lfence指令[4,36]。保护条件分支最安全(但最慢)的方法是在每个条件分支的两个结果上添加这样的指令。然而,这相当于禁用分支预测,我们的测试表明,这将显著降低性能[36]。一种改进的方法是使用静态分析[36]来减少所需的推测阻塞指令的数量,因为许多代码路径不具有读取和溢出内存的可能性。相比之下,微软的C编译器MSVC[54]采取了一种默认无保护代码的方法,除非静态分析器检测到已知的错误代码模式,但因此错过了许多易受攻击的代码模式[40]。

插入序列化指令也有助于减轻间接分支毒化。在间接分支之前插入lfence指令可以确保分支之前的管道被清除,并且分支被快速解析[4]。这反过来又减少了分支毒化时推测执行的指令数量。

该方法要求对所有可能存在漏洞的软件进行检测。因此,为了防护性,需要更新软件的二进制文件和库。这可能是遗留软件的一个问题。

B. 阻止访问秘密数据

其他对策可以防止推测执行的代码访问机密数据。谷歌Chrome浏览器使用的一种方法是在一个单独的过程中执行每个网站[67]。由于幽灵攻击仅利用受害者的权限,因此我们使用JavaScript(参见第IV-C节)执行的攻击将无法访问分配给其他网站的进程中的数据。

WebKit采用两种策略,通过推测执行的代码来限制对机密数据的访问[57]。第一种策略用索引屏蔽代替数组边界检查。WebKit没有检查数组索引是否在数组的范围内,而是对索引应用位掩码,确保它不会比数组大小大太多。虽然屏蔽可能会导致访问超出阵列边界,但这会限制边界冲突的距离,从而防止攻击者访问任意内存。

第二种策略通过使用伪随机毒药值对指针进行异或来保护对指针的访问。毒药以两种不同的方式保护指针。首先,不知道毒药值的攻击者不能使用毒化指针(尽管各种缓存攻击可能会泄漏毒药值)。更重要的是,poison值确保用于类型检查的分支指令上的预测失误将导致与类型相关的指针用于另一个类型。

这些方法对于即时(JIT)编译器、解释器和其他基于语言的保护非常有用,因为运行时环境可以控制执行的代码,并希望限制程序可以访问的数据。

C. 防止数据进入隐蔽信道

未来的处理器可能会跟踪数据是否是由于推测操作而获取的,如果是这样的话,就可以防止该数据被用于可能会泄露数据的后续操作中。然而,目前的处理器通常不具备这种能力。

D. 限制隐蔽信道的数据提取

为了从瞬时指令中过滤信息,幽灵攻击使用隐蔽的通信信道。已经提出了多种缓解此类信道的方法(参见[19])。作为对我们基于JavaScript的攻击的尝试缓解,主要浏览器提供商进一步降低了JavaScript计时器的分辨率,可能会增加抖动[50、57、66、71]。这些补丁还禁用了可以用来创建计时源[60]的SharedArrayBuffers

虽然这种对策需要对第IV-C节中的攻击进行额外的平均,但它提供的保护级别尚不清楚,因为错误源只会降低攻击者过滤数据的速度。此外,正如[18]所示,当前的处理器缺乏完全消除隐蔽信道所需的机制。因此,虽然这种方法可能会降低攻击性能,但不能保证攻击不可能发生。

E. 防止分支毒化

为了防止间接分支毒化,英特尔和AMD通过控制间接分支[4,34]的机制扩展了 ISA 。该机制由三个控件组成。第一种是间接分支限制投机(IBRS),它可以防止特权代码中的间接分支受到非特权代码中分支的影响。处理器进入一个特殊的IBRS模式,该模式不受IBRS模式之外的任何计算的影响。第二种是单线程间接分支预测(STIBP),它限制在同一内核的超线程上执行的软件之间共享分支预测。最后,间接分支预测器屏障(IBPB)防止在设置屏障之前运行的软件通过在屏障之后运行的软件影响分支预测,即通过刷新BTB状态。这些控件是在微码补丁后启用的,需要操作系统或BIOS支持才能使用。性能影响从几%到4倍或更多不等,这取决于采用的对策、它们的应用程度(例如,内核中的有限使用与所有进程的完全保护),以及硬件和微码实现的效率。

谷歌提出了一种预防间接分支毒化的替代机制,称为retpoline[70]。retpoline是用返回指令替换间接分支的代码序列。该构造还包含确保返回指令通过返回堆栈缓冲区预测为良性无止境循环的代码,同时通过将其推到堆栈上并返回到它(即使用ret指令)来达到实际目标目的地。当返回指令可以通过其他方式预测时,该方法可能不切实际。英特尔发布了一些处理器的微码更新,这些处理器会返回BTB进行预测,以禁用这种返回机制[36]。

VIII. 结论

支持软件安全技术的一个基本假设是,处理器将忠实地执行程序指令,包括其安全检查。本文介绍了幽灵攻击,它利用了推测执行这一违反假设的事实。我们展示的技术是实用的,不需要任何软件漏洞,允许攻击者读取私有内存并注册来自其他进程和安全上下文的内容。

软件安全从根本上取决于硬件和软件开发人员对CPU实现允许(或不允许)从计算中暴露的信息有一个明确的共识。因此,虽然上一节中介绍的对策可能有助于在短期内限制对其的实际利用,但只是权宜之计,因为通常需要有正式的体系结构保证,以确定任何特定代码构造在当今处理器上是否安全 —— 更不用说未来的设计了。因此我们认为,长期解决方案将需要从根本上改变指令集体系结构。

更广泛地说,安全性和性能之间存在权衡。本文中的漏洞以及其他许多漏洞源于技术行业长期以来对性能最大化的关注。因此,处理器、编译器、设备驱动程序、操作系统和许多其他关键组件已经进化出复杂的优化层次,带来了安全风险。随着不安全成本的上升,这些设计选择需要重新审视。在许多情况下,需要针对安全性进行优化的替代实现。

IX. 致谢

本文的几位作者独立发现了幽灵攻击,并最终促成了这项合作。我们感谢谷歌零号项目的Mark Brand提供的创意。我们感谢英特尔专业地处理了这个问题,通过明确的时间表把控以及联系了所有相关研究人员。我们感谢ARM就这个问题的各个方面进行技术讨论。我们感谢高通公司和其他供应商在披露该问题时的快速反应。最后,我们要感谢审稿人的宝贵意见。

Daniel Gruss、Moritz Lipp、Stefan Mangard和Michael Schwarz得到了欧洲研究理事会(ERC)在欧盟地平线2020研究和创新计划(第681402号赠款协议)下的支持。

Daniel Genkin获得了美国商务部、国家标准与技术研究所、2017-2018罗斯柴尔德博士后奖学金和国防高级研究计划局(DARPA)根据FA8650-16-C-7622合同授予的NSF奖 #1514261 和 #1652259 、金融援助奖 70NANB15H328 的支持。

参考文献

详见原文

附录 A,B,C

详见原文