Appearance
6.1810 2024 第21讲:Meltdown
为什么是这篇论文?
- 安全是操作系统的一个关键目标
- 内核的主要策略:隔离
- 用户/监控模式、页表、防御性系统调用等
- 如果你把一切都设置正确了,还可能出什么问题?
Meltdown
- 允许恶意用户代码读取内核内存,尽管有页面保护。
- 令人惊讶和不安
- 最近一系列“微架构”攻击之一
- 利用了CPU隐藏的实现细节
- 可修复,但人们担心会有源源不断的相关意外出现
- Linux:
grep . /sys/devices/system/cpu/vulnerabilities/*
- Linux:
这是攻击的核心(这是用户代码):
c
1. char buf[8192]
2. r1 = <一个内核虚拟地址>
3. r2 = *r1
4. r2 = r2 & 1
5. r2 = r2 * 4096
6. r3 = buf[r2]第3行的加载会把内核数据加载到r2吗?
假设内核被映射在用户页表中,且PTE_U标志位被清除。
- [图:0 到 2^64]
- 在这些攻击被发现之前,这几乎是普遍做法。
- 用户内存从零开始;内核在高地址。
- 同时映射用户和内核使系统调用更快。
- 重点是:
*r1是有意义的,即使被禁止。
那么这段代码怎么可能对攻击者有用呢?
- 答案与一堆大多隐藏的CPU实现细节有关。
- 推测执行(Speculative execution)和CPU缓存。
首先,推测执行。
- 这还与安全无关。
- 想象一下这段普通代码。
- 这是类C代码;"r0"等是寄存器,"*r0"是内存引用。
c
r0 = <某个地址>
r1 = 从RAM加载x // r1是寄存器;x是RAM中的变量
if(r1 == 1){
r2 = *r0
r3 = r2 + 1
} else {
r3 = 0
}- 从RAM加载 "r1 = x" 需要数百个周期。
- "if(r1 == 1)" 需要那个RAM内容。
- 如果CPU必须暂停直到RAM获取完成,那就太糟糕了。
- 相反,CPU会预测分支可能走向哪边,
- 并继续执行。
- 这被称为“推测”。
- 所以在 "r1 == 1" 被解析之前,
- CPU可能会推测性地执行 "r2 = *r0",
- 然后是 "r3 = r2 + 1"。
如果CPU错误预测了一个分支,例如x结果是零怎么办?
- CPU会刷新错误推测的结果。
- 具体来说,CPU会恢复r2和r3的内容。
- 并重新开始执行,在“else”分支中。
推测执行有助于提高性能,因为它有助于
- 避免CPU在等待慢速内存时停顿
- (其他慢速操作也一样,比如除法)。
如果CPU推测性地假设r1 == 1,但r0持有一个非法指针怎么办?
- 如果结果是x == 1,"r2 = *r0" 应该引发一个异常。
- 如果结果是x == 0,"r2 = *r0" 不应该引发异常。
CPU只有在确定指令不会因错误推测而被取消时才会“提交”它们。
- 并且CPU按顺序提交指令,只有在所有
- 先前的指令都已提交后,因为它只有到那时才
- 知道没有先前的指令出错。
- 因此,由推测执行的指令引起的错误可能在指令完成后一段时间才发生。
推测原则上是不可见的——是CPU实现的一部分,
- 但不是规范的一部分。
- 也就是说,推测旨在提高性能而不改变程序计算的结果——是透明的。
- CPU通过在意识到推测错误时撤销寄存器赋值,并且不从错误推测的指令中引发异常,来使推测透明。
一些术语:
- “架构特性”——CPU手册中的东西,对程序员可见。
- “微架构”——手册中没有,旨在不可见。
另一个微架构特性:CPU数据和TLB缓存。
- 核心
- L1: va,pa | data / TLB: va | pa
- L2: pa | data
- RAM
- 如果加载未命中,数据被获取,并放入缓存。
- L1(“一级”)缓存是虚拟索引的,为了速度。
- 系统调用返回用户空间后,会将内核数据留在L1缓存中。
- (假设页表同时有用户和内核映射)
- CPU必须同时查询L1和TLB,后者用于权限
- 和L1关联性标签的物理地址,
- 来决定加载是否在L1中命中。
- (Intel L1是虚拟索引,物理标记 (VIPT))
- L1未命中时:TLB查找,用物理地址进行L2查找。
- 时间:
- L1命中——几个周期。
- L2命中——十几个或二十几个周期。
- RAM——300个周期。
- 一个周期是1/时钟频率,例如0.5纳秒。
当执行用户代码时,L1缓存可能包含内核地址和数据。
- 例如,在系统调用返回后。
为什么在执行用户代码时L1包含内核数据是安全的?
- 用户程序能直接从缓存中读取内核数据吗?
在现实生活中,微架构并非完全不可见。
- 它影响指令和程序需要多长时间。
- 对编写性能关键代码的人来说,它引起了极大的兴趣。
- 对编译器编写者也是如此。
- Intel等公司发布优化指南,包含一些细节,但不是全部。
一个有用的技巧:感知某物是否被缓存。
- 这是论文的Flush+Reload。
- 你想知道函数f()是否使用了地址Z处的内存。
- 确保地址Z处的内存未被缓存。
- Intel CPU有clflush指令。
- 或者加载足够多的内存位置以强制将其他所有内容从缓存中挤出。
- 调用f()
- 记录时间。
- 现代CPU让你读取一个周期计数器。
- 对于Intel CPU,是rdtsc指令。
- 从地址Z加载一个字节
- (你需要内存栅栏来确保加载真正发生)
- 再次记录时间。
- 如果时间差小于(比如说)50,那么#4中的加载命中了,
- 这意味着f()可能使用了地址Z处的内存。
- 确保地址Z处的内存未被缓存。
回到Meltdown——这次更详细:
c
char buf[8192]
// Flush+Reload的Flush部分
clflush buf[0]
clflush buf[4096]
1. r1 = <一个内核虚拟地址>
2. r2 = *r1
3. r2 = r2 & 1 // 推测执行
4. r2 = r2 * 4096 // 推测执行
5. r3 = buf[r2] // 推测执行<来自*r1的页错误;r2和r3被回滚,但缓存没有><处理页错误>
c
// Flush+Reload的Reload部分
a = rdtsc
r0 = buf[0]
b = rdtsc
r1 = buf[4096]
c = rdtsc
if b-a > c-b:
低位可能是一个1也就是说,你可以根据两个缓存行中哪一个被加载(buf[0] vs buf[4096])来推断内核数据的低位。
要点: 来自 "r2 = *r1" 的错误被延迟到加载提交时,
- 这可能需要一段时间,为后续的推测指令执行提供了时间。
要点: 显然,"r2 = *r1" 确实执行了加载,即使PTE
- 禁止它,并将结果放入r2,尽管只是暂时的,因为
- 在提交时因错误而被恢复。
要点: "r3 = buf[r2]" 将buf[]的一些内容加载到缓存中,
- 即使对r3的更改因错误推测而被取消。
- 因为Intel将缓存内容视为隐藏的微架构。
攻击常常不成功
- 列表3/4中的每个XX都是一次失败
- 失败的原因尚不清楚。
- 也许所需的内核数据不在L1缓存中?
- 并且由于PTE权限而没有从RAM中获取?
- 或者加载在RAM获取完成前就到达提交阶段?
- 也许缓存冲突将数组踢出?
- 也许TLB未命中或有冲突?
- 也许机器上的其他活动?
- 也许加载需要不同的时间来提交和出错?
- 第6.2节说,如果内核数据未缓存,速度为10字节/秒
- 重试有帮助——为什么?
- 可能需要数千次重试——为什么?
- 成功的条件尚不清楚。
- 也许如果内核数据在L1中就可靠,否则就不可靠。
Meltdown如何在真实世界的攻击中使用?
- 攻击者需要在受害者机器上运行他们的代码。
- 分时系统: 内核可能有其他用户的秘密,例如密码、密钥。
- 并且内核可能映射所有物理内存,包括其他进程。
- 云: 一些容器和VMM系统可能易受攻击,
- 所以你可以从其他云客户那里窃取数据。
- 你的浏览器: 它在沙箱中运行不受信任的代码,例如插件,
- 也许一个插件可以从你的内核中窃取你的密码。
然而,Meltdown尚未被知晓在任何实际攻击中使用过。
防御措施呢?
一个软件修复:
- 不要在用户页表中映射内核。
- 论文称之为“KAISER”;Linux现在称之为KPTI。
- 需要在每次系统调用进入/退出时进行页表切换。
- 这就是RISC-V xv6的工作方式。
- 页表切换可能很慢——它可能需要TLB刷新。
- PCID可以避免TLB刷新,尽管仍有一些开销。
- 许多内核在Meltdown被知晓后不久就采用了KAISER/KPTI。
一个硬件修复:
- 只从推测性加载中返回允许的数据!
- 如果PTE_U/R/V被清除,返回零,而不是实际数据。
- 这可能几乎没有成本,因为CPU无论如何都必须在每次L1命中时查看TLB中的PTE。
- AMD CPU显然一直都是这样工作的。
- 最新的Intel CPU似乎也这样做(称为RDCL_NO)。
这些防御措施已经部署并被认为是有效的;但是:
- 页面保护结果并不稳固,这令人不安!
- 更多的微架构意外正在出现。
- 根本问题是可修复的错误吗?还是策略上的错误?
- 敬请关注,这仍在发展中。
参考资料:https://googleprojectzero.blogspot.com/2018/01/reading-privileged-memory-with-side.htmlhttps://cyber.wtf/2017/07/28/negative-result-reading-kernel-memory-from-user-mode/https://eprint.iacr.org/2013/448.pdfhttps://gruss.cc/files/kaiser.pdfhttps://en.wikipedia.org/wiki/Kernel_page-table_isolationhttps://spectrum.ieee.org/computing/hardware/how-the-spectre-and-meltdown-hacks-really-workedhttps://lwn.net/Articles/741878/