购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

8.2 硬件侧信道漏洞

现代CPU可以在内部寄存器之间以非常快的速度(皮秒级别)计算并移动数据。处理器的寄存器是一种稀缺资源,因此,操作系统和应用程序代码总是通过指令让CPU将数据从CPU寄存器移动至主存,反之亦然。CPU可以访问不同类型的内存。位于CPU封装内部以及可由CPU执行引擎直接访问的内存称为缓存(Cache),缓存具有高速和昂贵的特点。CPU通过外部总线访问的内存通常可称为RAM(随机访问内存,Random Access Memory),RAM速度更慢,价格更低,但容量更大。内存与CPU之间的位置关系定义了一种所谓的“基于内存层次结构”的内存,这些内存有着不同的速度和容量(位置越接近CPU的内存,速度就越快,但容量就越小)。如图8-2所示,现代计算机的CPU通常包含L1、L2和L3这三级高速缓存内存,每个物理内核均可直接访问这些高速缓存。L1和L2缓存距离CPU的内核最近,并且是每个内核专用的。L3缓存距离最远,并且始终被所有CPU内核共享(不过嵌入式处理器一般不具备L3缓存)。

访问时间是缓存的一个重要特征,其访问时间几乎等同于CPU的寄存器(其实缓存比寄存器略慢一些)。主存的访问时间则会慢数百倍。这意味着,如果CPU按顺序执行所有指令,由于需要通过指令访问位于主存中的数据,整体速度会慢很多倍。为了解决这个问题,现代CPU采取了不同的策略。在历史上,这些策略曾引发了侧信道攻击(也叫预测式攻击),事实证明,这会极大地影响终端用户系统的整体安全性。

图8-2 现代CPU的缓存和存储内存及其平均容量与访问时间

为了准确描述侧信道硬件攻击以及Windows所采取的缓解措施,我们首先需要通过一些基本概念了解CPU内部的工作原理。

8.2.1 乱序执行

现代微处理器通过自己的流水线执行计算机指令。流水线包含很多阶段,如指令获取、解码、寄存器分配和更名、指令重排序、执行,以及退出。CPU应对内存访问速度不够快的一种常用策略是:让执行引擎忽略指令顺序,优先执行所需资源已可用的指令。这意味着CPU并不会按照某种严格一致的顺序执行指令,借此能够通过让所有内核尽可能满载的方式将所有执行单元的利用率提升至最大限度。在确定某些指令很快将会被用到并被提交(退出)之前,现代处理器能够以预测性的方式执行数百条此类指令。

上述乱序执行方法最大的问题之一在于分支指令。一条带有附带条件的分支指令会在机器代码中定义两个可能的路径,而最终要执行的“正确”路径取决于之前执行过的指令。在计算具体情况时,因为所依赖的“之前执行过的指令”需要访问速度缓慢的RAM,因此整体速度也会被拖慢。此时,执行引擎需要等待定义条件的指令退出(意味着需要等待内存总线完成内存访问操作),随后才能以乱序执行的方式执行正确路径下所包含的后续指令。间接分支也会遇到类似情况。在间接分支中,CPU的执行引擎并不知道分支(通常为Jump或Call)的具体目标,因为必须从主存中获取相关地址。在这个语境中,“推测执行”(speculative execution)这个术语意味着CPU的流水线需要以并行或乱序的方式解码并执行多条指令,但其结果并不会退出至永久性寄存器中,在分支指令最终执行完毕之前,内存写入操作依然会处于挂起状态。

8.2.2 CPU分支预测器

在彻底评估分支条件前,CPU如何得知哪个分支(路径)需要执行?(由于目标地址未知,间接分支同样存在类似问题。)答案位于CPU封装所包含的两个组件中:分支预测器(branch predictor)和分支目标预测器(branch target predictor)。

