跳转到内容

Spectre — CPU 猜错路时也会泄密

待复核

日常类比:餐厅服务员为了快,客人还没完全点完就先猜一道菜送去厨房;如果猜错了,菜不会上桌,但厨房已经用过的锅、油烟和排队痕迹还留在那里。

Spectre 讲的就是这件事在 CPU 里的版本:现代处理器会为了快而提前猜程序下一步要走哪条路,这叫推测执行。如果猜错,寄存器这些“正式账本”会回滚;但缓存、分支预测器这类“厨房痕迹”可能不会一起抹掉。

攻击者不需要找到传统软件漏洞,而是诱导受害程序在推测阶段读到本来不该被攻击者知道的数据,再用缓存访问速度把这个数据侧面测出来。它击穿的是一个老假设:只要程序最后结果正确,中间临时做过的事就无害。

不理解 Spectre,下面这些事都没法解释:

  • 为什么一个普通数组边界检查写对了,仍然可能在 CPU 推测窗口里泄漏秘密
  • 为什么浏览器、内核、虚拟机和 JIT 都要同时修补,而不是只改某个应用
  • 为什么“把错误执行回滚”不等于“把所有硬件痕迹清干净”
  • 为什么性能优化会变成安全边界:越会猜、越会缓存,攻击面也越复杂

Spectre 可以拆成 三步

  1. 训练 CPU 猜错:攻击者先喂很多正常输入,让分支预测器形成习惯。类比:每天都从 A 门进楼,保安下意识先开 A 门。

  2. 在临时路线里碰到秘密:随后攻击者给一个恶意输入,让 CPU 还没等检查结果出来,就按旧习惯先执行越界路径。正式状态会回滚,但这段“临时执行”已经拿秘密参与过计算。

  3. 从缓存痕迹读答案:秘密值被编码成某个缓存行是否变快。类比:不知道厨房做了哪道菜,但闻到哪口锅热过,就能反推菜单。

这篇论文最重要的洞见是:安全边界不能只看指令集承诺的最终状态,还要看微架构留下的可观察痕迹。

换句话说,Spectre 把“硬件实现细节”从性能课题推到了安全课题中心。

案例 1:边界检查为什么会被绕过

Section titled “案例 1:边界检查为什么会被绕过”
if (x < len) {
y = probe[array[x] * 4096];
}

逐部分解释

  • x < len 是程序员写的安全门,正常执行时它会挡住越界读取
  • array[x] 如果在推测阶段先跑,可能读到数组外的秘密字节
  • probe[secret * 4096] 把秘密值变成 256 个缓存行里的某一个被访问过
  • 4096 不是魔法安全数,只是把不同字节值隔开,减少缓存预取和同一缓存行干扰

案例 2:攻击者怎么从缓存里“听见”秘密

Section titled “案例 2:攻击者怎么从缓存里“听见”秘密”
for (let i = 0; i < 256; i++) {
const slowOrFast = timeRead(probe[i * 4096])
if (slowOrFast === "fast") guess = i
}

逐部分解释

  • 攻击者不直接读取 secret,而是测 probe 哪一格读得快
  • 读得快通常意味着那一格刚被 CPU 放进缓存
  • 如果 probe[83 * 4096] 最快,攻击者就猜秘密字节是 83
  • 论文里的 JavaScript 版本没有 clflush,所以用缓存驱逐和高精度计时替代
if (x < len) {
lfence(); // 等检查真正完成
y = array[x];
}

逐部分解释

  • lfence 让后面的读操作别抢跑,像“等审批盖章再进门”
  • 好处是缩小推测窗口,避免秘密在临时路径里被拿去编码
  • 代价是 CPU 少了提前工作的机会,热点路径会变慢
  • 所以真实系统常用静态分析、进程隔离、retpoline、IBRS / STIBP / IBPB 等组合拳
  1. 以为边界检查写对就安全:Spectre 利用的是检查结果出来前的短暂推测窗口,正式控制流正确也挡不住临时路径。

  2. 以为回滚会清掉所有痕迹:CPU 回滚的是寄存器和程序可见状态,缓存、预测器、总线竞争这些微架构状态可能仍可被测量。

  3. 把 Spectre 和 Meltdown 混成一类:Meltdown 更像权限检查失效读内核,Spectre 更像诱导受害程序用自己的权限读自己的秘密再泄漏。

  4. 以为降计时精度就彻底解决:粗计时会让攻击变慢、噪声变大,但论文强调可观察通道很多,缓存并不是唯一出口。

