初始化

记录技术与生活

幽灵、熔断漏洞论文《利用旁道攻击读取特权内存》部分翻译

由Google Project Zero的Jann Horn发布

 

我们已经发现,利用处理器的缓存数据读取时间可以有效从预测执行中窃取信息,这导致了一个(在最坏情况下)在各种上下文环境中绕过边界检查的任意虚拟内存读取漏洞。

现在已知这个问题的变种可以影响很多的现代处理器,包括英特尔,AMD和ARM的部分处理器。在一部分英特尔和AMD的处理器上,我们已经成功利用该漏洞从真正的软件中窃取信息。我们在2017年6月1日向上述的三家厂商报告了该问题。

到现在为止,有三个已知的该漏洞变种:

  1. 绕过边界检查(CVE-2017-5753)
  2. 分支目标注入(CVE-2017-5715)
  3. 强行数据缓存加载(CVE-2017-5754)

在这些问题被公开披露之前:Daniel Gruss,Moritz Lipp,Yuval Yarom,Paul Kocher,Daniel Genkin,Michael Schwarz,Mike Hamburg,Stefan Mangard,Thomas Prescher和Werner Haas也曾报道过这些问题:

 

在我们的研究过程中,我们开发了以下的一些概念验证(译者注:用于证明其可行性的原型):

  1. 这一模型演示了:在 Intel Haswell Xeon CPU, AMD FX CPU, AMD PRO CPU 与 ARM Cortex A57 CPU 上运行变种一的基本原理。该模型中仅针对从分支执行中读取数据的可能性进行了测试,而并未绕过任何特权边界。
  2. 这一模型演示了:在使用标准发行版配置的现代Linux内核中,在Intel Haswell Xeon CPU上,当以普通用户权限运行程序时,可以实现从内核的虚拟内存中读取到4GB范围内的任意内存。而如果启用了内核的BPF JIT(非默认配置),这一漏洞对于AMD PRO CPU上仍将有效。在 Intel Haswell Xeon CPU 上,当程序启动大约4秒后,可以以大约每秒2KB的速度读取到内核虚拟内存数据。
  3. 这一模型演示了:利用漏洞变种二,当在使用Intel Haswell Xeon CPU时,在使用virt-manager创建的KVM 虚拟机上,对于特定版本的Debian Linux内核(这是一个已经过期的版本),可以以每秒1.5KB的速度读取到内核虚拟内存,而这一速度仍有优化空间。在攻击开始之前,对于64GB的主机,需要10-30分钟的准备时间;而所需的准备时间与主机的内存大小大约成线性关系(如果2MB的大页面文件可用的话,准备的速度将会快许多,但未经过测试。)
  4. 这一模型演示了:利用漏洞变种三,当以普通用户权限在Intel Haswell Xeon CPU运行程序时,在某些先决条件下,可以从内核内存中读取数据。而我们相信这个先决条件是当目标内核内存在处理器的一级缓存中。

 

如果对文中的主题感兴趣,请看下面的论文部分。

关于文章中对于处理器内部状态描述的说明:在本篇文章中包含很多基于观察行为对于硬件内部行为的推测,这些行为可能并不会在真实的处理器上发生。

我们有一些缓解该问题的想法,并且已经向处理器供应商提供了它们。然而,我们相信处理器供应商拥有更好的评估与缓解问题的措施,我们也期望他们能成为权威的指导者。

我们发送给处理器供应商的测试模型可以在以下链接找到:https://bugs.chromium.org/p/project-zero/issues/detail?id=1272

测试的处理器型号

  • Intel(R) Xeon(R) CPU E5-1650 v3 @ 3.50GHz (文中称为 “Intel Haswell Xeon CPU”)
  • AMD FX(tm)-8320 Eight-Core Processor (文中称为 “AMD FX CPU”)
  • AMD PRO A8-9600 R7, 10 COMPUTE CORES 4C+6G (文中称为 “AMD PRO CPU”)
  • An ARM Cortex A57 core Google Nexus 5x (文中称为 “ARM Cortex A57”)

 

