Spectre(v2)的内部运作

时间:2019-02-05 18:49:13

标签: x86 intel cpu-architecture branch-prediction spectre

我已经阅读了有关Spectre v2的一些文章,显然您会得到非技术性的解释。彼得·科德斯(Peter Cordes)对explanation有更深入的了解,但并未完全解决一些细节。注意:我从未进行过Spectre v2攻击,因此没有实际经验。我只读过有关该理论的文章。

我对Spectre v2的理解是您对实例if (input < data.size)进行了间接分支错误预测。如果间接目标数组(我不太确定其详细信息,即为什么与BTB结构分开)(在解码时针对间接分支的RIP重新检查)不包含预测,则它将插入新的跳转RIP(分支执行最终将插入分支的目标RIP),但目前它尚不知道跳转的目标RIP,因此任何形式的静态预测均不起作用。我的理解是,它将始终预测不使用新的间接分支,并且当端口6最终确定跳转目标RIP并进行预测时,它将使用BOB进行回滚并使用正确的跳转地址更新ITA,然后更新本地和全局分支历史记录寄存器和饱和计数器相应地。

黑客需要训练饱和计数器,以始终预测已采取的措施,我想,他们是通过将if(input < data.size)设置为确实小于input(相应地捕获错误),并在循环的最后迭代中,使data.size大于input(例如1000);间接分支将被预测为已采用,并将跳转到发生缓存加载的if语句的主体。

if语句包含data.size(包含秘密数据的特定内存地址(数据[1000])的目标是从内存加载到高速缓存),则它将以推测方式分配给加载缓冲区。先前的间接分支仍在分支执行单元中,并等待完成。

我认为前提是必须在错误预测前刷新装载缓冲区之前执行装载(分配行填充缓冲区)。如果已经为其分配了行填充缓冲区,则无法执行任何操作。有意义的是,没有取消行填充缓冲区分配的机制,因为在将行填充缓冲区返回到加载缓冲区之后再存储到缓存之前,必须先填充行填充缓冲区。这可能会导致行填充缓冲区变得饱和,因为不是在需要时进行分配(将其保留在此处以加快其他加载到同一地址的速度,而是在没有其他可用行缓冲区时进行分配)。直到收到刷新信号 not 的信号时,它才能取消分配,这意味着它必须暂停执行上一个分支,而不是立即使行填充缓冲区可用于其他逻辑核心的存储。这种信令机制可能难以实现,也许没有引起他们的注意(Spectre思维),并且如果分支执行花费足够的时间来挂起行填充缓冲区以引起性能影响,则还会引入延迟。在循环的最后一次迭代之前,有意从缓存(secret = data[1000])中清除了data.size,这意味着分支执行可能需要多达100个周期。

我希望我的想法是正确的,但我不确定100%。如果有人要添加或更正任何内容,请这样做。

3 个答案:

答案 0 :(得分:4)

有时,术语“ BTB”被统称为指分支预测单元使用的所有缓冲区。但是,实际上有多个缓冲区,每个周期都使用所有缓冲区来进行目标和方向预测。特别是,BTB用于直接分支的预测,ITB(间接目标缓冲区)用于除了收益以外的间接分支的预测,而RSB用于收益的预测。 ITB也称为IBTB或间接目标阵列。所有这些术语都由不同的供应商和研究人员使用。通常,当其他缓冲区未命中时,BTB用于对各种分支指令进行初始预测。但是后来,预测变量了解了有关分支的更多信息,其他缓冲区开始起作用。如果同一间接分支的多个动态实例具有相同的目标,则也可以使用BTB代替ITB。当同一个分支有多个目标并且专门用于处理此类分支时,ITB会更加准确。请参阅:Branch prediction and the performance of interpreters — Don't trust folklore。奔腾M是第一个实现单独的BTB和ITB结构的英特尔处理器。所有以后的Intel Core处理器都具有专用的ITB。