适用

  • 理解现代 CPU 优化如何改变安全威胁模型
  • 审视浏览器 JIT、解释器、内核 eBPF、虚拟机这类“运行不可信代码”的系统
  • 设计缓存侧信道、分支预测、推测执行相关的缓解策略
  • 分析“性能优化”和“隔离边界”互相打架的工程取舍

不适用

  • 不适合把它当成通用漏洞扫描清单,具体 CPU、编译器和补丁状态差异很大
  • 不适合只靠应用层代码证明系统安全,因为硬件和微码也参与了威胁面
  • 不适合用来替代厂商补丁、内核配置和浏览器隔离策略
  • 不适合初学者直接复现实验;论文里的 PoC 涉及计时、缓存和平台细节
  • 1996 年:Paul Kocher 的计时攻击提醒大家,密码算法的运行时间也会泄露秘密。
  • 2014-2017 年:Flush+Reload、Rowhammer、SGX cache attack 等工作不断证明,缓存和内存系统不是安静的背景板。
  • 2017 年:多个团队独立发现推测执行问题,并和 CPU、浏览器、操作系统厂商做协同披露。
  • 2018 年:Spectre / Meltdown 公开,行业紧急上浏览器计时降精度、内核隔离、微码和编译器缓解。
  • 2019 年:IEEE S&P 论文正式发表,把 Spectre 从单个漏洞整理成一类微架构攻击方法。
  1. “程序结果正确”不等于“执行过程不泄密”:安全分析必须把临时执行和硬件痕迹也算进去。
  2. Spectre 的核心不是越界本身,而是“猜错 + 侧信道”组合:没有可观察通道,秘密很难被带出来。
  3. 隔离是分层契约:语言、操作系统、浏览器、CPU 如果对“什么会泄露”理解不同,就会出现缝隙。
  4. 短期补丁多是降风险,长期答案要改硬件契约:论文最后强调 ISA 和微架构需要更清楚地定义可泄露状态。
  • 论文 PDF:Spectre Attacks: Exploiting Speculative Execution
  • meltdown-2018 —— 和 Spectre 同期爆发,但攻击点更偏权限检查和乱序执行
  • foreshadow-2018 —— 把瞬态执行攻击推进到 SGX enclave 场景
  • sgxpectre-2019 —— 把 Spectre 思路推进到 Intel SGX enclave:飞地内推测执行同样可能泄密
  • dawg-2018 —— 从硬件缓存隔离角度防御推测执行侧信道
  • flush-reload-2014 —— Spectre 常用的缓存读数工具,负责把“痕迹”变成“字节”
  • moesi-cache-coherence-1986 —— Spectre 读缓存痕迹,理解缓存一致性有助于理解硬件状态为什么会留下脚印
  • gpu-cache-coherence-2013 —— 同样讨论缓存设计,只是舞台从 CPU 安全换到 GPU/异构架构
  • rowhammer-2014 —— 都说明硬件优化的物理副作用可以越过软件抽象层
  • meltdown-2018 —— 同属瞬态执行攻击,常和 Spectre 一起构成 2018 年补丁风暴
  • tor-2004 —— Tor 关心网络流量侧信道,Spectre 关心 CPU 内部侧信道,都是“看不到正文也能猜秘密”
  • libsignal —— 加密协议能保护传输内容,但端点内存里的秘密仍要面对本机侧信道
  • costan-sgx-explained-2016 —— Intel SGX Explained — 把云主机里的一小块程序锁进硬件保险箱
  • flush-reload-2014 —— FLUSH+RELOAD 2014 — 用缓存时间偷看程序访问了哪行内存
  • foreshadow-2018 —— Foreshadow 2018 — SGX 保险箱也挡不住瞬态执行脚印
  • kim-rowhammer-2014 —— RowHammer 2014 — 反复读一行内存也能翻转邻居比特
  • sanctum-2016 —— Sanctum 2016 — 用少量硬件改动做强隔离 enclave