文章目录
- 前言
- 一、Introduction
- 二、 Background
- 2.1 Out-of-order execution
- 2.2 Address Spaces
- 2.3 Cache Attacks
- 三、A Toy Example
- 四、Building Blocks of the Attack
- 4.1 Executing Transient Instructions
- 4.2 Building a Covert Channel
- 五、Meltdown
- 5.1 Attack Description
- 5.2 Optimizations and Limitations
- 六、Evaluation
- 6.1 Leakage and Environments
- 6.1.1 Linux
- 6.1.2 Linux with KAISER Patch
- 6.1.3 Microsoft Windows
- 6.1.4 Android
- 6.1.5 Containers
- 6.1.6 Uncached and Uncacheable Memory
- 6.2 Meltdown Performance
- 6.3 Limitations on ARM and AMD
- 七、Countermeasures
- 7.1 Hardware
- 7.2 KAISER
- 7.3 KPTI
- 八、Discussion
- 九、Conclusion
- 参考资料
前言
Meltdown是两个最初的瞬态执行CPU漏洞之一(另一个是Spectre)。Meltdown影响Intel x86微处理器、IBM POWER处理器以及一些基于ARM的微处理器。它允许恶意进程读取所有内存,使用用户进程也可以读取内核空间的数据,即使它没有被授权这样做。
Meltdown:利用现代处理器 乱序执行的特性。
Spectre:利用现在待处理 分支预测,推测执行的特性。
在 Meltdown 和 Spectre 中,攻击者主要利用计算机高速缓存和物理内存不同的访问延时来做侧信道攻击。
Meltdown影响广泛的系统。在其被披露时(2018年),这包括所有运行任何非最新和修补版本的iOS、Linux、macOS或Windows的设备。因此,许多服务器和云服务受到影响,以及使用基于ARM处理器(移动设备、智能电视、打印机等)的大多数智能设备和嵌入式设备,包括各种网络设备。
在计算机系统中,安全性基本上依赖于内存隔离,例如将内核地址范围标记为不可访问,并保护其免受用户访问。Meltdown利用现代处理器上乱序执行(out-of-order execution)的副作用,可以读取任意内核内存位置,包括个人数据和密码。乱序执行是一种不可或缺的性能特性,广泛存在于现代处理器中。该攻击与操作系统无关,并且不依赖于任何软件漏洞。Meltdown破坏了地址空间隔离以及基于此基础构建的任何安全机制,因此破坏了所有提供的安全保证。在受影响的系统上,Meltdown使攻击者能够在云中读取其他进程或虚拟机的内存,而无需任何权限或特权,影响数百万客户和几乎每个个人电脑用户。我们展示了对于内核地址空间布局随机化(KASLR)的KAISER防御机制在无意中阻止了Meltdown攻击的重要副作用。我们强调必须立即部署KAISER以防止这种严重信息泄漏的大规模利用。
Meltdown出现的原因:现代处理器上乱序执行(out-of-order execution)。
Linux 对 Meltdown 防御机制是:KAISER — KPTI。
Linux内核的原始KAISER补丁已经通过各种优化进行了改进,例如支持PCID。在合并到主线内核之前,它被重命名为内核页表隔离(KPTI)。KPTI被合并到Linux内核版本4.15,并且也已经回溯到较旧的版本中。
KAISER是一种提出的对抗侧信道攻击的对策,专门设计用于保护内核地址空间布局随机化(KASLR)免受侧信道攻击的影响。然而,后来发现KAISER也无意中阻碍了Meltdown攻击。
KASLR:kernel address space layout randomization — 内核地址空间布局随机化
KAISER:Kernel Address Isolation to have Side-channels Efficiently Removed — 内核地址隔离以有效消除侧信道
KPTI(PTI):内核页表隔离,基于KAISER。KPTI被合并到Linux内核版本4.15。并向后移植到Linux内核4.14.11、4.9.75和4.4.110等内核版本。
KPTI如下图所示:
KPTI将一组进程页表分割成两组页表:用户态页表和内核态页表,两组PGD,2 * 4KB = 8KB
没有开启页表隔离进程只有一组页表:一个PGD,4KB。
KPTI通过完全分离用户空间和内核空间的页表来修复这些泄漏。
(1)其中一组页表仍然包含内核空间和用户空间的地址,与以前一样,但只在系统在内核模式下运行时使用。
内核模式下可以访问全部的地址空间(内核空间和用户空间的地址),运行在内核态的进程访问用户空间地址时,需要通过 copy_to_user()以及 copy_from_user()等安全接口来访问用户空间地址。是为了防止内核随意访问用户空间地址而造成安全问题。
(2)用于用户模式的第二组页表包含用户空间的副本以及一组最小的内核空间映射,提供进入或退出系统调用、中断和异常所需的信息。
一组最小的内核空间映射:当发生系统调用、中断和异常时,用户态陷入内核态,进入内核空间后,通过一小段内核跳板(trampoline)程序将用户页表切换到内核页表。当进程从内核空间返回到用户空间时,页表再次切换成用户页表。当程序运行在内核态时,进程可以访问内核页表和用户页表,内核页表包含了全部内核空间的映射,因此可以访问全部内核空间和用户空间。当进程运行在用户态时,内核页表仅仅包含跳板页表,而其他内核空间都是无效映射,因此进程无法访问内核空间的数据了。
一、Introduction
当今操作系统的一个核心安全特性是内存隔离。操作系统确保用户程序无法访问彼此的内存或内核内存。这种隔离是我们计算环境的基石,使得个人设备上可以同时运行多个应用程序,或者在云中在单台机器上执行多个用户的进程。
在现代处理器上,内核和用户进程之间的隔离通常通过处理器的一个特权位来实现,该位定义了内核内存页面是否可以被访问。基本思想是这个特权位只能在进入内核代码时设置,而在切换到用户进程时清除。这个硬件特性允许操作系统将内核映射到每个进程的地址空间中,并且在用户进程切换到内核时具有非常高效的转换,例如用于中断处理。因此,在实践中,从用户进程切换到内核时通常不会改变内存映射。
Meltdown是一种全新的攻击方法,通过为任何用户进程提供一种简单的方式,完全突破内存隔离,可以读取所在机器的整个内核内存,包括内核区域中映射的所有物理内存。Meltdown不利用任何软件漏洞,也就是说,它适用于所有主流操作系统。相反,Meltdown利用了大多数现代处理器上的侧信道信息,例如自2010年以来的现代Intel微架构,以及其他厂商的CPU。
传统的侧信道攻击通常需要对目标应用程序有非常具体的了解,并且专门针对性地泄露其密钥等信息,而Meltdown允许攻击者能够在受影响的处理器上运行代码,并获取整个内核地址空间的转储,包括任何映射的物理内存。
Meltdown之所以具有简单性和强大性的根本原因在于乱序执行引起的副作用。乱序执行是现代处理器使用的一种技术,它以非顺序的方式执行指令,以提高处理器的利用率。然而,这个特性引发了Meltdown所利用的副作用。
Meltdown利用了现代许多CPU设计中固有的竞争条件(race condition)。这种竞争条件发生在指令处理过程中的内存访问和权限检查之间。此外,结合缓存侧信道攻击,该漏洞允许一个进程绕过正常的权限检查,使得攻击进程可以访问属于操作系统和其他正在运行的进程的数据。该漏洞允许未经授权的进程从映射到当前进程内存空间的任何地址读取数据。由于受影响的处理器采用了指令流水线技术,**在乱序执行期间,未经授权地址的数据几乎总是会被暂时加载到CPU的缓存中,从而可以恢复该数据。**即使原始的读取指令由于权限检查失败或者根本没有产生可读的结果,这种情况仍然可能发生。
乱序执行是当今处理器的一个重要性能特性,用于克服繁忙执行单元的延迟,例如,内存获取单元需要等待来自内存的数据到达。现代处理器不会让执行过程停顿,而是会对操作进行乱序执行,即提前查看并安排后续操作到核心的空闲执行单元中。然而,这种操作往往会产生意外的副作用,例如,时间差异可以从顺序执行和乱序执行中泄露信息。这些副作用已经在研究中得到了证实。
从安全角度来看,一个特别重要的观察是:存在漏洞的乱序执行CPU允许非特权进程将数据从特权(内核或物理)地址加载到临时CPU寄存器中。此外,CPU甚至基于这个寄存器的值执行进一步的计算,例如,根据寄存器的值访问数组。通过简单地丢弃内存查找的结果(例如,修改后的寄存器状态),如果发现某个指令不应该被执行,处理器确保了正确的程序执行。因此,在体系结构级别(例如,处理器应该如何执行计算的抽象定义)不存在安全问题。
然而,我们观察到,乱序内存查找会影响缓存,而缓存可以通过缓存侧信道进行检测。因此,攻击者可以通过在乱序执行流中读取特权内存,并通过微体系结构隐蔽信道(例如,Flush+Reload)将数据传输到外部世界,从而转储整个内核内存。在隐蔽信道的接收端,可以重构寄存器的值。因此,在微体系结构级别(例如,实际的硬件实现)存在一个可利用的安全问题。
Meltdown破坏了CPU的内存隔离能力所提供的所有安全保证。我们在现代台式机、笔记本电脑以及云端服务器上评估了这种攻击。Meltdown允许非特权进程读取内核地址空间中映射的数据,包括Linux、Android和OS X上的整个物理内存,以及Windows上大部分物理内存。这可能包括其他进程的物理内存、内核的内存,以及在内核共享沙盒解决方案(如Docker、LXC)或处于半虚拟化模式的Xen中,内核(或超级监视程序)和其他共存实例的内存。
尽管性能严重依赖于具体的机器,例如处理器速度、TLB和缓存大小以及DRAM速度,但我们可以以每秒3.2 KB到503 KB的速度转储任意内核和物理内存。因此,大量的系统受到了影响。
由于许多操作系统将物理内存、内核进程和其他正在运行的用户空间进程映射到每个进程的地址空间中,因此Meltdown实际上使得一个恶意进程能够读取任何物理内存、内核或其他进程的映射内存,而不管它是否有权限这样做。对抗Meltdown的防御需要避免以容易受到此类攻击的方式使用内存映射(即基于软件的解决方案),或者避免底层竞争条件(即修改CPU的微码或执行路径)。
对抗措施KAISER最初是为了防止针对KASLR的侧信道攻击而开发的,但它也无意中对抗了Meltdown。我们的评估显示,KAISER在很大程度上可以防止Meltdown。因此,我们强调立即在所有操作系统上部署KAISER至关重要。庆幸的是,在负责任的披露期内,三个主要操作系统(Windows、Linux和OS X)实施了KAISER的变体,并最近发布了这些补丁。
Meltdown与Spectre攻击在几个方面有所不同,特别是Spectre需要针对受害进程的软件环境进行调整,但适用范围更广泛,涉及的CPU更多,并且无法通过KAISER来缓解。
二、 Background
在本节中,我们将提供乱序执行(Out-of-order execution)、地址转换(address translation)和缓存攻击(cache attacks)的背景知识。
2.1 Out-of-order execution
乱序执行是一种优化技术,它允许最大程度地利用CPU核心的所有执行单元。与按照顺序执行程序指令不同,CPU会在所有必要的资源可用时立即执行它们。当前操作的执行单元被占用时,其他执行单元可以提前运行。因此,只要它们的结果符合架构定义,指令可以并行执行。
在实践中,支持乱序执行的CPU允许以推测的方式运行操作,即处理器的乱序逻辑在CPU确定指令将被需要和提交之前就处理指令。在本文中,我们将推测执行的含义限制得更多,指的是在分支之后的指令序列,而将乱序执行一词用于指在处理器提交所有先前指令的结果之前执行操作的任何方式。
在1967年,Tomasulo开发了一种算法,实现了动态调度指令以支持乱序执行。Tomasulo引入了一个统一的保留站,允许CPU在计算数据值后立即使用,而不是将其存储在寄存器中并重新读取。保留站通过重命名寄存器,使操作相同物理寄存器的指令可以使用最后一个逻辑寄存器来解决读后写(RAW)、写后读(WAR)和写后写(WAW)的冲突。此外,保留单元通过一个共享数据总线(CDB)连接所有执行单元。如果操作数不可用,保留单元可以监听CDB直到其可用,然后直接开始执行指令。
在Intel体系结构上,流水线( pipeline)由前端(front-end)、执行引擎(后端 - back-end)和内存子系统组成。x86指令由前端从内存中获取,并解码为微操作(µOPs),然后连续发送到执行引擎。乱序执行是在执行引擎内部实现的,如图1所示。重新排序缓冲区负责寄存器分配、寄存器重命名和退役。此外,重新排序缓冲区还直接处理其他优化,如移动消除或零化习惯用法的识别。µOPs被发送到统一保留站(调度器),该调度器将操作排队到与执行单元连接的出口端口上。每个执行单元可以执行不同的任务,如ALU操作、AES操作、地址生成单元(AGU)或内存加载和存储。AGU以及加载和存储执行单元直接连接到内存子系统以处理其请求。
由于CPU通常不会运行线性指令流,它们具有分支预测单元,用于获取下一条执行的指令的有根据的猜测。分支预测器在实际评估条件之前尝试确定分支的方向。位于该路径上且没有任何依赖关系的指令可以提前执行,并且如果预测正确,可以立即使用其结果。如果预测错误,重新排序缓冲区可以通过清除重新排序缓冲区并重新初始化统一保留站来回滚到一个正常状态。
有多种方法可以预测分支:静态分支预测仅基于指令本身进行预测。动态分支预测在运行时收集统计信息以预测结果。一级分支预测使用1位或2位计数器记录分支的最后结果。现代处理器通常使用具有最近n个结果历史的两级自适应预测器,以预测定期重复出现的模式。最近,使用神经分支预测的思想已经被采用并集成到CPU体系结构中。
2.2 Address Spaces
为了将进程相互隔离,CPU支持虚拟地址空间,其中虚拟地址被转换为物理地址。虚拟地址空间被划分为一组页面,可以通过多级页转换表将这些页面单独映射到物理内存中。转换表定义了实际的虚拟到物理映射,并且还定义了保护属性,用于执行特权检查,例如可读、可写、可执行和用户可访问等。当前使用的转换表被保存在特殊的CPU寄存器中。
在每次上下文切换时,操作系统使用下一个进程的转换表地址更新该寄存器,以实现每个进程的虚拟地址空间。因此,每个进程只能引用属于其虚拟地址空间的数据。每个虚拟地址空间本身分为用户部分和内核部分。虽然运行的应用程序可以访问用户地址空间,但只有在CPU以特权模式运行时才能访问内核地址空间。操作系统通过禁用相应转换表的用户可访问属性来强制执行此限制。内核地址空间不仅映射了内核自身使用的内存,还需要对用户页面执行操作,例如填充数据。因此,通常将整个物理内存映射到内核中。在Linux和OS X上,这是通过直接物理映射来实现的,即整个物理内存直接映射到预定义的虚拟地址(如下图2所示)。
图二:物理内存在内核中以特定的偏移量直接映射。通过直接映射,用户空间可以访问的物理地址(蓝色)也在内核空间中进行了映射。这种映射关系使得用户空间和内核空间可以共享相同的物理地址,但通过不同的虚拟地址进行访问。
与直接物理映射不同,Windows操作系统采用了多个称为分页池(paged pools)、非分页池(non-paged pools)和系统缓存(system cache)的内存池。这些池是内核地址空间中的虚拟内存区域,将物理页面映射到虚拟地址上,其中一些页面需要保留在内存中(非分页池),而另一些页面可以从内存中移除,因为已经在磁盘上存有副本(分页池)。系统缓存还包含所有文件支持的页面的映射。综合起来,这些内存池通常将物理内存的大部分映射到每个进程的内核地址空间中。
针对内存损坏漏洞的利用通常需要了解特定数据的地址。为了阻止此类攻击,引入了地址空间布局随机化(ASLR)、非可执行堆栈和堆栈保护机制。为了保护内核,内核地址空间布局随机化(KASLR)在每次启动时随机化驱动程序的位置偏移量,使攻击变得更加困难,因为攻击者现在需要猜测内核数据结构的位置。然而,侧信道攻击可以探测到内核数据结构的确切位置,或者在JavaScript中解除ASLR的随机化。通过软件漏洞和对这些地址的了解的结合,可能导致特权代码的执行。
2.3 Cache Attacks
为了加快内存访问和地址转换速度,CPU内部包含一些小型内存缓存,称为高速缓存(caches),用于存储频繁使用的数据。CPU缓存通过在更小更快的内部内存中缓冲频繁使用的数据,隐藏了较慢的内存访问延迟。现代CPU具有多级缓存,可以是每个核心私有的,也可以是它们之间共享的。地址空间转换表也存储在内存中,并且通常也会被缓存在常规缓存中。
高速缓存侧信道攻击利用了缓存引入的时间差异。过去提出和证明了不同的缓存攻击技术,包括Evict+Time、Prime+Probe和Flush+Reload等。Flush+Reload攻击以单个缓存行为粒度工作。攻击者使用clflush指令频繁刷新目标内存位置。通过测量重新加载数据所需的时间,攻击者可以确定数据是否被其他进程加载到缓存中。Flush+Reload攻击已被用于攻击各种计算,例如加密算法、Web服务器函数调用、用户输入和内核地址信息等。
侧信道攻击的一个特殊用例是隐蔽通道(covert channel)。在这种情况下,攻击者控制引发副作用的部分和测量副作用的部分。这可以用于将信息从一个安全域泄露到另一个安全域,同时绕过架构级别或更高级别上的任何边界。Prime+Probe和Flush+Reload都被用于高性能隐蔽通道。
三、A Toy Example
在本节中,我们从一个简单的代码片段,也就是一个玩具示例开始,来说明乱序执行如何改变微体系结构状态并泄露信息。尽管这个示例很简单,但它被用作第4节和第5节的基础,我们在这些节中展示了如何利用这种状态变化进行攻击。
清单1展示了一个简单的代码片段,首先引发一个(未处理的)异常,然后访问一个数组。异常的特点是控制流不会继续执行异常之后的代码,而是跳转到操作系统中的异常处理程序。无论这个异常是由于内存访问(例如,访问无效地址)还是由于其他CPU异常(例如除零操作),控制流都会在内核中继续执行,而不是继续执行下一个用户空间指令。
因此,我们的玩具示例在理论上无法访问数组,因为异常会立即陷入内核并终止应用程序。然而,由于乱序执行,CPU可能已经执行了后续的指令,因为这些指令不依赖于触发异常的指令。这在图3中有所说明。由于异常的发生,乱序执行的指令不会被回退(retired),因此从不会产生体系结构效果。
图3:如果执行的指令引发异常,并将控制流转到异常处理程序,则不应执行后续指令。由于乱序执行,后续的指令可能已经部分执行,但尚未回退。然而,执行的体系结构效果将被丢弃。
尽管乱序执行的指令对寄存器或内存没有任何可见的体系结构效果,但它们具有微体系结构的副作用。在乱序执行期间,引用的内存被提取到一个寄存器中,并且也存储在缓存中。如果乱序执行被丢弃,寄存器和内存内容将永远不会提交。然而,缓存中的内存内容仍然保留在缓存中。我们可以利用一种微体系结构侧信道攻击,如Flush+Reload,来检测特定的内存位置是否被缓存,从而使这种微体系结构状态可见。其他侧信道也可以检测特定的内存位置是否被缓存,包括Prime+Probe、Evict+Reload或Flush+Flush等。由于Flush+Reload是目前已知最准确的缓存侧信道攻击并且易于实施,我们在这个例子中不考虑其他侧信道。
在这个例子中,根据数据的值,当执行乱序访问内存时,访问缓存的不同部分。由于数据乘以4096,对探测数组的数据访问在数组上以4 KB的间隔分散(假设探测数组的数据类型为1字节)。因此,数据的值与内存页之间存在一种单射映射,即不同的数据值永远不会导致对同一个页的访问。因此,如果一个页面的缓存行被缓存,我们就知道了数据的值。由于分散在不同页面上,这消除了由于预取器导致的误报,因为预取器不能跨越页面边界访问数据。
图4显示了在执行具有data = 84的乱序代码片段后,通过Flush+Reload测量迭代所有页面的结果。尽管由于异常,数组访问不应该发生,但我们可以清楚地看到本应访问的索引已经被缓存。在迭代所有页面(例如在异常处理程序中)时,只有页面84的缓存命中。**这表明即使指令实际上从未被执行,它们也会改变CPU的微体系结构状态。**第4节将修改这个玩具示例,不是读取一个值,而是泄漏一个无法访问的秘密。
图4:即使在乱序执行期间只访问了一个内存位置,它仍然被缓存。在探测数组的256个页面上迭代时,只有一个缓存命中,恰好是在乱序执行期间访问的页面上。
四、Building Blocks of the Attack
在第3节中的玩具示例说明了乱序执行的副作用如何修改微体系结构状态以泄露信息。尽管代码片段揭示了传递给缓存侧信道的数据值,但我们想展示如何利用这种技术来泄漏其他无法访问的秘密。在本节中,我们希望概括并讨论利用乱序执行进行攻击所需的基本构建块。
攻击者的目标是一个保存在物理内存中的秘密值。需要注意的是,寄存器内容在上下文切换时也存储在内存中,即它们也存储在物理内存中。如第2.2节所述,每个进程的地址空间通常包括整个用户空间和整个内核空间,而内核空间通常还包含所有(正在使用的)物理内存的映射。然而,这些内存区域只在特权模式下可访问(参见第2.2节)。
在这项工作中,我们通过绕过特权模式隔离来演示泄漏秘密,使攻击者完全可以读取整个内核空间,包括任何映射的物理内存,从而包括任何其他进程和内核的物理内存。需要注意的是,Kocher等人的工作追求的是一种称为Spectre攻击的正交方法,它通过欺骗被推测执行的指令来泄漏受害者进程有权限访问的信息。因此,Spectre攻击缺乏Meltdown的特权升级方面,并且需要针对受害者进程的软件环境进行定制,但适用于更广泛支持推测执行且未受KAISER阻止的CPU。
完整的Meltdown攻击由两个构建块组成,如图5所示。Meltdown的第一个构建块是使CPU执行一个或多个在执行路径中从不发生的指令。在玩具示例中(参见第3节),这是对一个数组的访问,通常不会被执行,因为前一个指令总是引发异常。我们称这样的指令为瞬态指令(transient instruction),它在乱序执行并留下可测量的副作用。此外,我们将包含至少一个瞬态指令的指令序列称为瞬态指令序列(transient instruction sequence)。
图5:Meltdown攻击使用异常处理或 suppression(例如,TSX)来运行一系列瞬态指令。这些瞬态指令获取一个(持久的)秘密值,并根据该秘密值改变处理器的微体系结构状态。这构成了一个微体系结构隐蔽信道的发送部分。接收方读取微体系结构状态,使其成为体系结构级别的信息,并恢复秘密值。
为了利用瞬态指令进行攻击,瞬态指令序列必须利用攻击者想要泄漏的秘密值。第4.1节描述了运行具有对秘密值依赖的瞬态指令序列的构建块。
Meltdown的第二个构建块是将瞬态指令序列的微体系结构副作用转移到体系结构状态中,以进一步处理泄露的秘密值。因此,第4.2节中描述的第二个构建块描述了使用隐蔽信道将微体系结构副作用转移到体系结构状态的构建块。
4.1 Executing Transient Instructions
Meltdown的第一个构建块是执行瞬态指令。瞬态指令经常发生,因为CPU持续地超前于当前指令运行,以最小化经历的延迟,并因此最大化性能(参见第2.1节)。如果瞬态指令的操作依赖于秘密值,则会引入可利用的侧信道。我们专注于在攻击者进程内映射的地址,即可由用户访问的用户空间地址以及用户无法访问的内核空间地址。需要注意的是,攻击目标是在另一个进程的上下文(即地址空间)中执行的代码是可能的,但不在本文的讨论范围内,因为无论如何都可以通过内核地址空间读取所有物理内存(包括其他进程的内存)。
访问用户无法访问的页面(例如内核页面)会触发异常,通常会终止应用程序。如果攻击者的目标是位于用户无法访问的地址上的秘密值,攻击者必须处理这个异常。我们提出了两种方法:exception handling和exception suppression。通过exception handling,我们有效地捕获在执行瞬态指令序列后发生的异常,而通过exception suppression,我们完全防止异常发生,而是在执行瞬态指令序列后重定向控制流。我们将在接下来详细讨论这些方法。
(1)Exception handling
一种简单的方法是在访问导致进程终止的无效内存位置之前,在攻击应用程序中进行分叉,并只在子进程中访问无效内存位置。CPU在崩溃之前在子进程中执行瞬态指令序列。然后,父进程可以通过观察微体系结构状态(例如通过侧信道)来恢复秘密值。
还可以安装一个信号处理程序,当发生某个特定的异常(例如段错误)时执行该处理程序。这允许攻击者发出指令序列并防止应用程序崩溃,从而减少开销,因为不需要创建新的进程。
(2)Exception suppression
处理异常的另一种方法是防止它们首先引发。事务性内存允许将内存访问组合为一个看似原子的操作,并提供在错误发生时回滚到先前状态的选项。如果事务内发生异常,体系结构状态将被重置,并且程序执行将继续而不会中断。
此外,推测执行会发出可能不会在执行的代码路径上发生的指令,这是由于分支错误预测引起的。依赖于前面的条件分支的指令可以进行推测执行。因此,将无效的内存访问放在一个推测的指令序列中,只有在先前的分支条件评估为真时才执行。通过确保在执行的代码路径中条件永远不会评估为真,我们可以 suppress 发生的异常,因为内存访问仅仅是推测执行的。这种技术可能需要对分支预测器进行复杂的训练。Kocher等人在其正交工作中追求了这种方法,因为这种结构经常可以在其他进程的代码中找到。
4.2 Building a Covert Channel
Meltdown的第二个构建块是将由瞬态指令序列改变的微体系结构状态转移到体系结构状态中(参见图5)。瞬态指令序列可以被视为微体系结构隐蔽信道的发送端。隐蔽信道的接收端接收微体系结构状态变化,并从状态中推断出秘密值。需要注意的是,接收端不是瞬态指令序列的一部分,可以是不同的线程,甚至是不同的进程,例如在分叉崩溃方法中的父进程。
我们利用了来自缓存攻击的技术,因为缓存状态是一种微体系结构状态,可以使用各种技术可靠地转移到体系结构状态中。具体而言,我们使用Flush+Reload,因为它可以构建一个快速且低噪声的隐蔽信道。因此,根据秘密值,瞬态指令序列(参见第4.1节)执行常规的内存访问,例如在玩具示例中所示(参见第3节)。
在瞬态指令序列访问了一个可访问的地址后,即发送端的隐蔽信道,该地址将被缓存以供后续访问。接收端可以通过测量对该地址的访问时间来监视该地址是否已加载到缓存中。因此,发送端可以通过访问加载到受监视缓存中的地址来传输“1”位,通过不访问这样的地址来传输“0”位。
在我们第3节的玩具示例中,使用多个不同的缓存行可以一次传输多个比特。对于256个不同的字节值,发送端访问不同的缓存行。通过对所有256个可能的缓存行执行Flush+Reload攻击,接收端可以恢复一个完整的字节,而不仅仅是一个比特。然而,由于Flush+Reload攻击所需的时间比瞬态指令序列要长得多(通常需要几百个周期),一次只传输一个比特更有效。攻击者可以通过相应地进行位移和屏蔽来实现这一点。
需要注意的是,隐蔽信道不仅限于依赖于缓存的微体系结构状态。任何受指令(序列)影响并通过侧信道可观测的微体系结构状态都可以用于构建隐蔽信道的发送端。例如,发送端可以发出一个占用某个执行端口(如ALU)的指令(序列)来发送一个“1”比特。接收端在执行相同执行端口上的指令(序列)时测量延迟。高延迟意味着发送端发送一个“1”比特,而低延迟意味着发送端发送一个“0”比特。Flush+Reload缓存隐蔽信道的优点是抗干扰和高传输速率。此外,泄漏可以从任何CPU核心观察到,即重新调度事件对隐蔽信道的影响不大。
五、Meltdown
在本节中,我们介绍了Meltdown攻击,这是一种强大的攻击,允许从非特权用户程序中读取任意物理内存,由第4节中介绍的构建块组成。首先,我们讨论攻击设置,以强调该攻击的广泛适用性。其次,我们提供了攻击概述,展示了Meltdown如何在个人电脑上的Windows和Linux系统、手机上的Android系统以及云环境中进行攻击。最后,我们讨论了Meltdown的具体实现,可以以3.2 KB/s至503 KB/s的速度转储任意内核内存。
Attack setting:在我们的攻击中,我们考虑个人电脑和云中的虚拟机。在攻击场景中,攻击者在被攻击系统上具有任意的非特权代码执行权限,也就是说,攻击者可以以普通用户的权限运行任何代码。然而,攻击者无法物理访问该机器。此外,我们假设系统完全受到最先进的基于软件的防御措施的保护,例如ASLR和KASLR,以及CPU功能,如SMAP、SMEP、NX和PXN。最重要的是,我们假设操作系统完全没有缺陷,因此不存在可以利用的软件漏洞来获取内核权限或泄漏信息。攻击者的目标是秘密的用户数据,例如密码和私钥,或者任何其他有价值的信息。
5.1 Attack Description
Meltdown结合了第4节讨论的两个构建块。首先,攻击者使CPU执行一个瞬态指令序列,该序列使用存储在物理内存某个位置上的不可访问的秘密值(参见第4.1节)。瞬态指令序列充当了一个隐蔽信道的发送端(参见第4.2节),最终将秘密值泄漏给攻击者。
Meltdown由三个步骤组成:
第一步:将攻击者选择的不可访问的内存位置的内容加载到一个寄存器中。
第二步:一个瞬态指令根据寄存器中的秘密内容访问一个缓存行。
第三步:攻击者使用Flush+Reload来确定已访问的缓存行,从而确定存储在选择的内存位置上的秘密内容。
通过对不同的内存位置重复这些步骤,攻击者可以转储内核内存,包括整个物理内存。
Listing 2展示了使用x86汇编指令的瞬态指令序列和隐蔽信道的发送部分的基本实现。需要注意的是,攻击的这部分代码也可以完全使用高级语言(如C)来实现。接下来,我们将讨论Meltdown的每个步骤以及Listing 2中相应的代码行。
Listing 2: Meltdown的核心部分。一个不可访问的内核地址被移动到一个寄存器中,引发异常。在引发异常之前,后续的指令被乱序执行,通过间接内存访问泄漏内核地址的数据。
(1)Step 1: 读取秘密数据。为了将数据从主存加载到寄存器中,需要使用虚拟地址引用主存中的数据。在将虚拟地址转换为物理地址的同时,CPU还会检查虚拟地址的权限位,即该虚拟地址是用户可访问还是仅内核可访问。如第2.2节中所讨论的,通过权限位进行硬件级别的隔离被认为是安全的,并且硬件供应商推荐采用这种方式。因此,现代操作系统会将整个内核映射到每个用户进程的虚拟地址空间中。
这里的关键点是,操作系统将内核映射到用户进程的虚拟地址空间,并且设置权限位,使得用户进程无法直接访问内核的内存。这种硬件隔离机制通常被认为是安全的,因为用户进程无法直接读取或修改内核内存中的数据。然而,Meltdown利用了CPU在执行指令序列期间的乱序执行特性,从而绕过了这种隔离机制,将内核内存中的数据泄漏给攻击者。
由于将整个内核映射到每个用户进程的虚拟地址空间中,所有内核地址在进行地址转换时都会映射到有效的物理地址,使得CPU可以访问这些地址的内容。与访问用户空间地址的唯一区别在于,当访问内核地址时,CPU会引发异常,因为当前的权限级别不允许访问该地址。因此,用户空间程序不能直接读取这些地址的内容。
然而,Meltdown利用了现代CPU的乱序执行特性。在非法内存访问(读取内核地址)和引发异常之间的短暂时间窗口内,CPU继续乱序执行指令。这意味着即使异常最终会被引发并拒绝非法访问,但在非法访问之后的指令可能已经被执行(指令会被丢弃,但是会保留在缓存中)。因此,通过这个侧信道,可以泄漏内核地址的内容,使攻击者间接访问和获取敏感的内核数据。
在乱序执行期间,引用的内存被提取到一个寄存器中,并且也存储在缓存中。如果乱序执行被丢弃,寄存器和内存内容将永远不会提交。然而,缓存中的内存内容仍然保留在缓存中。我们可以利用一种微体系结构侧信道攻击,如Flush+Reload,来检测特定的内存位置是否被缓存,从而使这种微体系结构状态可见。
在第2行的第4行,我们将存储在RCX寄存器中的目标内核地址的字节值加载到RAX寄存器的最低有效字节中,由AL表示。如第2.1节中更详细地解释的那样,MOV指令由核心获取,解码为μOPs,分配并发送到重排序缓冲区。在那里,体系结构寄存器(例如列表2中的RAX和RCX)被映射到底层物理寄存器,从而实现乱序执行。为了尽可能利用流水线,后续指令(第5-7行)已经被解码并分配为μOPs。μOPs进一步发送到保留站,保留站会保存μOPs,等待相应的执行单元执行它们。如果执行单元已经达到其相应容量限制,或者操作数值尚未计算出来,μOP的执行可能会延迟。
当第4行加载内核地址时,CPU很可能已经作为乱序执行的一部分发出了后续指令,并且它们对应的μOPs在保留站中等待内核地址的内容到达。一旦从共享数据总线上观察到获取的数据,μOPs就可以开始执行。此外,处理器互连和缓存一致性协议保证在多核或多CPU系统中读取内存地址的最新值,无论存储位置如何。
当μOPs完成执行时,它们按顺序retire,因此它们的结果被提交到体系结构状态。在retirement过程中,处理任何在指令执行过程中发生的中断和异常。因此,如果加载内核地址的MOV指令被retired,异常被注册,并且流水线被刷新以消除乱序执行的所有后续指令的结果。然而,引发异常和我们下面描述的攻击步骤2之间存在竞争条件。
根据Gruss等人的报道,预取内核地址有时会成功。我们发现,在某些系统上,预取内核地址可以稍微提高攻击的性能。
(2)第2步:传输秘密。必须以一种方式选择在第1步中乱序执行的指令序列,使其成为一种短暂的指令序列。如果这个短暂的指令序列在MOV指令退休之前执行(即引发异常),并且短暂的指令序列基于秘密进行了计算,那么可以利用它将秘密传输给攻击者。
正如前面讨论的那样,我们利用缓存攻击,利用CPU的缓存构建快速且低噪声的隐蔽信道。因此,短暂的指令序列必须将秘密编码到微架构缓存状态中,类似于第3节中的示例。
我们在内存中分配了一个探测数组,并确保该数组的任何部分都不会被缓存。为了传输秘密,短暂的指令序列包含对地址的间接内存访问,该地址根据秘密(无法访问)值计算得出。在列表2的第5行,第1步中的秘密值乘以页面大小,即4 KB。秘密的乘法确保对数组的访问在空间上彼此之间有很大的距离。这样可以防止硬件预取器将相邻的内存位置加载到缓存中。在这里,我们一次读取一个字节。因此,我们的探测数组大小为256×4096字节,假设4 KB页面大小。
需要注意的是,在乱序执行中,我们对寄存器值为“0”的噪声偏向。我们在第5.2节中讨论了这个原因。然而,出于这个原因,我们在短暂的指令序列中引入了重试逻辑。如果我们读取到“0”,我们尝试重新读取秘密(第1步)。在列表2的第7行,将乘以的秘密值加上探测数组的基地址,形成隐蔽信道的目标地址。该地址被读取到缓存对应的缓存行中。该地址将被加载到请求核心的L1数据缓存中,并且由于包容性,也会加载到L3缓存中,其他核心可以从L3缓存中读取。因此,我们的短暂的指令序列基于在第1步中读取的秘密值影响缓存状态。
由于第2步中的短暂的指令序列与引发异常竞争,减少第2步的运行时间可以显著提高攻击的性能。例如,确保探测数组的地址转换被缓存在转换后备缓冲器(TLB)中,可以提高某些系统上的攻击性能。
(3)第3步:接收秘密。在第3步中,攻击者通过利用微体系结构侧信道攻击(即微体系结构隐蔽信道的接收端)将缓存状态(第2步)转移到体系结构状态中,从而恢复秘密值(第1步)。正如在第4.2节中讨论的那样,我们的MeltDown实现依赖于Flush+Reload。
当执行第2步的短暂指令序列时,探测数组中的恰好一个缓存行被缓存。缓存行在探测数组中的位置仅取决于在第1步中读取的秘密。因此,攻击者遍历探测数组的所有256个页面,并测量每个页面上第一个缓存行(即偏移量)的访问时间。包含被缓存缓存行的页面编号直接对应于秘密值。
5.2 Optimizations and Limitations
(1)固有偏向于0。虽然CPU通常会在乱序加载操作中的值不可用时暂停执行,但CPU可能会继续进行乱序执行,假设加载的值为某个值 。我们观察到,在我们的Meltdown实现中,非法内存加载(列表2中的第4行)经常返回一个’0’,当使用add指令而不是mov指令实现时,这一点可以清楚地观察到。导致这种对’0’的偏向的原因可能是内存加载被权限检查失败屏蔽掉了,或者是因为被暂停加载的数据尚不可用,因此返回了一个推测的值。
这种固有的对’0’的偏向是由于乱序执行中的竞争条件导致的,该竞争条件可能会成功(即读取正确的值),但通常会失败(即读取一个’0’的值)。这种偏向性在不同的机器、硬件和软件配置以及Meltdown的具体实现之间会有所变化。在未经优化的版本中,错误地返回一个’0’的概率很高。因此,我们的Meltdown实现在列表2中的代码读取到来自Flush+Reload攻击的一个’0’值时,会执行一定数量的重试操作。最大重试次数是一个影响攻击性能和错误率的优化参数。在使用异常处理的Intel Core i5-6200U上,我们的未优化版本平均读取到’0’的概率为5.25%(标准差为4.15)。通过简单的重试循环,我们将概率降低到0.67%(标准差为1.47)。在Core i7-8700K上,我们平均读取到’0’的概率为1.78%(标准差为3.07)。使用Intel TSX,概率进一步降低至0.008%。
(2)优化0值情况。由于Meltdown的固有偏向,Flush+Reload测量中对缓存行’0’的命中不会为攻击者提供任何信息。因此,可以省略测量缓存行’0’的步骤,并且如果在任何其他缓存行上没有缓存命中,则可以假设值为’0’。为了最大程度减少没有在非零行上进行缓存命中的情况,我们重试读取瞬态指令序列中的地址,直到遇到一个与’0’不同的值(第6行)。这个循环的终止条件可以是读取到非零值,或者是由于无效的内存访问而引发的异常。无论哪种情况,直到异常处理或异常抑制返回控制流的时间与无效内存访问后的循环无关,即循环不会显著减慢攻击速度。因此,这些优化可能会提高攻击性能。
(3)单比特传输。在第5.1节的攻击描述中,攻击者一次通过隐蔽通道传输了8个比特,并执行了28 = 256个Flush+Reload测量来恢复秘密信息。然而,运行更多的瞬态指令序列和执行更多的Flush+Reload测量之间存在权衡。攻击者可以通过使用MOV指令读取更多比特来在单次传输中传输任意数量的比特。此外,攻击者还可以使用瞬态指令序列中的附加指令来屏蔽比特。我们发现,在瞬态指令序列中添加的附加指令数量对攻击性能影响微乎其微。
上述通用攻击的性能瓶颈实际上是花费在Flush+Reload测量上的时间。事实上,使用这种实现方式,几乎整个时间都用于Flush+Reload测量。通过一次只传输一个比特,我们可以省略除了在缓存行1上进行的测量之外的所有Flush+Reload测量。如果传输的比特为’1’,我们观察到在缓存行1上有缓存命中。否则,在缓存行1上我们观察不到缓存命中。
一次只传输一个比特也有缺点。如上所述,我们的侧信道对秘密值为’0’有偏向性。如果我们一次读取并传输多个比特,实际用户数据中所有比特都为’0’的概率可能非常小。单个比特为’0’的概率通常接近50%。因此,一次读取和传输的比特数量是在一定程度上减少错误和隐蔽通道整体传输速率之间的权衡。
然而,由于错误率在任一情况下都非常小,我们的评估(参见第6节)基于单比特传输机制。
(4)使用Intel TSX进行异常 Suppression。在第4.1节中,我们讨论了通过禁止由于无效内存访问而引发异常的选项。使用Intel TSX,一种硬件事务内存实现,我们可以完全 suppress异常的发生。
通过Intel TSX,多个指令可以被组合成一个事务,该事务看起来是一个原子操作,即要么全部指令执行,要么全部不执行。如果事务中的某个指令失败,已经执行的指令将被回滚,但不会引发异常。
如果我们使用这样的TSX指令将列表2中的代码包装起来,任何异常都会被 suppressed。然而,微体系结构效应仍然可见,即缓存状态在硬件事务内部被持续操纵。这导致通道容量更高,因为suppress异常比陷入内核处理异常并在之后继续执行的过程要快得多。
(5)处理KASLR(内核地址空间布局随机化)。在2013年,内核地址空间布局随机化(KASLR)被引入到Linux内核中(从3.14版本开始),允许在引导时随机化内核代码的位置。然而,直到2017年5月,KASLR才在4.12版本中默认启用。通过KASLR,直接物理映射也被随机化,而不是固定在某个地址上,因此攻击者需要在进行Meltdown攻击之前获取随机化的偏移量。然而,随机化仅限于40位。
因此,假设目标机器拥有8GB的RAM,我们只需要以8GB的步长测试地址空间即可。这样,在最坏情况下,只需进行128次测试即可覆盖40位的搜索空间。如果攻击者能够成功从测试地址中获取一个值,攻击者可以继续从该位置转储整个内存。这使得攻击者可以在受到KASLR保护的系统上在几秒钟内执行Meltdown攻击。
六、Evaluation
在本节中,我们评估了Meltdown漏洞以及我们概念验证实现的性能。第6.1节讨论了Meltdown可以泄露的信息,第6.2节评估了Meltdown的性能,包括对策。最后,我们在第6.3节中讨论了针对AMD和ARM的限制。
表格1列出了我们成功复现Meltdown的一些配置。在对Meltdown的评估中,我们使用了装有Intel Core CPU的笔记本电脑和台式计算机,以及一款基于ARM的手机。对于云环境的设置,我们在亚马逊弹性计算云上运行的基于Intel Xeon CPU的虚拟机以及DigitalOcean上测试了Meltdown。
6.1 Leakage and Environments
我们对Linux(参见第6.1.1节)、Windows 10(参见第6.1.3节)和Android(参见第6.1.4节)进行了Meltdown的评估,没有应用引入KAISER机制的补丁。在这些操作系统上,Meltdown可以成功泄漏内核内存。我们还评估了KAISER补丁对Linux上Meltdown的影响,以展示KAISER如何防止内核内存的泄漏(参见第6.1.2节)。此外,我们还讨论了在容器(如Docker)中运行时的信息泄漏情况(参见第6.1.5节)。最后,我们评估了Meltdown对非缓存和不可缓存内存的影响(参见第6.1.6节)。
6.1.1 Linux
我们成功地在多个Linux内核版本上评估了Meltdown,从2.6.32到4.13.0,没有应用引入KAISER机制的补丁。在所有这些Linux内核版本中,内核地址空间也映射到用户地址空间中。因此,所有内核地址也映射到用户空间应用程序的地址空间中,但由于这些地址的权限设置,任何访问都被阻止。
由于Meltdown绕过了这些权限设置,如果知道内核基址的虚拟地址,攻击者可以泄漏完整的内核内存。由于所有主要操作系统也将整个物理内存映射到内核地址空间中(参见第2.2节),因此也可以读取所有物理内存。
在内核版本4.12之前,默认情况下未激活内核地址空间布局随机化(KASLR)。如果KASLR被激活,Meltdown仍然可以通过搜索地址空间来找到内核(参见第5.2节)。攻击者还可以通过迭代虚拟地址空间来简单地取消随机化的直接物理映射。在没有KASLR的情况下,直接物理映射起始地址为0xffff 8800 0000 0000,并且线性映射了整个物理内存。在这样的系统上,攻击者可以使用Meltdown通过从0xffff 8800 0000 0000开始的虚拟地址读取,简单地转储整个物理内存。
在较新的系统上,默认情况下激活了KASLR,并且直接物理映射的随机化被限制在40位。由于映射的线性性质,它甚至进一步受到限制。假设目标系统至少有8GB的物理内存,攻击者可以以8GB的步长测试地址,最多需要测试128个内存位置。从一个发现的位置开始,攻击者再次可以转储整个物理内存。因此,在评估中,我们可以假设随机化要么被禁用,要么偏移量已经在预计算步骤中获取到了。
6.1.2 Linux with KAISER Patch
Gruss等人提出的KAISER补丁实现了更强的内核和用户空间隔离。KAISER不在用户空间中映射任何内核内存,除了一些x86架构所需的部分(例如中断处理程序)。因此,在用户空间中无法找到任何有效的映射,无论是内核内存还是物理内存(通过直接物理映射)。因此,Meltdown无法泄露任何内核或物理内存,除非少数必须在用户空间中映射的内存位置。
我们验证了KAISER确实可以防止Meltdown,并且没有任何内核或物理内存的泄露。此外,如果KASLR被激活,并且少数剩余的内存位置被随机化,由于它们的大小只有几千字节,找到这些内存位置并不容易。第7.2节讨论了这些映射的内存位置的安全性影响。
6.1.3 Microsoft Windows
我们成功地在最近更新的Microsoft Windows 10操作系统上评估了Meltdown,这个更新是在针对Meltdown的补丁发布之前进行的。与Linux上的结果一致(参见第6.1.1节),Meltdown在Windows上也可以泄漏任意内核内存。这并不令人意外,因为Meltdown不利用任何软件问题,而是由硬件问题引起的。
与Linux不同,Windows没有身份映射的概念,即将物理内存线性映射到虚拟地址空间中。相反,大部分物理内存被映射到分页池、非分页池和系统缓存中。此外,Windows还将内核映射到每个应用程序的地址空间中。因此,Meltdown可以读取内核内存,该内核内存映射在内核地址空间中,即内核中未被交换出的任何部分,以及映射在分页池、非分页池和系统缓存中的任何页面。
需要注意的是,有些物理页面在一个进程中被映射,但在另一个进程的(内核)地址空间中没有映射,即使用Meltdown无法攻击的物理页面。然而,大部分物理内存仍然可以通过Meltdown访问。
我们成功地使用Meltdown读取了Windows内核的二进制文件。为了验证泄漏的数据是实际的内核内存,我们首先使用Windows内核调试器获取包含实际数据的内核地址。在泄漏数据之后,我们再次使用Windows内核调试器将泄漏的数据与实际内存内容进行比较,确认Meltdown可以成功地泄漏内核内存。
6.1.4 Android
我们在运行LineageOS Android 14.1的三星Galaxy S7手机上成功评估了Meltdown,该手机使用Linux内核3.18.14。该设备配备了三星Exynos 8 Octa 8890 SoC,其中包含一个具有4个核心的ARM Cortex-A53 CPU和一个具有4个核心的Exynos M1 “Mongoose” CPU。尽管我们无法对Cortex-A53 CPU进行攻击,但我们成功地在三星定制核心上进行了Meltdown攻击。使用第4.1节中描述的exception suppression技术,我们成功地使用位于虚拟地址0xffff ffbf c000 0000处的直接物理映射泄漏了一个预定义的字符串。
6.1.5 Containers
我们在共享内核的容器中评估了Meltdown,包括Docker、LXC和OpenVZ,并发现攻击可以在没有任何限制的情况下进行。在容器内运行Meltdown不仅可以泄漏底层内核的信息,还可以泄漏同一物理主机上运行的所有其他容器的信息。
大多数容器解决方案的共同之处是每个容器使用相同的内核,即内核在所有容器之间共享。因此,每个容器通过共享内核的直接物理映射拥有整个物理内存的有效映射。此外,Meltdown无法在容器中被阻止,因为它只使用内存访问。特别是在使用Intel TSX时,只执行非特权指令,甚至不会陷入内核。
因此,通过Meltdown可以完全破坏共享内核的容器隔离。这对于廉价的托管提供商来说尤其关键,因为用户之间并没有通过完全虚拟化的机器进行分隔,而只是通过容器进行分隔。我们通过成功地从我们控制的不同用户的容器中泄漏内存内容来验证我们的攻击在这样的设置中起作用。
6.1.6 Uncached and Uncacheable Memory
在这一部分,我们评估了Meltdown泄漏的数据是否需要位于L1数据缓存中。为此,我们构建了一个设置,将两个进程固定在不同的物理核心上。通过使用clflush指令刷新值,并且只在另一个核心上重新加载它,我们创建了一种目标数据不在攻击者核心的L1数据缓存中的情况。正如在第6.2节中描述的那样,我们仍然可以以较低的读取速率泄漏数据。这清楚地表明数据是否存在于攻击者的L1数据缓存中并不是Meltdown的要求。此外,其他研究人员也证实了这一观察结果。
Meltdown可以泄漏非缓存内存的原因可能是Meltdown隐式地缓存了数据。我们设计了第二个实验,其中我们将页面标记为不可缓存,并试图从这些页面泄漏数据。这将导致对这些页面的每次读写操作直接访问主内存,从而绕过缓存。实际上,只有极少量的系统内存被标记为不可缓存。我们观察到,如果攻击者能够在Meltdown攻击发生的同一CPU核心上触发目标地址的合法加载,例如通过发出系统调用(常规调用或者在推测执行中),攻击者可以泄漏不可缓存页面的内容。我们怀疑Meltdown从行填充缓冲区中读取值。由于填充缓冲区在运行在同一核心上的线程之间共享,Meltdown攻击中对同一地址的读取可能会从其中一个填充缓冲区中提供服务,从而使攻击成功。然而,我们将进一步的研究留给未来的工作。
在对系统管理模式的Spectre攻击中,也对不可缓存内存进行了类似的观察。虽然攻击可以在通过内存类型范围寄存器设置为不可缓存的内存上生效,但它无法在内存映射的I/O区域上生效,这是预期的行为,因为对内存映射的I/O的访问总是可能具有架构效果。
6.2 Meltdown Performance
为了评估Meltdown的性能,我们从内核内存中泄漏了已知的值。这不仅可以确定攻击者泄漏内存的速度有多快,还可以确定错误率,即预期的字节错误数。Meltdown中的竞态条件(参见第5.2节)对攻击的性能有重要影响,然而竞态条件总是可以获胜。如果目标数据位于核心附近,例如在L1数据缓存中,竞态条件以很高的概率获胜。在这种情况下,我们在Core i7-8700K上使用异常抑制,在10次运行的10秒内,实现了平均读取速度高达582 KB/s(µ = 552.4,σ = 10.2),并且错误率低至0.003%(µ = 0.009,σ = 0.014)。在Core i7-6700K上,我们实现了569 KB/s(µ = 515.5,σ = 5.99)的平均读取速度,最低错误率为0.002%(µ = 0.003,σ = 0.001),以及491 KB/s(µ = 466.3,σ = 16.75)的平均读取速度,最低错误率为10.7%(µ = 11.59,σ = 0.62)的Xeon E5-1630。然而,在平均读取速度为137 KB/s的较慢版本中,我们能够将错误率降低到0。此外,在Intel Core i7-6700K上,如果数据位于L3数据缓存而不是L1中,竞态条件仍然经常获胜,但平均读取速率降低到12.4 KB/s,使用异常抑制时错误率低至0.02%。然而,如果数据是未缓存的,赢得竞态条件就更加困难,因此我们观察到大多数系统上的读取速率低于10 B/s。尽管如此,有两种优化方法可以提高读取速率:首先,同时让其他线程预取目标值周围的内存位置并访问目标内存位置(使用异常抑制或处理)。这增加了监视线程在数据竞争期间正确时刻看到秘密数据值的概率。其次,通过对目标值周围的内存位置进行推测性访问,触发硬件预取器。通过这两种优化,我们可以将未缓存数据的读取速率提高到3.2 KB/s。
在所有测试中,我们使用Flush+Reload作为隐蔽信道,按照第5节的描述泄漏内存,并使用Intel TSX来抑制异常。Kocher等人对使用条件分支进行异常抑制进行了广泛的评估,为了简洁起见,本文省略了这部分内容。
6.3 Limitations on ARM and AMD
我们还尝试在几个ARM和AMD CPU上复现Meltdown漏洞。虽然我们能够在不同的Intel CPU和三星Exynos M1处理器上成功地泄漏内核内存,使用第5节中描述的攻击方法,但我们未能在其他ARM核心或AMD上实施Meltdown攻击。在ARM的情况下,唯一受影响的处理器是Cortex-A75 ,但该处理器尚未上市,因此不在我们的测试设备之列。然而,相应的内核补丁已经提供[2]。此外,Meltdown的一种修改攻击,针对系统寄存器而不是无法访问的内存位置,适用于多个ARM处理器。与此同时,AMD公开表示他们的CPU都不受Meltdown的影响,原因是架构上的差异。
微体系结构的主要部分通常没有公开文档。因此,除非拥有专有知识和个人CPU制造商的知识产权,否则几乎不可能了解允许或防止Meltdown的实现差异。关键在于,在微体系结构级别,对非特权地址的加载和后续指令是在故障处理程序执行之前执行的,只有在故障指令退役时才处理故障。可以假设ARM、AMD和Intel上的加载和TLB的执行单元设计不同,因此加载的特权检查方式不同,故障处理方式也不同,例如,只有在检查了页面表项中的权限位之后才发出加载指令。然而,从性能的角度来看,同时发出加载指令或仅在指令退役时检查权限是一个合理的决策。由于从用户空间加载内核地址并不是程序通常要做的事情,并且通过确保状态不会在架构上可见,不压制加载是合法的。然而,由于状态在微体系结构级别上变得可见,这种实现是容易受到攻击的。
然而,对于ARM和AMD,如第3节所述的示例可以可靠地工作,这表明乱序执行通常会发生,并且会执行非法内存访问后的指令。
七、Countermeasures
在这一部分,我们讨论对抗Meltdown攻击的对策。首先,由于问题根源在硬件本身,我们讨论可能的微码更新和硬件设计的一般变化。其次,我们讨论了KAISER对策,该对策已经被开发出来以减轻针对KASLR的侧信道攻击,无意中也对抗Meltdown攻击。
7.1 Hardware
Meltdown绕过了硬件强制执行的安全域隔离。Meltdown中没有涉及软件漏洞。任何软件补丁(例如KAISER)都会留下少量的内存暴露(参见第7.2节)。没有文档表明修复是否需要开发全新的硬件,或者是否可以通过微码更新修复。由于Meltdown利用了乱序执行,一个简单的对策是完全禁用乱序执行。然而,这将对性能产生严重影响,因为现代CPU的并行性将无法利用。因此,这不是一个可行的解决方案。
Meltdown是一种内存地址获取和相应权限检查之间的竞争条件。通过对权限检查和寄存器获取进行串行化,可以防止Meltdown攻击,因为如果权限检查失败,就不会获取内存地址。然而,这会给每个内存获取带来显著的开销,因为内存获取必须等待权限检查完成后才能继续。
一个更现实的解决方案是引入用户空间和内核空间的硬分离。现代内核可以通过在CPU控制寄存器(例如CR4)中引入一个新的硬分离位来可选地启用此功能。如果设置了硬分离位,内核必须驻留在地址空间的上半部分,而用户空间必须驻留在地址空间的下半部分。通过这种硬分离,内存获取可以立即识别是否获取目标地址将违反安全边界,因为特权级别可以直接从虚拟地址中推导出来,无需进一步查找。我们预计这种解决方案对性能的影响将是最小的。此外,向后兼容性得到了保证,因为默认情况下不设置硬分离位,只有支持硬分离功能的内核才会设置它。
需要注意的是,这些对策只能防止Meltdown攻击,而不能防止Kocher等人描述的Spectre攻击类别 。同样,他们提出的对策不会影响Meltdown攻击。我们强调,部署对抗这两种攻击的对策非常重要。
7.2 KAISER
KAISER是在2016年构思的,并于2017年6月发布,当时还不知道Meltdown的存在。
KAISER技术使得绕过KASLR变得更加困难。KASLR是一种2014年采用的防护措施,用于减轻一个较不严重的问题。它通过随机化内核地址空间布局,使得攻击者更难利用其他内核漏洞。KAISER技术增强了KASLR的抵御能力,使其更难被绕过。
KAISER(内核地址隔离以有效地消除侧信道)是一种实现x86-64内核分离地址空间的补丁,它在内核模式和用户模式下使用两个不同的页表集合。其中一个页表集合基本保持不变,包括内核空间和用户空间的地址,但仅在系统运行在内核模式下使用。第二个“影子”页表包含所有用户空间映射的副本,但不包含内核部分。相反,有一组最小化的内核空间映射,提供处理系统调用和中断所需的信息,但不多余。页表的复制听起来可能效率低下,但复制只发生在页表层次结构的顶层,因此大部分数据在两个副本之间共享。
每当一个进程在用户模式下运行时,影子页表将被激活。因此,内核的大部分地址空间将完全对进程隐藏,从而防止已知的基于硬件的攻击。每当系统需要切换到内核模式时,例如响应系统调用、异常或中断,将切换到另一个页表。管理返回用户空间的代码必须再次激活影子页表。
KAISER提供的防御并不完全,因为仍然需要一小部分内核信息来管理切换回内核模式。在补丁描述中,Hansen写道:
最小化的内核页表仅尝试映射进入/退出内核所需的内容,例如入口/退出函数、中断描述符(IDT)和内核跳板堆栈。这个最小化的数据集仍然可能透露内核的ASLR基址。但是,这个最小化的内核数据是可信的,相比包含大量用户可控数据的内核直接映射,它更难被利用。虽然补丁没有提到,但可以想象,如果剩余信息的存在被证明泄露了内核,它可能被单独定位在与内核的其余部分不同的随机化地址上。
当然,驱动使用单一页表集合的性能问题并没有消失。然而,较新的处理器提供了一些帮助,即处理上下文标识符(PCIDs)。这些标识符标记TLB中的条目;在TLB中进行查找仅在与处理器中运行的线程的PCID相匹配时才成功。使用PCID可以消除上下文切换时刷新TLB的需求;这大大减少了系统调用期间切换页表的成本。令人高兴的是,内核在4.14开发周期中支持了PCID。
在2014年,Linux内核采用了内核地址空间布局随机化(KASLR),使得利用其他内核漏洞变得更加困难,它依赖于内核地址映射对用户空间保持隐藏。尽管禁止访问这些内核映射,但事实证明现代处理器中存在几种侧信道攻击可以泄漏这些内存的位置,从而绕过KASLR。
KAISER通过消除一些地址泄漏的来源来解决KASLR中的这些问题。而KASLR仅仅防止地址映射泄漏,KAISER还防止数据泄漏,从而涵盖了Meltdown的情况。
由于现有的硬件不容易修补,需要软件解决方案来应对,直到部署新的硬件。Gruss等人 提出了KAISER,这是对内核的修改,使内核不会映射到用户空间。这个修改旨在防止侧信道攻击破坏KASLR 。然而,它也可以防止Meltdown攻击,因为它确保在用户空间中没有有效的映射到内核空间或物理内存的可用性。与KAISER同时进行的工作中,Gens等人提出了LAZARUS作为对Linux内核的修改,通过类似KAISER的地址空间分离来阻止破坏KASLR的侧信道攻击。随着Linux内核继续开发原始的KAISER补丁,Windows和macOS基于KAISER的概念来实现以抵御Meltdown攻击,我们将更深入地讨论KAISER。
尽管KAISER对Meltdown提供了基本的保护,但它仍然有一些限制。由于x86架构的设计,仍然需要将几个特权内存位置映射到用户空间中,这为Meltdown留下了一定的攻击面,即这些内存位置仍然可以从用户空间读取。尽管这些内存位置不包含任何机密信息(例如凭据),但它们可能包含指针。泄漏一个指针足以破坏KASLR,因为随机化可以从指针值中计算出来。
尽管如此,KAISER仍然是目前最好的短期解决方案,因此应该立即在所有系统上部署。即使在Meltdown攻击下,KAISER也可以避免在映射到用户空间的内存位置上存在任何内核指针,这些指针可能泄漏关于随机偏移的信息。这将需要为每个内核指针提供跳板(trampoline)位置,即中断处理程序不直接调用内核代码,而是通过跳板(trampoline)函数进行调用。跳板函数只能映射到内核中,并且必须使用与其余内核不同的随机偏移进行随机化。因此,攻击者只能泄漏指向跳板代码的指针,而无法泄漏其余内核的随机偏移。对于仍然需要映射到用户空间并包含内核地址的每个内核内存,都需要这样的跳板代码。这种方法在性能和安全性之间需要权衡,需要在未来的工作中进行评估。
Linux内核的原始KAISER补丁已经通过各种优化进行了改进,例如支持PCID。在合并到主线内核之前,它被重命名为内核页表隔离(KPTI)。KPTI在最新版本的Linux内核中处于活动状态,并且也已经回溯到较旧的版本中。
Microsoft实现了一个受KAISER启发的类似补丁,名为KVA Shadow。KVA Shadow仅映射了用于在地址空间之间切换所需的最少内核转换代码和数据页面,但它不能防止针对KASLR的侧信道攻击。
苹果在iOS 11.2、macOS 10.13.2和tvOS 11.2中发布了更新,以应对Meltdown漏洞。与Linux和Windows类似,在这些更新之前,macOS在64位模式下共享内核和用户地址空间,除非设置了"-no-shared-cr3"引导选项。然而,该选项仅在内核模式下取消映射用户空间,而在用户模式下不取消映射内核空间。因此,它对缓解Meltdown漏洞没有任何影响。
7.3 KPTI
KPTI是基于KAISER的。如果没有启用KPTI,当执行用户空间代码(应用程序)时,Linux会在页表中保留整个内核内存的映射,虽然受到保护,但是可以访问。优点是当应用程序调用系统调用进入内核或接收到中断时,内核页表始终存在,因此大部分与上下文切换相关的开销(TLB刷新、页表交换等)可以避免。
在2018年1月,Meltdown漏洞被公开,已知影响Intel的x86 CPU和ARM Cortex-A75处理器。这是一个比KAISER最初打算修复的KASLR绕过漏洞更为严重的漏洞:发现可以泄漏内核内存的内容,而不仅仅是先前认为只能泄漏内存映射的位置。
KPTI(概念上基于KAISER)通过防止将大多数受保护的位置映射到用户空间来防止Meltdown漏洞。
目前尚不清楚AMD的x86处理器是否受到Meltdown漏洞的影响,它们不需要KPTI来进行缓解。然而,当禁用KPTI时,AMD处理器仍然容易受到KASLR绕过的攻击。
KPTI通过完全分离用户空间和内核空间的页表来修复这些泄漏。其中一组页表仍然包含内核空间和用户空间的地址,与以前一样,但只在系统在内核模式下运行时使用。用于用户模式的第二组页表包含用户空间的副本以及一组最小的内核空间映射,提供进入或退出系统调用、中断和异常所需的信息。
在支持进程上下文标识符(PCID)的处理器上,可以避免转换后备缓存(TLB)刷新,但即使如此,性能开销仍然很大,特别是在系统调用和中断频繁的工作负载中。
KPTI(页表隔离):
(1)每次进入/退出操作系统内核时,需要对核心控制寄存器进行昂贵的写操作。
例如,在受影响的ARMv8处理器上是TTBR写操作,在受影响的x86处理器上是CR3写操作。
(2)仅在已知有漏洞的微处理器上默认启用。
定义了一种枚举来发现未来的非受影响硅片。
(3)地址空间标识符(ASIDs)可以显著提高性能。
在ARMv8上是ASIDs,在x86处理器上是PCIDs(进程上下文ID)。
TLB条目带有地址空间标签,因此不需要完全无效化。
老旧的(2010年之前的)核心和新核心之间存在显著的性能差异。
根据KAISER的原始作者测量,开销约为0.28%;一位Linux开发人员测量发现,对于大多数工作负载,开销大约为5%,在某些情况下甚至高达30%,即使使用了PCID优化;对于数据库引擎PostgreSQL,在Intel Skylake处理器上进行的只读测试受到了7-17%的影响(如果没有PCID,则为16-23%),而完整的基准测试则损失了13-19%(Coffee Lake对比Broadwell-E)。Phoronix进行了许多基准测试,其中Redis的性能下降了6-7%。在Haswell处理器上,Linux内核编译速度下降了5%。
可以通过内核引导选项"nopti"部分禁用KPTI。此外,还制定了一些措施,如果新的处理器修复了信息泄漏问题,可以禁用KPTI。
详细请参考:内核页表隔离 (KPTI) 详解
八、Discussion
Meltdown对于我们对操纵微体系结构元素状态的硬件优化的安全性观念产生了根本性的改变。硬件优化可以改变微体系结构元素的状态,并因此对安全软件实现构成威胁,这个事实已经被知晓超过20年了。迄今为止,无论是工业界还是科学界,都将这视为高效计算的必要之恶。当一个加密算法未能保护自身免受由硬件优化引入的微体系结构泄漏时,如今这被认为是一个漏洞。而Meltdown彻底改变了这种情况。Meltdown将粒度从相对较低的空间和时间粒度(例如,每隔几百个周期的64字节的缓存攻击)转变为任意粒度,使攻击者能够读取每一个单独的位。这是任何(加密)算法都无法自我保护的。KAISER是一个短期的软件修复方案,但我们揭示的问题要重要得多。
我们预计现代CPU中将会有更多影响微体系结构状态的性能优化,甚至不一定通过缓存实现。因此,为了避免Meltdown和Spectre等攻击,设计用于提供特定安全保证的硬件(例如运行不受信任代码的CPU)需要重新设计。Meltdown还表明,即使是无错误的软件,明确编写以阻止侧信道攻击,如果不考虑底层硬件的设计,也是不安全的。
通过将KAISER集成到所有主要操作系统中,已经迈出了防止Meltdown的重要一步。KAISER是操作系统设计上的根本性变革。与其总是将所有内容映射到地址空间中,只映射最低限度所需的内存位置似乎是减少攻击面的第一步。然而,这可能还不足够,可能需要更强的隔离。在这种情况下,我们可以通过对每个操作系统强制执行某种虚拟内存布局来在灵活性、性能和安全性之间进行权衡。由于大多数现代操作系统已经使用类似的内存布局,这可能是一个有希望的方法。
Meltdown也严重影响云服务提供商,特别是如果客户机没有完全虚拟化。基于性能考虑,许多托管或云服务提供商没有为虚拟内存提供抽象层。在这种通常使用容器(如Docker或OpenVZ)的环境中,内核在所有客户机之间共享。因此,使用Meltdown可以简单地绕过客户机之间的隔离,完全暴露出同一主机上所有其他客户机的数据。对于这些提供商来说,将基础架构更改为完全虚拟化或使用诸如KAISER之类的软件解决方案都会显著增加成本。
有关通过乱序或推测执行读取内核内存的研究已经进行了并行工作,但尚未成功。我们是第一个证明这是可能的。即使修复了Meltdown,Spectre仍将是一个问题,需要不同的防御措施。仅减轻其中之一将使整个系统的安全性面临风险。Meltdown和Spectre开辟了一个新的研究领域,探究性能优化在多大程度上改变了微体系结构状态,如何将这种状态转化为体系结构状态,并如何预防此类攻击。
九、Conclusion
在这篇论文中,我们介绍了Meltdown,这是一种新型的基于软件的攻击,利用现代处理器的乱序执行和侧信道来从非特权用户空间程序读取任意内核内存。Meltdown无需任何软件漏洞,并且与操作系统无关,使得对手能够以每秒高达503 KB的速度读取云中其他进程或虚拟机的敏感数据,影响了数百万台设备。我们展示了最初提出用于保护KASLR免受侧信道攻击的KAISER对Meltdown的不良影响。我们强调,KAISER需要作为一项短期解决方案在每个操作系统上部署,直到Meltdown在硬件上修复,以防止对Meltdown的大规模利用。
参考资料
https://lwn.net/Articles/738975/
https://www.kernel.org/doc/html/next/arch/x86/pti.html#
https://meltdownattack.com/meltdown.pdf
https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)
https://en.wikipedia.org/wiki/Kernel_page-table_isolation
https://blog.csdn.net/pwl999/article/details/112686914
https://zhuanlan.zhihu.com/p/32757727
https://zhuanlan.zhihu.com/p/391325673
https://zhuanlan.zhihu.com/p/405558470