名词释义

退出:已经结束执行的指令。例如:写入缓存或内存时,指令被提交之后对于其他系统是可见的。指令可以乱序执行,但是一定要顺序退出。

逻辑处理器核心:逻辑处理器核心是指操作系统所能够利用的处理器核心,当开启超线程设置之后,逻辑处理器核心的数量将会是物理核心的两倍。

缓存/未缓存数据:在本文中,“未缓存的数据”指仅存储在内存中的数据,而不在任意级别的缓存中存在。

推测执行:处理器可以直接执行选择条件语句的分支代码而并不知道其分支代码是否应该被执行。如果推测不正确,处理器可以撤销执行的结果与未保存的状态,并继续执行正确的分支。而在知道哪一个分支是正确的之前,推测执行将不会停止。(译者注:这段原文意思较为难理解。意思是CPU可以直接执行if语句的代码块,而暂时并不需要知道if的条件是否成立;因为从内存中查询推测条件需要很多个时钟周期,在这段时间内CPU为了最大化利用计算资源,会不断计算每个分支的结果并存储到寄存器知道返回if的条件;如果计算的分支都是错误的,将重置内部状态,继续计算正确的分支。)

错误推测窗口:处理器在执行错误代码并且还未探测到已经发生错误推测的这一段时间。

暂时翻译到这里,明天继续…

漏洞变种一:绕过边界检查

本节解释三种漏洞变种的通用理论并针对“模型一”的原理进行阐述,启用以下配置,在Debian发行版 Linux内核的用户环境下运行程序时:可以任意读取4GB范围内的内核内存:

  • Intel Haswell Xeon CPU, eBPF JIT 关闭 (默认状态)
  • Intel Haswell Xeon CPU, eBPF JIT 打开 (非默认状态)
  • AMD PRO CPU, eBPF JIT 打开 (非默认状态)

使用 net.core.bpf_jit_enable sysctl 可以切换 eBPF JIT 的状态。

理论阐述

在这篇《英特尔优化参考手册》的第 2.3.2.3 小节(“分支预测”)中有以下关于Sandy Bridge微架构的阐述(以后的微架构中也同样存在):

分支预测会预测分支的目标,并使处理器在知道哪一条分支路径条件为真之前就开始执行指令。

在第 2.3.5.2 小节(“一级缓存”)中有以下阐述:

载入变量可以:

  • 进行随机性的推测,在分支预测被解决之前。
  • 让缓存未命中以重复和没有顺序的方式发生。

在英特尔的《软件开发手册》的3A卷、11.7节(“隐式高速缓存”)中也有以下阐述:

隐式高速缓存发生在元素可以被缓存的情况下,尽管元素可能永远不会按照正常的冯诺依曼序列被访问。隐式高速缓存在P6以及更新的处理器上可以实现,这是因为积极预加载,分支预测以及TLB的未命中处理(译者注:页表缓存)技术的出现。隐式缓存技术是对现有 Intel386, Intel486 和 Pentium 处理器行为的扩展,因为在这些处理器系列上运行软件时不能够对读取指令的行为进行准确的预测。

考虑一个代码实例:当 arr1-> length 元素未被缓存时,处理器可以预测性地从 arr1->data[untrusted_offset_from_caller] 加载数据,当发生越界访问时,处理器应该能够有效的回滚到分支执行前的状态;而所有预测执行的指令都不会退出(例如:导致缓存和其他系统被影响)

但是,在下面这个例子中,则会产生一个问题:如果 arr1->length, arr2->data[0x200] 和 arr2->data[0x300] 都未被缓存;但是其他访问的数据都被缓存了;并且预测的分支条件为真;那么,在 arr1->length 被加载之前,处理器则会做出下面这样随机性的预测:

  • 加载 value = arr1->data[untrusted_offset_from_caller]
  • 从 arr2->data 的相关偏移量开始加载数据,将相应的数据加载到一级缓存中