分支预测器是CPU中一种复杂的数字电路,在最终得以确认前,它会尽可能猜测每个分支最终的行进路径。借助类似方式,CPU中所包含的分支目标预测器会在最终确定前,尽可能预测间接分支的目标。虽然实际的硬件实现主要取决于CPU制造商,但这两个组件都用到了一种名为分支目标缓冲(Branch Target Buffer,BTB)的内部缓存,BTB可以使用由索引函数生成的地址标签记录分支的目标地址(或有关条件分支过去曾经做过什么的相关信息),该地址标签与缓存生成标签的方法类似,下一节会详细介绍。当分支指令首次执行时,会将目标地址存储在BTB中。通常,当执行流水线首次停机时,会迫使CPU等待从主存中成功获取条件或目标地址。当同一个分支第二次执行时,会使用BTB中的目标地址来获取预测的目标并将其置于流水线中。图8-3展示了CPU分支目标预测器简化后的架构范例。

图8-3 CPU分支目标预测器简化后的架构范例

如果预测出错,并且已经以预测的方式执行了错误的路径,那么指令流水线会被刷新,之前预测执行的结果会被丢弃。随后会向CPU流水线中送入其他路径,并从正确的分支开始重新执行。这个过程也叫分支预测错误。在这种情况下,浪费掉的CPU周期总数并不会多于顺序执行并等待分支条件的结果或评估间接地址所使用的CPU周期数。然而,CPU依然会在预测执行的过程中产生各种副作用,例如CPU缓存行污染。不幸的是,一些副作用可能会被攻击者发现并利用,进而危及系统的整体安全性。

8.2.3 CPU缓存

正如上一节所述,CPU缓存(Cache)是一种高速内存,可大幅缩短获取和存储数据与指令所需的时间。数据会以固定大小的块(通常为64或128字节)在内存和缓存之间传输,这种数据块也叫缓存行或缓存块。当一个缓存行从内存复制到缓存时,会创建一个缓存项。该缓存项中包含数据副本以及用于分辨所请求内存位置的标签。与分支目标预测器不同,缓存始终会通过物理地址创建索引(否则多个地址空间之间的映射和变更过程将变得极为复杂)。从缓存的角度来看,一个物理地址可以拆分为不同的成分,其中较高的位通常代表标签,较低的位代表缓存行以及行本身的偏移量。标签具备唯一性,可用于区分每个缓存块所属的内存地址,如图8-4所示。

当CPU读/写内存位置时,首先会检查缓存中是否存在对应的项(会在可能包含来自该地址数据的任何缓存行中检查。但某些缓存可能存在不同的“向”,下文很快将会提到)。如果处理器发现来自该位置的内存数据已经位于缓存中,此时就出现了“缓存命中”的情况,处理器会立即通过该缓存行读/写数据;如果数据不在缓存中,此谓之“缓存未命中”,此时CPU会在缓存中分配一个新项,并将数据从主存中复制进去,随后进行访问。

图8-4 48位单向CPU缓存范例

图8-4展示了一个单向CPU缓存,该缓存最大可寻址48位虚拟地址空间。在本例中,CPU正在从虚拟地址0x19F566030中读取48字节数据。内存内容最开始已从主存读取到缓存块0x60,该块已经被完全装满,但所请求的数据位于偏移量0x30处。范例缓存只有256块,每块256字节,因此多个物理地址可以装入编号为0x60的块中。标签(0x19F56)能够唯一地区分数据在主存中所在的物理地址。

通过类似的方式,当CPU接到指令向一个内存地址写入新内容时,它首先会更新该内存地址所属的一个或多个缓存行。某些时候,CPU还会将数据写回至物理RAM,这主要取决于内存页面所应用的缓存类型(write-back、write-through、uncached等)。请注意,在多处理器系统中这具有重要的意义:必须设计某种缓存一致协议,以避免出现主CPU更新某个缓存块后,其他CPU针对陈旧数据执行操作的情况(多CPU缓存一致算法是存在的,但超出了本书的讨论范畴)。

