本文是Reading privileged memory with a side-channel的翻译文章。

首发于先知社区:https://xz.aliyun.com/t/2273

Table of Contents

  1. 前言
  2. 测试处理器
  3. 词汇表
  4. 变体1:边界检查旁路
    1. 理论解释
    2. 内核攻击
  5. 分支目标注入
    1. 基础
    2. Haswell分支预测内部
      1. 通用预测器
      2. 间接调用预测器
      3. 逆向工程分支预测器内部
    3. 从KVM guest虚拟机读取主机内存
      1. 找到主机内核
      2. 识别缓存集
      3. 查找guest页面的主机虚拟地址
      4. 缓存集选择
      5. 数据泄露
  6. 变体3:恶意数据缓存加载
  7. 进一步研究的想法
    1. 没有数据高速缓存时序的泄露
    2. 其他微架构
    3. 其他JIT引擎
    4. 更高效地扫描主机虚拟地址和缓存集
    5. 倾倒完整的BTB状态
    6. 变体2:泄露更有效的gadget
    7. 各种加速
    8. 使用return预测器泄露或注入
    9. 从间接调用预测器泄露数据
  8. 供应商声明
    1. 英特尔
    2. AMD
    3. ARM
  9. 文献
  10. 参考

前言

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

已知这个问题的变体会影响许多现代处理器,包括英特尔,AMD和ARM的某些处理器。对于少数英特尔和AMD CPU型号,我们有针对真正软件的攻击。我们在2017-06-01向英特尔,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也报告了这些问题; 他们的[writeups/博文/论文稿]可以在下面找到:

在我们的研究过程中,我们开发了以下概念验证(PoC):

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

有关此主题的有趣资源,请参阅“文献”部分。

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

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

我们发送给CPU供应商的PoC代码和写法可以在这里找到: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 of a Google Nexus 5x phone [6] (本文档的其余部分称为“ARM Cortex A57”)

词汇表

退出(retire):当其结果(例如寄存器写入和存储器写入)被提交并使其对系统的其他部分可见时,指令退出。指令可以不按顺序执行,但必须按顺序退出。

逻辑处理器核心(logical processor core):逻辑处理器核心是操作系统认为的处理器核心。启用超线程后,逻辑核心的数量是物理核心数量的倍数。

缓存/未缓存的数据(cached/uncached data):在本文中,“未缓存”的数据是仅存在于主内存中的数据,而不是CPU的任何缓存级别中的数据。加载未缓存的数据通常需要超过100个CPU时间周期。

推测性执行(speculative execution):处理器可以执行经过分支而不知道其是否被采用或其目标在何处,因此在知道它们是否应该被执行之前执行指令。如果这种推测结果不正确,那么CPU可以放弃没有架构效应的结果状态,并继续在正确的执行路径上执行。在知道它们处于正确的执行路径之前,指令不会退出。

错误推测窗口(mis-speculation window):CPU推测性地执行错误代码并且尚未检测到错误发生的时间窗口。

变体1:边界检查旁路

本节解释所有三种变体背后的常见理论,以及我们PoC变体1背后的理论,在Debian distro内核下运行在用户空间中时,可以在内核内存的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系列处理器)”中声明:

隐式高速缓存发生在内存元素具有可缓存性时,尽管该元素可能永远不会以正常的冯诺依曼序列被访问。由于积极的预取,分支预测和TLB未命中处理,隐式高速缓存出现在P6和更新的处理器系列上。隐式缓存是现有Intel386,Intel486和Pentium处理器系统行为的扩展,因为在这些处理器系列上运行的软件也无法确定性地预测指令预取的行为。

考虑下面的代码示例。如果arr1-> length 未缓存,则处理器可以推测性地从arr1-> data [untrusted_offset_from_caller] 加载数据。这是一个超出界限的阅读。这应该不重要,因为处理器将在分支执行时有效回滚执行状态; 推测性执行的指令都不会退出(例如导致寄存器等被影响)。

