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”)开发的缓解措施以及此类漏洞的其他背景信息,可以在此处找到。

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

漏洞描述与背景

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

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

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

操作系统影响

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

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

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

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

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

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

Windows内核中的KVA Shadow实现

虽然KVA Shadow的设计要求可能看起来相对无害(特权内核模式内存不能映射到地址空间中当非信任用户模式代码运行时),但这些要求的影响在整个Windows内核架构中是深远的。这触及了内核的大量核心设施,如内存管理、陷阱和异常分派等。情况进一步复杂化,因为要求相同的内核代码和二进制文件必须能够在启用和未启用KVA Shadow的情况下运行。系统在两种配置下的性能必须最大化,同时试图将KVA Shadow所需的更改范围尽可能 contained。这最大化了KVA Shadow和非KVA Shadow配置下的代码可维护性。

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

请注意,本节中描述的实现细节可能在未来更改而不另行通知。驱动程序和应用程序不得依赖下面描述的任何内部行为,而不首先检查更新的文档。

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

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

作为Windows内核如何在x64处理器上分派陷阱和异常的快速回顾,传统上,内核将当前线程的内核堆栈指针编程到当前处理器的TSS(任务状态段)中,具体到KTSS64.Rsp0字段,这通知处理器在环转换到环0(内核模式)代码时要加载哪个堆栈指针(RSP)值。这个字段传统上由内核在上下文切换时更新,以及几个其他相关的内部事件;当切换到不同线程时,处理器KTSS64.Rsp0字段被更新以指向新线程的内核堆栈的基址,这样任何在该线程运行时发生的内核进入事件都会进入内核 already on that thread’s stack。这个规则的例外是系统服务调用,它们通常使用“syscall”指令进入内核;这个指令不切换堆栈指针,操作系统陷阱处理代码有责任手动加载适当的内核堆栈指针。

在典型的内核进入时,硬件已经在内核堆栈上推入了一个称为“机器帧”(内部为MACHINE_FRAME)的内容;这是处理器定义的数据结构,IRETQ指令消耗并从堆栈中移除以 effect an interrupt-return,并包括调用应用程序的返回地址、代码段、堆栈指针、堆栈段和处理器标志等细节。Windows内核中的陷阱处理代码构建了一个称为陷阱帧(内部为KTRAP_FRAME)的结构,该结构以硬件推送的MACHINE_FRAME开始,然后包含各种软件推送的字段,描述被中断上下文的易失寄存器状态。如上所述,系统调用是这个规则的例外,必须在效应堆栈切换到当前线程的适当内核堆栈后手动构建整个KTRAP_FRAME,包括MACHINE_FRAME。

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

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

在设计KVA Shadow时,有几个设计考虑适用于陷阱处理当KVA Shadow激活时,即满足安全要求,最小化系统性能影响,并尽可能将陷阱处理代码的更改保持 compartmentalized,以简化代码并提高可维护性。例如,希望尽可能在KVA Shadow和非KVA Shadow配置之间共享陷阱处理代码,以便将来更容易对内核的陷阱处理设施进行更改。

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

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

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

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

这种配置确保当KVA Shadow激活时 fielded一个内核进入事件时,当前堆栈既影子到用户模式地址空间,又不包含敏感内存内容,这些内容如果披露给用户模式会有风险。然而,为了维护这些属性,陷阱分派代码必须小心不要在任何时间将任何敏感信息推送到任何转换堆栈上。这需要KVA Shadow的第一个几个规则,以避免任何其他内存内容被存储到转换堆栈上:在转换堆栈上执行时,内核必须 fielding一个内核进入或内核退出事件,中断必须被禁用并必须始终保持禁用,并且在转换堆栈上执行的代码必须小心永远不引发任何其他类型的内核陷阱。这也意味着KVA Shadow陷阱分派代码可以假设在内核模式中引发的陷阱已经以正确的CR3执行,并且在正确的内核堆栈上(除了一些IST交付的陷阱的特殊考虑,如下所述)。

使用KVA Shadow激活 fielding一个陷阱

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

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

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

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

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

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

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

注意,这个设计选择的一个含义是KVA Shadow不保护 against attacks against kernel ASLR using speculative side channels。这是一个深思熟虑的决定,考虑到KVA Shadow的设计复杂性、涉及的时间线以及影响相同处理器设计的其他侧信道问题的现实。值得注意的是,容易受到恶意数据缓存加载影响的处理器通常也容易受到其BTB(分支目标缓冲区)和其他微架构资源的其他攻击,这些可能允许内核地址空间布局披露给执行任意本机代码的本地攻击者。

KVA Shadow的内存管理考虑

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

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

然而,VA空间内核一半的顶级页目录页面条目不被复制,而是仅对应于需要映射显式影子到用户模式地址空间的最小页表页面集。如上所述,影子到用户模式地址空间的页面为简单起见留为不可分页。在实践中,这对KVA Shadow来说不是一个实质性的困难,因为只有非常少量的固定分配 ever shadowed system-wide。(记住只有每处理器转换堆栈被影子,而不是任何每线程数据结构,如每线程内核堆栈。)

然后,内存管理必须在任何更新发生时在两个进程地址空间之间复制顶级用户模式页目录页面条目的任何更新,并且用于工作集老化和其他目的的访问位处理必须逻辑或两个用户和内核地址空间的

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