Spectre V1漏洞利用程序是基于使用攻击者程序训练BTB的,因此,当受害者执行别名相同BTB条目的分支时,处理器就会被诱使以推测方式执行指令(称为小工具)以泄漏信息。 Spectre V2攻击类似,但基于训练ITB。此处的关键区别在于,在V1中,处理器错误地预测了分支的方向,而在V2中,处理器错误地预测了分支的 target (并且条件间接分支,以及方向,因为我们希望采用它。在解释,JIT编译或利用动态多态性的程序中,可能有许多间接分支(除了返回)。特定的间接分支可能永远不会到达某个位置,但是通过对预测变量进行训练,可以使它跳转到我们想要的任何位置。正是出于这个原因,V2非常强大。无论小工具在哪里,程序的故意控制流是什么,您都可以选择一个间接分支之一,并使其推测性地跳转到小工具。

请注意,通常,静态直接分支目标的线性地址在程序的整个生命周期中都保持不变。只有一种情况可能不是这种情况:动态代码修改。因此,至少在理论上,可以基于直接分支的 target 错误预测来开发Spectre漏洞。

关于LFB的回收,我真的不明白您在说什么。当错过L1D的负载请求将数据接收到LFB中时,数据将立即转发到管道的旁路互连。需要有一种方法来确定哪个负载uop已请求此数据。返回的数据必须使用加载的uop ID进行标记。 RS中等待数据的微指令的源表示为负载的微指令ID。另外,保存负载uop的ROB条目需要标记为已完成,以便可以撤消,并且在SnB之前,需要将返回的数据写入ROB。如果在管道刷新时未取消LFB中未完成的加载请求,并且如果负载uop ID被其他uop重用,则在数据到达时,它可能会错误地转发到管道中当前存在的任何新uops,从而破坏了微体系结构状态。因此,需要一种方法来确保在任何情况下都不会发生这种情况。通过简单地将所有有效LFB条目标记为“已取消”,取消管道刷新上的未完成的负载请求和推测性RFO是非常有可能的,只是这样就不会将数据返回到管道。但是,仍可能会提取数据并将其填充到一层或多层缓存中。 LFB中的请求由行对齐的物理地址标识。可能还有其他可能的设计。

我已决定进行实验以确定LFB在Haswell上何时重新分配的确切时间。运作方式如下:

Outer Loop (10K iterations):

Inner Loop (100 iterations):
10 load instructions to different cache lines most of which miss the L2.
LFENCE.
A sequence of IMULs to delay the resolution of the jump by 18 cycles.
Jump to inner.

3 load instructions to different cache lines.
LFENCE.
Jump to outer.

要使其正常工作,需要关闭超线程和两个L1预取器,以确保我们拥有L1的所有10个LFB。

LFENCE指令可确保在正确预测的路径上执行时,我们不会耗尽LFB。此处的关键思想是内部跳转每次外部迭代都会被错误预测一次,因此在错误预测路径上最多可以在LFB中分配10个内部迭代负载。请注意,LFENCE会阻止分配来自后续迭代的负载。几个周期后,将解决内部分支并发生错误预测。清除管道,并恢复前端,以获取并执行外循环中的加载指令。

有两种可能的结果:

  • 已经为错误路径上的负载分配的LFB将作为管道清除操作的一部分立即释放,并且可用于其他负载。在这种情况下,不会因LFB不可用而停顿(使用L1D_PEND_MISS.FB_FULL进行计数。)
  • 仅在负载得到服务时才释放LFB,无论它们是否在错误的路径上。

当内部跳转之后外部循环中有三个负载时,L1D_PEND_MISS.FB_FULL的测量值大约等于外部迭代的次数。每个外循环迭代只有一个请求。这意味着,当正确路径上的三个负载发送到L1D时,来自错误路径的负载仍将占据8个LFB条目,从而导致第三个负载的FB满事件。这表明LFB中的负载只有在负载实际完成时才进行脱脂处理。

如果我在外部循环中放置的负载少于两个,则基本上没有FB满事件。我注意到一件事:外循环中每增加三个负载,L1D_PEND_MISS.FB_FULL就会增加约20K,而不是预期的10K。我认为正在发生的事情是,当首次向L1D发出加载uop的加载请求并且所有LFB都在使用时,它被拒绝了。然后,当LFB可用时,会将负载缓冲区中的两个未决负载发送到L1D,一个将在LFB中分配,另一个将被拒绝。因此,每增加一个负载,我们就会得到两个LFB完整事件。但是,当外循环中有三个负载时,只有第三个负载在等待LFB,因此每次外循环迭代都会得到一个事件。本质上,加载缓冲区无法区分是使用一个LFB还是两个LFB。只能知道至少有一个LFB是空闲的,因此由于有两个加载端口,它尝试同时发送两个加载请求。

答案 1 :(得分:3)

对于分支,有些类似于jc .somewhere,其中CPU仅真正需要猜测分支是否将被采用,以便能够推测出所猜测的路径。但是,有些分支类似于jmp [table+eax*8],其中可能有超过40亿个可能的方向,对于这些情况,CPU需要猜测目标地址才能推测出所猜测的路径。因为分支的类型非常不同,所以CPU使用的预测器类型也非常不同。

对于Spectre,有一个“元模式”-攻击者使用推测执行来诱使CPU将信息留在某些东西中,然后从某些东西中提取该信息。 “事物”有多种可能性(数据缓存,指令缓存,TLB,分支目标缓冲区,分支方向缓冲区,返回堆栈,写合并缓冲区等),因此,幽灵有很多可能的变化(而不仅仅是(在2018年初公开的“众所周知的前两个变体”)。

对于幽灵v1(其中“某物”是数据高速缓存),攻击者需要某种方法来诱骗CPU将数据放入数据高速缓存(例如,加载,然后是第二加载,具体取决于第一次加载的值) ,可以通过推测的方式执行)和某种方式来提取信息(刷新缓存中的所有内容,然后使用加载所花费的时间来确定数据缓存状态的变化方式。)

对于幽灵v2(其中“某物”是用于诸如jc .somewhere之类的指令的分支方向缓冲区),攻击者需要某种方法来诱骗CPU将数据放入分支方向缓冲区(例如,加载然后取决于负载的分支(可以通过推测方式执行)和某种提取信息的方式(事先将分支方向缓冲区设置为已知状态,然后使用分支所花费的时间来确定分支的状态分支方向缓冲区已更改)。

对于所有可能的光谱变化而言,(对于防御而言)唯一重要的事情是“某物”的含义(以及如何防止信息进入“某物”或刷新/覆盖/破坏信息)进入了“某物”)。其他所有内容(攻击多种可能的幽灵变体中的任何一种的多种可能的代码实现中的特定细节)都不重要。

幽灵的模糊历史

原始Spectre(v1,使用缓存计时)于2017年发现,并于2018年1月公开宣布。就像大坝爆炸一样,随后很快出现了其他一些变体(例如,v2,使用分支预测)。这些早期的变化吸引了很多宣传。在大约六个月左右的时间内,发现了多个其他变体,但没有得到足够的宣传,并且很多人不知道(并且仍然不知道)。到2018年的“下半年”,人们(例如我)开始失去对哪些变体已被证明(通过“概念验证”实现)和尚未得到证实的跟踪,一些研究人员开始尝试枚举可能性并建立命名约定为他们。到目前为止,我所见过的最好的例子是“对瞬态执行攻击和防御的系统评估”(请参阅​​https://arxiv.org/pdf/1811.05441.pdf)。

但是,“坝壁上的洞”不是很容易被塞住的东西,(随机猜测)我认为这需要几年的时间才能确定所有可能性都已经探究了(我认为缓解的需求永远不会消失。

答案 2 :(得分:2)

感谢布伦丹(Brendan)和哈迪·布雷斯(Hadi Brais),在阅读了您的答案并最终阅读了《幽灵论》之后,现在很清楚我的想法出了什么问题,我使两者有些混淆。

我只是部分描述了Spectre v1,它通过错误地将跳转的分支历史(即if (x < array1_size))训练到幽灵小工具上而导致边界检查绕过。这显然不是间接分支。黑客通过调用包含带有合法参数的幽灵小工具的函数来对分支预测变量(PHT + BHT)进行初始化,然后使用非法参数进行调用以将array1[x]带入缓存来实现此目的。然后,他们通过提供合法参数来重新初始化分支历史记录,然后从缓存中刷新array1_size(我不确定它们的操作方式,因为即使攻击者进程知道array1_size的VA,也不能通过刷新是因为TLB包含用于该进程的其他PCID,因此必须以某种方式将其驱逐,即在该虚拟地址处填充集)。然后,它们使用与之前相同的非法参数进行调用,并且array1[x]不在缓存中,而array1_size不在缓存中,array[x]将快速解析并开始加载array2[array1[x]],同时仍在等待在array1_size上,它基于超越array2边界的任意x处的秘密在array1中加载位置。然后,攻击者使用有效值x调用该函数,并调用该函数一次(我假设攻击者必须知道array1的内容,因为如果array2[array1[8]]导致访问速度更快,则他们需要知道什么是在array1[8]处,因为这是秘密,但可以肯定的是,该数组必须包含每个2 ^ 8位组合(对)。

另一方面,

Spectre v2需要第二个攻击过程,该过程知道受害者进程中间接分支的虚拟地址,以便它可以毒化 target 并替换它与另一个地址。如果攻击过程包含一条跳转指令,该跳转指令与受害人间接分支位于IBTB中的同一集合,方式和标签中,则它只是训练该分支指令来预测已采取并跳转到恰好是受害者进程中的小工具。当受害者进程遇到间接分支时,来自攻击程序的错误目标地址在IBTB中。至关重要的是,它是一个间接分支,因为通常会在解码时检查由于过程切换而导致的虚假,即如果分支目标与该RIP的BTB中的目标不同,则它将刷新之前提取的指令。间接分支无法做到这一点,因为它直到执行阶段才知道目标,因此,想法是所选的间接分支取决于需要从缓存中获取的值。然后,它跳转到该小工具的目标地址,依此类推,以此类推。

攻击者需要知道受害者进程的源代码才能识别小工具,并且他们需要知道它将驻留的VA。我认为可以通过可预测地知道将代码加载到何处来完成此操作。我相信例如,.exe通常在x00400000处加载,然后在PE标头中有一个BaseOfCode。


编辑:我刚刚阅读了Spectre论文的附录B,它使Spectre v2可以很好地在Windows中实现。

  

作为概念验证,我们构造了一个简单的目标应用程序,该应用程序提供了计算键和输入消息的SHA1哈希的服务。此实现由一个程序组成,该程序连续运行一个循环,该循环调用Sleep(0),从文件中加载输入,调用Windows加密功能以计算哈希值,并在输入更改时打印哈希值。我们发现Sleep()调用是使用寄存器ebx,edi中输入文件中的数据完成的,并且攻击者知道edx的值,即两个寄存器的内容由攻击者控制。这是本节开头所述的Spectre小工具类型的输入标准。

它使用ntdll.dll(充满本机API系统调用存根的.dll)和kernel32.dll(Windows API),它们始终沿ASLR的方向映射在用户虚拟地址空间中(在.dll映像),但由于写入时复制的视图映射到页面缓存中,物理地址可能相同。毒药的间接分支将在Windows API Sleep()中的kernel32.dll函数中,该函数似乎间接调用NtDelayExecution()中的ntdll.dll。然后,攻击者确定间接分支指令的地址,并将包含目标地址的受害者地址的页面映射到其自己的地址空间,并将存储在该地址的目标地址更改为他们标识为驻留在某个地方的小工具的地址。 ntdll.dll中的相同或另一个函数中(由于ASLR,我不能完全确定攻击者如何确定受害者进程在其地址空间中将kernel32.dllntdll.dll映射到哪里为了在Sleep()中为受害者找到间接分支的地址,附录B声称他们使用“简单指针操作”来定位包含目标的间接分支和地址-我不知道该如何工作当然)。然后,以牺牲者相同的亲和力启动线程(这样,牺牲者和受过训练的线程在同一物理核心上的超线程)会自己调用Sleep()来间接地对其进行训练,这将在黑客进程的地址空间上下文中进行。跳转到小工具的地址。小工具会暂时替换为ret,以便从Sleep()平稳返回。这些线程还将在间接跳转之前执行一个序列,以模拟受害者在遇到间接跳转之前的全局分支历史记录,以完全确保该分支记录在合金历史记录中。然后,使用受害者的线程亲和力的补充启动一个单独的线程,该线程反复逐出包含跳转目标的受害者的内存地址,以确保当受害者确实遇到间接分支时,它将花费很长的RAM访问时间来进行解析,从而允许小工具,可以在根据BTB条目检查分支目的地并刷新管道之前先进行推测。在JavaScript中,逐出是通过将相同的缓存集(即4096的倍数)加载到相同的缓存集来完成的。在此阶段,错误训练线程,逐出线程和受害线程都在运行并循环。当受害者进程循环调用Sleep()时,由于IBTB条目被黑客先前毒害,因此间接分支推测该小工具。将以受害进程线程亲和力的补充来启动探测线程(以免干扰训练和受害分支历史)。当调用ebx时,探测线程将修改受害进程使用的文件的标头,从而导致这些值驻留在ediSleep()中,这意味着探测线程可以直接影响这些值存储在ebxedi中。在示例中分支到的幽灵小工具将[ebx+edx+13BE13BDh]中存储的值添加到edi,然后将值存储在edi中的地址处,并将其带有进位符添加到{{1} }。这允许探测线程学习存储在dl中的值,就像它选择原始[ebx+edx+13BE13BDh]为0一样,然后第二次操作中访问的值将从虚拟地址范围0x0 – 0x255加载。间接分支将解决的时间,但副作用已经存在。攻击过程需要确保已将相同的物理地址映射到其虚拟地址空间中的相同位置,以便利用定时攻击来探测探测阵列。不确定如何执行此操作,但是在Windows中,AFIAK需要映射受害人在该位置打开的页面文件支持的节对象的视图。否则,它可能会操纵受害者以TC edi负值调用幽灵小工具,使得ebx ebx+edx+13BE13BDh= 0,...,{{1} }以及调用该电话的时间。这也可能通过使用APC注射来实现。

相关问题