1
2
3
4
5
6
7
8
9
10
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-> lengtharr2-> data [0x200] 和arr2-> data [0x300] 没有被缓存,但所有其他被访问的数据都是,并且分支条件预测为true,处理器可以在加载arr1-> length并重新执行之前进行如下推测:

  • load value = arr1-> data [ untrusted_offset_from_caller ]
  • arr2-> data中的数据相关偏移量开始加载,将相应的高速缓存行加载到L1高速缓存中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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依赖于此(但它原则上不应该是一个先决条件)。

此外,至少在经过测试的Intel机器上,在内核之间弹跳修改后的缓存行很慢,显然是因为MESI协议用于缓存一致性[8]。 在一个物理CPU内核上更改eBPF阵列的引用计数器会导致包含引用计数器的高速缓存行被跳转到该CPU内核,从而使所有其他CPU内核上的引用计数器的读取速度变慢,直到已更改的引用计数器已被写回到内存。 由于eBPF阵列的长度和引用计数器存储在同一个高速缓存行中,这也意味着更改一个物理CPU内核上的引用计数器会导致eBPF阵列的长度读取在其他物理CPU内核上较慢(故意为false共享)。

该攻击使用两个eBPF程序。 第一个通过页面对齐的eBPF函数指针数组prog_map在可配置索引处尾部调用。 简单地说,这个程序用于通过猜测从prog_map到用户空间地址的偏移量并在猜测的偏移量处调用throughprog_map来确定prog_map的地址。 为了使分支预测预测偏移量低于prog_map的长度,在两者之间执行对边界索引的尾调用。 为了增加错误猜测窗口,包含prog_map长度的高速缓存行被反弹到另一个核心。 要测试偏移猜测是否成功,可以测试用户空间地址是否已加载到高速缓存中。

因为这种直接暴力猜测地址的方法会很慢,所以我们使用下面的优化:
在用户空间地址user_mapping_area处创建2^15个相邻用户空间存储器映射[9] ,每个由2^4个页面组成,覆盖总面积为2^31 字节。每个映射映射相同的物理页面,并且所有映射都存在于页面表中。

这允许攻击以2^31个字节为单位执行。 对于每一步,在通过prog_map导致越界访问之后,只需要测试user_mapping_area的前2^4个页面中的每一个缓存行以获取缓存内存。 由于L3高速缓存物理索引,因此对映射物理页面的虚拟地址的任何访问都将导致映射同一物理页面的所有其他虚拟地址也被高速缓存。

当这种攻击发现一个hit(缓存的内存位置时),内核地址的高33位是已知的(因为它们可以根据发生hit的地址猜测得出),并且地址的低16位也是已知的 (来自user_mapping_area内找到命中的偏移量)。 user_mapping_area的地址是剩余的中间部分。

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

此时,可以使用第二个eBPF程序实际泄漏数据。 在伪代码中,这个程序看起来如下:

1
2
3
4
5
6
7
8
9
10
uint64_t bitmask = <runtime-configurable>;
uint64_t bitshift_selector = <runtime-configurable>;
uint64_t prog_array_base_offset = <runtime-configurable>;
uint64_t secret_data_offset = <runtime-configurable>;
// index will be bounds-checked by the runtime,
// but the bounds check will be bypassed speculatively
uint64_t secret_data = bpf_map_read(array=victim_array, index=secret_data_offset);
// select a single bit, move it to a specific position, and add the base 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 bit,并对该值进行位移,使得一个bit映射到相距2^7个字节的两个值中的一个 (当用作数组索引时足以不落入相同或相邻的缓存行)。 最后,它添加一个64 bit的偏移量,然后使用结果值作为prog_map的偏移量,以用于尾部调用。

这个程序可以用来通过反复调用eBPF程序来将内存偏移调用到victim_map中来泄漏内存,这个偏移量指定了要泄漏的数据,并且在prog_mapthat中出现了一个超出边界的偏移量,从而导致prog_map + offset指向用户空间的一个内存区域。 误导分支预测和弹跳缓存线的方式与第一个eBPF程序的方式相同,不同之处在于现在保存victim_map长度的缓存线也必须退回到另一个内核。

分支目标注入

本节介绍了我们PoC的变体2背后的理论,当在使用Intel Haswell Xeon CPU上的virt-manager创建的KVM guest虚拟机中使用root权限运行时,可以读取主机上运行的特定版本的Debian发行版内核内核内存的速度大约为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将它们折叠在一起。 具体而言,以下几位被折叠在一起:

