6

我已经阅读了一些关于 Spectre v2 的内容,显然你得到了非技术性的解释。Peter Cordes 有更深入的解释,但并没有完全解决一些细节。注意:我从未执行过 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超过data.size(例如 1000); 间接分支将被预测并跳转到发生缓存加载的 if 语句的主体。

if 语句包含secret = data[1000](包含秘密数据的特定内存地址(data[1000]),目标是从内存加载到缓存),那么这将被推测性地分配给加载缓冲区。前面的间接分支还在分支执行单元中等待完成。

我相信前提是需要在加载缓冲区因错误预测而被刷新之前执行加载(分配一个行填充缓冲区)。如果它已经被分配了一个行填充缓冲区,那么什么也做不了。没有取消行填充缓冲区分配的机制是有道理的,因为行填充缓冲区在将其返回到加载缓冲区之后必须在存储到缓存之前挂起。这可能会导致行填充缓冲区变得饱和,因为不是在需要时解除分配(将其保留在那里以加快其他加载到同一地址的速度,但在没有其他可用行缓冲区时解除分配)。直到它收到一些信号表明刷新不是将发生,这意味着它必须停止执行前一个分支,而不是立即使行填充缓冲区可用于其他逻辑核心的存储。这种信号机制可能难以实现,也许他们没有想到(幽灵之前的想法),如果分支执行需要足够的时间来挂行填充缓冲区导致性能影响,它也会引入延迟,即如果在循环的最终迭代之前data.size故意从缓存 ( CLFLUSH) 中刷新,这意味着分支执行可能需要多达 100 个循环。

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

4

3 回答 3

4

有时,术语“BTB”被统称为分支预测单元使用的所有缓冲区。然而,实际上有多个缓冲区,所有这些缓冲区都在每个周期中用于进行目标和方向预测。具体来说,BTB用于对直接分支进行预测,ITB(indirect target buffer)用于对除return以外的间接分支进行预测,RSB用于对return进行预测。ITB 也称为 IBTB 或间接目标阵列。所有这些术语都被不同的供应商和研究人员使用。通常,BTB 用于在其他缓冲区未命中时对各种分支指令进行初始预测。但后来预测器了解了更多关于分支的信息,其他缓冲区开始发挥作用。如果同一间接分支的多个动态实例都具有相同的目标,则也可以使用 BTB 代替 ITB。当同一个分支有多个目标并且专门为处理这些分支而设计时,ITB 会更加准确。看:分支预测和解释器的表现——不要相信民间传说。第一个实现独立 BTB 和 ITB 结构的 Intel 处理器是 Pentium M。所有后来的Intel Core处理器都有专用的 ITB。

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

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

关于LFB的回收,我真的不明白你在说什么。当错过 L1D 的加载请求将数据接收到 LFB 时,数据会立即转发到流水线的旁路互连。需要有一种方法来确定哪个加载 uop 请求了这些数据。返回的数据必须带有负载的 uop ID 标记。RS 中等待数据的微指令的来源表示为负载的微指令 ID。此外,需要将保存加载 uop 的 ROB 条目标记为已完成,以便可以将其退役,并且在 SnB 之前,需要将返回的数据写入 ROB。如果在管道刷新时,未取消 LFB 中的未完成加载请求,并且如果加载 uop ID 被重用于其他 uop,则当数据到达时,它可能会被错误地转发到当前正在管道中的任何新 uop,从而破坏微架构状态。所以需要有一种方法来确保在任何情况下都不会发生这种情况。通过简单地将所有有效的 LFB 条目标记为“已取消”,就可以取消未完成的加载请求和管道刷新上的推测性 RFO,这样数据就不会返回到管道。但是,数据可能仍会被提取并填充到一级或多级缓存中。LFB 中的请求由行对齐的物理地址标识。可以有其他可能的设计。很可能通过简单地将所有有效的 LFB 条目标记为“已取消”来取消未完成的加载请求和管道刷新上的推测 RFO,这样数据就不会返回到管道。但是,数据可能仍会被提取并填充到一级或多级缓存中。LFB 中的请求由行对齐的物理地址标识。可以有其他可能的设计。很可能通过简单地将所有有效的 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 仅在负载得到维修时才被释放,无论它们是否在错误预测的路径上。