之后,由于处理器注意到了 untrusted_offset_from_caller 大于 arr1-> length,包含 arr2->data[index2] 的缓存项将继续保留在一级缓存。通过测量载入 arr2->data[0x200] 和 arr2->data[0x300] 的所需要的时间(译者注:旁道攻击),攻击者就可以确定变量 index2 的值是 0x200 还是 0x300 – 这样就能够推测出 arr1->data[untrusted_offset_from_caller]&1 的值是 0 还是 1。

为了能够将这种行为用于实际的攻击,攻击者需要在目标上下文环境中使用越界索引去执行这种容易被攻击的代码。因此,因受攻击的代码必须存在于现有代码中,或者需要有解释器或JIT引擎能够生成这一种模式的代码。到目前为止,我们还没有确认任何现有的漏洞实例拥有这种模式的代码;在这个利用漏洞变种一泄漏内核内存的例子中使用了eBPF解释器或eBPF JIT引擎,而它们内建于内核中,并且对于普通用户是可见的。

代替这一种攻击模式的另一种模式,可以通过越界读取函数指针从错误的推测执行中获取到系统控制权。我们没有针对这一种模式进行研究。

暂时翻译到这里,去休息吃饭,明天继续翻译…

攻击内核

这部分更加详细的描述了攻击者是如何利用漏洞变种一来攻击使用 eBPF 字节解释器和 JIT 引擎的Linux内核的。虽然利用这种方法可以攻击许多有趣的潜在目标,但是我们选择攻击Linux内核 eBPF JIT 解释器,因为它能够提供比其他JIT更多的控制权。

Linux从3.18版本开始支持eBPF. 没有授权的用户空间代码可以提交字节码给内核,经过内核验证之后:

  • 由内核字节码解释器进行解释
  • 或者翻译成机器码,交由JIT引擎运行在内核上下文中(只进行翻译,而不对指令做任何优化)

字节码执行可以通过将 eBPF 字节码作为过滤器附加在套接字上,然后通过套接字另一端发送数据来触发执行。

JIT引擎是否启用取决于运行时的配置设置 – 但至少在测试过的Intel处理器上,攻击独立于此项设置。

与传统的BPF不同,eBPF具有数据类型,例如数据数组与函数指针数组,ePBF可以根据字节码指令在其中进行索引。因此,可以使用eBPF字节码在内核中创建上述的攻击代码模式。

eBPF的普通数组效率比它的函数指针数组效率低,因此在攻击中如果有可能将使用后者。

两台用于测试的机器都没有SMAP,PoC的结果依赖于此(但是事实上这不应成为一个先决条件)。

另外,至少在测试过的 Intel 机器上,在内核与修改过的缓存直接切换会很慢,这显然是因为MESI协议会保存缓存一致性,在物理处理器上修改 eBPF 数组的引用计数会导致包含引用计数的行在缓存和CPU内核之间的切换,从而使得所有其他CPU内核上的引用计数器读取速度变慢,直到对引用计数的更改被写回内存。由于 ePBF 数组长度与引用计数器在同一缓存行,这也就意味着在物理处理器上改变引用计数会导致访问 ePBF 数组长度在其他物理内核上也会变得缓慢(伪共享)。

(译者注:缓存行是处理器缓存的最小单位,如果两个变量存储在同一缓存行中,而不同物理核心中的不同线程要对同一缓存行中的数据进行写(数据可以不同),就需要不断对该缓存行发出RFO占有请求,核心通过内存控制器共享数据,而其他核心在处理对应的RFO请求的时候将自己的缓存行失效。这种多个线程操作不同变量,而不同变量却在同一缓存行中的情况称为伪共享,这种情况会造成很大的性能消耗,编程中应当尽量避免)。