换句话说,如果一个源地址与这个表的一行中的两个数字异或,那么在执行查找时,分支预测器将无法将结果地址与原始源地址区分开来。
例如,分支预测器可以区分源地址0x100.0000和0x180.0000,也可以区分源地址0x100.0000和0x180.8000,但不能区分源地址0x100.0000和0x140.2000 或源地址0x100.0000和0x180.4000。 在下文中,这将被称为别名源地址。

当使用别名源地址时,分支预测器仍然会预测与未混淆源地址相同的目标。 这表明分支预测器存储截断的绝对目标地址,但尚未验证。

根据观察到的不同源地址的最大前向和后向跳转距离,目标地址的低32位可以存储为绝对32位值,并附加一个bit,指定从源跳转到目标是否跨越2^32边界; 如果跳转跨越这样的边界,则源地址的31 bit确定指令指针的高位一半是应该递增还是递减。

间接调用预测器

该机制的BTB查找的输入似乎是:

  • 源指令地址的低12位(我们不确定它是第一个还是最后一个字节的地址)或它们的一个子集。
  • 分支历史缓冲区状态。

如果间接调用预测器无法解析分支,则由通用预测变量解析。英特尔的优化手册暗示了这种行为:“间接调用和跳转,它们可能被预测为具有单调目标或具有根据最近程序行为而变化的目标。”

分支历史缓冲区(BHB)存储关于最后29个采取分支的信息(基本上是最近控制流程的指纹)并用于更好地预测可能有多个目标的间接调用。

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

1
2
3
4
5
6
7
8
9
10
11
12
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的初始PoC中使用的方法,针对在Haswell上运行的修改后的虚拟机监控程序。)

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

基于分支预测器状态在超线程之间共享的假设[10],我们编写了一个程序,其中两个实例分别固定到在特定物理内核上运行的两个逻辑处理器中的一个,其中一个实例尝试执行分支注入,而另一个实例测量分支注入成功的频率。 这两个实例都是在禁用ASLR的情况下执行的,并且具有相同地址的相同代码。
注入过程对访问(每个进程)测试变量的函数执行间接调用;测量过程对函数进行间接调用,该函数根据时序测试每个进程的测试变量是否被缓存,然后将其逐出 使用CLFLUSH。 这两个间接呼叫都是通过相同的呼叫站点执行的。 在每次间接调用之前,使用CLFLUSH将存储在内存中的函数指针刷新到主内存以扩大推测时间窗口。
此外,由于英特尔优化手册中提及“最近的程序行为”,因此总是采用的一组条件分支插入到间接调用之前。

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

然后我们试图找出预测方案的细节。 我们假设预测方案使用某种全局分支历史缓冲区。

为了确定分支信息保持在历史缓冲区中的持续时间,仅在两个程序实例中的一个中采用的条件分支被插入在一系列始终采用的条件跳转的前面,则总是采用的条件跳转(N)的数量是变化的。 结果是,对于N = 25,处理器能够区分分支(误预测率低于1%),但是对于N = 26,它没有这样做(错误预测率超过99%)。
所以分支历史缓冲区必须至少能存储最后的26个分支的信息。

两个程序实例之一的代码随后在内存中移动。 这表明只有源地址和目标地址的低20位对分支历史缓冲区有影响。

在两个程序实例中使用不同类型的分支进行测试表明,静态跳转,条件跳转,调用和返回以同样的方式影响分支历史缓冲区; 未采取有条件的跳跃不会影响它; 源指令的最后一个字节的地址是计数的地址; IRETQ不会影响历史缓冲区状态(这对测试很有用,因为它允许创建历史缓冲区不可见的程序流)。

在间接调用内存之前,移动最后的条件分支多次显示分支历史缓冲区内容可用于区分最后一个条件分支指令的许多不同位置。 这表明历史缓冲区不存储小历史值列表; 相反,它似乎是历史数据混合在一起的更大的缓冲区。