在内层跳转后外层循环有3个载荷时,测得的值L1D_PEND_MISS.FB_FULL约等于外层迭代次数。这是每个外部循环迭代的一个请求。这意味着当正确路径上的三个负载被发送到 L1D 时,来自错误预测路径的负载仍然占用 8 个 LFB 条目,从而导致第三个负载的 FB 满事件。这表明 LFB 中的负载仅在负载实际完成时才被解除涂层。

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

于 2019-02-06T10:56:00.843 回答
3

对于分支,有些就像jc .somewhereCPU 只需要猜测分支是否会被采用就能够推测出猜测的路径。然而,有些分支就像jmp [table+eax*8]可能有超过 40 亿个方向一样,在这些情况下,CPU 需要猜测目标地址才能推测出猜测的路径。因为有非常不同类型的分支,CPU 使用非常不同类型的预测器。

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

对于 Spectre v1(其中“某物”是数据缓存),攻击者需要某种方法来欺骗 CPU 将数据放入数据缓存中(例如加载,然后根据第一次加载的值进行第二次加载,这可以推测性地执行)和某种提取信息的方法(刷新缓存中的所有内容,然后使用加载所花费的时间来确定数据缓存的状态如何更改)。

对于 Spectre v2(其中“某物”是用于类似指令的分支方向缓冲区jc .somewhere),攻击者需要一些方法来欺骗 CPU 将数据放入分支方向缓冲区(例如,加载,然后是依赖于加载的分支,可以推测性地执行)和某种提取信息的方法(预先将分支方向缓冲区设置为已知状态,然后使用分支花费的时间量来确定分支方向缓冲区的状态如何变化)。

对于幽灵的所有可能变化,唯一重要的事情(对于防御)是“某物”可以是什么(以及如何防止信息进入“某物”,或刷新/覆盖/销毁进入的信息“某事”)。其他一切(攻击许多可能的幽灵变体中的任何一种的代码的许多可能实现之一的具体细节)都不重要。

幽灵的模糊历史

最初的 Spectre(v1,使用缓存时序)于 2017 年被发现,并于 2018 年 1 月公开宣布。它就像一个大坝决堤,其他一些变种(例如 v2,使用分支预测)迅速跟进。这些早期的变化引起了很多宣传。在那之后的大约 6 个月左右的时间里,发现了多个其他变体,但没有得到太多的宣传,而且很多人没有(并且仍然没有)意识到它们。到 2018 年的“下半年”,人们(例如我)开始忘记哪些变体已被证明(通过“概念证明”实现)以及哪些仍未被证明,一些研究人员开始尝试列举可能性并建立命名约定为他们。到目前为止,我见过的最好的例子是“https://arxiv.org/pdf/1811.05441.pdf)。

然而,“坝墙上的洞”不是很容易堵塞的东西,而且(随机猜测)我认为我们需要几年时间才能假设所有可能性都已经被探索过了(我认为需要缓解永远不会消失)。

于 2019-02-05T20:30:03.677 回答
2

感谢 Brendan 和 Hadi Brais,在阅读了您的答案并最终阅读了幽灵论文之后,现在很清楚我的想法哪里出错了,我将两者混淆了一点。

我部分描述了 Spectre v1,它通过错误训练跳转的分支历史(即if (x < array1_size)到幽灵小工具)来绕过边界检查。这显然不是间接分支。黑客通过调用一个包含具有合法参数的幽灵小工具的函数来启动分支预测器 (PHT+BHT),然后使用非法参数调用以将array1[x]其放入缓存中来做到这一点。然后他们通过提供合法参数来重新设置分支历史记录,然后array1_size从缓存中刷新(我不确定他们是怎么做的,因为即使攻击者进程知道array1_size,无法刷新该行,因为 TLB 包含进程的不同 PCID,因此必须以某种方式将其驱逐,即填充该虚拟地址处的集合)。然后,它们使用与以前相同的非法参数进行调用,并且array1[x]缓存中的原样,但不是,array1_sizearray[x]快速解决并array2[array1[x]]在仍在等待的同时开始array1_size加载 攻击者然后调用具有有效值 x 的函数并计算函数调用的时间(我假设攻击者必须知道的内容,因为如果导致更快的访问,他们需要知道在array2array1array1array2[array1[8]]array1[8]因为这是秘密,但该数组肯定必须包含每个 2^8 位组合权利)。

