使用边信道读取特权内存 (Reading privileged memory with a side-channel)
本文机器翻译自:https://googleprojectzero.blogspot.com/2018/01/reading-privileged-memory-with-side.html
我们发现CPU数据缓存的定时可能被滥用于错误推测的执行中,从而导致(最坏情况下)任意虚拟内存读取漏洞在不同的上下文中跨越本地安全边界。
这个问题的变体已知影响许多现代处理器,包括英特尔,AMD和ARM的某些处理器。
对于一些英特尔和AMD的CPU模型,我们已经利用了实际生产系统中的硬件环境。
我们在2017-06-01[1]向英特尔、AMD和ARM报告了这一问题.。
到目前为止,这个问题有三个已知的变体:
变体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汉堡、Stefan Mangard、Thomas Prescher和Werner Haas也报告了这些问题;他们[撰写/博客/纸面草稿]也报告了这些问题:
spectre(variant 1和2)。
meltdown(variant 3)
在我们的研究过程中,我们开发了以下概念证明(POCS):
一个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范围内的任意读取。
如果内核的BPF JIT被启用(非默认配置),它也可以在AMD PRO CPU上工作。
在Intel Haswell Xeon CPU上,内核虚拟内存可以在大约4秒的启动时间后以每秒2000字节的速度读取。[4]。
用于变体2的PoC,当在使用Intel Haswell Xeon CPU上的virt管理器创建的KVM客户机中以root权限运行时,在主机上运行Debian发行版的特定版本(现在已经过时),可以以每秒1500字节的速度读取主机内核内存,并有优化的空间。
在执行攻击之前,必须执行一些初始化操作,对于拥有64 GiB RAM的机器,大约需要10到30分钟;所需时间应该与主机RAM的数量大致成线性关系。
(如果客户可以使用2MB的Hugepage,那么初始化应该会更快,但还没有经过测试。)。
变体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个计算核心4C+6G(在本文档的其余部分称为“AMD pro CPU”)。
GoogleNexus5x电话的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已开启(非默认状态)。
AMDpro CPU,eBPF JIT处于开启状态(非默认状态)。
可以使用net.core.bpf_jit_Enablesysctl切换eBPF JIT的状态。
理论解释。
英特尔优化参考手册在第2.3.2.3节(“分支预测”)中提到了关于SandyBridge(以及后来的微结构修订)的以下内容:
分支预测预测分支目标,并启用。
处理器在分支之前很久就开始执行指令。
真正的执行路径是已知的。
第2.3.5.2节(“L1 DCache”):
负载可以:
[…]。
在前面的分支被解决之前,先进行推测。
以重叠的方式将缓存错误排除在顺序之外。
英特尔软件开发人员手册。[7]第3A卷第11.7节(“隐式缓存(奔腾4、Intel Xeon和P6系列处理器”)中规定:
隐式缓存发生在内存元素可能是可缓存的时候,尽管该元素可能从未按正常的vonNeumann序列被访问过。隐式缓存发生在P6和最近的处理器家族上,原因是主动预取、分支预测和TLB丢失处理。隐式缓存是现有Intel 386、Intel486和奔腾处理器系统行为的扩展,因为在这些处理器系列上运行的软件也无法确定地预测指令预取的行为。
考虑下面的代码示例。如果arr 1->length未被缓存,处理器可以从arr 1->data中推测加载数据。[不可信的_偏移量]。这是一个越界的读取。这不重要,因为当分支执行时,处理器将有效地回滚执行状态;任何投机性执行的指令都不会退出(例如,导致寄存器等受到影响)。
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]; ... }
但是,在下面的代码示例中,存在一个问题。如果arr 1->length,arr 2->data。和ARR 2->data[0×200]。[0×300],但是所有其他访问的数据都是这样,并且将分支条件预测为真,处理器可以在加载arr 1->length和重定向执行之前推测地执行以下操作:
加载 value = arr1->data[untrusted_offset_from_caller]
从arr 2->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]; } }
在执行返回到非推测路径后,因为处理器注意到【不可信_偏移量】大于arr 1->length,包含arr 2->data的缓存行。arr2->data[index2]停留在L1缓存中。通过测量加载arr 2->data[0×200]所需的时间。和arr2->data[0×300],然后攻击者就可以确定在投机执行期间index2的值是0×200还是0×300,这就揭示了arr 1->data[不可信_偏移量] & 1为0或1。
要能够实际使用此行为进行攻击,攻击者需要能够在目标上下文中执行这种具有外部索引的易受攻击的代码模式。
为此,易受攻击的代码模式必须存在于现有代码中,或者必须有一个解释器或JIT引擎来生成易受攻击的代码模式。
到目前为止,我们还没有发现任何现有的、可利用的易受攻击的代码模式实例;使用变体1泄漏内核内存的PoC使用eBPF解释器或eBPF JIT引擎,这些解释器或eBPF JIT引擎内置于内核中,并可供普通用户访问。
这方面的一个次要变体可能是使用对函数指针的越界读取来获得错误推测路径中的执行控制。
我们没有进一步调查这种变异。
攻击内核。
本节将更详细地描述变体1如何使用eBPF字节码解释器和JIT引擎泄漏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核心上的引用计数器会导致在其他物理CPU核上读取eBPF数组的长度变慢(故意错误共享)。
攻击使用两个eBPF程序。
二尾调用通过可配置索引处的对页对ebpf函数指针数组prog_map进行。
在简化的术语中,该程序通过猜测prog_map到用户空间地址的偏移量以及在猜测的偏移量处通过prog_map进行尾调用来确定prog_map的地址。
为了使分支预测预测偏移量低于prog_map的长度,在中间执行对内界索引的尾调用。
为了增加错误推测窗口,包含prog_map长度的缓存行被弹到另一个核心。
要测试偏移量猜测是否成功,可以测试用户空间地址是否已加载到缓存中。
由于这种直接的蛮力猜测地址的速度很慢,因此使用了以下优化:在userspace地址user_map_area中创建了215个相邻的用户空间内存映射[9],每个映射由24页组成,覆盖231个字节的总面积。
每个映射映射相同的物理页面,所有映射都存在于可分页中。