然而,为了对分支预测有用,历史缓冲区在需要一定数量的新分支之后会“忘记”过去的分支。 因此,当新数据混合到历史缓冲区中时,不会导致历史缓冲区中已经存在的位中的信息向下传播 - 并且考虑到向上组合的信息可能不会非常有用。 考虑到分支预测也必须非常快,我们认为,历史缓冲区的更新功能可能左移旧历史缓冲区,然后XOR处于新状态(参见图)。

如果这个假设是正确的,那么历史缓冲区包含大量关于最近分支的信息,但只包含与每个历史缓冲区更新有关的最后一个含有任何数据的分支所移动的信息位数。因此,我们测试了翻转跳转的源地址和目标地址中的不同位,然后是带有静态源和目标的32个始终采用的跳转,允许分支预测消除间接调用的歧义。[11]

中间有32个静态跳转,似乎没有位翻转被影响,所以我们减少了静态跳转的次数,直到可以观察到差异。28次跳转的结果是目标的0x1和0x2 bits以及源的0x40和0x80 bits被影响。但是翻转目标中的0x1和源中的0x40或目标中的0x2和源的0x80不允许消除歧义。
这表明历史缓冲区每次插入的移位是2位,并显示哪些数据存储在历史缓冲区的最低有效位中。然后,我们在跳转位后通过减少固定跳转次数来确定哪些信息存储在剩余位中。

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

找到主机内核

我们的PoC通过几个步骤来定位主机内核。下一步攻击所确定和必要的信息包括:

  • 低于kvm-intel.ko地址的20位
  • kvm.ko的完整地址
  • vmlinux的完整地址

回顾一下,这并不是必要的,但它很好地演示了攻击者可以使用的各种技术。 更简单的方法是首先确定vmlinux的地址,然后平分kvm.ko和kvm-intel.ko的地址。

第一步,kvm-intel.ko的地址被泄露。 为此,guest输入后的分支历史缓冲区状态被转出。然后,对于kvm-intel.ko的加载地址的位12..19的每个可能的值,历史缓冲区的预期最低16位是基于加载地址推测和guest入口之前的最后8个分支的已知偏移来计算的,并将结果与泄漏历史缓冲区状态的最低16位进行比较。

通过测量具有两个目标的间接调用的误预测速率,分支历史缓冲区状态以2位的步长泄漏。间接调用的一种方式是从vmcall指令开始,然后是一系列N个分支,其相关的源地址位和目标地址位均为零。间接调用的第二种方式来自用户空间中的一系列受控分支,可用于将任意值写入分支历史缓冲区。

错误预测率的测量方法与“向工程分支预测器内部”部分相同,使用一个调用目标加载缓存行,另一个检查是否加载了相同的缓存行。

在N = 29的情况下,如果受控分支历史缓冲器值为零,则由于来自超级调用的所有历史缓冲器状态已被擦除,错误预测将以高速率发生。
在N = 28的情况下,如果受控分支历史缓冲值为 0<<(282), 1<<(282), 2<<(282), 3<<(282) 之一,通过测试所有四种可能性,可以检测哪一种是正确的。然后,为了减少N的值,四种可能性是{0|1|2|3}<<(28*2) | (history_buffer_for(N+1) >> 2)。
通过重复此操作以减少N的值,可以确定N = 0的分支历史缓冲值。

此时,kvm-intel.ko的低20位是已知的; 下一步是大致找到kvm.ko.
为此,使用通用分支预测器,和从kvm.ko到kvm-intel.ko的间接调用插入到BTB中的数据,该数据发生在每个超级调用上;这意味着间接调用的源地址必须从BTB中泄漏出去。

kvm.ko可能位于从0xffffffffc0000000到0xffffffffc4000000的范围内,页面对齐(0x1000)。 这意味着“通用预测变量”一节中表中的前四项适用; 会有24-1 = 15个别名的地址。 但这也是一个优点:它将搜索空间从0x4000减少到0x4000 / 2^4 = 1024。

要找到源或其某个别名地址的正确地址,通过特定寄存器加载数据的代码被放置在所有可能的调用目标(kvm-intel.ko的泄漏低20位加上调用目标的模块内偏移加上220的倍数)并且间接呼叫被放置在所有可能的呼叫源。
然后,交替执行超级调用,并通过不同的可能的非别名调用源执行间接调用,并使用随机历史缓冲区状态,防止专用预测工作。 在此步骤之后,kvm.ko的加载地址仍有216个可能性。

