KVA Shadow:Windows上的Meltdown漏洞缓解技术详解

本文深入解析Windows内核针对Meltdown漏洞(CVE-2017-5754)的KVA Shadow缓解技术,包括其设计原理、内存管理机制、陷阱处理优化及性能调优策略,涵盖PCID加速和用户/全局加速等关键技术实现。

KVA Shadow:缓解Windows上的Meltdown漏洞

2018年1月3日,Microsoft发布了一份公告和安全更新,涉及一类新发现的硬件漏洞,称为推测执行侧信道漏洞,这些漏洞影响了许多现代微处理器的设计方法和实现决策。本文深入探讨了内核虚拟地址(KVA)Shadow的技术细节,这是Windows内核针对一种特定推测执行侧信道漏洞:恶意数据缓存加载漏洞(CVE-2017-5754,也称为“Meltdown”或“Variant 3”)的缓解措施。KVA Shadow是Microsoft最近宣布的推测执行侧信道赏金计划范围内的缓解措施之一。

需要注意的是,推测执行侧信道漏洞有多种不同类型,每种类型都需要不同的缓解措施。有关Microsoft为其他推测执行侧信道漏洞(“Spectre”)开发的缓解措施以及此类漏洞的额外背景信息,请参见此处

请注意,本文中的信息截至发布日期为止。

漏洞描述与背景

恶意数据缓存加载硬件漏洞涉及某些处理器如何处理虚拟内存的权限检查。处理器通常实现一种机制,将虚拟内存页标记为内核所有(有时称为超级用户)或用户模式所有。在用户模式下执行时,处理器通过在对特权内核所有页进行访问尝试时引发故障(或异常)来防止对特权内核数据结构的访问。这种保护内核所有页免受直接用户模式访问的能力是内核和用户模式代码之间权限分离的关键组成部分。

某些能够进行推测乱序执行的处理器,包括许多当前市场上的Intel处理器和一些基于ARM的处理器,容易受到一种推测侧信道的影响,该侧信道在访问页面时发生权限故障时暴露。在这些处理器上,执行内存访问并引发权限故障的指令不会更新机器的架构状态。然而,在某些情况下,这些处理器可能仍然允许故障的内部内存加载微操作(µop)将加载结果转发给后续依赖的微操作。可以说,这些处理器将权限故障的处理推迟到指令退休时间。

乱序处理器有义务“回滚”推测执行路径的架构可见效果,这些路径被证明在程序顺序执行期间从未可达,因此,任何消耗故障加载结果的微操作最终都会被处理器取消和回滚,一旦故障加载指令退休。然而,这些依赖的微操作可能已经基于(故障的)特权内存加载发出了后续缓存加载,或者可能在其他方面在处理器缓存中留下了它们执行的额外痕迹。这创建了一个推测侧信道:被取消的、推测的微操作对由引发权限故障的加载返回的数据进行操作所留下的残余可能通过处理器缓存的扰动被检测到,这可能使攻击者能够推断出他们原本无法访问的特权内核内存的内容。实际上,这使非特权用户模式进程能够披露特权内核模式内存的内容。

操作系统影响

大多数操作系统,包括Windows,依赖每页用户/内核所有权权限作为强制执行内核模式和用户模式之间权限分离的基石。一个推测侧信道使非特权用户模式代码能够推断特权内核内存的内容是有问题的,因为敏感信息可能存在于内核的地址空间中。在受影响的市场上硬件上缓解此漏洞尤其具有挑战性,因为用户/内核所有权页面权限必须被假定为不再防止从用户模式披露(即读取)内核内存内容。因此,在易受攻击的处理器上,恶意数据缓存加载漏洞影响了现代操作系统内核用来保护自己免受非受信任用户模式应用程序披露特权内核内存的主要工具。

为了保护内核内存内容在受影响的处理器上不被披露,因此有必要重新设计内核如何将其内存内容与用户模式隔离。随着用户/内核所有权权限不再有效防止内存读取,防止特权内核内存内容披露的唯一其他广泛可用机制是在执行用户模式代码时完全从处理器的虚拟地址空间中移除所有特权内核内存。

然而,这是有问题的,因为应用程序经常进行系统服务调用,请求内核代表它们执行操作(例如打开或读取磁盘上的文件)。这些系统服务调用以及其他关键内核功能(如中断处理)只有在它们的必要特权代码和数据映射到处理器的地址空间中时才能执行。这 presents a conundrum:为了满足内核与用户模式权限分离的安全要求,不能将任何特权内核内存映射到处理器的地址空间中,然而为了合理处理从用户模式应用程序到内核的任何系统服务调用请求,这相同的特权内核内存必须快速可访问,以便内核本身能够运行。

