[MIT 6.S081] Lec 22: Meltdown 笔记

Lec 22: Meltdown

核心代码

char buf[8192]
r1 = <a kernel virtual address>
r2 = *r1
r2 = r2 & 1
r2 = r2 * 4096
r3 = buf[r2]

基本流程

  • 攻击者在内存中声明了一个缓冲区 buf, 这个缓冲区为普通的用户内存且可被正常访问.
  • 攻击者拥有了内核中的一个虚拟内存地址, 其中包含了一些想要窃取的数据
  • 这里的程序是 C 和汇编的混合. 第 3 行代码的意思是从这个内存地址取值出来并保存在寄存器 r2 中
  • 第 4 行获取寄存器 r2 的低比特位, 所以此处这种特定的攻击只是从内核一个内存地址中读取一个比特
  • 第 5 行将这个值乘以 4096, 因为低比特位要么是 1, 要么是 0, 所以这意味着 r2 要么是 4096, 要么是 0
  • 第 6 行中读取前面申请的缓冲区, 要么读取位置 0, 要么读取位置 4096.

不能工作的原因

在大部分操作系统中, 内核内存会被完整映射到用户空间, 即所有内核的 PTE 都会出现在用户程序的页表中, 通过 PTE_U 等标志位来进行权限隔离, 使得用户代码不能直接访问内核内存地址.
因此, 第 3 行读取内核内存地址指向的数据是不被允许的, 会触发 Page Fault.

攻击成功的关键

预测执行(Speculative execution)

论文中称之为乱序执行(Out-of-order execution)

r0 = <something>
r1 = valid    // r1 is a  register; valid is in RAM
if(r1 == 1) {  
    r2= *r0
    r3 = r2 +1
} else {
    r3 = 0
}

代码第 2 行需要一个 load 指令将内存数据加载到寄存器, 会花费上百个时钟周期.
第 3 行代码是个 if 条件分支, CPU 会通过预测执行选择一个分支执行(在将 valid 实际加载到 r1 完成之前). 直到第 2 行代码执行完成后, CPU 再确定预测的分支是否正确, 若预测正确则继续执行, 若预测错误会回滚预测执行的代码.
CPU 的分支预测可能基于之前的分支选择.
若 r0 不是有效的指针, 超前执行到第 4 行时不会产生错误, 直到第 2 行代码运行完成, 因此错误的产生可能会延后数百个 CPU 周期.
确定一条指令是否正确的超前执行了而不是被抛弃了这个时间点, 对应的技术术语是 Retired.
在此例子中, CPU 进行了两个推测: 一个是 CPU 推测了 if 分支的走向, 并选择了一个分支提前执行; 除此之外, CPU 推测了代码第 4 行能够成功完成. 对于 load 指令, 如果数据在 CPU 缓存中且相应的 PTE 存在于页表, 不论当前代码是否有权限,Intel CPU 总是能将数据取出. 如果没有权限, 只有在代码第 4 行 Retired 时, 才会生成 Page Fault, 并导致预测执行被取消.
在论文中提到, Meltdown 发生在 Intel CPU 上而不会发生在 AMD 的 CPU 上. 普遍接受的观点是, AMD CPU 预测执行时在没有权限读取内存时, 不会将内存地址中的数据读出(即先进行了权限检查), 而 Intel CPU 则可以.

CPU 缓存

CPU 包含 L1 缓存, L2 缓存等多级缓存.
当 CPU 执行 load/store 指令时, 首先会访问 L1 缓存. L1 缓存最快, 只需要几个 CPU 周期, 通过虚拟地址索引.
若地址不在 L1 缓存中, 则需要判断目标虚拟地址是否在 TLB 中记录, 若在, 便通过 L2 缓存获取数据. L2 缓存比 L1 缓存更大, 需要几十个 CPU 周期, 记录着物理地址.
若不在 L2 缓存最终会花费上百个 CPU 周期从内存中读取数据.
在这里插入图片描述
在一个多核 CPU 上, 每个 CPU 核会有一个 L1 缓存, 很小很快; 同时每个 CPU 核还有一个大一些的 L2 缓存; 此外通常还有一个共享的 L3 缓存. 另一种架构是, 所有的 L2 缓存结合起来, 所有 CPU 共享. 通常 L1 缓存是虚拟地址寻址, 而 L2 和 L3 缓存是物理地址寻址.
在这里插入图片描述
在这里插入图片描述

Flush + Reload

作用

一段特定的代码是否使用了(被允许访问的)特定地址的内存

步骤

在这里插入图片描述

  1. 确保目标地址 x 不在缓存中. Intel 提供了指令 clFlush 用于刷新缓存确保接收的地址不在缓存中.
  2. 调用某段可能使用内存地址 x 的代码.
  3. 使用 rdtsc 指令记录时间(返回 CPU 启动后经过的 CPU 周期数).
  4. 加载内存地址 x 的数据到对象
  5. 再通过 rdtsc 指令读取时间. 若两次读取时间的差是个位数, 则第 2 步中使用了内存地址 x 的数据; 若读取时间的差值较大(超过 100), 则使用了内存地址 x.

熔断攻击

char buf[8192]

// the Flush of Flush+Reload
clflush buf[0]
clflush buf[4096]

<some expensive instruction like divide>

r1 = <a kernel virtual address>
r2 = *r1
r2 = r2 & 1     // speculated
r2 = r2 * 4096  // speculated
r3 = buf[r2]    // speculated

<handle the page fault from "r2=*r1">

// the Reload of Flush+Reload
a = rdtsc
r0 = buf[0]
b = rdtsc
r1 = buf[4096]
c = rdtsc
if b-a < c-b:
    low bit was probably a 0

声明了一个缓冲区 buf, 现在从内核地址窃取 1 比特数据.

  1. 首先 clflush 缓存中的数据.
  2. 执行费时的指令, 从而使得后续代码 CPU 可以预测执行
  3. 预测执行 10~13 行
  4. 由于第 10 行访问了不可访问的内核地址引发 page fault, 进入到下面的 page fault 处理程序
  5. 检测 buf[0] 和 buf[4096] 读取的 CPU 用时, 来判断内核地址的 1 比特数据.

Meltdown 修复

KAISER(KPTI)

在 Linux 中称为 KPTI (Kernel page table isolation). 不将内核内存映射到用户的页表中. 在执行系统调用时切换到拥有内核内存映射的另一个页表(类似 xv6).
缺点: 降低系统调用性能(需要切换页表, 同时导致 TLB , L1 缓存被清空).

硬件修复

一般数据的权限标志位会在 L1 缓存中, 因此 CPU 可以在获取数据时检查权限标志位. 通过在更早的时间检测权限标识位保证预测执行指令不会访问到无权访问的数据.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值