接下来,可以使用从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]。

查找guest页面的主机虚拟地址

由于此攻击使用FLUSH + RELOAD方法泄漏数据,因此需要知道一个guest页面的主机内核虚拟地址。 诸如PRIME + PROBE等替代方法应该没有这个要求。

攻击这一步的基本思路是对管理程序使用分支目标注入攻击来加载攻击者控制的地址并测试是否导致加载客户拥有的页面。为此,可以使用从R8指定的内存位置进行简单加载的gadget - 在此内核版本上达到guest退出后的第一个间接调用时,R8-R11仍包含guest控制值。

我们预计攻击者需要知道在这一点上必须使用哪个驱逐集或者同时暴力驱逐,但是,通过实验,使用随机驱逐集也可以。 我们的理论是观察到的行为实际上是L1D和L2驱逐的结果,这可能足以允许一些指令值的推测性执行。

主机内核映射(近似于)physmap区域中的所有物理内存,包括分配给KVM guest虚拟机的内存。 但是,physmap的位置是随机的(1GiB对齐),在大小为128PiB的区域。 因此,直接强制guest页面的主机虚拟地址需要很长时间。 这是可能的; 作为估计值,假设每秒12000次成功注入和30个并行测试的guest页面,应该可能在一天左右或更短的时间内完成; 但不像几分钟内那么令人印象深刻。

为了优化这个问题,可以将问题分解:首先,使用可以从物理地址加载的小工具蛮力物理地址,然后蛮力施加physmap区域的基地址。
因为通常可以假定物理地址远远低于128PiB,所以它可以更有效地暴力破解,并且之后暴力破坏physmap区域的基地址也更容易,因为可以使用1GiB对齐的地址猜测。

要强制使用物理地址,可以使用以下gadget:

1
2
3
4
5
6
7
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

这个gadget允许通过适当地设置R9来加载内核文本部分周围8字节对齐的值,尤其允许加载physmap的起始地址page_offset_base。 然后,原来在R8中的值 - 物理地址推测值减去0xf8 - 被添加到先前加载的结果中,将0xfa加入其中,并且结果被解除引用。

缓存集选择

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

数据泄露

在这一点上,通常有必要在主机内核代码中定位可用于实际泄漏数据的小工具,方法是从攻击者控制的位置读取数据,对结果进行适当的移位和掩码,然后将结果作为抵消攻击者控制地址的负载。但是将gadget拼凑在一起,弄清楚哪些在推测环境中起作用看起来很烦人。
所以作为替代,我们决定使用eBPF解释器,它是内置于宿主内核的 - 虽然没有合法的方式从虚拟机内部调用它,但主机内核的文本部分中代码的存在足以使其可用于攻击,就像普通的ROP gadget一样。

eBPF解释器入口点具有以下函数签名:

1
static unsigned int __bpf_prog_run(void *ctx, const struct bpf_insn *insn)

第二个参数是指向要执行的静态预验证eBPF指令数组的指针 - 这意味着__bpf_prog_run()不会执行任何类型检查或边界检查。 第一个参数只是作为初始模拟寄存器状态的一部分存储,所以它的值并不重要。

eBPF解释器提供了以下内容:

  • 多个仿真的64位寄存器
  • 64位立即写入仿真寄存器
  • 内存从存储在仿真寄存器中的地址读取数据
  • 按位操作(包括位移)和算术运算

要调用解释器入口点,需要一个给定R8-R11控制和受控数据在已知内存位置的RSI和RIP控制的gadget。 以下gadget提供了此功能:

1
2
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:恶意数据缓存加载

基本上看Anders Fogh的博文就够了: https://cyber.wtf/2017/07/28/negative-result-reading-kernel-memory-from-user-mode/

总之,使用此问题变体的攻击尝试从用户空间读取内核内存,而不会误导内核代码的控制流。
这通过使用用于以前变体的代码模式,但是是在用户空间中作用。
其基本思想是,访问地址的权限检查可能不是关于从内存向寄存器读取数据的关键路径,权限检查可能会对性能产生重大影响。相反,内存读取可以使读取的结果立即可用于以下指令,并且只是异步执行权限检查,如果权限检查失败,则在重新排序缓冲区中设置一个标志,导致引发异常。

