Spectre-V4-SSB漏洞复现分析
1 漏洞原理
内存消歧器的错误预测
Spectre-v4的核心并非传统意义上的软件“缺陷”,而是对现代高性能处理器中一项关键性能优化机制的恶意利用 。这个机制被称为
内存消歧(Memory Disambiguation),其目的是为了打破内存读写操作之间的严格顺序,从而提升乱序执行(Out-of-Order Execution)的效率 。
- 存储缓冲区与加载延迟 在现代CPU中,
store
(存储/写入)指令不会立即将数据写入主内存,而是先放入一个高速的“存储缓冲区”(Store Buffer)中 。这样做可以避免CPU因等待缓慢的内存写入而停顿。这也带来一个问题:如果一条load
(加载/读取)指令需要读取的地址,恰好是存储缓冲区中某条尚未完成的store
指令的目标地址,那么load
必须等待,以确保能读到最新的值,而不是内存中的旧数据 。 - 内存消歧器 (Memory Disambiguator) 为了避免不必要的等待,CPU引入了一个预测单元,即内存消歧器。它的工作是预测一条
load
指令是否与它前面任何地址尚未完全解析的store
指令存在依赖关系(即读写同一地址)。- 如果预测“不相关”:CPU会允许
load
指令“绕过”待定的store
指令,提前进行推测性执行,直接从L1缓存中读取数据。这正是“推测性存储绕过”(Speculative Store Bypass)这一优化的本质 。 - 如果预测“相关”:
load
指令则会等待,直到前面的store
指令地址确定。
- 如果预测“不相关”:CPU会允许
- 漏洞触发:一次可被操纵的错误预测,SSB漏洞的根源在于,内存消歧器的预测并非总是正确,并且其行为可以被攻击者诱导 。当CPU错误地预测
load
与store
不相关,但实际上它们操作的是同一地址时,漏洞就被触发了。 这会导致load
指令在store
指令更新内存之前,就推测性地执行,结果是加载到了该内存地址中陈旧的(stale)数据。
尽管CPU最终会发现这次预测错误,并丢弃所有基于错误数据推测执行的结果(这个过程称为“瞬态执行”),但在这个短暂的执行窗口内,对陈旧数据的操作已经对CPU的微架构状态(如缓存)造成了不可逆转的、可被观测的副作用 。这正是攻击者窃取信息的物理基础。
攻击原理和利用方式
攻击者利用SSB漏洞构建一个完整的信息泄露攻击链,通常遵循以下精确的步骤 :
执行“慢速存储” (Slow Store)
攻击者精心构造一次store
操作,使其目标地址的计算变得缓慢或高延迟。这为后续的load
指令创造了一个可以进行错误推测的时间窗口。实现高延迟的方式通常有:
- 让地址计算依赖于一次必然的缓存未命中。
- 执行一连串复杂的、相互依赖的算术运算。
执行“快速加载” (Fast Load)
攻击者执行一次从完全相同内存位置的load
操作。load
指令的地址可以被快速计算出来。由于store
的地址尚未解析,而load
的地址已就绪,内存消歧器很可能会做出错误的预测,认为二者不相关,从而触发推测性存储绕过 。
利用陈旧数据与Disclosure Gadget
由于SSB的发生,load
指令读取到的是该内存地址在被store
更新之前的陈旧值。这个陈旧值(即秘密信息)在短暂的瞬态执行窗口内,被用作后续指令的输入。这些后续指令被称为“泄露小工具”,其作用是将秘密信息编码到CPU的缓存状态中。
一个经典的泄露小工具如下:
- 攻击者预先准备一个大型数组,称为“探测数组”(Probe Array),并确保其所有内容都不在CPU缓存中(通常使用
clflush
指令)。 - 在瞬态执行期间,将加载到的陈旧值(
stale_secret_value
)用作探测数组的索引,进行一次内存访问,例如probe_array[stale_secret_value * cache_line_size]
。 - 这次内存访问虽然是推测性的,但它会产生一个物理后果:探测数组中对应于秘密值索引的那个缓存行,会被加载到CPU的L1缓存中 。
通过侧信道恢复秘密
当CPU检测到预测错误并回滚状态后,从程序逻辑上看,对探测数组的访问从未发生过。然而,CPU缓存的状态已经被永久改变了。攻击者此时只需:
- 遍历整个探测数组,测量访问其中每一个元素所需的时间。
- 绝大多数元素的访问会很慢(缓存未命中),但只有一个元素的访问会极快(缓存命中)。
- 这个快速访问的元素的索引,就直接暴露了
stale_secret_value
的值 。通过重复这个过程,攻击者可以逐字节地读出内存中的敏感信息。
推测性类型混淆 (Speculative Type Confusion)
Google Project Zero的研究人员展示了一种尤为强大的SSB攻击技术,称为“推测性类型混淆” 。其逻辑如下:
- 攻击者首先将一个已知的、安全的整数
N
存储到栈上的地址A
。这个整数N
现在是地址A
的“陈旧数据”。 - 然后,攻击者执行一次“慢速存储”,将一个指针
P
(指向真正的机密数据)写入到同一个地址A
。 - 接着,执行一次“快速加载”,从地址
A
读取数据。由于SSB,这次加载操作推测性地读取到了陈旧的整数N
。 - 在泄露小工具中,程序代码原本期望从地址
A
读出的是一个指针,并会对其进行解引用操作。在瞬态执行期间,这段代码会错误地将整数N
当作一个指针来使用,并尝试访问内存地址*N
。 - 这次对地址
N
的推测性访问,会将该地址的内容加载到缓存中,攻击者随后即可通过计时攻击探测到该内容,从而泄露信息 。
2 poc 解析
关键数据结构
1 |
|
受害者函数 (victim_function)
1 |
|
关键点分析:
- 第2行:存储操作,试图将
memory_slot
指向public_key
- 第3行:加载操作,使用
memory_slot[idx]
作为索引访问probe
数组 - 漏洞触发:在推测执行中,第3行可能在第2行完成之前执行,导致使用旧的
memory_slot
值(指向secret_key
)
攻击者函数 (attacker_function)
1 |
|
1. 设置阶段
1 |
|
2. 缓存清理
1 |
|
目的:
- 确保后续的内存访问会产生可测量的时间差异
- 为侧信道攻击创造条件
3. 触发推测执行
1 |
|
推测执行过程:
- 处理器开始执行
victim_function
- 遇到存储操作:
*memory_slot_slow_ptr = public_key
- 推测性地执行加载操作,使用旧的
memory_slot
值(指向secret_key
) - 访问
probe[secret_key[idx] * 4096]
,将对应缓存行加·载到缓存中 - 推测执行被回滚,但缓存状态已被改变
4. 侧信道检测
1 |
|
检测原理:
- 遍历所有可能的字节值 (0-255)
- 测量访问
probe[i * 4096]
的时间 - 如果访问时间很短(缓存命中),说明该位置在推测执行中被访问过
- 排除
public_key[idx]
对应的值,因为正常执行也会访问它
3 漏洞复现
- i7-8700