这次的攻击使用了两个ePBF程序,第一个通过页面对齐的 ePBF 函数指针数组 prog_map 在索引处进行尾调用。在简化情况下,程序可以通过确定 prog_map 的地址来猜测 prog_map 对于用户空间的偏移量,并通过 prog_map 在猜测出的偏移量上进行函数调用。为了使分支预测只索引到低于 prog_map 长度的偏移量,尾调用会指向一个界内的地址。为了增大错误推测窗口,包含 prog_map 长度的缓存行将被存储到其他处理器核心缓存。为了知道是否成功推测出偏移量,可以测试用户空间的地址是否被载入到缓存。

由于这种对地址的暴力猜测会很慢,可以做出如下的优化:在用户空间地址 user_mapping_area 创建 2^15 个相邻的对用户空间内存的映射,每一个由 2^4 个页面组成,覆盖总面积是 2^31 字节。每一个映射都映射到相同的物理页面,并且所有映射都存在于页面表中。

《幽灵、熔断漏洞论文《利用旁道攻击读取特权内存》部分翻译》

这允许攻击以 2^31 字节的步长进行,对应于每一步,通过 prog_map 导致越界访问之后,仅有user_mapping_data 前 2^4 个页面的一个缓存行需要经过缓存测试。由于L3缓存是物理索引的,每一个对于映射到物理页面的虚拟地址访问将会导致其他映射到相同物理页面的映射被缓存。

当攻击命中时:找到了一个缓存中的内存地址 – 内核的高33位地址是已知的(可以从发生命中的地址中猜出),并且低16位也是知道的(可以从 user_mapping_area 找到命中地址的偏移量),user_mapping_area 剩余的地址部分便是中间的部分。

《幽灵、熔断漏洞论文《利用旁道攻击读取特权内存》部分翻译》

中间剩余的地址部分可以通过二分法来确定地址空间:将两个物理页面映射到相邻的虚拟地址,每个虚拟地址的范围是剩余搜索空间的一半的大小,然后逐位确定剩余的地址。

利用这一点,第二个eBPF程序可以用来在实际泄中漏数据。程序的伪代码如下所示:

该程序根据运行时的偏移量和位掩码配置读取8字节对齐的64字节 eBPF 数组 “victim_map” 值,并对该值进行偏移(以保证处理器在载入数组索引时不落入相同或相邻的缓存行)。最后,它添加一个64位的偏移量,然后使用结果值作为到 prog_map 的偏移量来进行尾部调用。

这个程序可以通过反复调用eBPF程序,并使用一个超出边界偏移量的 victim_map 值来指定要泄漏的数据,并将一个超出边界的偏移量放到 prog_map 中 ,导致 prog_map + offset 指向一个用户空间内存区域。误导分支预测和发生伪共享的方式和程序一相同。另外,保存 victim_map 长度的值也必须被保存到另外一个处理器核心。

今天的翻译难度陡增,涉及了非常多与处理器和操作系统内核相关的知识(例如:MESI协议、伪共享、内存地址推测、页面映射等等),而这些知识都是我从未接触过的。今天的翻译中有一部分我没有能够理解其原理与运行机制,所以可能出现翻译错误,还请原谅并指正,谢谢!

漏洞变种二:分支目标注入

本节描述了在装有Intel Haswell Xeon CPU的物理机器上,使用virt-manager创建的KVM虚拟机上,在root权限下,利用漏洞变种二进行攻击的模型理论。当虚拟机运行特定版本的Debian发行版内核时,可以通过该漏洞以1.5KB/S的速度泄漏内核内存。

基础原理

之前的研究(可以在文末的“论文部分找到”)表明在不同安全上下文中的代码可能会影响彼此的分支预测情况。并且到目前为止,我们只用它来推测了代码的地址信息(换句话说,是为了创造受害者被攻击的可能性);而在这一漏洞变体中,攻击者还可以重定向受害者的在上下文中的代码执行(换句话说,它创造了攻击者攻击受害者的可能性;反之亦然)。

《幽灵、熔断漏洞论文《利用旁道攻击读取特权内存》部分翻译》