我们对Anders Fogh的博文有一些补充:

“想象一下在usermode中执行的以下指令
mov rax,[somekernelmodeaddress]
它在退出时会导致中断,[…]“

还可以在高延迟预测错误分支后面执行该指令以避免发生页面错误。 也可以通过增加从内核地址读取和传送相关异常之间的延迟来扩大推测窗口。

“首先,我调用一个系统调用触及这个内存,其次,我使用prefetcht0指令来提高我在L1中加载地址的几率。”

当我们在系统调用之后使用预取指令时,攻击停止了对我们的工作,我们不知道为什么。 也许是CPU以某种方式存储访问是否在上次访问时被拒绝,并在这种情况下阻止了攻击?

“幸运的是,当不允许访问时,我没有得到一个缓慢的read suggesting的英特尔的null.”

(从内核地址读取返回全零)似乎发生的内存不足够缓存,但对于哪些可跳转表项存在,至少在重复读取尝试后。 对于未映射的内存,内核地址读取根本不返回结果。

进一步研究的想法

我们相信,我们的研究提供了许多尚未研究的剩余研究课题,我们鼓励其他公共研究人员研究这些课题。
本节包含的博客数量比本博客的其余部分还要高 - 它包含未经测试的想法,这可能毫无用处。

没有数据高速缓存时序的泄露

除了测量数据高速缓存时序之外,研究是否存在微架构攻击会很有趣,这些数据高速缓存时序可用于从推测执行中泄露数据。

其他微架构

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

其他JIT引擎

我们针对Linux内核中内置的JIT引擎开发了成功的变体1攻击。看看对系统控制较少的更先进的JIT引擎的攻击是否也是实用的 - 尤其是JavaScript引擎是很有意义的。

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

在变体2中,在扫描客户拥有的页面的主机虚拟地址的同时,尝试首先确定其L3缓存集合可能是有意义的。这可以通过使用通过physmap的逐出模式执行L3逐出,然后测试逐出是否影响客户拥有的页面来完成。

对于缓存集合也可能有效 - 使用L1D + L2驱逐集合来驱逐主机内核上下文中的函数指针,使用内核中的小配件使用物理地址驱逐L3集合,然后使用它来确定哪些缓存集合是来宾直到宾客拥有的驱逐集已经构建完成为止。

倾倒完整的BTB状态

考虑到通用BTB似乎只能区分2个31-8个或更少的源地址,似乎可行的是在约几个小时的时间范围内转储由例如超级调用产生的完整BTB状态。(扫描跳转源,然后对于每个发现的跳转源,将跳转目标等分)。即使主机内核是定制的,也可能用于识别主机内核中函数的位置。

源地址别名会在某种程度上降低实用性,但由于目标地址不会受到这种影响,因此可能会将来自具有不同KASLR偏移量的机器的(源,目标)对关联起来,并且可能会基于KASLR而减少候选地址的数量加法,而混叠是按位。

然后,这可能允许攻击者根据跳转偏移或函数之间的距离来猜测主机内核版本或用于构建它的编译器。

变体2:泄露更有效的gadget

如果对变体2使用足够高效的gadget,那么可能根本不需要从L3缓存驱逐主机内核函数指针,只用L1D和L2驱逐它们就足够了。

各种加速

特别是变体2 PoC仍然有点慢。这可能是因为:

  • 它一次只泄漏一点点; 一次泄漏更多点应该是可行的。
  • 它大量使用IRETQ来隐藏处理器的控制流。

使用变体2可以实现哪些数据泄漏率会很有趣。

使用return预测器泄露或注入

如果返回预测器在特权级别更改时也不会丢失状态,它可能对于从VM内部定位主机内核(在这种情况下,可以使用二分法来快速发现主机内核的完整地址)或注入返回目标(特别是如果返回地址存储在高速缓存行中,可以被攻击者清除并且在返回指令之前不重新加载)。

然而,我们还没有对迄今为止取得确凿结果的返回预测因子进行任何实验。

