Spectre (v2) 的内部运作
The inner workings of Spectre (v2)
我已经阅读了一些有关 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
大于 data.size
(例如 1000);间接分支将被预测采用,它将跳转到缓存加载发生的 if 语句的主体。
如果语句包含secret = data[1000]
(包含秘密数据的特定内存地址(数据[1000])旨在从内存加载到缓存)那么这将被推测地分配到加载缓冲区。前面的间接分支仍在分支执行单元中等待完成。
我相信前提是在错误预测刷新加载缓冲区之前需要执行加载(分配行填充缓冲区)。如果它已经被分配了一个行填充缓冲区,那么什么也做不了。没有取消行填充缓冲区分配的机制是有道理的,因为行填充缓冲区在将其返回到加载缓冲区后存储到缓存之前必须挂起。这可能会导致行填充缓冲区变得饱和,因为而不是在需要时取消分配(将其保留在那里以提高其他加载到同一地址的速度,但在没有其他可用行缓冲区时取消分配)。它无法解除分配,直到它收到一些信号表明刷新不会 将要发生,这意味着它必须暂停以执行前一个分支而不是立即使行填充缓冲区可用于其他逻辑核心的存储。这种信号机制可能难以实现,也许他们没有想到(幽灵之前的想法),并且如果分支执行花费足够的时间来挂线填充缓冲区导致性能影响,它还会引入延迟,即如果data.size
在循环的最后一次迭代之前有意从缓存 (CLFLUSH
) 中清除,这意味着分支执行可能需要多达 100 个周期。
我希望我的想法是正确的,但我不是 100% 确定。如果有人有什么要补充或更正的,请补充。
对于分支,有些像 jc .somewhere
,其中 CPU 只需要真正猜测分支是否会被采用,就能够推测出猜测的路径。然而,有些分支像 jmp [table+eax*8]
可能有超过 40 亿个可能的方向,对于这些情况,CPU 需要猜测目标地址才能推测出猜测的路径。因为有非常不同类型的分支,CPU 使用非常不同类型的预测变量。
对于 Spectre,有一个 "meta pattern" - 攻击者使用推测执行来欺骗 CPU 将信息留在某物中,然后从某物中提取该信息。 "something" 有多种可能性(数据高速缓存、指令高速缓存、TLB、分支目标缓冲区、分支方向缓冲区、return 堆栈、写组合缓冲区,...),因此有许多可能的变体幽灵(而不仅仅是 2018 年初 public 制作的 "well known first two variations")。
对于 spectre v1(其中 "something" 是数据缓存),攻击者需要一些方法来欺骗 CPU 将数据放入数据缓存(例如加载然后第二次加载取决于第一次加载的值,可以推测地执行)和一些提取信息的方法(刷新缓存中的所有内容,然后使用加载所需的时间来确定数据缓存的状态如何更改) .
对于 spectre v2(其中 "something" 是用于 jc .somewhere
等指令的分支方向缓冲区),攻击者需要一些方法来欺骗 CPU 将数据放入分支方向缓冲区(例如加载,然后是依赖于负载的分支,可以推测地执行)和一些提取信息的方法(预先将分支方向缓冲区设置为已知状态,然后使用分支的时间量需要确定分支方向缓冲区的状态如何更改)。
对于幽灵的所有可能变体,唯一重要的(对于防御)是 "something" 可以是什么(以及如何防止信息进入 "something",或者flush/overwrite/destroy 进入 "something" 的信息。其他一切(攻击许多可能的幽灵变体中的任何一种的代码的许多可能实现之一的具体细节)都不重要。
幽灵的模糊历史
最初的 Spectre(v1,使用缓存时序)于 2017 年被发现,public于 2018 年 1 月才宣布。它就像一个大坝决堤,以及其他一些变体(例如 v2,使用分支预测)很快跟了上去。这些早期的变体吸引了很多 public 的关注。在大约 6 个月之后,发现了多个其他变体,但没有得到尽可能多的 public 关注度,而且很多人没有(现在仍然没有)意识到它们。到 2018 年 "latter half" 时,人们(例如我)开始忘记哪些变体已被证明(通过 "proof of concept" 实现),哪些仍未得到证实,一些研究人员开始尝试列举可能性并建立命名他们的约定。到目前为止,我见过的最好的例子是 "A Systematic Evaluation of Transient Execution Attacks and Defenses"(参见 https://arxiv.org/pdf/1811.05441.pdf)。
然而,"hole in the dam wall" 不是可以轻易插入的东西,并且(对于随机猜测)我认为我们需要几年时间才能假设所有可能性都已被探索(而且我认为缓解需求永远不会消失)。
有时术语"BTB"被统称为分支预测单元使用的所有缓冲区。然而,实际上有多个缓冲区,所有这些缓冲区都在每个循环中用于进行目标和方向预测。特别地,BTB用于对直接分支进行预测,ITB(间接目标缓冲区)用于对除returns以外的间接分支进行预测,RSB用于对[=53=进行预测]. ITB 也称为 IBTB 或间接目标阵列。所有这些术语都由不同的供应商和研究人员使用。通常,BTB 用于在其他缓冲区未命中时对各种分支指令进行初始预测。但后来预测器了解了更多关于分支的信息,其他缓冲区开始发挥作用。如果同一个间接分支的多个动态实例都具有相同的目标,那么也可以使用 BTB 而不是 ITB。当同一个分支有多个目标并且专门设计用于处理此类分支时,ITB 会更加准确。请参阅:Branch prediction and the performance of interpreters — Don't trust folklore. The first Intel processor that implemented separate BTB and ITB structures is the Pentium M. All later Intel Core 处理器具有专用 ITB。
Spectre V1 漏洞基于使用攻击者程序训练 BTB,这样当受害者执行别名为同一 BTB 条目的分支时,处理器就会被诱骗推测性地执行指令(称为小工具)以泄露信息. Spectre V2 漏洞类似,但基于训练 ITB。这里的关键区别在于,在 V1 中,处理器错误预测了分支的 direction,而在 V2 中,处理器错误预测了分支的 target (并且,在有条件的间接分支的情况下,方向也是如此,因为我们希望采用它)。在解释、JIT 编译或使用动态多态性的程序中,可以有许多间接分支(returns 除外)。一个特定的间接分支可能永远不会去某个位置,但是通过错误训练预测器,它可以跳到我们想要的任何地方。正是由于这个原因,V2 非常强大;无论小工具在哪里,无论程序的意图控制流是什么,您都可以选择一个间接分支并使其推测性地跳转到小工具。
请注意,通常静态直接分支目标的线性地址在程序的整个生命周期内保持不变。只有一种情况可能不是这样:动态代码修改。所以至少在理论上,可以根据直接分支的 target 错误预测来开发 Spectre exploit。
关于LFB的回收,我不太明白你在说什么。当错过 L1D 的加载请求将数据接收到 LFB 中时,数据会立即转发到管道的旁路互连。需要有一种方法来确定哪个负载 uop 请求了此数据。返回的数据必须用负载的 uop ID 标记。 RS 中等待数据的微指令的来源表示为负载的微指令 ID。此外,需要将保存加载uop的ROB条目标记为已完成,以便可以将其退役,并且在pre-SnB中,需要将返回的数据写入ROB。如果在管道刷新时,LFB 中未完成的加载请求没有被取消,并且如果加载 uop ID 被其他一些 uop 重用,当数据到达时,它可能会被错误地转发到管道中当前的任何新 uops,从而破坏微架构状态。因此需要有一种方法来确保在任何情况下都不会发生这种情况。通过简单地将所有有效的 LFB 条目标记为 "cancelled",很有可能在管道刷新时取消未完成的加载请求和推测性 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满事件。我注意到一件事:外循环中每增加三个负载,L1D_PEND_MISS.FB_FULL
就会增加大约 20K,而不是预期的 10K。我认为正在发生的事情是,当第一次向 L1D 发出加载 uop 的加载请求并且所有 LFB 都在使用时,它会被拒绝。然后,当 LFB 可用时,加载缓冲区中待处理的两个加载被发送到 L1D,一个将在 LFB 中分配,另一个将被拒绝。因此,每次额外加载我们都会得到两个 LFB 完整事件。然而,当外循环中有三个负载时,只有第三个会等待 LFB,因此我们在每次外循环迭代中获得一个事件。本质上,加载缓冲器无法区分是有一个 LFB 可用还是有两个 LFB;它只会知道至少有一个 LFB 是空闲的,因此它会尝试同时发送两个加载请求,因为有两个加载端口。
感谢 Brendan 和 Hadi Brais,在阅读了您的答案并最终阅读了幽灵论文之后,现在我的想法哪里出错了,我有点混淆了两者。
我部分描述了 Spectre v1,它通过错误训练跳转的分支 history 导致边界检查绕过,即 if (x < array1_size)
到幽灵小工具。这显然不是间接分支。黑客通过调用包含具有合法参数的幽灵小工具的函数来启动分支预测器 (PHT+BHT),然后使用非法参数调用将 array1[x]
放入缓存中来实现此目的。然后他们通过提供合法参数重新启动分支历史记录,然后从缓存中刷新 array1_size
(我不确定他们是怎么做的,因为即使攻击者进程知道 array1_size
的 VA,该行也不能flushed 因为 TLB 包含进程的不同 PCID,所以它必须以某种方式被驱逐,即填充该虚拟地址的集合)。然后他们使用与之前相同的非法参数调用,因为 array1[x]
在缓存中但 array1_size
不在缓存中,array[x]
将快速解析并开始加载 array2[array1[x]]
,同时仍在等待在 array1_size
上,它根据超出 array1
边界的任何 x 处的秘密加载 array2
中的位置。然后,攻击者使用 x 的有效值调用函数并计算函数调用次数(我假设攻击者必须知道 array1
的内容,因为如果 array2[array1[8]]
导致更快的访问,他们需要知道什么是在 array1[8]
因为这是秘密,但肯定该数组必须包含每个 2^8 位组合)。
另一方面,Spectre v2 需要第二个攻击进程,该进程知道受害进程中 间接 分支的虚拟地址,以便它可以毒化 target 并将其替换为另一个地址。如果攻击过程包含一个跳转指令,该指令将驻留在 IBTB 中与受害者间接分支相同的集合、方式和标记中,那么它只训练该分支指令以预测采用并跳转到一个虚拟地址,该地址恰好是受害者进程中的小工具。当受害者进程遇到间接分支时,攻击程序的 错误 目标地址位于 IBTB 中。它是一个间接分支是至关重要的,因为通常在解码时检查由于进程切换而导致的错误,即如果分支目标与该 RIP 的 BTB 中的目标不同,那么它会刷新在它之前获取的指令。这不能用间接分支来完成,因为它直到执行阶段才知道目标,因此想法是选择的间接分支取决于需要从缓存中获取的值。然后它会跳转到这个目标地址,也就是小工具的地址,依此类推。
攻击者需要知道受害进程的源代码才能识别小工具,并且他们需要知道小工具所在的 VA。我假设这可以通过可预测地知道代码将被加载到哪里来完成。例如,我相信 .exes 通常加载到 x00400000,然后 PE header.
中有一个 BaseOfCode
编辑:我刚刚阅读了 spectre 论文的附录 B,它很好地 Windows 实现了 Spectre v2。
As a proof-of-concept, we constructed a simple target application which provides the service of computing a SHA1 hash of a key and an input message. This implementation consisted of a program which continuously runs a loop which calls Sleep(0), loads the input from a file, invokes the Windows cryptography functions to compute the hash, and prints the hash whenever the input changes. We found that the Sleep()
call is done with data from the input file in registers ebx, edi, and an attacker-known value for edx, i.e., the content of two registers is controlled by the attacker. This is the input criteria for the type of Spectre gadget described in the beginning of this section.
它使用 ntdll.dll
(充满本机 API 系统调用存根的 .dll)和 kernel32.dll
(Windows API),它们总是映射到用户虚拟地址 space 在 ASLR 的方向上(在 .dll 图像中指定),除了由于 copy-on-write 视图映射到页面缓存,物理地址可能是相同的。中毒的间接分支将在 kernel32.dll
中的 Windows API Sleep()
函数中,它似乎间接调用 ntdll.dll
中的 NtDelayExecution()
。然后攻击者确定间接分支指令的地址并将包含目标地址的受害者地址的页面映射到其自己的地址 space 并将存储在该地址的目标地址更改为他们识别的小工具的地址驻留在 ntdll.dll
中的同一个或另一个函数中的某个地方(我不完全确定(由于 ASLR)攻击者如何确定受害者进程映射 kernel32.dll
和 ntdll.dll
的位置在其地址 space 中,以便为受害者找到 Sleep()
中间接分支的地址。附录 B 声称他们使用 'Simple pointer operations' 来定位包含目标的间接分支和地址 - - 我不确定它是如何工作的)。然后以与受害者相同的亲和力启动线程(以便受害者和错误训练线程在同一物理内核上运行超线程),这些线程调用 Sleep()
自己间接训练它,在地址 space 的上下文中黑客进程现在将跳转到小工具的地址。小工具暂时换成了ret
,这样就可以顺利从Sleep()
returns了。这些线程还将在间接跳转之前执行一个序列,以模拟受害者的全局分支历史记录在遇到间接跳转之前,以充分确保该分支是在合金历史中采取的。然后启动一个单独的线程,并补充受害者的线程亲和力,反复驱逐受害者的包含跳转目的地的内存地址,以确保当受害者确实遇到间接分支时,它将需要很长时间的 RAM 访问来解决,从而允许在可以根据 BTB 条目检查分支目的地并刷新管道之前,小工具可以提前推测。在 JavaScript 中,逐出是通过加载到相同的缓存集(即 4096 的倍数)来完成的。误训练线程、逐出线程和受害线程都在 运行 并在此阶段循环。当受害进程循环调用 Sleep()
时,间接分支会根据黑客先前中毒的 IBTB 条目推测该小工具。探测线程与受害者进程线程关联的补充一起启动(以免干扰错误训练和受害者分支历史)。探测线程将修改受害进程使用的文件的 header,当 Sleep()
被调用时,这些值驻留在 ebx
和 edi
中,这意味着探测线程可以直接影响存储在 ebx
和 edi
中的值。示例中分支到的 spectre gadget 将存储在 [ebx+edx+13BE13BDh]
的值添加到 edi
,然后在存储在 edi
的地址加载一个值并将其与进位一起添加到 dl
.这允许探测线程学习存储在 [ebx+edx+13BE13BDh]
的值,就好像它选择了原始的 edi
0,然后在第二个操作中访问的值将从虚拟地址范围 0x0 – 0x255 加载,由此间接分支将解决但副作用已经存在的时间。攻击过程需要确保它已将相同的物理地址映射到其虚拟地址 space 中的相同位置,以便使用定时攻击来探测探测阵列。不确定它是如何做到这一点的,但在 windows 中,据我所知,它需要映射受害者在该位置打开的 page-file 支持部分 object 的视图。或者它会操纵受害者调用具有负 TC ebx
值的幽灵小工具,这样 ebx+edx+13BE13BDh
= 0
, =1
,..., =255
并以某种方式计算调用时间。这也可能通过使用 APC 注入来实现。
我已经阅读了一些有关 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
大于 data.size
(例如 1000);间接分支将被预测采用,它将跳转到缓存加载发生的 if 语句的主体。
如果语句包含secret = data[1000]
(包含秘密数据的特定内存地址(数据[1000])旨在从内存加载到缓存)那么这将被推测地分配到加载缓冲区。前面的间接分支仍在分支执行单元中等待完成。
我相信前提是在错误预测刷新加载缓冲区之前需要执行加载(分配行填充缓冲区)。如果它已经被分配了一个行填充缓冲区,那么什么也做不了。没有取消行填充缓冲区分配的机制是有道理的,因为行填充缓冲区在将其返回到加载缓冲区后存储到缓存之前必须挂起。这可能会导致行填充缓冲区变得饱和,因为而不是在需要时取消分配(将其保留在那里以提高其他加载到同一地址的速度,但在没有其他可用行缓冲区时取消分配)。它无法解除分配,直到它收到一些信号表明刷新不会 将要发生,这意味着它必须暂停以执行前一个分支而不是立即使行填充缓冲区可用于其他逻辑核心的存储。这种信号机制可能难以实现,也许他们没有想到(幽灵之前的想法),并且如果分支执行花费足够的时间来挂线填充缓冲区导致性能影响,它还会引入延迟,即如果data.size
在循环的最后一次迭代之前有意从缓存 (CLFLUSH
) 中清除,这意味着分支执行可能需要多达 100 个周期。
我希望我的想法是正确的,但我不是 100% 确定。如果有人有什么要补充或更正的,请补充。
对于分支,有些像 jc .somewhere
,其中 CPU 只需要真正猜测分支是否会被采用,就能够推测出猜测的路径。然而,有些分支像 jmp [table+eax*8]
可能有超过 40 亿个可能的方向,对于这些情况,CPU 需要猜测目标地址才能推测出猜测的路径。因为有非常不同类型的分支,CPU 使用非常不同类型的预测变量。
对于 Spectre,有一个 "meta pattern" - 攻击者使用推测执行来欺骗 CPU 将信息留在某物中,然后从某物中提取该信息。 "something" 有多种可能性(数据高速缓存、指令高速缓存、TLB、分支目标缓冲区、分支方向缓冲区、return 堆栈、写组合缓冲区,...),因此有许多可能的变体幽灵(而不仅仅是 2018 年初 public 制作的 "well known first two variations")。
对于 spectre v1(其中 "something" 是数据缓存),攻击者需要一些方法来欺骗 CPU 将数据放入数据缓存(例如加载然后第二次加载取决于第一次加载的值,可以推测地执行)和一些提取信息的方法(刷新缓存中的所有内容,然后使用加载所需的时间来确定数据缓存的状态如何更改) .
对于 spectre v2(其中 "something" 是用于 jc .somewhere
等指令的分支方向缓冲区),攻击者需要一些方法来欺骗 CPU 将数据放入分支方向缓冲区(例如加载,然后是依赖于负载的分支,可以推测地执行)和一些提取信息的方法(预先将分支方向缓冲区设置为已知状态,然后使用分支的时间量需要确定分支方向缓冲区的状态如何更改)。
对于幽灵的所有可能变体,唯一重要的(对于防御)是 "something" 可以是什么(以及如何防止信息进入 "something",或者flush/overwrite/destroy 进入 "something" 的信息。其他一切(攻击许多可能的幽灵变体中的任何一种的代码的许多可能实现之一的具体细节)都不重要。
幽灵的模糊历史
最初的 Spectre(v1,使用缓存时序)于 2017 年被发现,public于 2018 年 1 月才宣布。它就像一个大坝决堤,以及其他一些变体(例如 v2,使用分支预测)很快跟了上去。这些早期的变体吸引了很多 public 的关注。在大约 6 个月之后,发现了多个其他变体,但没有得到尽可能多的 public 关注度,而且很多人没有(现在仍然没有)意识到它们。到 2018 年 "latter half" 时,人们(例如我)开始忘记哪些变体已被证明(通过 "proof of concept" 实现),哪些仍未得到证实,一些研究人员开始尝试列举可能性并建立命名他们的约定。到目前为止,我见过的最好的例子是 "A Systematic Evaluation of Transient Execution Attacks and Defenses"(参见 https://arxiv.org/pdf/1811.05441.pdf)。
然而,"hole in the dam wall" 不是可以轻易插入的东西,并且(对于随机猜测)我认为我们需要几年时间才能假设所有可能性都已被探索(而且我认为缓解需求永远不会消失)。
有时术语"BTB"被统称为分支预测单元使用的所有缓冲区。然而,实际上有多个缓冲区,所有这些缓冲区都在每个循环中用于进行目标和方向预测。特别地,BTB用于对直接分支进行预测,ITB(间接目标缓冲区)用于对除returns以外的间接分支进行预测,RSB用于对[=53=进行预测]. ITB 也称为 IBTB 或间接目标阵列。所有这些术语都由不同的供应商和研究人员使用。通常,BTB 用于在其他缓冲区未命中时对各种分支指令进行初始预测。但后来预测器了解了更多关于分支的信息,其他缓冲区开始发挥作用。如果同一个间接分支的多个动态实例都具有相同的目标,那么也可以使用 BTB 而不是 ITB。当同一个分支有多个目标并且专门设计用于处理此类分支时,ITB 会更加准确。请参阅:Branch prediction and the performance of interpreters — Don't trust folklore. The first Intel processor that implemented separate BTB and ITB structures is the Pentium M. All later Intel Core 处理器具有专用 ITB。
Spectre V1 漏洞基于使用攻击者程序训练 BTB,这样当受害者执行别名为同一 BTB 条目的分支时,处理器就会被诱骗推测性地执行指令(称为小工具)以泄露信息. Spectre V2 漏洞类似,但基于训练 ITB。这里的关键区别在于,在 V1 中,处理器错误预测了分支的 direction,而在 V2 中,处理器错误预测了分支的 target (并且,在有条件的间接分支的情况下,方向也是如此,因为我们希望采用它)。在解释、JIT 编译或使用动态多态性的程序中,可以有许多间接分支(returns 除外)。一个特定的间接分支可能永远不会去某个位置,但是通过错误训练预测器,它可以跳到我们想要的任何地方。正是由于这个原因,V2 非常强大;无论小工具在哪里,无论程序的意图控制流是什么,您都可以选择一个间接分支并使其推测性地跳转到小工具。
请注意,通常静态直接分支目标的线性地址在程序的整个生命周期内保持不变。只有一种情况可能不是这样:动态代码修改。所以至少在理论上,可以根据直接分支的 target 错误预测来开发 Spectre exploit。
关于LFB的回收,我不太明白你在说什么。当错过 L1D 的加载请求将数据接收到 LFB 中时,数据会立即转发到管道的旁路互连。需要有一种方法来确定哪个负载 uop 请求了此数据。返回的数据必须用负载的 uop ID 标记。 RS 中等待数据的微指令的来源表示为负载的微指令 ID。此外,需要将保存加载uop的ROB条目标记为已完成,以便可以将其退役,并且在pre-SnB中,需要将返回的数据写入ROB。如果在管道刷新时,LFB 中未完成的加载请求没有被取消,并且如果加载 uop ID 被其他一些 uop 重用,当数据到达时,它可能会被错误地转发到管道中当前的任何新 uops,从而破坏微架构状态。因此需要有一种方法来确保在任何情况下都不会发生这种情况。通过简单地将所有有效的 LFB 条目标记为 "cancelled",很有可能在管道刷新时取消未完成的加载请求和推测性 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满事件。我注意到一件事:外循环中每增加三个负载,L1D_PEND_MISS.FB_FULL
就会增加大约 20K,而不是预期的 10K。我认为正在发生的事情是,当第一次向 L1D 发出加载 uop 的加载请求并且所有 LFB 都在使用时,它会被拒绝。然后,当 LFB 可用时,加载缓冲区中待处理的两个加载被发送到 L1D,一个将在 LFB 中分配,另一个将被拒绝。因此,每次额外加载我们都会得到两个 LFB 完整事件。然而,当外循环中有三个负载时,只有第三个会等待 LFB,因此我们在每次外循环迭代中获得一个事件。本质上,加载缓冲器无法区分是有一个 LFB 可用还是有两个 LFB;它只会知道至少有一个 LFB 是空闲的,因此它会尝试同时发送两个加载请求,因为有两个加载端口。
感谢 Brendan 和 Hadi Brais,在阅读了您的答案并最终阅读了幽灵论文之后,现在我的想法哪里出错了,我有点混淆了两者。
我部分描述了 Spectre v1,它通过错误训练跳转的分支 history 导致边界检查绕过,即 if (x < array1_size)
到幽灵小工具。这显然不是间接分支。黑客通过调用包含具有合法参数的幽灵小工具的函数来启动分支预测器 (PHT+BHT),然后使用非法参数调用将 array1[x]
放入缓存中来实现此目的。然后他们通过提供合法参数重新启动分支历史记录,然后从缓存中刷新 array1_size
(我不确定他们是怎么做的,因为即使攻击者进程知道 array1_size
的 VA,该行也不能flushed 因为 TLB 包含进程的不同 PCID,所以它必须以某种方式被驱逐,即填充该虚拟地址的集合)。然后他们使用与之前相同的非法参数调用,因为 array1[x]
在缓存中但 array1_size
不在缓存中,array[x]
将快速解析并开始加载 array2[array1[x]]
,同时仍在等待在 array1_size
上,它根据超出 array1
边界的任何 x 处的秘密加载 array2
中的位置。然后,攻击者使用 x 的有效值调用函数并计算函数调用次数(我假设攻击者必须知道 array1
的内容,因为如果 array2[array1[8]]
导致更快的访问,他们需要知道什么是在 array1[8]
因为这是秘密,但肯定该数组必须包含每个 2^8 位组合)。
另一方面,Spectre v2 需要第二个攻击进程,该进程知道受害进程中 间接 分支的虚拟地址,以便它可以毒化 target 并将其替换为另一个地址。如果攻击过程包含一个跳转指令,该指令将驻留在 IBTB 中与受害者间接分支相同的集合、方式和标记中,那么它只训练该分支指令以预测采用并跳转到一个虚拟地址,该地址恰好是受害者进程中的小工具。当受害者进程遇到间接分支时,攻击程序的 错误 目标地址位于 IBTB 中。它是一个间接分支是至关重要的,因为通常在解码时检查由于进程切换而导致的错误,即如果分支目标与该 RIP 的 BTB 中的目标不同,那么它会刷新在它之前获取的指令。这不能用间接分支来完成,因为它直到执行阶段才知道目标,因此想法是选择的间接分支取决于需要从缓存中获取的值。然后它会跳转到这个目标地址,也就是小工具的地址,依此类推。
攻击者需要知道受害进程的源代码才能识别小工具,并且他们需要知道小工具所在的 VA。我假设这可以通过可预测地知道代码将被加载到哪里来完成。例如,我相信 .exes 通常加载到 x00400000,然后 PE header.
中有一个 BaseOfCode编辑:我刚刚阅读了 spectre 论文的附录 B,它很好地 Windows 实现了 Spectre v2。
As a proof-of-concept, we constructed a simple target application which provides the service of computing a SHA1 hash of a key and an input message. This implementation consisted of a program which continuously runs a loop which calls Sleep(0), loads the input from a file, invokes the Windows cryptography functions to compute the hash, and prints the hash whenever the input changes. We found that the
Sleep()
call is done with data from the input file in registers ebx, edi, and an attacker-known value for edx, i.e., the content of two registers is controlled by the attacker. This is the input criteria for the type of Spectre gadget described in the beginning of this section.
它使用 ntdll.dll
(充满本机 API 系统调用存根的 .dll)和 kernel32.dll
(Windows API),它们总是映射到用户虚拟地址 space 在 ASLR 的方向上(在 .dll 图像中指定),除了由于 copy-on-write 视图映射到页面缓存,物理地址可能是相同的。中毒的间接分支将在 kernel32.dll
中的 Windows API Sleep()
函数中,它似乎间接调用 ntdll.dll
中的 NtDelayExecution()
。然后攻击者确定间接分支指令的地址并将包含目标地址的受害者地址的页面映射到其自己的地址 space 并将存储在该地址的目标地址更改为他们识别的小工具的地址驻留在 ntdll.dll
中的同一个或另一个函数中的某个地方(我不完全确定(由于 ASLR)攻击者如何确定受害者进程映射 kernel32.dll
和 ntdll.dll
的位置在其地址 space 中,以便为受害者找到 Sleep()
中间接分支的地址。附录 B 声称他们使用 'Simple pointer operations' 来定位包含目标的间接分支和地址 - - 我不确定它是如何工作的)。然后以与受害者相同的亲和力启动线程(以便受害者和错误训练线程在同一物理内核上运行超线程),这些线程调用 Sleep()
自己间接训练它,在地址 space 的上下文中黑客进程现在将跳转到小工具的地址。小工具暂时换成了ret
,这样就可以顺利从Sleep()
returns了。这些线程还将在间接跳转之前执行一个序列,以模拟受害者的全局分支历史记录在遇到间接跳转之前,以充分确保该分支是在合金历史中采取的。然后启动一个单独的线程,并补充受害者的线程亲和力,反复驱逐受害者的包含跳转目的地的内存地址,以确保当受害者确实遇到间接分支时,它将需要很长时间的 RAM 访问来解决,从而允许在可以根据 BTB 条目检查分支目的地并刷新管道之前,小工具可以提前推测。在 JavaScript 中,逐出是通过加载到相同的缓存集(即 4096 的倍数)来完成的。误训练线程、逐出线程和受害线程都在 运行 并在此阶段循环。当受害进程循环调用 Sleep()
时,间接分支会根据黑客先前中毒的 IBTB 条目推测该小工具。探测线程与受害者进程线程关联的补充一起启动(以免干扰错误训练和受害者分支历史)。探测线程将修改受害进程使用的文件的 header,当 Sleep()
被调用时,这些值驻留在 ebx
和 edi
中,这意味着探测线程可以直接影响存储在 ebx
和 edi
中的值。示例中分支到的 spectre gadget 将存储在 [ebx+edx+13BE13BDh]
的值添加到 edi
,然后在存储在 edi
的地址加载一个值并将其与进位一起添加到 dl
.这允许探测线程学习存储在 [ebx+edx+13BE13BDh]
的值,就好像它选择了原始的 edi
0,然后在第二个操作中访问的值将从虚拟地址范围 0x0 – 0x255 加载,由此间接分支将解决但副作用已经存在的时间。攻击过程需要确保它已将相同的物理地址映射到其虚拟地址 space 中的相同位置,以便使用定时攻击来探测探测阵列。不确定它是如何做到这一点的,但在 windows 中,据我所知,它需要映射受害者在该位置打开的 page-file 支持部分 object 的视图。或者它会操纵受害者调用具有负 TC ebx
值的幽灵小工具,这样 ebx+edx+13BE13BDh
= 0
, =1
,..., =255
并以某种方式计算调用时间。这也可能通过使用 APC 注入来实现。