« Windows 下 UTF-16 的坑 | 返回首页 | 为什么说执行 996 工作制的脑力劳动者非蠢即坏 »

通过对缓存测速提取信息的旁路攻击

最近爆出对 Intel 系 CPU 的新的安全漏洞。MDS 攻击 可绕过安全屏障,取得同一个核心上运行的其它进程(或其它虚拟机)处理的数据。安全界的建议是暂时关闭超线程,避免泄露信息。

我对这个安全漏洞颇感兴趣,花了一些时间研究,弄明白了来龙去脉。

其实,新的 MDS 攻击的思路继承至更早发现的 Meltdown 攻击方式。除了原始论文,我找到了 这篇 blog 把其原理介绍的非常清晰。

如果不介意我可能有理解偏差,那么可以看看下面我对其原理做的一点中文复述:

现代 CPU 为了运行的更快,会把我们编写的复杂指令集指令内部转换为更细粒度的微指令运行。这些微指令无非就是做一些寄存器运算、存取数据、根据条件运算结果修改地址寄存器(达到指令运行流程分支的效果)等。为了让这些工作可以工作的更快,CPU 内还提供了不同的处理单元处理不同的业务,这样就可以并行工作。

表面上看,我们的程序是依次循序执行的。但实际上,许多微指令都是并行工作。为了提供性能,并行的运算单元间很难保证先后次序。这就是所谓的乱序执行。但最终的结果在面对用户层面还是用算法保证的。

有些指令天生就次序无关,比如对 A 寄存器加一和把 B 寄存器放到内存中,这两件事情就是无关的。谁先谁后都无所谓;有些事情却是相关的,比如从内存读一个字到 A 寄存器,然后再以 A 寄存器内的字为地址,去访问对应的内存,取出数据放到 B 寄存器,这就是相关的。你不读出 A 就不知道怎样访问 B 。

再比如,根据前面的一个条件运算结果,修正指令执行寄存器的值,也就是条件分支,再没有得到条件运算结果的时候,就无法知道后面要执行什么指令。

现代 CPU 为了适应多任务环境,以及操作系统和应用层的隔离。我们的程序运行时访问的内存地址,并不是真实地物理内存地址,而是虚拟地址。访问一个虚拟地址时未必有权限访问。所以理论上,不同内存访问的行为也是有次序依赖的。


CPU 为了加速这些有次序依赖的行为的方法就是,不管后面真的要做什么,我都去按最可能发生的事情先处理,如果事后发现做错了,就废弃已经做好的事情,把逻辑纠正回正确的轨道上。

对于内存访问的依赖,CPU 根据当下缓存中的数据提前预测马上要访问的内存,尽早通知缓存处理模块加载缓存;对于分支程序,提前处理最可能走的分支,如果预测错了,就退回重来;对于内存权限校验,等校验失败再退回已经做的事情……

在很长时间,我们都认为这些都是安全的。因为这是 CPU 内部的事情,应用层无法直接访问 CPU 的内部状态。CPU 即使加载了不需要的数据到缓存中,恶意程序也无法读取它。直到 Meltdown 攻击方式出现。


Meltdown 攻击允许用户程序在没有权限的情况下,获取操作系统保护起来的数据。它是怎样做到的呢?看这样一小段示意代码:

; rcx = a protected kernel memory address
; rbx = address of a large array in user space
mov al, byte [rcx]         ; read from forbidden kernel address
shl rax, 0xc               ; multiply the result from the read operation with 4096
mov rbx, qword [rbx + rax] ; touch the user space array at the offset that we just calculated

rcx 指向的是一个内核保护起来的地址。当我们运行到 mov al, byte[rcx] 时,是无法获得真正的值的,因为在用户权限下 cpu 拒绝读这处内存。

但是,访问失败这件事是稍后才处理的。现代 CPU 中,其实后面两行代码是被真的(被乱序)运行过了,只是最终的用户可访问的寄存器中,看不到结果而已。CPU 会废弃做过的事,但 CPU 内部缓存的状态却不一样了。

这里,把原本企图读取的值(一个 0-FF 的数字),乘上 4096 ,也就是一个缓存页的大小。然后去读一个 rbx (用户空间地址)的相对地址。