从间接调用预测器泄露数据

我们试图从间接调用预测器泄露目标信息,但尚未使其工作。

供应商声明

Project Zero向其透露此漏洞的供应商向我们提供了有关此问题的以下声明:

英特尔

英特尔致力于提高计算机系统的整体安全性。这里描述的方法依赖于现代微处理器的共同特性。因此,对这些方法的敏感度不仅限于英特尔处理器,也不意味着处理器超出其预期的功能规格。英特尔正在与我们的生态系统合作伙伴以及其他处理器受到影响的芯片供应商密切合作,为这些方法设计和分发软件和硬件缓解措施。

有关更多信息和有用资源的链接,请访问:
https://security-center.intel.com/advisory.aspx?intelid=INTEL-SA-00088&languageid=en-fr
http://newsroom.intel.com/wp-content/uploads/sites/11/2018/01/Intel-Analysis-of-Speculative-Execution-Side-Channels.pdf

AMD

AMD提供了以下链接: http://www.amd.com/en/corporate/speculative-execution

ARM

Arm认识到,尽管按预期工作,许多现代高性能处理器的推测功能可以与缓存操作的时间结合使用,以泄漏本博客中描述的一些信息。相应地,Arm开发了我们推荐部署的软件缓解措施。

有关受影响的处理器和缓解的具体细节可以在此网站上找到:https://developer.arm.com/support/security-update

Arm包含详细的技术白皮书以及来自Arm架构合作伙伴关于其特定实施和缓解措施的信息的链接。

文献

请注意,其中一些文档 - 特别是英特尔的文档 - 会随着时间而改变,所以引用和引用它们可能不会反映英特尔文档的最新版本。

参考

[1] 这个最初的报告没有包含关于变体3的任何信息。我们曾经讨论过直接读取内核内存是否可行,但认为这不太可能。在https://cyber.wtf/2017/07/28/negative-result-reading-kernel-memory-from-user-mode/发布Anders Fogh的工作之前,我们后来测试并报告了变体3 。
[2] “测试处理器”部分列出了精确的型号名称。用于重现此操作的代码位于bugtracker的writeup_files.tar归档中,位于文件夹userland_test_x86和userland_test_aarch64中。
[3] 攻击者控制的偏移量用于通过此PoC对阵列执行超出边界的访问,是一个32位值,将可访问地址限制为内核堆区域中的4GiB窗口。
[4] 此PoC不支持SMAP支持的CPU; 然而,这不是一个基本的限制。
[5] linux-image-4.9.0-3-amd64,版本为4.9.30-2 + deb9u2(http://snapshot.debian.org/archive/debian/20170701T224614Z/pool/main/l/linux/ linux-image-4.9.0-3-amd64_4.9.30-2%2Bdeb9u2_amd64.deb,sha256 5f950b26aa7746d75ecb8508cc7dab19b3381c9451ee044cd2edfd6f5efff1f8,通过Release.gpg,Release,Packages.xz签名); 那是我安装机器时的当前发行版内核版本。PoC不太可能与其他内核版本无变化地协作; 它包含许多硬编码地址/偏移量。
[6] 手机从2017年5月开始运行Android版本。
[7] https://software.intel.com/en-us/articles/intel-sdm
[8] https://software.intel.com/en-us/articles/avoiding-and-identifying-false-sharing-among-threads,section“background
[9] 超过2 15种映射效率更高,但内核对流程可以拥有的VMA数量设置了2 16 的硬限制。
[10] 英特尔的优化手册指出:“在HT技术的第一个实现中,物理执行资源是共享的,并且每个逻辑处理器的体系结构状态都是重复的”,因此预测状态可以共享。虽然预测器状态可能由逻辑核心标记,但这可能会降低多线程进程的性能,因此似乎不太可能。
[11] 如果历史缓冲区比我们测量的大一点,我们增加了一些余量 - 特别是因为我们在不同的实验中看到了稍微不同的历史缓冲区长度,并且因为26不是一个非常整数。
[12] 基本思想来自http://palms.ee.princeton.edu/system/files/SP_vfinal.pdf,第四节,尽管该论文的作者仍然使用了大量的页面。