当出现缓存未命中情况时,为了给新的缓存项腾出空间,CPU有时会清除某个现有的缓存块。选择要清除的缓存项(意味着选择用哪个缓存块来存储新数据)时所用的算法叫作放置策略(placement policy)。如果放置策略只能替换特定虚拟地址的一个块,这种情况可以叫作直接映射(如图8-4所示缓存只有一个方向,且属于直接映射)。相反,如果缓存可以自由选择(具备相同块编号的)任意项来保存新数据,这样的缓存也叫全相联(fully associative)缓存。很多缓存机制在实现方面进行了妥协,使得主存中的每个项可保存到缓存中 N 个位置中的任何一个位置内,这种机制也叫N向组相联(N-ways set associative)。因此一个“向”可以看作缓存的一个组成部分,缓存中每个向的容量相等,并按照相同的方式进行索引。图8-5展示了一个四向组相联缓存。图中所示的缓存可以存储分属于四个不同物理地址的数据,并通过不同的四个缓存组(使用不同标记)对相同的缓存块创建索引。

图8-5 一个四向组相联缓存

8.2.4 侧信道攻击

如上节内容所述,现代CPU的执行引擎只有在指令真正退出后才会写入计算结果。这意味着,就算有多条指令已经乱序执行完毕,并且对CPU寄存器和内存架构不会产生任何可见的影响,但这样做依然会对微架构(microarchitecture)产生一定的副作用,尤其是会影响到CPU缓存。2017年年底出现了一种针对CPU乱序引擎和分支预测器发起的新颖攻击,这种攻击所依赖的前提条件是,微架构所产生的副作用是可衡量的,尽管这些影响无法通过任何软件代码直接访问。

围绕这种方式产生的最具破坏性且最有效的硬件侧信道攻击分别名为Meltdown和Spectre。

Meltdown

Meltdown,又被称为恶意数据缓存负载(Rogue Data Cache Load,RDCL),可供恶意的用户模式进程读取所有内存,而该进程完全不需要具备相关授权。该攻击利用了处理器的乱序执行引擎,以及内存访问指令处理过程中内存访问和特权检查两个环节之间存在的内部争用条件。

在Meltdown攻击中,恶意的用户模式进程首先会刷新整个缓存(从用户模式调用可执行该操作的指令),随后该进程会执行一个非法的内核内存访问,并执行指令以可控的方式(使用一个probe数组)填满缓存。因为该进程无法访问内核内存,所以此时处理器会产生异常,该异常会被应用程序捕获,进而导致进程被终止。然而由于乱序执行的缘故,CPU已经执行了(但未退出,这意味着在任何CPU寄存器或RAM中均无法检测到对架构产生的影响)非法内存访问之后发出的指令,因此已经使用非法请求的内核内存内容填满了缓存。

随后恶意应用程序会衡量访问数组(该数组已被用于填充CPU的缓存块)中每个页面所需的时间,借此探测整个缓存。如果访问时间落后于某个阈值,则意味着数据位于缓存行中,攻击者进而就可以通过从内核内存读取的数据推断出准确的内容。图8-6取自最早有关Meltdown的研究论文(详见https://meltdownattack.com/),其中展示了1 MB probe数组(由256个4KB的页组成)的访问时间。

图8-6 访问一个1 MB probe数组所需的CPU时间

如图8-6所示,每个页面的访问时间都是类似的,只有一个页面的时间有较大差异。假设一次可读取1字节的机密数据,而1字节只能有256个值,那么只要准确得知数组中的哪个页面导致了缓存命中,攻击者就可以知道内核内存中到底存储了哪一字节的数据。

Spectre

Spectre攻击与Meltdown攻击类似,意味着它也依赖上文介绍的乱序执行漏洞,但Spectre所利用的CPU组件主要为分支预测器和分支目标预测器。起初,Spectre攻击曾出现过两种变体,这两种变体都可以总结为如下三个阶段:

1)在设置阶段,攻击者会通过低特权(且由攻击者控制的)进程反复执行多次操作,误导CPU分支预测器,此举意在通过训练让CPU执行(合法的)条件分支或精心定义好的间接分支目标。