这个困境的解决方案是在内核模式和用户模式之间的转换时,也在处理器的地址空间之间切换, between a kernel address space(映射整个用户和内核地址空间) and a shadow user address space(映射进程的整个用户内存内容,但只映射切换到内核地址空间和从内核地址空间切换出来所需的最小内核模式转换代码和数据页面子集)。处理这些地址空间切换细节的选择特权内核代码和数据转换页面,这些页面被“影子化”到用户地址空间中,是“安全”的,因为它们不包含任何如果披露给非受信任用户模式应用程序会对系统有害的特权数据。在Windows内核中,这种为用户和内核模式使用不相交的影子地址空间称为“内核虚拟地址影子化”,或简称KVA shadow。

为了支持这个概念,每个进程现在可能有两个地址空间:内核地址空间和用户地址空间。由于当非受信任用户模式代码执行时,没有其他可能敏感的特权内核数据的虚拟内存映射,恶意数据缓存加载推测侧信道被完全缓解。然而,这种方法并非没有 substantial complexity and performance implications,正如后面将讨论的。

从历史角度来看,一些操作系统以前出于各种不同和不相关的原因实现了类似的机制:例如,在2003年(在大多数广泛可用的消费硬件中普遍引入64位处理器之前),为了在32位系统上寻址更大的虚拟内存量,向32位x86 Linux内核添加了可选支持,以便为用户模式提供4GB虚拟地址空间,并为内核提供单独的4GB地址空间,需要在每次用户/内核转换时进行地址空间切换。最近,一种类似的方法,称为KAISER,被提倡用于缓解由于处理器侧信道导致的内核虚拟地址空间布局信息泄漏。这与恶意数据缓存加载推测侧信道问题不同,因为在发现推测侧信道之前,没有内核内存内容(与地址空间布局信息相对)被认为有风险。

Windows内核中的KVA Shadow实现

虽然KVA Shadow的设计要求可能看起来相对无害(特权内核模式内存不能在非受信任用户模式代码运行时映射到地址空间中),但这些要求的影响在整个Windows内核架构中 far-reaching。这触及了内核的大量核心设施,如内存管理、陷阱和异常分发等。情况 further complicated by a requirement that the same kernel code and binaries must be able to run with and without KVA shadow enabled。系统在两种配置下的性能必须最大化,同时试图将KVA Shadow所需的更改范围尽可能 contained。这最大化了KVA Shadow和非KVA Shadow配置下的代码可维护性。

本节主要关注KVA Shadow对64位x86(x64)Windows内核的影响。x64上KVA Shadow的大多数考虑也适用于32位x86内核,尽管两种架构之间存在一些分歧。这是由于64位和32位模式之间的ISA差异,特别是在陷阱和异常处理方面。

请注意,本节中描述的实现细节可能在将来更改而不另行通知。驱动程序和应用程序不得依赖下面描述的任何内部行为, without first checking for updated documentation。

理解KVA Shadow所涉及的复杂性的最佳方式是从内核中处理用户模式和内核模式之间转换的底层低级接口开始。这个接口称为陷阱处理代码,负责处理可能从内核模式或用户模式发生的陷阱(或异常)。它还负责分发系统服务调用和硬件中断。陷阱处理代码必须处理 several events,但最相关于KVA Shadow的是那些称为“内核入口”和“内核退出”事件。这些事件分别涉及从用户模式到内核模式的转换,以及从内核模式到用户模式的转换。

陷阱处理和系统服务调用分发概述与回顾

作为Windows内核在x64处理器上如何分发陷阱和异常的快速回顾,传统上,内核将当前线程的内核堆栈指针编程到当前处理器的TSS(任务状态段)中, specifically into the KTSS64.Rsp0 field,这 informs the processor which stack pointer(RSP)value to load up on a ring transition to ring 0(kernel mode)code。这个字段传统上由内核在上下文切换时更新,以及 several other related internal events;当切换到不同线程时,处理器KTSS64.Rsp0字段被更新以指向新线程的内核堆栈的基址, such that any kernel entry event that occurs while that thread is running enters the kernel already on that thread’s stack。这个规则的例外是系统服务调用,它们通常通过“syscall”指令进入内核;这个指令不切换堆栈指针,操作系统陷阱处理代码有责任手动加载适当的内核堆栈指针。