攻击的基本思路是将从内存加载的含有间接分支的代码作为攻击目标,并将它们从处理器缓存中清除。之后,当处理器运行到间接分支时,它便不知道运行哪一个分支是正确的;并且在处理器从内存中把分支条件重新加载回缓存的这个过程中,处理器也不知道哪一个分支是正确的,而这一窗口期通常有几百个指令周期。通常,这一窗口时间会大于一百个指令周期,在这一段时间内处理器将给予分支预测推测性的执行分支指令。

Haswell 内部分支预测情况

Intel处理器的内部分支预测实现已经被公布了;然而,如果想让攻击正常进行,通常需要实验确定更多的细节。

Haswell处理器貌似有多种分支预测的模式,并且其工作方式完全不同:

  • 通用分支预测器:每一个源地址只能存储一个目标;被用来处理所有类型的跳转,例如绝对跳转、相对跳转等等。
  • 一个专用的间接调用预测器:每一个地址可以储存多个目标;被用来处理所有间接调用。
  • (根据Intel的处理器优化手册,该处理器也有一个专门的返回预测器,但是我们没有对这个预测器进行详细的分析。如果这个预测器可以用来可靠的转储一些虚拟机内部的调用堆栈,那将非常有趣。)

通用预测器

根据以前已经记录的研究,这个通用分支预测器,仅使用原指令的最后一个字节的低31位进行预测。例如,如果从0x4141.0004.1000 至 0x4141.0004.5123 的跳转在缓冲区存在,那么通用预测器也将据此预测从 0x4242.0004.1000 的跳转。可以发现,当两个源地址的高位不同时,目标地址的高位也将随着它一起改变;显然,这个预测器不存储完整的绝对目标地址。

在使用源地址的低31位进行运算之前,将对他们进行或与运算。具体而言,以下几位会参与运算:

bit A
bit B
0x40.0000
0x2000
0x80.0000
0x4000
0x100.0000
0x8000
0x200.0000
0x1.0000
0x400.0000
0x2.0000
0x800.0000
0x4.0000
0x2000.0000
0x10.0000
0x4000.0000
0x20.0000

换句话说,如果源地址与表中一行的两个数字进行了异或,分支预测在进行查找时就不能将源地址与得到的地址区分开来。举个栗子:分支预测器可以区分分别为 0x100.0000 和 0x180.0000 的两个源地址;但不能区分源地址 0x100.0000 和 0x140.2000 或源地址 0x100.0000 和 0x180.4000。

当使用别名源地址时,分支预测器仍将根据未经处理的源地址进行预测;这表示分支预测其可能会存储阶段的绝对目标地址,但是这未经过验证。

根据观察到的不同源地址的最大前向和后向跳转距离,目标地址的低32位可能被存储为绝对的31位值,在此基础上附加一位用于区分指定源到目标地址的跳转是否超过了 2^32 的边界;如果跳转跨过了这个边界,那么利用源地址的第31位决定指令的一半高位增加或是减少。

明天就上课了,今天少翻译一些(其实是偷懒,逃ヾ(〃゚∀゚♥)ノ),好好休息,我会视是否有空闲时间,尽量在周内继续翻译。

间接调用预测器

BTB的预测机制大致如下:

  • 根据原指令地址的低12位(我们不能确定是第一个还是最后一个字节的地址)或者是该地址的一部分
  • 分支预测历史缓冲区的状态

如果间接调用预测器无法解析分支,则用通用预测器解析。英特尔的优化手册中有对此行为的暗示:“间接调用和跳转,这些行为可能会被预测为具有单向的分支或者是根据程序行为而变化的分支。”

分支历史缓冲区(BHB)存储最后29个分支预测的信息 – 基本上可以反映近期控制流的变化,并且据此可以更好的预测具有多个目标的间接调用。

BHB刷新的工作原理如下(伪代码中src是源指令最后一个字节的地址,dst是目标地址):

当存储的信息被BTB访问时,BHB状态的一些比特位似乎进一步被或与运算,但是进行该运算的功能尚不清楚。