2)在第二阶段,攻击者会迫使作为受害者的高特权应用程序(或上一阶段所使用的进程)以预测执行的方式执行错误预测分支中所包含的指令。这些指令通常会将机密信息从受害者应用程序的上下文中转移至微架构信道(通常为CPU缓存)。

3)在最终阶段,攻击者会通过低特权进程恢复存储在CPU缓存(微架构信道)中的敏感信息,为此攻击者会探测整个缓存(与Meltdown攻击的做法相同),借此即可获得本应在受害者高特权地址空间中受到保护的机密信息。

Spectre攻击的第一个变体可通过迫使CPU分支预测器以预测执行的方式执行条件分支中错误的分支,进而获取存储在受害者进程地址空间(该地址空间可以是攻击者所控制的地址空间,或不受攻击者控制的地址空间)中的机密信息。该分支通常是一个函数的一部分,这个函数会在访问内存缓冲区中所包含的某些非机密数据之前执行边界检查。如果该缓冲区与某些机密数据相邻,并且攻击者控制了提供给分支条件的偏移量,攻击者即可反复训练分支预测器并提供合法的偏移量值,借此顺利通过边界检查并让CPU执行正确的路径。

随后,攻击者会准备一个精心定义的CPU缓存(通过精心调整内存缓冲区大小,使得边界检查无法位于缓存中)并为实现边界检查分支的函数提供一个非法的偏移量。通过训练,CPU分支预测器会始终沿用最初的合法路径,然而这一次的路径是错误的(此时本应选择其他路径)。因此访问内存缓冲区的指令会以预测执行的方式来执行,进而导致在边界之外执行以机密数据为目标的读取操作。通过这种方式,攻击者即可探测整个缓存并读取机密数据(与Meltdown攻击的做法类似)。

Spectre攻击的第二个变体利用了CPU分支目标预测器,并会对间接分支投毒。通过这种方式,即可在攻击者控制的上下文中,借助间接分支错误预测的路径读取受害者进程(或操作系统内核)的任意内存数据。如图8-7所示,对于变体2,攻击者会通过恶意目标对分支预测器进行误导性训练,使得CPU能在BTB中构建出足够的信息,进而以乱序执行的方式执行位于攻击者所选择的地址中的指令。在受害者的地址空间内,该地址本应指向Gadget。Gadget是一组可以访问机密数据,并将其存储在缓冲区(该缓冲区会以受控的方式进行缓存)中的指令(攻击者需要间接控制受害者一个或多个CPU寄存器的内容,如果API接受不可信的输入数据,那么这种目的很好实现)。

在攻击者完成对分支目标预测器的训练后,即可刷新CPU缓存并调用由目标高特权实体(进程或操作系统内核)提供的服务。实现该服务的代码必须同时实现与攻击者控制的进程类似的间接分支。随后,CPU分支目标预测器会以预测执行的方式执行位于错误目标地址中的Gadget。这与变体1和Meltdown攻击一样,会在CPU缓存中产生微架构副作用,进而使其可以从低特权上下文中读取。

图8-7 Spectre攻击变体2的结构

其他侧信道攻击

Spectre和Meltdown攻击一经曝光,就催生了多种类似的侧信道硬件攻击。与Meltdown和Spectre相比,虽然其他攻击方式的破坏性和影响范围并没有那么大,但我们依然有必要了解这类全新侧信道攻击所采用的整体方法。

CPU性能优化措施所催生的预测式存储旁路(Speculative Store Bypass,SSB),可以让CPU评估过的加载指令不再依赖之前所用的存储,而是能够在存储的结果退出前以预测执行的方式执行。如果预测错误,则可能导致加载操作读取陈旧数据,其中很可能包含机密信息。读取到的数据可以转发给预测过程中执行的其他操作。这些操作可以访问内存并生成微架构副作用(通常位于CPU缓存中)。借此攻击者即可衡量副作用并从中恢复机密信息。

