英特尔AMD和ARM处理器读取特权内存跨本地安全边界漏洞POC

Project Zero Bugs发现,CPU数据高速缓存时间可能会被滥用,以有效地泄漏错误推测的执行信息,导致(最坏的情况下)任意虚拟内存读取在各种情况下跨本地安全边界的漏洞。

 

这个漏洞的变种已知会影响许多现代处理器,包括英特尔,AMD和ARM的某些处理器。对于一些英特尔和AMD CPU型号,我们有攻击真正的软件的攻击。我们在2017-06-01向Intel,AMD和ARM报告了这个问题[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也报道了这些问题。他们的[写作/博客帖子/论文稿]在:
在我们的研究过程中,我们开发了以下概念证明(PoC):

 

  1. PoC演示了经过测试的Intel Haswell Xeon CPU,AMD FX CPU,AMD PRO CPU和ARM Cortex A57 [2]中的用户空间中的变体1的基本原理。这个PoC只能测试在同一个进程中错误推测执行的数据读取能力,而不会跨越任何特权边界。
  2. 对于版本1的PoC,在具有发行版标准配置的现代Linux内核下以普通用户权限运行时,可以在Intel Haswell Xeon CPU上的内核虚拟内存中的4GiB范围[3] 中执行任意读取。如果启用了内核的BPF JIT(非默认配置),那么它也适用于AMD PRO CPU。在Intel Haswell Xeon CPU上,启动时间大约4秒后,内核虚拟内存可以以每秒2000字节左右的速度读取。[4]
  3. 对于版本2的PoC,在使用Intel Haswell Xeon CPU上的virt-manager创建的KVM guest虚拟机内以超级用户权限运行时,可以读取在主机上运行的特定(已过时)版本的Debian发行版内核[5]以1500字节/秒的速度托管内核内存,并具有优化空间。在执行攻击之前,对于具有64GiB RAM的机器,需要执行大约10到30分钟的初始化; 所需的时间应该与主机RAM的数量大致成线性关系。(如果客户可以使用2MB的大页面,初始化应该快得多,但是还没有经过测试。)
  4. 对于变种3的PoC,当以正常的用户权限运行时,可以在某种先决条件下读取Intel Haswell Xeon CPU上的内核内存。我们相信这个先决条件是目标内核内存在L1D缓存中。

 

有关这个主题的有趣资源,请看“文献”部分。

 

在这篇博文中关于处理器内部解释的警告:这篇博文包含了很多关于基于观察行为的硬件内部的推测,这可能不一定对应于实际处理器。

 

我们对可能的缓解有一些想法,并向处理器供应商提供了一些想法。然而,我们相信处理器供应商的地位比我们设计和评估缓解措施要好得多,我们希望他们成为权威指导的来源。

 

我们发送给CPU供应商的PoC代码和写法将在稍后提供。

经过测试的处理器

  • Intel(R)Xeon(R)CPU E5-1650 v3 @ 3.50GHz(本文档的其余部分称为“Intel Haswell Xeon CPU”)
  • AMD FX(tm)-8320八核处理器(本文档的其余部分称为“AMD FX CPU”)
  • AMD PRO A8-9600 R7,10个COMPUTE CORES 4C + 6G(本文档的其余部分称为“AMD PRO CPU”),
  • 谷歌Nexus 5x手机的ARM Cortex A57内核[6] (本文档的其余部分称为“ARM Cortex A57”),

词汇表

退休:一个指令退出时,其结果,例如寄存器写入和内存写入,提交并使其他系统可见。指令可以不按顺序执行,但必须始终按顺序退出。
逻辑处理器核心:逻辑处理器核心是操作系统认为是处理器核心的东西。启用超线程后,逻辑核心的数量是物理核心数量的倍数。
缓存/未缓存的数据:在这篇博文中,“未缓存”数据是仅存在于主存储器中的数据,而不是CPU的任何缓存级别中的数据。加载未缓存的数据通常需要超过100个CPU时间周期。
推测性执行:处理器可以执行经过分支而不知道是否将被采用或者其目标是何处,因此在知道它们是否应该被执行之前执行指令。如果这种推测结果是不正确的,CPU可以放弃没有架构效应的结果状态,并继续执行正确的执行路径。在知道它们处于正确的执行路径之前,指令不会退出。
错误猜测窗口:CPU推测性地执行错误代码并且还没有检测到发生错误猜测的时间窗口。

变体1:边界检查旁路

本节解释所有三种变体背后的常见理论,以及我们的变种1的PoC背后的理论,当在Debian发行版内核中的用户空间中运行时,可以在内核内存的4GiB区域中执行至少以下配置的任意读取:
  • Intel Haswell Xeon CPU,eBPF JIT已关闭(默认状态)
  • Intel Haswell Xeon CPU,eBPF JIT打开(非默认状态)
  • AMD PRO CPU,eBPF JIT打开(非默认状态)
eBPF JIT的状态可以使用net.core.bpf_jit_enable sysctl进行切换。

理论解释

“ 英特尔优化参考手册 ”在第2.3.2.3节(“分支预测”)中对以下有关Sandy Bridge(以及后来的微体系结构修订版)
分支预测预测分支目标并启用
处理器在分支之前就开始执行指令
真正的执行路径是已知的。
在第2.3.5.2节(“L1 DCache”)中:
负载可以:
[…]
  • 在前面的分支解决之前进行投机性的推测。
  • 不按顺序和重叠的方式进行缓存未命中。
英特尔软件开发人员手册[7]在第3A卷第11.7节(“隐式高速缓存(Pentium 4,Intel Xeon和P6系列处理器)”中指出:
隐式高速缓存发生在内存元素可能被缓存的情况下,尽管元素可能永远不会以正常的冯诺依曼序列被访问。隐式高速缓存出现在P6和更新的处理器系列上,这是由于积极的预取,分支预测和TLB未命中处理。隐式缓存是现有Intel386,Intel486和Pentium处理器系统行为的延伸,因为在这些处理器系列上运行的软件也不能确定性地预测指令预取的行为。
考虑下面的代码示例。如果arr1-> length 未缓存,处理器可以推测性地从arr1-> data [untrusted_offset_from_caller] 加载数据。这是一个超出界限的阅读。这应该不重要,因为处理器将有效地回滚分支执行时的执行状态; 推测性执行的指令都不会退出(例如导致寄存器等被影响)。
struct array {
 unsigned long length;
 unsigned char data[];
};
struct array *arr1 = …;
unsigned long untrusted_offset_from_caller = …;
if (untrusted_offset_from_caller < arr1->length) {
 unsigned char value = arr1->data[untrusted_offset_from_caller];
 …
}
但是,在下面的代码示例中,有一个问题。如果arr1-> length ,arr2-> data [0x200] 和arr2-> data [0x300] 没有被缓存,但所有其他访问的数据都是,并且分支条件被预测为true,处理器可以在arr1 之前进行推测- >长度已经被加载并且执行被重新引导:
  • load value = arr1-> data [ untrusted_offset_from_caller ]
  • 从arr2-> data中的数据相关偏移量开始加载,将相应的高速缓存行加载到L1高速缓存中
struct array {
 unsigned long length;
 unsigned char data[];
};
struct array *arr1 = …; /* small array */
struct array *arr2 = …; /* array of size 0x400 */
/* >0x400 (OUT OF BOUNDS!) */
unsigned long untrusted_offset_from_caller = …;
if (untrusted_offset_from_caller < arr1->length) {
 unsigned char value = arr1->data[untrusted_offset_from_caller];
 unsigned long index2 = ((value&1)*0x100)+0x200;
 if (index2 < arr2->length) {
   unsigned char value2 = arr2->data[index2];
 }
}
由于处理器注意到untrusted_offset_from_caller 大于arr1-> length ,执行返回到非推测路径后,包含arr2-> data [index2] 的缓存行停留在L1缓存中。通过测量加载arr2-> data [0x200]和arr2-> data [0x300] 所需的时间,攻击者可以确定在推测执行过程中index2 的值是0x200还是0x300 – 它揭示了arr1-> data [ untrusted_offset_from_caller ] &1 是0或1。
为了能够实际将这种行为用于攻击,攻击者需要能够在目标上下文中使用超出边界的索引来执行这样一个易受攻击的代码模式。为此,易受攻击的代码模式必须存在于现有代码中,或者必须有一个解释器或JIT引擎可用于生成易受攻击的代码模式。到目前为止,我们还没有确定任何现有的,可利用的易受攻击代码模式的实例; 使用变体1泄漏内核内存的PoC使用eBPF解释器或eBPF JIT引擎,它们内置在内核中,可供普通用户访问。
这样做的一个小的变体可能是使用一个越界读取函数指针来获取错误推测路径中的执行控制权。我们没有进一步调查这个变种。

攻击内核

本节将更详细地介绍如何使用eBPF字节码解释器和JIT引擎,使用变体1来泄漏Linux内核内存。虽然变种1攻击有许多有趣的潜在目标,但是我们选择攻击Linux内核eBPF JIT /解释器,因为它比其他大多数JIT提供更多的对攻击者的控制。
Linux内核自3.18版本开始支持eBPF。未授权的用户空间代码可以将字节码提供给内核验证的内核,然后:
  • 或者由内核字节码解释器解释
  • 或翻译成本机机器码,该机器码也使用JIT引擎运行在内核上下文中(其翻译各个字节码指令而不执行任何进一步的优化)
字节码的执行可以通过将eBPF字节码作为过滤器附加到套接字来触发,然后通过套接字的另一端发送数据。
JIT引擎是否启用取决于运行时配置设置 – 但至少在测试过的Intel处理器上,攻击独立于此设置工作。
与传统的BPF不同,eBPF具有数据类型,如数据数组和函数指针数组,eBPF字节码可以在其中进行索引。因此,可以使用eBPF字节码在内核中创建上述代码模式。
eBPF的数据数组比它的函数指针数组效率低,所以在可能的情况下攻击将使用后者。
两台机器都没有SMAP,PoC依赖于这个(但是原则上这不应该是一个先决条件)。
另外,至少在测试过的英特尔机器上,在内核之间反弹修改后的缓存行很慢,显然是因为MESI协议用于缓存一致性[8] 。在一个物理CPU内核上更改eBPF阵列的引用计数器会导致包含引用计数器的高速缓存行被跳转到该CPU内核,从而使所有其他CPU内核上的引用计数器的读取速度变慢,直到写入已更改的引用计数器回到记忆。由于eBPF阵列的长度和引用计数器存储在同一个高速缓存行中,这也意味着更改一个物理CPU内核上的引用计数器会导致eBPF阵列的长度读取在其他物理CPU内核上较慢(故意为false共享)。
这次袭击使用了两个eBPF程序。第一个通过页面对齐的eBPF函数指针数组prog_map 在可配置索引处尾部调用。在简化的计算,该程序被用于确定的地址prog_map 通过猜测从偏移prog_map 到用户空间地址和尾调用通过prog_map 在猜到偏移。为了使分支预测能够预测偏移量低于prog_map 的长度,尾调用一个入界索引。增加mis-speculation窗口,缓存行包含prog_map 的长度被反弹到另一个核心。为了测试偏移猜测是否成功,可以测试用户空间地址是否已经加载到缓存中。
由于地址的这种直接的蛮力猜测会很慢,所以使用以下优化:在用户空间地址user_mapping_area 处创建2 15个相邻的用户空间存储器映射[9] ,每个由2 4个页面组成,覆盖总面积2 31 字节。每个映射映射相同的物理页面,并且所有映射都存在于页面表中。
这允许攻击以2 31 字节的步长进行。对于每个步骤,使通过外的边界访问之后prog_map 中,只有一个高速缓存线中的每个从所述第一2 4 页的user_mapping_area 必须对高速缓存的存储器测试。由于L3高速缓存物理地被索引,所以对映射物理页面的虚拟地址的任何访问都将导致映射同一物理页面的所有其他虚拟地址也被高速缓存。
当这种攻击发现一个命中的时候 – 一个缓存的内存位置 – 内核地址的高33位是已知的(因为它们可以从发生命中的地址猜测中得出),并且地址的低16位也是已知的(来自user_mapping_area 内找到命中的偏移量)。user_mapping_area 的地址的剩余部分是中间的。
中间剩余的位可以通过平分剩余的地址空间来确定:将两个物理页面映射到相邻的虚拟地址范围,每个虚拟地址的范围是剩余搜索空间的一半的大小,然后逐位确定剩余的地址。
在这一点上,第二个eBPF程序可以用来实际泄漏数据。在伪代码中,这个程序如下所示:
uint64_t bitmask = <runtime-configurable>;
uint64_t bitshift_selector = <运行时配置>;
uint64_t prog_array_base_offset = <运行时配置>;
uint64_t secret_data_offset = <运行时配置>;
//索引将由运行时进行边界检查,
//但是边界检查将被推测绕过
uint64_t secret_data = bpf_map_read(array = victim_array,index = secret_data_offset);
//选择一个位,将其移动到特定的位置,并添加基准偏移量
uint64_t progmap_index =(((secret_data&bitmask)>> bitshift_selector)<< 7)+ prog_array_base_offset;
bpf_tail_call(prog_map,progmap_index);
该程序在运行时可配置的偏移量和位掩码处从eBPF数据阵列“ victim_map ” 读取8字节对齐的64位值,并对该值进行位移,使得一个位被映射到2个7 字节(当用作数组索引时足以不落入相同或相邻的缓存行)。最后,它添加一个64位的偏移量,然后使用结果值作为到prog_map 的偏移量来进行尾部调用。
这个程序可以用来通过反复调用eBPF程序,使用一个超出边界偏移量的victim_map 来指定要泄漏的数据,并将一个超出边界的偏移量放到prog_map中,导致prog_map + offset 指向一个用户空间内存区域。误导分支预测和弹跳高速缓存行的方式与第一个eBPF程序相同,除了现在,保存victim_map 长度的高速缓存行也必须被反弹到另一个核心。

变体2:分支目标注射

本节描述了我们的PoC的变体2的理论,当在使用Intel Haswell Xeon CPU上的virt-manager创建的KVM guest虚拟机中使用root权限运行时,可以读取主机上运行的特定版本的Debian的distro内核内核内存的速度大约为1500字节/秒。

基本

之前的研究(见最后文献部分)已经表明,在不同安全上下文中的代码可能影响彼此的分支预测。到目前为止,这只被用来推断代码所在位置的信息(换句话说,就是造成受害者对攻击者的干扰)。然而,这种攻击变体的基本假设是,它也可以用来重定向受害者上下文中的代码的执行(换句话说,创造从攻击者到受害者的干扰;反之亦然)。
攻击的基本思想是将包含目标地址从内存加载的间接分支的受害代码作为目标,并将包含目标地址的缓存行清除到主内存。然后,当CPU到达间接分支时,它不会知道跳转的真正目的地,并且在完成把高速缓存线加载回CPU之后,它将不能计算真正的目的地,几百个周期。因此,通常有超过100个周期的时间窗,其中CPU将基于分支预测推测性地执行指令。

Haswell分支预测内部

英特尔处理器实施的分支预测内部部分已经发布; 然而,让这种攻击正常工作需要进一步的实验来确定更多的细节。
本节重点介绍从Intel Haswell Xeon CPU实验获得的分支预测内部结构。
Haswell似乎有多个分支预测机制,工作方式非常不同:
  • 通用分支预测器,每个源地址只能存储一个目标; 用于各种跳转,如绝对跳转,相对跳转等。
  • 一个专门的间接调用预测器,可以为每个源地址存储多个目标; 用于间接呼叫。
  • (根据英特尔的优化手册,还有一个专门的回报预测器,但是我们还没有详细分析,如果这个预测器可以用来可靠地转储一些虚拟机进入的调用堆栈,非常有趣。)

通用预测

如先前研究中所记录的,通用分支预测器仅使用源指令的最后一个字节的地址的低31位来进行预测。例如,如果跳转从0x4141.0004.1000到0x4141.0004.5123存在分支目标缓冲区(BTB)条目,通用预测器也将使用它来预测从0x4242.0004.1000跳转。当源地址的高位如此不同时,预测目的地的高位与它一起改变 – 在这种情况下,预测的目标地址将是0x4242.0004.5123-显然,这个预测器不存储完整的绝对目的地地址。
在使用源地址的低31位来查找BTB条目之前,使用XOR将它们折叠在一起。具体而言,以下几位被折叠在一起:
位A
位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和0x180.8000,但不能区分源地址0x100.0000和0x140.2000或源地址0x100.0000和0x180.4000。在下文中,这将被称为别名源地址。
当使用别名源地址时,分支预测器仍将预测与未混淆源地址相同的目标。这表示分支预测器存储截断的绝对目标地址,但尚未验证。
根据观察到的不同源地址的最大前向和后向跳转距离,目标地址的低32位可以存储为绝对32位值,并附加一位,指定源到目标的跳转是否跨越2 32 边界; 如果跳转跨越这样的边界,则源地址的位31确定指令指针的高位一半是增加还是减少。

间接呼叫预测器

BTB查找这个机制的输入似乎是:
  • 源指令地址的低12位(我们不确定它是第一个还是最后一个字节的地址)还是它们的一个子集。
  • 分支历史缓冲区状态。
如果间接调用预测器无法解析分支,则由通用预测变量解析。英特尔的优化手册暗示了这种行为:“间接调用和跳转,这些可能被预测为具有单调目标或具有根据最近程序行为而变化的目标”。
分支历史缓冲区(BHB)存储关于最后29个采取的分支的信息 – 基本上是最近的控制流的指纹 – 并且被用来允许更好地预测可以具有多个目标的间接呼叫。
BHB的更新功能的工作原理如下(伪代码; src 是源指令的最后一个字节的地址,dst 是目标地址):
void bhb_update(uint58_t * bhb_state,unsigned long src,unsigned long dst){
 * bhb_state << = 2;
 * bhb_state ^ =(dst&0x3f);
 * bhb_state ^ =(src&0xc0)>> 6;
 * bhb_state ^ =(src&0xc00)>>(10 – 2);
 * bhb_state ^ =(src&0xc000)>>(14 – 4);
 * bhb_state ^ =(src&0x30)<<(6 – 4);
 * bhb_state ^ =(src&0x300)<<(8 – 8);
 * bhb_state ^ =(src&0x3000)>>(12 – 10);
 * bhb_state ^ =(src&0x30000)>>(16 – 12);
 * bhb_state ^ =(src&0xc0000)>>(18 – 14);
}
当用于BTB访问时,BHB状态的一些位似乎被XOR进一步折叠在一起,但是精确的折叠功能尚未被理解。
BHB很有趣,有两个原因。首先,为了能够在间接呼叫预测器中准确地引起冲突,需要关于其近似行为的知识。但是它也允许在攻击者可以执行代码的任何可重复的程序状态下抛出BHB状态 – 例如,当攻击管理程序时,直接在超级调用之后。然后可以使用转储的BHB状态来指导管理程序,或者如果攻击者能够访问管理程序二进制,则确定管理程序加载地址的低20位(在KVM的情况下:加载地址的低20位kvm-intel.ko)。

逆向工程分支预测器内部

本小节描述了我们如何反向设计Haswell分支预测器的内部。有些是从记忆中写下来的,因为我们没有详细记录我们正在做的事情。
我们最初尝试使用通用预测器对内核进行BTB注入,使用先前研究的知识,即通用预测器仅查看源地址的下半部分,并且仅存储部分目标地址。这种工作 – 然而,注射成功率很低,低于1%。(这是我们在方法2中使用的方法,用于对运行在Haswell上的修改的虚拟机管理程序进行初始化。
我们决定编写一个用户空间测试用例,以便能够更轻松地测试不同情况下的分支预测器行为。
基于分支预测器状态在超线程之间共享的假设[10]我们编写了一个程序,其中两个实例分别固定在运行在特定物理内核上的两个逻辑处理器中的一个,其中一个实例试图执行分支注入,而另一个实例测量分支注入成功的频率。这两个实例都是在禁用ASLR的情况下执行的,并且在相同的地址上具有相同的代码。注入过程对访问(每进程)测试变量的函数进行间接调用; 测量过程对函数进行间接调用,该函数根据时序测试每个进程的测试变量是否被缓存,然后使用CLFLUSH将其逐出。这两个间接呼叫都是通过相同的呼叫站点执行的。在每次间接调用之前,使用CLFLUSH将存储器中存储的函数指针刷新到主存储器,以扩大推测时间窗口。另外,
在这个测试中,注射成功率在99%以上,为我们今后的实验奠定基础。
然后,我们试图找出预测方案的细节。我们假定预测方案使用某种全局分支历史记录缓冲区。
为了确定分支信息保留在历史缓冲区中的持续时间,仅在两个程序实例中的一个中获取的条件分支被插入在一系列始终采用的条件跳转的前面,则始终采用条件跳转的数量跳跃(N)是变化的。结果是,对于N = 25,处理器能够区分分支(在1%以下的误预测率),但是对于N = 26,未能这样做(误预测率超过99%)。
因此,分支历史缓冲区必须能够存储关于至少最后的26个分支的信息。
两个程序实例之一中的代码随后在内存中移动。这揭示了只有源地址和目标地址的低20位对分支历史缓冲器有影响。
在两个程序实例中使用不同类型的分支进行测试,发现静态跳转,采用有条件的跳转,调用和返回都以同样的方式影响分支历史缓冲区; 没有采取有条件的跳跃不影响它; 源指令的最后一个字节的地址是计数的地址;IRETQ不会影响历史缓冲区状态(这对测试很有用,因为它允许创建历史缓冲区不可见的程序流)。
在内存中间接调用多次之前移动最后的条件分支,显示分支历史缓冲区内容可以用来区分最后一个条件分支指令的许多不同位置。这表明历史缓冲区不存储小历史值列表; 相反,它似乎是历史数据混合在一起的更大的缓冲区。
然而,为了对分支预测有用,一个历史缓存需要在已经采用了一定数量的新分支之后“忘记”过去的分支。因此,当新的数据被混合到历史缓冲器中时,这不会导致已经存在于历史缓冲器中的比特中的信息向下传播 – 并且因此,信息的向上组合也可能不会很有用。考虑到分支预测也必须非常快,我们得出结论:历史缓冲区的更新功能可能左移旧历史缓冲区,然后XOR处于新状态(见图)。
如果这个假设是正确的,则历史缓冲区包含许多关于最近分支的信息,但是只包含与每个历史缓冲区更新关于包含任何数据的最后一个分支所移动的信息位数。因此,我们测试了翻转跳转的源地址和目标地址中的不同位,然后是带有静态源和目标的总是32个跳转,允许分支预测消除间接调用的歧义。[11]
中间有32个静态跳转,没有任何翻转似乎有影响,所以我们减少了静态跳转的次数,直到可以观察到差异。总共有28次跳转的结果是目标的0x1和0x2位和源的0x40和0x80位有这样的影响; 但是翻转目标中的0x1和源中的0x40或目标中的0x2和源中的0x80不允许消除歧义。这表明历史缓冲区的每插入移位是2位,并且显示哪些数据存储在历史缓冲区的最低有效位中。然后,我们在跳转位后通过减少固定跳转来重复这一点,以确定哪些信息存储在其余位中。

从KVM guest虚拟机读取主机内存

找到主机内核

我们的PoC分几步找到主机内核。下一步攻击确定和必要的信息包括:
  • 低于kvm-intel.ko地址的20位
  • kvm.ko的完整地址
  • vmlinux的完整地址
回顾一下,这是不必要的复杂,但很好地演示了攻击者可以使用的各种技术。更简单的方法是首先确定vmlinux的地址,然后平分kvm.ko和kvm-intel.ko的地址。
在第一步,kvm-intel.ko的地址被泄露。为此,访客输入后的分支历史缓冲区状态被转出。然后,对于kvm-intel.ko的加载地址的第12..19位的每个可能的值,根据加载地址推测和最后8个分支的已知偏移量计算历史缓冲器的预期最低16位来宾条目,并将结果与​​泄漏历史缓冲区状态的最低16位进行比较。
通过测量两个目标的间接调用的误预测率,分支历史缓冲器状态以2位为单位泄漏。间接调用的一种方式是从vmcall指令跟随一系列N个分支,其相关的源地址位和目标地址位都是零。间接调用的第二种方式是来自用户空间中的一系列受控分支,可用于将任意值写入分支历史缓冲区。
错误预测率如“反向工程分支预测器内部”部分所述进行测量,使用一个调用目标加载缓存行,另一个检查是否加载了相同的缓存行。
在N = 29的情况下,如果受控分支历史缓冲器值为零,则由于来自超级调用的所有历史缓冲器状态已被擦除,所以误预测将以高速率发生。在N = 28的情况下,如果受控分支历史缓冲器值是0 <<(28 * 2),1 <<(28 * 2),2 <<(28 * 2),3 <<(28) * 2) – 通过测试所有四种可能性,可以检测哪一个是正确的。那么,为了减少N的值,四种可能性是{0 | 1 | 2 | 3} <<(28 * 2)| (history_buffer_for(N + 1)>> 2)。通过重复该操作以减少N的值,可以确定N = 0的分支历史缓冲器值。
此时,kvm-intel.ko的低20位是已知的; 下一步是大致找到kvm.ko.
为此,使用通用分支预测器,使用插入到BTB中的数据,通过从kvm.ko到kvm-intel.ko的间接调用,发生在每个hypercall上; 这意味着间接调用的源地址必须从BTB中泄漏出去。
kvm.ko可能位于从0xffffffffc0000000 到0xffffffffc4000000 的范围内,页面对齐(0x1000)。这意味着“通用预测变量”一节中表格的前四项适用; 将有正确的2 4 -1 = 15混叠地址。但这也是一个优点:它将搜索空间从0x4000减少到0x4000 / 2 4 = 1024。
为了找到合适的源地址或其别名地址,通过特定寄存器加载数据的代码被放置在所有可能的调用目标(kvm-intel.ko的低20位泄漏加上模块的模块内偏移呼叫目标加2的倍数20 )和间接呼叫被放置在所有可能的呼叫源。然后,交替执行超级调用,并通过不同的可能的非别名调用源执行间接调用,并使用随机历史缓冲区状态来防止专门的预测工作。在该步骤之后,有2种16 对于kvm.ko.的加载地址剩余的可能性
接下来,可以使用从vmlinux到kvm.ko的间接调用,以类似的方式确定vmlinux的加载地址。幸运的是,在vmlinux的加载地址中没有随机化的位被折叠在一起,所以与定位kvm.ko时不同,结果将是直接唯一的。vmlinux具有2MiB的对齐和1GiB的随机化范围,所以仍然只有512个可能的地址。
因为(就我们所知),一个简单的超级调用实际上并不会导致从vmlinux到kvm.ko的间接调用,而是使用来自模拟串行端口的状态寄存器的端口I / O,一个用virt-manager创建的虚拟机。
剩下的唯一信息是kvm.ko的16个别名加载地址中的哪一个实际上是正确的。由于对kvm.ko的间接调用的源地址是已知的,因此可以使用二分法来解决:将代码放置在各种可能的目标上,根据代码的哪个实例被推测执行,加载两个缓存行中的一个,以及测量哪一个缓存行被加载。

识别缓存集

PoC假定虚拟机无权访问巨大的页面。为了发现具有相对于4KiB页面边界的特定对齐的所有L3缓存集合的驱逐集合,PoC首先分配25600页的内存。然后,在循环中,它选择所有剩余未排序页面的随机子集,使得子集中包含驱逐集合的集合的期望数目为1,通过重复访问其高速缓存行将每个子集合减小到驱逐集合,以及测试缓存行是否总是被缓存(在这种情况下,它们可能不是驱逐集合的一部分),并尝试使用新的驱逐集合来驱逐所有剩余的未排序的缓存行,以确定它们是否在同一个缓存集合中[12 ]。

查找访客页面的主机虚拟地址

由于此攻击使用FLUSH + RELOAD方法泄漏数据,因此需要知道一个访客页面的主机内核虚拟地址。PRIME + PROBE等替代方法应该没有这个要求。
攻击这一步的基本思路是对管理程序使用分支目标注入攻击来加载攻击者控制的地址,并测试是否导致客户拥有的页面被加载。为此,可以使用从R8指定的内存位置进行简单加载的小工具 – 在此内核版本上达到访客退出后的第一个间接调用时,R8-R11仍包含访客控制的值。
我们期望攻击者需要知道在这一点上必须使用哪个驱逐集,或者同时暴力驱逐。然而,在实验上,使用随机驱逐集合也是如此。我们的理论是观察到的行为实际上是L1D和L2驱逐的结果,这可能足以允许几个指令值得投机执行。
主机内核映射(几乎?)物理内存区域中的所有物理内存,包括分配给KVM guest虚拟机的内存。然而,physmap的位置是随机的(1GiB对齐),在一个大小为128PiB的区域。因此,直接强制访客页面的主机虚拟地址需要很长时间。这不一定是不可能的; 作为一个估计值,应该可能在一天左右,或许更少,假设每秒12000次成功的注射和30个并行测试的客户页面; 但几分钟之内就没那么令人印象深刻了。
为了优化这个问题,可以分解这个问题:首先,使用可以从物理地址加载的小工具强制物理地址,然后暴力破坏physmap区域的基地址。因为通常可以假定物理地址远远低于128PiB,所以它可以被更有效地强制性地使用,并且随后强制physmap区域的基地址也更容易,因为可以使用具有1GiB对齐的地址猜测。
要蛮力的物理地址,可以使用下面的小工具:
ffffffff810a9def:4c 89 c0 mov rax,r8
ffffffff810a9df2:4d 63 f9 movsxd r15,r9d
ffffffff810a9df5:4e 8b 04 fd c0 b3 a6 mov r8,QWORD PTR [r15 * 8-0x7e594c40]
ffffffff810a9dfc:81
ffffffff810a9dfd:4a 8d 3c 00 lea rdi,[rax + r8 * 1]
ffffffff810a9e01:4d 8b a4 00 f8 00 00 mov r12,QWORD PTR [r8 + rax * 1 + 0xf8]
ffffffff810a9e08:00
这个小工具允许通过适当地设置R9从内核文本部分周围的区域加载一个8字节对齐的值,这尤其允许加载physmap 的起始地址page_offset_base。然后,原来在R8中的值 – 物理地址猜测减去0xf8 – 被加到前一次加载的结果,0xfa被加到它上面,结果被解除引用。

缓存集选择

为了选择正确的L3驱逐集,来自下一节的攻击基本上以不同的驱逐集执行,直到它工作。

泄露数据

在这一点上,通常有必要在主机内核代码中定位可用于通过从攻击者控制的位置读取来实际泄漏数据的小工具,适当地移动和掩盖结果,然后将结果用作偏移一个攻击者控制的负载地址。但是将小玩意拼凑在一起,搞清楚在推测环境中哪些工作起作用似乎很烦人。因此,我们决定使用内置于主机内核的eBPF解释器,而在虚拟机内部没有合法的方法调用它,主机内核文本部分中的代码足以使其可用对于攻击,就像普通的ROP小工具一样。
eBPF解释器入口点具有以下功能签名:
static unsigned int __bpf_prog_run(void * ctx,const struct bpf_insn * insn)
第二个参数是指向要执行的静态预先验证的eBPF指令数组的指针 – 这意味着__bpf_prog_run()不会执行任何类型的检查或边界检查。第一个参数只是作为初始模拟寄存器状态的一部分存储,所以它的值并不重要。
eBPF口译员除其他外提供:
  • 多个仿真的64位寄存器
  • 64位立即写入模拟寄存器
  • 内存从存储在仿真寄存器中的地址读取
  • 按位操作(包括位移)和算术运算
为了调用解释器入口点,给定R8-R11控制和受控数据在已知存储器位置的RSI和RIP控制的小工具是必需的。以下小工具提供了此功能:
ffffffff81514edd:4c 89 ce mov rsi,r9 ffffffff81514ee0:41 ff 90 b0 00 00 00 call QWORD PTR [r8 + 0xb0]
现在,通过将R8和R9指向physmap中客户拥有的页面的映射,可以在主机内核中推测性地执行任意未经验证的eBPF字节码。然后,可以使用相对简单的字节码将数据泄漏到缓存中。

变种3:恶意数据缓存加载

基本上,阅读安德斯·福格的博文:https ://cyber.wtf/2017/07/28/negative-result-reading-kernel-memory-from-user-mode/
总之,使用这个问题变体的攻击尝试从用户空间读取内核内存,而不会误导内核代码的控制流。这通过使用用于以前变体的代码模式,但在用户空间中起作用。其基本思想是,访问地址的权限检查可能不是从内存读取数据到寄存器的关键路径,权限检查可能会对性能产生重大影响。相反,内存读取可以使得读取的结果立即可用于以下指令,并且仅异步执行权限检查,在重新排序缓冲区中设置标志,如果权限检查失败,则会引发异常。
我们对Anders Fogh的博文有一些补充:
“想象一下在usermode中执行的下面的指令
mov rax,[somekernelmodeaddress]
退休时会造成中断,[…]“
在高延迟的预测错误分支后面也可以执行该指令,以避免发生页面错误。这也可以通过增加从内核地址读取和传送相关的异常之间的延迟来扩大猜测窗口。
“首先,我调用一个系统调用来触及这个内存;其次,我使用prefetcht0指令来提高我在L1中加载地址的几率。
当我们在系统调用之后使用预取指令时,攻击停止了对我们的工作,并且我们不知道为什么。也许CPU以某种方式存储访问是否在上次访问被拒绝,并防止攻击的工作,如果是这样的话?
“幸运的是,我没有得到一个缓慢的阅读暗示英特尔null的结果,当访问是不允许的。”
那(从内核地址读取返回全零)似乎发生的内存不足够缓存,但对于哪些可分页条目存在,至少在重复读取尝试。对于未映射的内存,内核地址读取根本不返回结果。

进一步研究的想法

我们相信我们的研究提供了许多尚未研究的剩余研究课题,我们鼓励其他公共研究人员研究这些课题。
这部分内容比这篇博文的其他内容还包含更多的猜测 – 它包含未经测试的想法,可能是没用的。

泄漏没有数据缓存时间

除了测量数据高速缓存时间之外,探讨是否存在微架构攻击将是有趣的,这些数据高速缓存时间可以用于从推测执行中渗出数据。

其他微架构

到目前为止,我们的研究相对以Haswell为中心。看到细节,例如其他现代处理器的分支预测如何工作,以及如何能够被攻击,这将是有趣的。

其他JIT引擎

我们针对内核中的JIT引擎开发了一个成功的变种1攻击。看看对系统控制较少的更先进的JIT引擎的攻击是否也是可行的,特别是JavaScript引擎是很有意思的。

更高效地扫描主机虚拟地址和缓存集

在变体2中,在扫描客户拥有的页面的主机虚拟地址的同时,尝试首先确定其L3缓存设置可能是有意义的。这可以通过使用通过physmap的逐出模式执行L3逐出,然后测试逐出是否影响客户拥有的页面来完成。
对于缓存集合也是一样的 – 使用L1D + L2驱逐集合来驱逐宿主内核上下文中的函数指针,使用内核中的小配件使用物理地址驱逐L3集合,然后使用它来确定哪些缓存设置了guest直到客人拥有的驱逐集已经建成。

倾倒完整的BTB状态

鉴于通用BTB似乎只能区分2 31-8个或更少的源地址,似乎可行的是在大约几个小时的时间周期内转储由例如hypercall生成的完整BTB状态。(扫描跳转源,然后为每个发现的跳转源,将跳转目标等分。)即使主机内核是定制的,也可能用于识别主机内核中函数的位置。
源地址别名会降低实用性,但是由于目标地址不会受到这种影响,因此可能会从具有不同KASLR偏移量的机器关联(源,目标)对,并根据KASLR降低候选地址的数量加法,而混叠是按位。
这样就可能允许攻击者根据跳转偏移量或函数间的距离来猜测主机内核版本或编译器。

变种2:泄漏更有效的小工具

如果对变体2使用足够高效的小配件,则根本不需要从L3缓存驱逐主机内核功能指针; 只从L1D和L2驱逐它们可能就足够了。

各种加速

特别是变种2的PoC仍然有点慢。这可能部分是因为:
  • 它一次只泄漏一点; 一次泄漏更多的应该是可行的。
  • 它大量使用IRETQ来隐藏处理器的控制流。
使用变体2可以实现什么样的数据泄漏率是很有意思的。

通过回报预测器泄漏或注射

如果返回预测器在特权级别更改时也不会丢失状态,那么从VM内部定位主机内核可能非常有用(在这种情况下,可以使用二分法来快速发现主机内核的完整地址)或注入返回目标(特别是如果返回地址存储在缓冲区行中,可以被攻击者清除并且在返回指令之前不重新加载)。
然而,我们还没有对迄今为止取得确凿结果的回报预测因子进行任何实验。

从间接呼叫预测器泄漏数据

我们试图从间接呼叫预测器中泄漏目标信息,但是还没有能够使其工作。

供应商声明

Project Zero向他们透露了这个漏洞的供应商向我们提供了以下声明:

英特尔

目前没有提供当前的声明。

AMD

Arm认识到许多现代高性能处理器的推测功能,尽管按预期工作,可以结合缓存操作的时间来泄漏一些信息,如本博客中所述。相应地,Arm开发了我们推荐部署的软件缓解措施。
有关受影响的处理器和缓解措施的详细信息,请访问以下网站:https//developer.arm.com/support/security-update
Arm包含了详细的技术白皮书,以及来自Arm架构合作伙伴关于其特定实施和缓解的信息的链接。

Leave a Reply