BHB是一个很有趣的东西;有两个原因。首先,为了能在间接调用预测过程中准确的发生冲突,需要关于它最近行为的信息。但是它也允许在攻击者可以执行代码的任何可重复的程序状态下查看BHB状态 – 例如,在攻击hypervisor之后,直接进行 hypercall。经过转储的BHB状态可以用于反映hypervisior的状态,如果供给制能够直接访问 hypervisor 的二进制代码,就能够确定加载程序的第20位(在KVM虚拟机的情况下:是kvm-intel.ko 的载入地址的低20位)。

对分支预测器内部进行反向工程

本小节描述了我们如何对Haswell分支预测器的内部进行反向工程。其中一部分是我们根据记忆写下来的,因为我们当时没有对实验进行详细记录。

我们最初尝试使用通用预测器对内核执行BTB注入,根据先前研究中的信息,通用预测器仅查看源地址的下半部分,并且仅存储部分目标地址。但实验表明这样的成功率很低 – 只有大约1%。(这是我们在方法2中使用的方法,用于对运行在Haswell上的修改的虚拟机管理程序进行初始化。)

我们决定编写一个在用户空间的测试用例,以便能够更轻松地测试不同情况下的分支预测器行为。

基于分支预测器状态在超线程之间共享的假设,我们编写了一个程序,两个实例分别固定运行在特定物理内核上的两个逻辑处理器之一,其中一个实例试图执行分支注入另一个测量分支注射成功的频率。这两个实例都是在禁用ASLR的情况下执行的,并且在相同的地址上具有相同的代码。注入过程对访问测试变量的函数进行间接调用。测量过程对一个函数进行间接调用,该函数根据时序测试每个进程的测试变量是否被缓存,然后使用CLFLUSH将其逐出。这两个间接调用都是通过相同的调用点执行的。在每次间接调用之前,使用CLFLUSH将存储器中存储的函数指针刷新到主存储器,以扩大推测时间窗口。

在这个测试中,注射成功率高于99%,这为我们未来的实验奠定了基础。

《幽灵、熔断漏洞论文《利用旁道攻击读取特权内存》部分翻译》

最后,我们试图推测预测方法的细节。我们假定预测使用某种全局分支历史记录缓冲区。

为了确定分支信息保留在历史缓冲区中的持续时间,条件分支仅在两个程序的一个中插入总是采用的分支跳转,采用的条件跳转量(N)始终是变化的。结果是,当N=25时,处理器能够保持在1%以下的误预测率;而当N=26时预测失误率达到了99%。因此,分支历史缓冲区必须能够存储至少最后的26个分支预测的信息。

两个实例之一的程序代码随后在内存中的位置发生变化。而实验结果相同,这说明了只有源地址和目标地址的低20位对分支历史缓冲器有影响。

在两个程序实例中使用不同类型的分支进行测试,发现静态跳转,采用有条件的跳转,调用和返回都以同样的方式影响分支历史缓冲区;IRETQ不会影响历史缓冲区状态(这对测试很有用,因为它允许创建对于历史缓冲区不可见的程序)。

在进行间接调用之前,对要执行的分支代码地址进行移动,说明分支历史缓冲区内容可以区分最后一个条件分支指令的许多不同位置。这表明了历史缓冲区不是一个小的历史值存储列表;相反,它似乎是由于多利市数据混合在一起的更大的缓冲区。

然而,为了使分支预测有用,一个历史缓存需要在已经输入了一定数量的新分支之后“忘记”过去的分支。考虑到分支预测也必须非常快,我们得出结论:历史缓冲区的更新功能可能会对旧历史缓冲区进行左移,然后对新状态进行或与XOR(如图)。

《幽灵、熔断漏洞论文《利用旁道攻击读取特权内存》部分翻译》

如果这个假设是正确的,则历史缓冲区包含许多关于最近分支的信息,但是每个历史缓冲区的更新仅包含最后一个分支大小的信息。

 翻译到这里停止,不会继续翻译了。
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.