在典型的内核入口上,硬件已经将所谓的“机器帧”( internally, MACHINE_FRAME)推送到内核堆栈上;这是处理器定义的数据结构,IRETQ指令消耗并从堆栈中移除以 effect an interrupt-return,并包括诸如返回地址、代码段、堆栈指针、堆栈段和调用应用程序的处理器标志等细节。Windows内核中的陷阱处理代码构建一个称为陷阱帧的结构( internally, KTRAP_FRAME),该结构以硬件推送的MACHINE_FRAME开始,然后包含各种软件推送的字段,这些字段描述被中断上下文的易失寄存器状态。如上所述,系统调用是这个规则的例外,并且必须手动构建整个KTRAP_FRAME,包括MACHINE_FRAME, after effecting a stack switch to an appropriate kernel stack for the current thread。

KVA Shadow陷阱和系统服务调用分发设计考虑

在基本了解没有KVA Shadow时如何处理陷阱之后,让我们深入探讨内核中陷阱处理的KVA Shadow特定考虑细节。

在设计KVA Shadow时, several design considerations applied for trap handling when KVA shadow were active, namely, that the security requirements were met, that performance impact on the system was minimized, and that changes to the trap handling code were kept as compartmentalized as possible in order to simplify code and improve maintainability。例如,希望在KVA Shadow和非KVA Shadow配置之间共享尽可能多的陷阱处理代码,以便将来更容易对内核的陷阱处理设施进行更改。

当KVA Shadow激活时,用户模式代码通常运行 with the user mode address space selected。陷阱处理代码的责任是在内核入口时切换到内核地址空间,并在内核退出时切换回用户地址空间。然而, additional details apply:仅仅切换地址空间是不够的,因为唯一允许存在于用户地址空间中(或“影子化”到用户地址空间中)的转换内核页面是那些包含“安全”内容、如果披露给用户模式不会对系统有害的页面。KVA Shadow遇到的第一个并发症是,将每个线程的内核堆栈页面影子化到用户模式地址空间是不合适的,因为这将允许通过恶意数据缓存加载推测侧信道泄漏内核线程堆栈上可能敏感的特权内核内存内容。

还希望将影子化到用户模式地址空间的代码和数据结构集保持在最小,并且如果可能,只影子化地址空间中的永久固定装置(如内核映像本身的部分,以及关键的每处理器数据结构,如GDT(全局描述符表)、IDT(中断描述符表)和TSS)。这简化了内存管理,因为处理新映射的设置和拆除,这些映射被影子化到用户模式地址空间,具有 associated complexities,正如使任何影子化映射可分页一样。出于这些原因,很明显,内核的陷阱处理代码继续使用每内核线程堆栈进行内核入口和内核退出事件是不可接受的。相反,需要一种新方法。

为KVA Shadow实现的解决方案是切换到一种操作模式,其中一小组每处理器堆栈( internally called KTRANSITION_STACKs)是唯一影子化到用户模式地址空间的堆栈。每个处理器存在八个这样的堆栈,第一个代表用于“正常”内核入口事件的堆栈,如异常、页面故障和大多数硬件中断,其余七个转换堆栈代表用于通过x64定义的IST(中断堆栈表)机制分发的陷阱的堆栈(注意Windows目前不使用所有7个可能的IST堆栈)。

当KVA Shadow激活时,那么,每个处理器的KTSS64.Rsp0字段指向每个处理器的第一个转换堆栈,并且每个KTSS64.Ist[n]字段指向该处理器的第n个KTRANSITION_STACK。为方便起见,转换堆栈位于一个连续的内存区域中, internally termed the KPROCESSOR_DESCRIPTOR_AREA,该区域还包含每处理器GDT、IDT和TSS,所有这些都需要影子化到用户模式地址空间,以便处理器本身能够正确处理环转换。这个连续内存块本身被完整地影子化。

这种配置确保当KVA Shadow激活时 fielded a kernel entry event,当前堆栈既影子化到用户模式地址空间,又不包含敏感内存内容,这些内容如果披露给用户模式会有风险。然而,为了维护这些属性,陷阱分发代码必须小心,不在任何时间将任何敏感信息推送到任何转换堆栈上。这 necessitates the first several rules for KVA shadow in order to avoid any other memory contents from being stored onto the transition stacks:当在转换堆栈上执行时,内核必须 fielding a kernel entry or kernel exit event,中断必须被禁用并且必须始终保持禁用,并且在转换堆栈上执行的代码必须小心, never incur any other type of kernel trap。这也意味着KVA Shadow陷阱分发代码可以假设在内核模式中引发的陷阱已经以正确的CR3执行,并且在正确的内核堆栈上(除了一些特殊考虑 for IST-delivered traps,如下所述)。

使用KVA Shadow激活处理陷阱

基于上述设计决策,有一组额外的KVA Shadow特定任务必须在内核的正常陷阱处理代码被调用以处理内核入口陷阱事件之前发生。此外,有一组类似的与KVA Shadow相关的任务必须在陷阱处理结束时发生,如果发生内核退出。

