跳转到内容

Meltdown — 从用户态读到内核内存的硬件漏洞

待复核

日常类比:操作系统像一栋公寓,用户程序只能进自己的房间,内核房间有门禁;Meltdown 发现有些 CPU 在”确认门禁之前”,会先把房间里的纸条拿出来看一眼,虽然马上又说”不准进”,但纸条已经在走廊留下了脚印。

Meltdown 是一类瞬态执行 + 缓存侧信道攻击。攻击者没有内核权限,也不利用操作系统 bug,却能在脆弱 CPU 上从用户态推断内核地址里的字节。

这篇论文的重要定位不是”又一个提权技巧”,而是把大家对硬件安全的信任边界往下推了一层:软件权限检查通过了,不代表微架构中间状态没有泄露。

读这篇时要把两种”看见”分开:

  • 程序语义看不见:异常会撤销寄存器和内存结果
  • 计时观察看得见:缓存命中/未命中的差异还留在那里

一句话记住:Meltdown 让”本来不该被执行的几条指令”短暂执行,用缓存把秘密带出来。

不理解 Meltdown,下面这些事很难解释:

  • 为什么一个没有软件漏洞的系统,也可能因为 CPU 优化而泄露内核内存
  • 为什么 Linux KPTI、Windows KVA Shadow、Apple Double Map 这类补丁会突然成为全行业事件
  • 为什么容器隔离不等于硬件级隔离:共享同一个内核时,边界会一起变薄
  • 为什么 Spectre / Meltdown 之后,安全研究开始系统性追问”性能优化会不会改变可观察状态”

Meltdown 可以拆成 三步

  1. 先拿到不该拿的值:CPU 为了快,会乱序执行后面的指令。类比:收银员先把商品扫了,再发现会员卡无效;账单取消了,但扫码枪已经响过。

  2. 把秘密写进缓存脚印:秘密字节不能直接留在寄存器里,但可以决定访问 probe array 的哪一页。类比:不能把答案喊出来,就去 256 个门牌中的一个门口踩一脚。

  3. 用计时把脚印读回来:攻击者测哪一页访问更快,就知道哪一页已经进了缓存。类比:看哪个门口地毯还热,就知道刚才有人站过。

论文最狠的地方在于:它不是读一个固定 secret,而是重复这三步,按地址扫描,就能把内核映射中的大量内存转成字节流。

案例 1:为什么”不会执行”不等于”没有痕迹”

Section titled “案例 1:为什么”不会执行”不等于”没有痕迹””
raise_exception()
// 按正常控制流,这一行不该生效
touch(probe_array[data * 4096])

逐部分解释

  • raise_exception() 代表会触发异常的前置指令,正常语义下程序应跳进内核异常处理
  • 后一行用 data 选择 probe array 中很远的一页,避免硬件预取器把相邻页也带进缓存
  • 乱序执行让后一行可能先跑一小段;架构状态会被撤销,缓存状态却可能留下

案例 2:把一个字节变成 256 个缓存位置之一

Section titled “案例 2:把一个字节变成 256 个缓存位置之一”
secret = transient_read(kernel_address)
index = secret * 4096
touch(probe_array[index])

逐部分解释

  • transient_read 不是正常读取成功,而是在异常退休前的短窗口中让后续指令依赖该值
  • secret * 4096 把 0 到 255 的字节映射到 256 个页面,减少相邻缓存线造成的误判
  • touch 只负责制造缓存痕迹,不需要把 secret 写回用户可见寄存器

案例 3:用时间判断哪一页被碰过

Section titled “案例 3:用时间判断哪一页被碰过”
for page in 0..255:
t = reload_time(probe_array[page * 4096])
if t is fast:
guess = page

逐部分解释

  • reload_time 测访问耗时,缓存命中快,没命中慢
  • 快的那一页就是瞬态指令刚才访问过的位置
  • 这段只是教学伪代码;真实系统修复后,这条路应被 KPTI / 硬件修复堵住
  1. 把 Meltdown 当成普通软件 bug:错在以为补一个内核函数就够了;根因是权限检查和数据使用在微架构流水线里出现了时间差。

  2. 把 Meltdown 和 Spectre 混成一类:错在只看”都和推测执行有关”;Meltdown 读的是越权地址,Spectre 更像诱导受害程序泄露自己本来能访问的数据。

  3. 以为异常已经抛出就没有影响:错在只看架构状态;缓存、TLB、执行端口这类微架构状态也能被计时观察。

  4. 以为容器天然安全:错在把 namespace 当成硬边界;共享内核意味着内核映射和物理内存窗口可能被同一类硬件问题影响。

适用

  • 理解 2018 年后操作系统为什么引入 KPTI / KVA Shadow / Double Map
  • 学习”架构状态”和”微架构状态”的区别
  • 分析缓存侧信道、瞬态执行攻击、云上隔离风险
  • 解释为什么硬件性能优化也需要安全审计

不适用

  • 不适合当作今天直接复现攻击的教程:现代系统大多已经打补丁
  • 不适合解释所有推测执行漏洞:Spectre、RIDL、ZombieLoad 的触发机制不同
  • 不适合替代密码学常量时间分析:Meltdown 读的是隔离边界,不只是算法分支泄露
  • 不适合证明某个具体 CPU 一定受影响:论文也强调 ARM / AMD / Intel 实现差异很关键
  • 1996 年:Paul Kocher 展示 timing attack,大家开始严肃看待”时间也会泄密”。
  • 2014 年:Flush+Reload 把缓存命中差异变成高分辨率侧信道,后来成为 Meltdown 的接收器。
  • 2017 年:KAISER 原本为缓解 KASLR 侧信道而生,后来意外成为 Meltdown 的短期防线。
  • 2018 年 1 月:Meltdown 和 Spectre 公开,三大操作系统快速推出隔离补丁。
  • 之后几年:Foreshadow、ZombieLoad、RIDL、LVI 等工作继续挖瞬态执行的其他角落。
  • 隔离不是一句权限检查:真正的边界要看数据是否进入了任何可观察的硬件状态。
  • 性能优化会改变攻击面:乱序执行、缓存、TLB 都是为了快,但快出来的中间状态也可能泄密。
  • 补丁思路是少映射:KAISER / KPTI 的核心是让用户态页表里根本没有大块内核映射,减少可读目标。
  • 安全边界要跨层看:CPU、操作系统、虚拟化、容器共同决定”谁能看见谁”。
  • 论文 PDF:Meltdown: Reading Kernel Memory from User Space
  • spectre-attacks-2019 —— 同期公开的推测执行攻击,机制更依赖受害程序路径
  • flush-reload-2014 —— Meltdown 常用的缓存侧信道接收器
  • foreshadow-2018 —— 把瞬态执行问题推进到 SGX 和虚拟化边界
  • zombieload-2019 —— 后续 MDS 类攻击,说明问题不只在页表权限
  • docker —— 理解为什么共享内核的容器隔离会被硬件漏洞放大
  • spectre-attacks-2019 —— 和 Meltdown 同时引爆瞬态执行安全研究,但攻击模型不同
  • flush-reload-2014 —— 提供”看缓存脚印”的高分辨率工具
  • foreshadow-2018 —— 继续追问瞬态执行能否突破 SGX / VM 抽象
  • ridl-2019 —— 从 line fill buffer 等结构泄漏,展示缓存之外的微架构通道
  • fallout-2019 —— 关注 store buffer,说明硬件缓冲区也可能成为泄密路径
  • docker —— 容器共享内核,Meltdown 让这种共享的风险更直观
  • 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