另一方面,Spectre v2 需要第二个攻击进程,该进程知道受害进程中间接分支的虚拟地址,以便它可以毒害目标并将其替换为另一个地址。如果攻击过程包含一个跳转指令,该指令将驻留在 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(.dll 充满本机 API 系统调用存根)和kernel32.dll(Windows API),它们始终映射在 ASLR 方向上的用户虚拟地址空间(在 .dll 图像中指定),除了物理地址可能是由于写入时复制视图映射到页面缓存,因此相同。毒药的间接分支将在 Windows APISleep()函数中,其中kernel32.dll似乎间接调用NtDelayExecution(). ntdll.dll然后,攻击者确定间接分支指令的地址,并将包含目标地址的受害者地址的页面映射到它自己的地址空间,并将存储在该地址的目标地址更改为他们识别为驻留在某处的小工具的地址在相同或另一个功能中ntdll.dll(我不完全确定(由于 ASLR)攻击者如何确定受害者进程映射的位置kernel32.dll及其ntdll.dll地址空间,以便为Sleep()受害者定位间接分支的地址。附录 B 声称他们使用了“简单指针操作'来定位包含目标的间接分支和地址——我不确定它是如何工作的)。然后以与受害者相同的亲和性启动线程(以便受害者和错误训练线程在同一物理内核上超线程)调用Sleep()自己来间接训练它,这在黑客进程的地址空间上下文中现在将跳转到小工具。小工具暂时被替换为 aret以便它从Sleep()顺利。这些线程还将在间接跳转之前执行一个序列,以模拟受害者在遇到间接跳转之前的全局分支历史,以完全确保分支是在合金历史中采用的。然后使用受害者的线程亲和性的补充启动一个单独的线程,该线程重复驱逐包含跳转目标的受害者的内存地址,以确保当受害者确实遇到间接分支时,它需要长时间的 RAM 访问才能解决,这允许在根据 BTB 条目检查分支目标并刷新管道之前,小工具可以提前推测。在 JavaScript 中,驱逐是通过加载到相同的缓存集来完成的,即 4096 的倍数。错误训练的线程,在这个阶段,驱逐线程和受害者线程都在运行和循环。当受害者进程循环调用时Sleep(),间接分支推测该小工具是由于黑客之前投毒的 IBTB 条目。探测线程与受害进程线程亲和性的补充一起启动(以免干扰训练错误和受害分支历史)。探测线程将修改受害进程使用的文件的标头,从而导致这些值驻留在ebxedi何时Sleep()被调用,这意味着探测线程可以直接影响存储在ebx和中的值edi。在示例中分支到的幽灵小工具将存储在的值添加[ebx+edx+13BE13BDh]edi,然后在存储的地址处加载一个值,edi并将其与进位相加dl。这允许探测线程学习存储在[ebx+edx+13BE13BDh]就好像它选择了edi0 的原始值,那么在第二个操作中访问的值将从虚拟地址范围 0x0 – 0x255 加载,此时间接分支将解决,但副作用已经存在。攻击进程需要确保它已将相同的物理地址映射到其虚拟地址空间中的相同位置,以便使用定时攻击来探测探测阵列。不确定它是如何做到的,但在 Windows 中,AFAIK 需要映射受害者在该位置打开的页面文件支持的部分对象的视图。要么这样,要么它会操纵受害者调用具有负 TCebx值的幽灵小工具,例如ebx+edx+13BE13BDh = 0, =1,...,=255并以某种方式计时。这也可能通过使用 APC 注入来实现。

于 2019-02-16T17:43:19.833 回答