在正常内核入口时,必须发生以下事件序列:

  1. 必须加载内核GS基值。这使得剩余的陷阱代码能够访问每处理器数据结构,如那些保存当前处理器的内核CR3值的结构。
  2. 处理器的地址空间必须切换到内核地址空间,以便所有内核代码和数据可访问(即,必须加载内核CR3值)。这 necessitates that the kernel CR3 value must be stored in a location that is, itself, shadowed。出于KVA Shadow的目的,一个单独的每处理器KPRCB页面,仅包含“安全”内容,维护当前处理器的内核CR3值的副本,以便KVA Shadow陷阱分发代码轻松访问。地址空间之间的上下文切换,以及进程附加/分离在进程地址空间更改时更新相应的KPRCB字段 with the new CR3 value。
  3. 先前由硬件作为从用户模式到内核模式的环转换的一部分推送的机器帧必须从当前(转换)堆栈复制到当前线程的每内核线程堆栈。
  4. 当前堆栈必须切换到每内核线程堆栈。此时,“正常”陷阱处理代码可以 largely proceed as usual, and without invasive modifications(保存内核GS基值已经加载)。

大致来说,在正常内核退出时必须发生逆事件序列;当前内核线程堆栈顶部的机器帧必须复制到处理器的转换堆栈,堆栈必须切换,CR3必须重新加载为当前进程的用户模式地址空间的相应值,用户模式GS基值必须重新加载,然后控制权可以返回给用户模式。

通过SYSCALL/SYSRETQ指令对的系统服务调用入口和退出处理略有特殊,因为处理器不 already push a machine frame,因为内核在显式加载一个堆栈指针之前逻辑上没有当前堆栈指针。在这种情况下,内核入口和内核退出时不需要复制机器帧,但其他基本步骤仍然必须执行。

KVA Shadow陷阱分发代码需要特别小心处理NMI、机器检查和双故障类型陷阱事件,因为这些事件可能中断甚至通常不可中断的代码。这意味着它们甚至可能中断通常不可中断的KVA Shadow陷阱分发代码本身, during a kernel entry or kernel exit event。这些类型的陷阱使用IST机制传递到它们自己独特的转换堆栈上,并且陷阱处理代码必须小心处理GS基值或CR3值由于这些事件可能发生时机器的不确定状态而处于任何状态的情况,并且必须保留预先存在的GS基值或CR3值。

此时,如何使用KVA Shadow进入和退出内核的基础已经就位。然而,将KVA Shadow陷阱分发代码内联到标准陷阱入口和陷阱退出代码路径中是不可取的,因为标准陷阱入口和陷阱退出代码路径可能位于内核.text代码部分中的任何位置,并且希望最小化需要影子化到用户地址空间的代码量。出于这个原因,KVA Shadow陷阱分发代码被收集到一系列并行入口点中,这些入口点打包在内核映像中的自己的代码部分内,并且 either the standard set of trap entry points, or the KVA shadow trap entry points are installed into the IDT at system boot time,基于系统启动时是否使用KVA Shadow。类似地,系统服务调用入口点也位于内核映像中的这个特殊代码部分。

KVA Shadow的内存管理考虑

现在KVA Shadow能够处理陷阱入口和陷阱退出,有必要理解KVA Shadow对内存管理的影响。与KVA Shadow的陷阱处理设计考虑一样,确保正确的安全属性、提供良好的性能特征以及最大化代码更改的可维护性都是重要的设计目标。在可能的情况下,建立了规则以简化内存管理设计实现。例如,所有影子化到用户模式地址空间的内核分配都是系统范围内影子化的,而不是每个进程或每处理器。作为另一个例子,所有这样的影子化分配在用户模式和内核模式地址空间中存在于相同的内核虚拟地址,并且在两个地址空间中共享相同的底层物理页面,并且所有这样的分配被视为不可分页,并被视为已锁定在内存中。

KVA Shadow最明显的内存管理后果是每个进程通常现在需要一个单独的地址空间(即页表层次结构或顶级页目录页) allocated to describe the shadow user address space,并且对应于用户模式VA的顶级页目录条目必须从进程的内核地址空间顶级页目录页复制到进程的用户地址空间顶级页目录页。

然而,VA空间内核一半的顶级页目录页条目不被复制,而是仅对应于一个最小页表页集, needed to map the small subset of pages that have been explicitly shadowed into the user mode address space。如上所述,影子化到用户模式地址空间的页面为简单起见 left nonpageable。在实践中,这对KVA Shadow来说并不是一个

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计