由于前面 al 并没有真的读出来,所以 rbx 也没有真的读出合理的数据。但是由于后两行指令其实被 cpu 处理过,所以对应的内存页其实被加载到缓存中了。

我们虽然无法直接查看 cpu 缓存的状态,但是这里有一个重要的区别:访问不在缓存中的内存地址比访问已经缓存的内存地址要慢的多。

所以,接下来我们只需要尝试访问 rbx 后的 256 个地址,统计分别的访问时间,那个最快访问到的地址对应的 al 值,就是我们原本没有权限访问的内存值。


从 Meltdown 攻击衍生的 Spectre 攻击更复杂一些。但可以用一个进程去偷取另一个进程的数据。这篇关于 Spectre 攻击的论文 写的浅显易懂,后面还附有概念验证的 C 代码,很值得一读。

对于那段 C 代码,其核心就是这么两行:

if (x < array1_size)
  y = array2[array1[x] * 4096];

这段代码对 y 取值的结果有几处不确定性,一是关于 x 的条件判断,二是 array1[x] 的值,三十 array2[...]的值。这三者是相互依赖的,从逻辑上必须循序执行。

但是 CPU 在处理时,会预测最有可能的情况,用最大速度运行。如果最终发现已运行过程中的某个环节是事实不符,再作废已经获得的数据。但是,整个过程中 CPU 的 cache 状态却保留了下来。

我们可以通过对需要攻击的程序的源代码或二进制进行还原,找到某处我们想关心的包含有敏感数据的地址位置,通过这个攻击手段猜测出那个进程中该地址处的数据。原理是这样的:

  1. 攻击代码先预先跑这段代码几次(5次足够),让第一个条件一直成立,这样就欺骗了 cpu 以后再跑这段代码,需要执行判断成立的代码。

  2. 让 array1[[x] 指向的虚拟地址等于想攻击的另一个进程的那个包含有敏感信息(比如密码)的地址,假设最近访问过那个虚拟地址,那么数据就在 cpu cache 中。

  3. 故意让条件判断失败,再跑一次上述函数。由于 CPU 会认为条件很可能成立,则会预算跑后面的这一段。这时,如果该虚拟地址在另一个进程中最近访问过,那么 cpu 认为数据就在 cache 中(虽然本进程该虚拟地址对应的物理内存并不是同一个位置),cpu 会取出 cache 中的值,也就是攻击对象中的敏感数据;并且去加载 array2[...] 的相关内存到 cache 。等到 cpu 意识到自己做了错误的操作:cache 对应的虚拟地址映射的内存并非当前正确的映射,它会放弃后续的操作,但对 array2[...] cache 的影响已经造成了(因为乱序执行的缘故)。

  4. 最后和 Meltdown 攻击一样,通过对 cache 访问测速,猜测出曾经存在于 cache 中的另一个进程的数据。


Meltdown 和 Spectre 攻击非常类似,它们利用了现代 CPU 流水线工作的机理,在各种 CPU 上(Intel AMD 甚至 ARM 的手机芯片上都能进行。它们都是在同一系统下,让只有有限用户权限的代码通过旁敲侧击获得它没有权限获取的内存数据(内核中的或其它进程中的)。通过给操作系统内核打一些补丁,可以在承认一定性能损失的情况下,尽可能地防范这类攻击。

这类攻击是针对内存 cache 的,对于新设计的 CPU ,也从硬件上阻止了漏洞。但是,新发现的 MDS 攻击 虽然原理类似,但它并非攻击内存 cache ,而是 CPU 的内部 Buffers 。开启超线程的 CPU ,同一个核心会共享一些内部 Buffers ,所以就让运行在同一个核心的两个不同超线程下的进程/虚拟机,有了绕过系统屏障,读取对方敏感信息的漏洞。

目前暂时还没有软件方法规避,所以在有安全风险的环境下,只能建议关闭超线程来避免攻击。

Comments

基本看懂了。

基本看懂了。

不明觉厉,以现在CPU的复杂程度已经超过一个普通人的理解能力

Post a comment

非这个主题相关的留言请到:留言本