MIT 6.1810 · 第 23 课 · rtm
Meltdown 攻击
Meltdown 攻击利用 CPU 的推测执行和缓存侧信道,允许恶意用户代码读取内核内存,绕过页保护。
安全是操作系统的顶级目标
- 恶意用户代码是攻击源之一
- 内核的主要策略:隔离
- 如果一切都设置正确,什么会出错?
为什么研究 Meltdown?
- 允许恶意用户代码读取内核内存,绕过页保护
- 令人惊讶和不安
- 是最近一系列"微架构"攻击之一
- 利用 CPU 隐藏的实现对细节
- 可修复,但人们担心会有更多相关惊喜
攻击核心
// 用户代码
char buf[8192]
r1 = <一个内核虚拟地址>
r2 = *r1 // ← 这会 page fault
r2 = r2 & 1
r2 = r2 * 4096
r3 = *(buf + r2) // ← 推测执行!
如果用户代码尝试使用内核地址会怎样?
加载会导致 page fault。但这只是故事的一部分...
前提假设
内核映射在每个用户页表中,PTE_U 清除。
- 用户内存从零开始;内核在高地址
- 在这些攻击发现之前几乎普遍如此
- 映射使用户和内核使系统调用更快
- 要点:*r1 是有意义的,即使被禁止
微架构特性 1:推测执行 (Speculative Execution)
示例
r0 = <某个地址>
r1 = load x // r1 是寄存器;x 是 RAM 中的变量
if(r1 == 1){
r2 = *r0
r3 = r2 + 1
} else {
r3 = 0
}
- "r1 = x" 需要数百个周期(如果 x 必须从 RAM 获取)
- "if(r1 == 1)" 需要 RAM 内容
- CPU 预测分支可能走向,并继续执行——这称为"推测"
如果 CPU 预测错误?
- 推测指令的寄存器更新是临时的,直到提交
- 如果 CPU 意识到分支预测错误,丢弃临时更新
- 重新开始执行,在 "else" 分支
推测执行帮助性能
避免 CPU 等待慢内存时的停顿。
关键洞察
- CPU 只在确定指令不会被取消后才"退役"指令
- CPU 按顺序退役指令——只有在前一条指令退役后才知道没有前一条指令 fault
- 因此,推测执行的指令的 fault 可能在指令完成后很久才发生
推测是透明的
- 推测是 CPU 实现的一部分,但在规范中不可见
- 推测旨在提高性能而不改变程序计算的结果
- CPU 通过以下方式使推测透明:
- 如果意识到推测错误,撤销寄存器赋值
- 不引发错误推测指令的异常
微架构特性 2:CPU 数据和 TLB 缓存
核心
L1:va,pa | data / TLB:va | pa (VIPT)
L2:pa | data
RAM
| 操作 | 时间 |
| L1 命中 | 几周期 |
| L2 命中 | ~20 周期 |
| RAM | ~300 周期 |
| TLB 未命中 | 数百周期 |
系统调用在内核返回后在 L1 缓存中留下内核数据
为什么 L1 包含内核数据时执行用户代码是安全的?
实际上,微架构并非完全不可见
- 它影响指令和程序花费多长时间
- 对编写性能关键代码的人非常有兴趣
- Intel 等发布优化指南
有用的技巧:感知某物是否在缓存中
Flush+Reload
想知道函数 f() 是否使用了地址 Z 的内存?
- 确保 Z 不在缓存中(clflush 指令)
- 调用 f()
- 记录时间(rdtsc 指令)
- 从 Z 加载一个字节
- 再次记录时间
- 如果时间差 < ~50,加载命中 → f() 可能使用了 Z
Meltdown 详细分析
// 在用户内存中
char buf[8192]
// Flush+Reload 的 Flush 部分
clflush buf[0]
clflush buf[4096]
1. r1 = <一个内核虚拟地址>
2. r2 = *r1 // ← page fault(延迟)
3. r2 = r2 & 1 // 推测执行
4. r2 = r2 * 4096 // 推测执行
5. r3 = *(buf+r2) // 推测执行——加载 buf[0] 或 buf[4096]
<最终来自 *r1 的 fault;r2 和 r3 回滚,但数据留在缓存中>
<处理 page fault>
// Flush+Reload 的 Reload 部分
a = rdtsc
r0 = buf[0]
b = rdtsc
r1 = buf[4096]
c = rdtsc
if c-b < b-a:
低位可能是 1
关键洞察
- "r2 = *r1" 的 fault 被延迟到加载退役,给后续推测执行指令时间执行
- "r2 = *r1" 确实做了加载,即使 PTE 禁止,将结果放入 r2(临时的)
- "r3 = buf[r2]" 将 buf[] 的一部分加载到缓存中,即使 r3 的更改因推测错误被取消
为什么攻击常常不工作?
- 期望的内核数据不在缓存中?
- 加载退役并在 RAM 获取完成前 fault?
- 缓存冲突踢出数组?
- TLB 未命中?
- 机器上的其他活动?
- fault 退役和数据返回之间的竞争?
第 6.2 节:如果内核数据未缓存,每秒 10 字节。
为什么攻击者觉得有用(即使通常不工作)?
安全游戏的规则:
- 防御必须 100% 获胜
- 如果攻击者 1% 的时间获胜,对防御是灾难
- 例如:攻击者只能窃取 1% 用户的密码
Meltdown 在实际攻击中如何使用?
- 分时:内核可能有其他用户的秘密(密码、密钥)
- 云:某些容器和 VMM 系统可能易受攻击
- 浏览器:在沙箱中运行不受信任的代码,可能窃取密码
防御
软件修复
KAISER / KPTI:不要在用户页表中映射内核
- 需要在每次系统调用入口/出口切换页表
- 这是 RISC-V xv6 的工作方式
- 页表切换可能很慢——可能需要 TLB 刷新
- PCID 可以避免 TLB 刷新,但仍有一些开销
硬件修复
推测加载只返回允许的数据!
- 如果 PTE_U/R/V 清除,返回零而不是实际数据
- 可能成本很小,因为 CPU 必须查看 PTE
- AMD CPU 似乎一直这样工作
- 现代 Intel CPU 似乎这样做(称为 RDCL_NO)
这些防御已部署并被认为有效,但...
- 令人不安的是,页保护被证明不牢固!
- 更多微架构惊喜不断出现
- 底层问题只是可修复的 bug?还是策略错误?
- 请继续关注,这仍在发展中
要点总结
推测执行是攻击的基础
CPU 推测执行指令,fault 被延迟,但微架构状态(缓存)保留。
缓存是侧信道
Flush+Reload 技巧通过测量加载时间推断缓存内容。
页保护被绕过
推测加载忽略 PTE 权限,将内核数据放入临时寄存器。
防御需要软硬件结合
KPTI 隔离页表,硬件修复阻止推测加载返回禁止数据。
测验
Q1:Meltdown 攻击如何利用推测执行?
Q2:Flush+Reload 技巧如何工作?
Q3:KPTI(内核页表隔离)如何防御 Meltdown?
延伸阅读