Foreshadow(又名L1TF)是一种更严重的攻击,在设计上,这种攻击最初是为了从硬件隔区(SGX)中窃取机密数据,随后广泛应用于在非特权上下文中执行的普通用户模式软件。Foreshadow利用了现代CPU预测执行引擎中的两个硬件漏洞,分别如下:

在不可访问的虚拟内存中进行预测。在本场景中,当CPU访问由页表项(Page Table Entry,PTE)所描述的虚拟地址中存储的某些数据时,如果未包含“存在”位(意味着该地址非有效地址),则将以正确的方式生成一个异常。然而,如果该项包含有效地址转换,CPU就可以根据读取的数据预测执行指令。与其他所有侧信道攻击方式类似,处理器并不会重试这些指令,但会产生可衡量的副作用。在这种情况下,用户模式应用程序即可读取内核内存中保存的机密数据。更严重的是,该应用程序在某些情况下还能读取其他虚拟机中的数据:当CPU转换客户物理地址(Guest Physical Address,GPA)时,如果在二级地址转换(Second Level Address Translation,SLAT)表中遇到了不存在的项,就会产生相同的副作用(有关SLAT、GPA以及转换机制的详细信息,请参阅本书卷1第5章,以及卷2第9章)。

在CPU内核的逻辑(超线程)处理器上进行预测。现代CPU的每个物理核心可以具备多条执行流水线,借此即可通过共享的执行引擎以乱序的方式同时执行多个指令(这是一种对称多线程(Symmetric Multi-Threading,SMT)架构,详见第9章)。在这种处理器中,两个逻辑处理器(Logical Processor,LP)共享同一个缓存。因此,当一个LP在高特权上下文中执行某些代码时,对端的另一个LP即可读取这个LP的高特权代码执行过程中产生的副作用。这会对系统的整体安全性造成极为严重的影响。与Foreshadow的第一个变体类似,在低特权上下文中执行攻击者代码的LP,甚至只需要等待虚拟机代码通过调度由对端LP执行,即可窃取其他高安全性虚拟机中存储的机密信息。Foreshadow的这个变体属于一种Group 4漏洞。

微架构副作用并非总是以CPU缓存为目标。为了更好地访问已缓存和未缓存的内存并对微指令重新排序,Intel的CPU使用了其他中等级别的高速缓冲区(不同缓冲区的介绍已超出本书范畴)。微架构数据采样(Microarchitectural Data Sampling,MDS)攻击可暴露下列微架构结构所包含的机密数据:

存储缓冲区(store buffer) 。在执行存储操作时,处理器会将数据写入一个名为存储缓冲区的内部临时微架构结构中,这样CPU就能在数据被真正写入缓存或主存(对于未缓存的内存访问)之前继续执行指令。当加载操作从与之前的存储相同的内存地址读取数据时,处理器可以从该存储缓冲区直接转发数据。

填充缓冲区(fill buffer) 。填充缓冲区是一种内部处理器结构,主要用于在一级数据缓存未命中(并且执行了I/O或特殊寄存器操作)时收集(或写入)数据。填充缓冲区在CPU缓存和CPU乱序执行引擎之间充当了中介的作用,其中可能保留了上一个内存请求所涉及的数据,这些数据可能会以推测的方式转发给加载操作。

加载端口(load port) 。加载端口是一种临时的内部CPU结构,主要用于从内存或I/O端口执行加载操作。

微架构缓冲区通常属于单一CPU内核,但会被SMT线程共享。这意味着,即使难以通过可靠的方式对这些结构发起攻击,在特定情况下依然有可能跨越SMT线程,通过推测的方式从中提取机密数据。

一般来说,所有硬件侧信道漏洞的后果都是相同的:可以从受害者地址空间中窃取机密数据。为了防范Spectre、Meltdown以及上文提到的其他各种侧信道攻击,Windows实现了多种缓解措施。 qiGfPRkQ7E34RMpH2YQI5peHzAQ8Nou5FbzN9JOU6rTs/qvfSMB7fqCawqbL9ia1

点击中间区域
呼出菜单
上一章
目录
下一章
×