从理论层面谈Meltdown与Spectre攻击 | 核武按钮终被劫持?

地球上有没有一种安全漏洞,广泛存在于各种计算设备之中,并且难以被检测,难以被修复?在2018年的第一个月里,Meltdown 与 Spectre 两大处理器安全漏洞给出了这个问题的部分答案。

边信道攻击进入热核时代

信息安全行业有一类很高端的攻击方法,叫做边信道攻击(side channel,也有译作侧信道攻击,或旁路攻击的),听着就感觉很高级有没有。这种攻击出现于上世纪 90 年代,以色列的高等学府似乎经常研制边信道攻击的奇技淫巧——所谓的边信道,就是不从正面进攻,而是从侧面窃取或传递信息的方式,比如利用设备运行时产生的电磁辐射,或者电流导致的机械波动噪声,来分析破解出密钥,甚至还能利用散热风扇产生的噪声、机箱上的 LED 灯闪烁变化来将信息传出。

许多边信道攻击条件都非常苛刻,而且攻击噪声比较大,情报获取的正确率有时很堪忧,所以边信道攻击在此之前只有 2016 年精妙的 Linux TCP 漏洞(CVE-2016-5696)造成广泛的影响[1]。

如果将各种漏洞比作攻击武器,那 Linux TCP 漏洞之前的各种边信道攻击类似冷兵器——致命但难以造成广泛破坏。Linux TCP 漏洞攻击思路则将边信道攻击水平提升到了热兵器时代,1 年之后,Meltdown 和 Spectre 漏洞正式开启了热核时代(即便边信道攻击本身严格意义上并不需要漏洞的介入)。

meltdown-spectre-kernel-vulnerability.png

核武 Meltdown(熔断)和 Spectre(幽灵)尽管只有两个名字,但却包含了 3 个漏洞分别是 CVE-2017-5753/5715/5754 [2])。理论上将影响一切现代处理器,借助这三个漏洞,黑客能窃取信息,比如你的密码、个人敏感数据,甚至拿下整个内核地址空间;运行个极为隐蔽的恶意程序,或者点个网址链接,就能让你的信息泄露。由于漏洞存在于硬件层面,因此杀毒软件和个人防火墙很难对攻击进行检测,遑论防护。

截至 2018 年 1 月 10 日,包括 Intel、高通、Apple、NVIDIA、ARM 均承认旗下处理器受 Meltdown 和 Spectre 两个漏洞影响。Intel 受伤最深,从 25 年前 Pentium 75MHz 开始全部中招,就连体系架构完全不同的 Itanium、Xeon Phi 都无法幸免。高通和 ARM 则确认所有带有 OoO(不是表情符号,Out of Order 乱序执行)的 ARM 架构都受到影响。相比之下,NVIDIA 则幸运得多,旗下的 GPU 只受到 Spectre 漏洞影响,而对 Meltdown 免疫。至于 AMD 处理器和其他处理器指令集结构(ISA)如 MIPS64、ALPHA 等,我们认为其有可能对 Meltdown 免疫,但大概率存在 Spectre 漏洞——大量企业级、电信级路由器防火墙都使用 Cavium 基于 MIPS64 ISA 的 SoC,不少超算包括太湖之光都使用了 ALPHA ISA。

热核攻击,从缓存开始

早在 2013 年,就有人根据 Intel 处理器的三级缓存架构提出了专门的缓存攻击思路,并将此攻击思路命名为 FLUSH+RELOAD。这种思路利用了现代处理器线程之间共享缓存的设计,通过冲刷和重载来实现目标线程的劫持和攻击。简单地说,现代处理器内执行的 N 个线程就相当于一幢楼中每个使用自来水的家庭,缓存就像大家共用一个自来水水箱。如果其中一个家庭在水箱中投毒,将会导致所有家庭都中毒。现代处理器的 Cache 本质上是 SRAM,也需要持续的冲刷,这给此类攻击制造了大量机会。

intel_reuters_1515041265999.jpg

这次有关 Meltdown 攻击和 Spectre 攻击,美国几家学府和安全公司的研究人员共同发布了两篇技术 paper,这两篇论文均提到了上述 FLUSH + RELOAD 的攻击方案 [3]。

当代操作系统有个特性叫 page sharing,就是不同的进程可以进行主内存的共享。Page sharing 的价值主要在于两个联合运行的进程通讯更方便,还能减少内存占用。本质上这是一种内存复用的思路,这种基于内容的 page sharing 用于在进程执行和使用共享库时,可执行文件正文段的共享。而且不止是操作系统,如今的主流虚拟化 Hypervisor 其实也用这套方案。比如云计算技术中普遍采用的 de-duplication,就是不同虚拟主机共享一个内存模块的技术,也是同理——像 VMware ESX、PowerVM 之类的 hypervisor 都是支持的。

系统对于共享 page 的映射,采用 copy-on-write(COW) 的方案——就是对改动的数据延后写入到内存中,这也是 FLUSH+RELOAD 攻击可导致信息泄露的一个基础。

两个进程共享内存页,到了线程层面也有很大概率共享处理器上的缓存 Cache 部分。Cache 是连接主内存和处理器前端的缓冲地带,其读写速度比内存要快得多。处理器在进行数据处理时,会首先从 cache 中提取数据和指令,唯有在发现 cache 中没有所需数据时才向主内存发出请求,要知道从主内存中提个数据,可能需要浪费多达 200 个时钟周期,这对处理器这种高速设备而言绝对是不太能忍的,所以你看当代 CPU 的 cache 命中率有多高。

QQ20180110-012633@2x.png

当代处理器都采用多层级 cache 方案, Cache 中的存储基本单位是 line,每条 line 包含固定字节数(如下图)。比如 i5-3470 的 cache line 尺寸为 64 字节。另外作为共享 cache,所有 cache 数据的副本在此处都有一份(至少 Intel 处理器是这样)。所以如果我们尝试从 L3 擦除一些数据,则其他各级 cache 的数据都也可以被移除——这是 FLUSH+RELOAD 攻击的基础。

比如上面这张图是 Intel 酷睿 i5-3470 处理器的 cache 架构。其 cache 分三级,分别是 L1、L2 和 L3 缓存,L1 离处理器核心最近,速度最快,容量也最小,L3 则离处理器最远,容量相对也更大——此例中是 6MB。从 2008 年 Intel Nehalem 架构开始,Intel 的所有多核酷睿处理器就有了共享 L3 缓存,无论这个处理器有多少内核均能共享 L3 至 LLC(Last Level Cache,末级缓存)中的所有数据,这也间接降低了攻击实施的难度。

c2.png

假设有这样一个目标进程 1,为了让它和我们构造的恶意进程 2 进行 page sharing,就将内存映射函数 mmaps 应用到目标可执行程序,令其进入恶意进程 2 的虚拟地址空间,完成映射文件的内存镜像共享(需保证两者在同一物理处理器上运行,可令其位于处理器的不同核心之上)。

攻击第一步,让恶意进程 2 监控某个特定的 cache line,并将其内容从 cache 中擦除;第二步,恶意进程 2 等待一段时间;第三步,恶意进程 2 重新载入刚才的那条 line。如果说载入这条 line 的时间比较久,就说明应该是从主内存中载入的(因为主内存很慢),最主要的是说明在等待过程中目标进程 1 并没有尝试载入这条 line;但如果在这一步中,恶意进程 2 载入这条 line 的时间很短,就说明目标进程 1 刚才也尝试过载入这行 line(因为目标进程 1 载入过之后,这部分数据就已经写到了 cache 中,而 cache 是很快的,所以恶意进城 2 载入这条 line 的时间就很短了)——撞上了!

QQ20180110-012735@2x.png

如此循环往复,如上图所示,这样一来就能得知哪些数据被存取过。这就是所谓的 FLUSH+RELOAD L3 cache 攻击。当然了,这种攻击还是存在精度问题的,比如说上图 C 的情况,恶意进程请求载入某条 line,在极短时间内目标进程也恰巧请求载入这条 line,这样观测到的结果就会有问题;另外还需要考虑一些处理器的性能优化方案的噪声干扰,比如说现在的处理器的空间局部性实现的数据预取(prefetch),还有像是分支预测,这要求在设计攻击程序(建议采用循环体之类以内存访问相对频繁的为目标,如上图 E)和对结果进行分析的时候有过滤策略。

QQ20180110-012918@2x.png

比如像上面这样,其中第 14 行 cflush 就是从所有 cache 中擦除某行 line;另外,如 mfence、lfence 这些指令实现指令流的串行序列化,避免并行和乱序执行的出现。这篇 paper 还特别举了攻击的实例,以这种攻击方法从 RSA 实施方案 GnuPG 中获取私钥。按照这篇 paper 所说,平均而言,这种攻击方案通过观察单独的一个签名或加密轮,平均就能恢复密钥 96.7% 的 bits。

至此我们不难看出,现代处理器特别是多内核处理器为了提升性能,大部分都采用了多级缓存设计,而各大处理器厂商更是投入巨资去研究和提升缓存命中率,然而这样的设计恰恰存在漏洞。那么,现代处理器如果没有缓存或者不共享缓存,是不是就会幸免于难?在热核时代,答案当然是否定的——还记得我们开篇所说么?Meltdown 和 Spectre 包含 3 个漏洞,缓存只是其一,乱序执行接踵而来。

Meltdown, 乱序执行惹的祸

上述 FLUSH+RELOAD 攻击实际上就是这次 Meltdown 和 Spectre 攻击最终环节的组成部分,也是这两个攻击被称为边信道攻击的原因:敏感信息的获取,靠的是检查读取某条 cache line 的时间,这已经是比较典型的边信道攻击了,虽然好像感觉不够神奇。不过在 Meltdown 和 Spectre 的攻击链条中,FLUSH+RELOAD 并非唯一实施方案,还有各种变体,比如 Prime+Probe 也是可行的,这里就不做展开了。但这部分构建了攻击链中的 convert channel,也就是最后获取到敏感数据的一个秘密通道。

因为 ARM 虽然也有擦除 cache line 的指令,但这些指令仅可用于高权限模式,ARM 架构不允许用户进程选择性地擦除 line;对 AMD 处理器攻击也无效,作者推测 AMD 处理器的缓存结构可能是非包含式的,即 L1 的数据不需要存在于 L2 或 L3 中,所以擦除 L3 cache 的某行 line,并不对 L1 构成影响。

在 Spectre 攻击的 paper 中[4],研究人员提到他们不仅采用 FLUSH+RELOAD 攻击方案,另外还融合了 EVICT+RELOAD 方案——这本质上算是前者的一个变体,只不过后者对 line 的擦除方法更复杂,这可能是 Meltdown 和 Spectre 得以在不同处理器平台上实现的一部分原因,有兴趣的同学可以去深挖后一种方案[5]。

intel_chip_bloomberg.jpg

就 Meltdown 而言,这种攻击方式主要利用的是当代处理器的乱序执行特性,可以在不需要进行系统提权的情况下,就读取任意内核内存位置,包括敏感数据和密码,甚至拿下整个内核地址空间。不过它对 AMD 和 ARM 的处理器也是无效的,但原因似乎并不像上面这样。研究人员在 paper 中提到[6],Meltdown 在 AMD 和 ARM CPU 上的攻击复现并不成功但可能只需要对攻击进行一定优化,深入挖掘依然可能会成功,比如对竞争条件进行一些调整,所以 Meldown 攻击或许也并不仅限于 Intel。

“乱序执行”本身不是个生词了,当代的高性能处理器都有乱序执行特性:早些年 CPU 性能每年翻番都并不稀罕,通过增加核心数、时钟频率,加宽管线外加工艺迭代就能实现。但是在顺序执行架构达到瓶颈之后,架构的优化就成为一个重要方向:顺序执行架构中,指令完全按照一个不变的顺序执行,就算 CPU 运算单元的执行速度很快,CPU 却浪费大量时间在等待,许多单元处在闲置状态,所以乱序执行成为提升效率的重要解决方案。好比你攒台 PC,如果显卡还没到货,肯定不会守在门口傻等,而是把其余部分先组装好。

1967 年,Tomasulo 最早开发出了可用于动态规划指令乱序执行的算法,当时他就提出了一个叫 Unified Reservation Station 的东西。CPU 在不需要将某个值存储到寄存器并读取的情况下,就可以在这里使用值。此外,Unified Reservation Station 还通过一个 CDB(共用数据总线)把所有执行单元连接起来。如果某个操作数尚未准备就绪,URS 单元可以监听 CDB,获取里面的数据,然后就可以直接执行指令了。(请注意,这一段对于理解后面的 Meltdown 攻击流程很重要)

QQ20180110-013448@2x.png

在 Intel 的处理器架构中,其整条管线包含了前端、执行引擎(后端)和存储子系统。前端会从存储系统中(包括 cache 和主内存)读取 x86 指令,随后分解成为 μOP(所谓的“微指令”,有些类似于将 CISC 转为 RISC 的过程),μOP 再发往执行引擎。乱序执行操作就是在执行引擎中发生的,如上图所示。

其中有个 Reorder buffer(重新排序缓冲器),负责寄存器分配、寄存器重命名和 retire。在经过此处之后,μOP 就转发到了上面提到的 Unified Reservation Station,此处对操作进行排序,排队完了就可以发往执行单元了;执行单元就能进行 ALU 加减乘除、AES、AGU 或者载入存储执行之类的操作了。AGU(地址生成单元)以及载入与存储执行单元直接与存储子系统相连,可以直接处理请求。

现在的处理器一般都已经不再直线型执行指令,比如分支预测单元可以猜测接下来该执行什么指令——分支预测器在某个 if then 语句中的条件语句还没有判断之前,就已经开始预测其中的分支运行结果了。在这条线路上,那些没有依赖关系的指令可以先执行,如果预测正确,运算结果就可以马上使用了。如果预测错误,Reorder buffer 清理回滚,并重新初始化 Unified Reservation Station。

选读:有关地址空间(可略过)

为了让进程彼此间隔离,CPU 支持虚拟地址空间,在此虚拟地址会转为物理地址。一个虚拟地址空间会被分成几个 page,这些 page 通过多层级页转换表(multi-level page translation)分别映射到物理内存。这里的转换表,定义了虚拟和物理间的映射转换,另外还定义了用于进行权限检测(可读属性、可写属性、可执行属性、用户可访问属性)的保护属性。现如今的转换表放在某个特定的 CPU 寄存器里面。

在每次进程上下文切换的时候,操作系统都会用另一个进程的转换表地址去更新该寄存器,所以每个进程都有个虚拟地址空间,每个进程只能参照其自有虚拟地址空间的数据。每个虚拟地址空间又切分成用户(User)和内核(Kernel)两部分。运行中的应用可以访问用户地址空间,但内核地址空间仅当 CPU 运行在特权模式下才可以访问。这一步是由操作系统决定的,操作系统在相应的转换表中禁用用户可访问属性即可。

QQ20180110-013601@2x.png

实际上内核地址空间不仅包含内核自己用的部分,也需要在用户 page 执行操作,比如往里面灌数据。因此,整个物理内存在内核中都有映射。在 Linux 和 OS X 系统中,这种映射比较直接,比如整个物理内存直接映射到预定义的虚拟地址;Windows 的情况则比较特殊,Windows 系统维护分页池(paged pool)、非分页池(non-paged pool)以及系统缓存(system cache)。这些“池”就是内核地址空间中的虚拟存储区域,将物理页映射到虚拟地址,其中非分页池要求地址位于内存中,分页池由于已经存储在了磁盘上,所以可以从内存中移除。而系统缓存部分则包含所有文件备份页的映射。

一般的内存破坏漏洞利用,就需要某个数据的地址。为了阻止内存破坏一类攻击,就有了 ASLR 等技术。为了保护内核,KASLR(内核地址空间布局随机化)在启动的时候会对内核地址进行随机化,令攻击难度更大。微软和苹果在Windows 7和Mac OS X 10.8 中引入了kALSR并强制开启。Linux在2017年5 月于4.12 版本中默认开启kALSR。

实施 Meltdown 攻击,要克服 KASLR 就需要获取到这种随机偏移量。但要做到这一点也并不难。

QQ20180110-013643@2x.png

这里我们看一个乱序执行的简单例子。这段代码第一行就产生了异常(不用管是怎么产生异常的),按照控制流来说,发生异常就该跳到操作系统的异常处理程序,应用终止,后面的代码就不会继续执行了。但因为有乱序执行的存在,第三行指令可能已经部分被执行了,只不过没有 retire,所以实际上并不会在架构层面产生可见的影响,即它对寄存器、内存都不会有影响。

这部分执行最终会被丢弃(恢复状态),寄存器和内存的内容也不会执行 commit。但它对微架构层面有影响——这里所谓的微架构也就是 cache 部分了。在乱序执行(第三行指令)过程中,引用的内存读取到寄存器上,也存储到了 cache 里面,即便最终 CPU 发现异常以后清空整条管线,cache 中的内容也依然会保留。这样一来,利用本文第一部分提到的 cache 边信道攻击就可以获知其中的信息了(此处是要提取 <em>data</em> 的值):也就是不停探测某个内存位置是否进入 cache。

完整的 Meltdown 攻击主要分成两个组成部分,第一部分就是让 CPU 执行某个永远不会在路径中被执行的指令,如上例中的第三行代码——我们将这种指令称作 transient instruction。真正要将这种 transient instruction 应用到实践攻击中,就要求指令序列中包含密钥之类的东西——就是攻击者想要窃取的数据。

QQ20180110-013950@2x.png

Meltdown 的第二部分主要就是我们这篇文章第一部分提到的那种基于时间的边信道攻击了,把密钥给恢复出来,或者说对 L3 cache 进行攻击获取数据的方法。这样我们基本上已经把 Meltdown 的攻击过程给说清楚了。但实际操作中可能会碰到很多复杂的情况,如攻击者要获取密钥,那么就意味着要访问用户不可访问的 page,比如说内核页——访问这样的位置,由于没有权限,所以会导致异常。攻击者需要去想办法处理这样的异常,否则的话进程就要被终止了。处理方法可以是把攻击程序分成不同部分,只在子进程中执行后面的 transient instruction 序列,而父进程通过后续的边信道攻击来恢复密钥。

另外,在第二部分的边信道攻击,也就是上图中所谓构建 covert channel 秘密通道,悄悄把已经存在于 cache 中的密钥给窃取到了。在这部分里,我们可以把 transient instruction 序列看成是这个通道的发射端,而接收端可以是不同的进程(和第一部分的 transient instruction 可以是无关的),比如可以是前面提到的父进程。

timg (5).jpeg

如果你还是不明白,这里举个简单例子:有家餐厅(CPU),这家餐厅有个收银员,还有个厨师(执行引擎)。小明和小红每天都去这家餐厅吃东西,小红(内核)每次点东西的方法都是对收银员说:我要和昨天一样的东西——然后厨师把东西做出来,收银员给小红打包带走。小明很想知道小红吃的究竟是什么,所以他有一天跟在小红身后;小红点完以后,小明跟收银员说:我要和小红一样的东西。收银员说:你这是侵犯人家的隐私,滚出我们餐厅(发生异常)!于是小明就被人收银员一脚踢出了餐厅(管线清空)。

小明想了一个新的办法,第二天他带着小方一起去,他俩跟在小红后面。小红照常点餐(点的是汉堡),小明大声喊:我要和小红点一样的东西。餐厅的厨师听到了,于是就做了两个汉堡出来(执行引擎进行乱序执行,第二个汉堡进入了 cache)。但小明再次因为侵犯隐私,被收银员扔出了餐厅。小方(cache 边信道攻击)这个时候上前了,他和小明是串通一气的,他对收银员说:我要点汉堡、薯条、鸡腿、土豆泥、红豆派、可乐、鸡翅……最终小方发现,最快送上前来的是汉堡(因为在 cache 里面),于是就知道小红点的其实是汉堡。

Spectre,处理器背后的幽灵

Spectre 的情况在这里只略作介绍,下文也不再进行展开。笔者认为,Spectre 的利用难度要大很多(攻击成本更高),但牵涉面更广。Spectre 基本思路和 Meltdown 是差不多的,最终也是通过 FLUSH+RELOAD 这样的 cache 边信道攻击来获取内存里面的东西,只不过切入方式利用的是分支预测——就是我们前面提到的,比如 If Then 语句出现,处理器在还没有判断 if 条件是否满足的情况下,就会去预测后面的分支,如果预测正确就可以有效提升运算效率。对分支预测的利用,无论是分析成本,还是攻击成本,都大了不少,只不过包括 AMD、ARM、Intel、等在内的处理器全部都中招。

QQ20180110-014829@2x.png

Spectre 攻击涉及到两个漏洞,分别是 CVE-2017-5753 和 5715。针对 CVE-2017-5753,如上面这个语句,绿色部分那一行是个 if 条件语句。上述代码,如果 arr1-&gt;length, arr2-&gt;data[0x200] 和 arr2-&gt;data[0x300] 都还没有进入 cache,处理器都还没有判断条件语句,但其它数据都已经 cache,而且分支条件预测为真,那么处理器在加载 arr1-&gt;length 之前就会加载 arr1-&gt;data[untrusted_offset_from_caller] 的值,并开始载入 arr2-&gt;data 数据依赖偏移,将相应 cache line 载入 L1 cache。

但最后处理器发现,预测错误,此刻包含 arr2-&gt;data[index2] 的 cache line 已经位于 L1 cache,那么此时由恶意程序去请求 arr2-&gt;data[0x200] 和 arr2-&gt;data[0x300],测量请求载入的时间,就能判断 index2 的值是 0×200 还是 0×300 了,也就可以知道 arr1-&gt;data[untrusted_offset_from_caller]&amp;1 是 0 还是 1。

篇幅有限,我们无法再展开做更为具体的分析和讲解,不过这个逻辑本质上和 Meltdown 对于乱序执行的利用有些相似,都建基于处理器率先执行了后面的指令,而且还把数据放在了 cache 中,但明显更为复杂。最终也都通过恶意程序去检查读取加载指令所需时间,来推测某值。

要利用这样的行为,攻击者需要在目标环境中构造这样的代码模式执行。有两种思路,要么这种代码模式在现有代码中就有,要么需要有个解释器(interpreter)或者 JIT 引擎,最终可以生成这种代码模式。第一种攻击思路,对实际环境的要求太苛刻或者说很难达成;第二种是谷歌采用的方案,他们选择了 eBPF 字节码解释器和 JIT 引擎。

37a70ff41bd5ad6e347cac028acb39dbb4fd3c8a.jpg

这种攻击还涉及一些复杂的问题,比如说需要对分支预测机制首先进行训练,令其足以产生错误的预测结果;然后操作目标进程,执行上述构造出来的代码模式;最后用上咱们文章第一部分提到的 cache 边信道攻击。

而相关 CVE-2017-5715,Spectre 攻击可利用 CPU 的间接分支预测器(Indirect branch predictor),执行特定代码片段;条件间接分支可被用于攻击,分支目标地址被攻击者控制;通过控制间接跳转目标位置,或其代码,利用边信道攻击来推测敏感信息。为此,谷歌的研究人员还专门逆向分析了 Intel Haswell 的分支预测器。这一思路的实现难度也不小,莫说不同 CPU 在分支预测机制方面的差异需要攻击者差别对待,应对超线程,Spectre 的攻击还需要解决同一 CPU 核心之上两个线程与间接分支预测器极为复杂的关系问题——AMD 方面甚至认为由于架构方面的差异,CVE-2017-5715 几乎不可能对 AMD 处理器造成影响。

Meltdown 的 paper 中提到,Spectre 攻击需要专门为目标进程的软件环境做定制。许多安全专家也都认为,Spectre 攻击是有目标明确指向性的,它很难造成大规模无差别攻击。笔者意见:Spectre 可能会成为未来 APT(高级持续性威胁)攻击中的一环,毕竟它适用于几乎所有处理器,但攻击难度略高;指望黑客专攻小网民,其成本收益可能并不对等。尤为值得一提的是,到目前为止,针对 Spectre 攻击的补丁无法从根本上杜绝其影响,充其量只是增加攻击难度的缓解方案。Spectre 的研究 paper 也提到,所有提出来的从硬件到软件层面的缓解建议只是折中选择,治标不治本。

谷歌倒是针对 Spectre 自己开发了一个名为 Retpoline 的补丁(该补丁也得到了 Intel 的支持)[7],这可能也是个缓解方案:可以避免内核中的“预测间接调用”,将预测执行与间接分支进行隔离,可应用于基于 Linux 的系统、系统程序、库以及软件程序,主要面向 x86 架构——而且按照谷歌的说法,从已经部署到 Linux 服务器的实验结果来看,这个补丁对性能的影响也比较小(这就叫自己挖洞,自己补)…

Mozilla 方面已经确认,Spectre 的又一厉害之处就在于,它能够使用 JavaScript 程序从 web 浏览器读取敏感信息[8, 9],不依赖于恶意程序,而是欺骗用户点击一个链接,通过 web 端来窃取信息——但笔者以为,通过浏览器进行攻击依然存在很大限制,可能需要结合其他漏洞进行组合攻击才能发挥比较大的成效。不过各浏览器厂商,包括 Chrome、IE/Edge、Firefox 都牟足劲儿开发新版本来缓解 Spectre 带来的影响。

按下核按钮,Meltdown 攻击实务

这里,我们对 Meltdown 再做更为细致的理论探讨。在上文 Meltdown 简介部分的代码中,攻击者通过 probe_array 开辟了一段内存,而且是 256 个 page 大小的内存(乘以 4096 是假设一个 page 为 4KB),而且要保证这部分内存没有 cache 过。接下来我们就彻底地理一理整个逻辑的过程究竟是什么样的:

第一步:读取密钥

从主内存中加载数据到寄存器,引用主内存中的数据需要使用虚拟地址。在将虚拟地址转为物理地址的过程中,CPU 还检查了该虚拟地址的访问权限,用户可访问,还是仅内核可访问。所有内核地址导向有效的物理地址,CPU 也就能够访问这些地址的内容了。当前权限不允许访问某地址,CPU 就会生成异常,用户空间无法简单地读取到这类地址的内容。

QQ20180110-014113@2x.png

不过乱序执行上线了,CPU 会在非法内存访问和产生异常这个很短的时间窗口内,乱序执行后面的指令。在上面的汇编代码中,第 4 行,RCX 寄存器里面有个内核地址,这一行载入了该地址中的字节值(密钥,实际上这个操作是不被允许的,所以理应产生异常);然后把它放到 RAX 寄存器(这也是 x86 架构中的一个 64 位寄存器)的最低有效位上。

如我们前面科普的那样,mov 指令被处理器读取,编码成 μOP,分配后发往 reorder buffer。在这个 buffer 中,RAX 和 RCX 这样的架构级寄存器,会映射到实际上的物理寄存器。

由于乱序执行的存在,代码第 5-7 行也已经解码分配成了 μOP,随后这些 μOP 被发往 unified reservation station(还记得文章前面提到的内容吗?翻回去看看)。如果后面的执行单元此时被占,或者执行指令所需的某个操作数还没准备好,μOP 就会在这里等着。实际上,后面这几行指令的 μOP 可能已经在 unified reservation station 里面等着前面第 4 行的内核地址抵达了。这个时候,如前文所述,由于 unified reservation station 通过共用数据总线监听执行单元,所以这部分 μOP 不需要等第 4 行指令 retire 就可以开始执行运算了!!!是不是感觉乱序执行顿时就高级了?

当 μOP 执行完成之后,他们按照顺序 retire,执行结果会 commit 到架构状态中。在 retire 阶段,CPU 才会处理,前面指令执行过程中产生的异常和中断。这个时候,第 4 行的 mov 指令显然要被毙掉了(因为没权限访问内核地址),异常此时才被处理,整条管线清空,消除乱序执行指令计算的所有结果。

但不要忘记,cache 里面还有后面乱序执行算出来的货呢,这些货没有清掉。

df0cc62145d73f11d58b6076617a0654.jpg

第二步:传输密钥

如果 transient instruction 序列在上面的 mov 指令 retire 之前执行,那么第一步就成功了。这里生成异常(即 mov 指令 retire),和 transient instruction 之间有个竞争状态,就是 transient instruction 必须要先执行,否则异常如果先生成了,后面指令的乱序执行就不会进行——所以攻击中需要减少 transient instruction 序列的运行时间,这才能够提升攻击的成功率。接下来就该是把密钥传出去了。

说穿了,transient instruction 所做的事情就是把前面的密钥放进 cache 里面。那么在传出密钥阶段,攻击者用 probe_array 开辟一部分内存(还记得前面的代码吗?),并且确保这部分内容没有被 cache。在上面汇编代码的第 5 行,是把从第一步中取得的密钥,乘以 page size,假定 page 大小是 4KB(对应于高级语言 access(probe_array[data * 4096]))。

这个乘法是为了保证对数组的访问,相隔距离足够大,阻止处理器的 prefetcher 去预取相邻内存位置。这样的话内存占用 256 x 4096(256 个 page)。代码的第 7 行,是将乘法运算过后的密钥,和 probe array 的地址相加,组成最后 convert channel 的目标地址。这个地址被读取,用以对相应 line 进行 cache。

第三步:接收密钥

这部分其实就是本文的第一部分,Meltdown 攻击描述也基本采用了 FLUSH+RELOAD 攻击方案。上面第二步中提到的 transient instruction 序列执行后,probe array 的一个 line 被 cache。probe array 的 cache line 位置就取决于第一步中读取的密钥。那么攻击者对 probe array 的所有 256 个 page 进行迭代,测量 page 的每个首行 cache line。包含已写入到缓存的 cache line 的那个页数(也就是哪个 cache line 写入时间最短),也就直接表示密钥的值了。

QQ20180110-013728@2x.png

<p style=”text-align: center;”>对 256 个 page 进行迭代,观察读取时间,仅有一个 cache 命中,这个命中的 page 就是 data 值啦!</p>

如果上文两部分无法看懂,则只需要搞清楚,恶意进程通过一些简单的方式试出敏感数据是什么——根据文章第一部分 FLUSH+RELOAD 的思路,再用编程的一些技巧就可以搞定。

通过对前面三个步骤进行重复操作,对所有不同的地址进行迭代,攻击者是可以还原出整个内存样貌的。研究人员利用这套方案,在 Intel 酷睿 i7-6700K 平台,外加 Ubuntu 16.10(Linux 内核 4.80) 操作系统中进行攻击,能够从内存中获取及其向 web 服务器发出请求的 HTTP header。另外,还能获取 Firefox 56 浏览器的内存部分,并找出存储在其内部密码管理器中的密码。

QQ20180110-014316@2x.png

Firefox 56 存储的密码一览无遗啊

Meltdown 的这套攻击方案,利用的是漏洞 CVE-2017-5754。实际上,这种攻击技术的提出并不是近期才有的,去年 7 月份就有人写过一篇警告文《Negative Result: Reading Kernel Memory From User Mode》[10],不过此人当时未能完美复现 Meltdown 造成的问题,但他自己说似乎是“开启了潘多拉魔盒”。

这是个实实在在的硬件级漏洞,属于 CPU 微架构实施层面的漏洞。鉴于硬件产品的特殊性,让 Intel 召回这 20 年的产品是不现实的。不过封堵这种程度的漏洞,最佳方案难道不是禁用乱序执行吗?但这对当代 CPU 性能影响将会是灾难性的。

Pentium_M-die.jpg

硬件层面实际上还可以对权限检查和寄存器读取,实施一个序列化过程——如果权限检查失败,则不读取该内存地址。但这么做的代价也会非常大,每次内存读取都要进行权限检查。更加现实的方案应该是从硬件层面实现对用户空间和内核空间的隔离:设立分隔比特位,内核必须位于地址空间上部,用户空间则必须位于地址空间下半部。这样一来,内存读取即便通过虚拟地址都能发现读取目标是否违反安全边界。

熔断易挡,幽灵难防

硬件层面的修复对我们来说都是空谈。操作系统层面,各主流操作系统这两天都已经陆续推了针对 Meltdown 的修复补丁。主要是一种研究人员推荐叫做 KAISER 的方案,在 Linux 内核更新中这项特性叫 KPTI(内核页表隔离)——其实现方法是,让内核不要映射到用户空间中。这种方案其实早两年就有了,当时是为了杜绝攻破 KASLR 的边信道攻击提出的。

QQ20180110-014701@2x.png

国外网友最早发布的 FS-Mark 测试,显示在打完 KAISER 补丁前后,Coffee Lake 的 I/O 性能差别可超 50%——当然了,这应该只是个案[12]

不过巧合的是,它也能预防 Meltdown 攻击,因为内核空间或物理内存在用户空间中已经没有有效的映射了。提出相对完整的解决方案的 paper 就是去年发布的,研究人员在 paper 中将这套方案称为用于内核地址隔离非常“高效的实践系统”[11],号称对性能影响仅有 0.28%。文中提到,实验中发现禁用全局比特位(也就是标记权限的比特位)对于性能的影响竟然是微乎其微的,而且当代 CPU 对于 TLB(可以看做是内存页的映射)的优化对性能影响并不会太大。有兴趣的可以去研究下这套方案,虽然 0.28% 这个数字很值得商榷。

毫无疑问,KAISER 对 I/O 性能会造成一定影响,因为 KAISER 的这套方案设计了个 Shadow 地址空间页结构,去掉了标记权限的全局比特位,需要频繁切换 CR3 寄存器,并清除非全局的 TLB 项。根据现在的测试,应用KAISER补丁之后可以观察到高性能NVMe设备、网络互联设备有明显的性能下降。

QQ20180112-055517@2x.png

Intel 方面已经确认,针对八代酷睿芯片,补丁会造成 6% 的性能下滑——只不过大部分计算机用户并不会造成太大的影响,主要在日常使用方面并不会有明显差异[13]。Intel 甚至还主动公布了跑分测试结果[14],就近两代酷睿产品,使用涉及复杂 JavaScript 操作的 web 应用的用户可能受到最大影响,补丁前后的最大差别可达 10% ,游戏等图形密集型工作负载或财务分析等计算密集型工作负载受到的影响最低。第六代 Skylake-S 平台性能影响可能稍高,SYSMark 2014 SE 测试总体差距在 8% 左右。

微软方面也已经发布声明[15],提到:

- 针对 Skylake、Kaby Lake 及更新的 CPU,Windows 10 系统在性能方面仅有百分比个位数的下滑,而且大部分用户基本是察觉不出来的;

– 针对 Haswell 及更早的 CPU,Windows 10 则在性能方面会有“更为显著的下滑”,“部分用户会察觉到系统性能的下降”;

– Windows 7/8 的情况更早,Haswell 及更老的 CPU 用户,绝大部分都会感知到系统性能的下降。

同时微软还提到,对于运行 Windows Server 的任意 CPU,尤其是对 I/O 操作比较敏感的服务任务,“作为缓解方案,如果在 Windows Server 实例中隔离不受信任的代码,性能方面会有更为明显的影响”。所以微软建议服务器用户,在安全和性能之间做出更为权衡的选择——这也是我们能够看到的,为数不多微软针对 IT 管理员给出这样的说辞。

值得一提的是,<strong>KAISER 本身也存在一定的局限性</strong>,鉴于 x86 架构的设计,某些高权限内存位置必须映射到用户空间。这仍然留下了一小撮攻击面,这些内存位置仍然可从用户空间读取,只不过这些内存位置并没有什么敏感信息——但善加利用,比如这些位置仍然可以包含指针,打破 KASLR 也就成为可能了。

另外,<strong>针对 Spectre,目前并没有真正行之有效的根治方案,包括 KAISER 对 Spectre 也是没用的。原 paper 提供的一些缓解思路并不能从根本上杜绝 Spectre 攻击。Meltdown 与 Spectre 官网在 Q&amp;A 中表态说[16],修复 Spectre 并不容易,未来可能会困扰我们很长一段时间;补丁会让攻击变得更困难。</strong>

性能与安全的割裂是否将永远持续?

Meltdown 和 Spectre 两者几乎影响到当代所有的电子设备,包括笔记本、笔记本、智能手机,以及对企业而言悲剧的云服务器。但凡操作系统(微软、苹果、谷歌、Linux)、芯片制造商(苹果、三星、Intel、AMD 等)似乎都在慌忙地对这次事故做响应,尤其 Intel 公关,以为去年忙完 Zen 的灭火之后今年得交好运了,没想到碰上这么个事儿。但从微软去年 11 月的动作来看,大约 Meltdown 的修复从来不是最近才开始的。

intel_logo_reuters.jpg

不过对个人设备的 Meltdown 和 Spectre 攻击,初始攻击向量要么是恶意程序,要么是 web 端的恶意链接:这仍然是需要用户去交互的,比如引诱用户下载恶意程序,或者点击某个恶意链接。但在云平台就不同了,The Verge 认为,这次漏洞波及最大的应该是云计算,毕竟云平台是个更大的威胁空间。一台云服务器上,就会有好些租户,所以亚马逊、谷歌、微软、阿里也都全面卷了进来。如 paper 中对虚拟环境的测试那样,meltdown 要针对同一台云主机上的其他租户窃取数据,也是完全可行的。大量中小型企业的基建现如今就在云上,这样一来隐患就显得相当之大。主要的这几家云服务提供商也已经很快打上了补丁,谷歌发言人说,其云服务器现在已经不受 Meltdown 和 Spectre 影响,但没有透露究竟是怎么保护 Spectre 的[17]。

硬件层面的某一种优化就会带来微架构元素的状态改变,并危及安全软件的实施——这其实是 20 年前的一个共识[18]。比如说出现一个漏洞,是由于硬件优化导致微架构状态的变化,如果某种加密算法没有及时针对可能出现的泄露做防护,就会产生这样的 BUG。不过 Meltdown 的出现颠覆了现状,因为攻击者甚至可以读取每个比特位,而无视精度问题,这是任何算法改进都无法做出防护的。就我们而言,唯一可以冠冕堂皇对企业做出的建议就是:把安全融入到开发中去。

信息安全行业现如今正在给企业、互联网产品灌输一个理念,即将安全融入到开发环节中去,而不是在开发结束后再检测安全问题——虽然这可能只是安全行业希望赚到钱的忽悠。这其实是相当理想化的一种开发理念,开发和安全一直以来都有不可逾越的鸿沟,光是开发人员和安全人员两者技能倾向性上的不同,而且开发和安全的职能是完全不一样的,安全在很多管理者眼中除了烧钱并没有什么卵用。就好像 Intel 的工程师们大部分都并非安全专家,何况 AMD 最近给的压力这么大,再说这次的攻击还是用了边信道这么赖皮的方法,任谁也很难想到。所以这种“融合”会不会发生需要打个巨大的问号。

但 MIT 的一份研究显示,去年一年召回的植入式医疗 IoT 设备达到 150 万台,都是因为安全问题,其中还包括心脏起搏器——这个数字听来似乎并没有很庞大。如果说现如今的某些硬件遭遇安全问题,比如这次的处理器漏洞,还能在操作系统层面加以弥补,那么医疗 IoT 设备出现安全问题就没有这种可行性了。而且这种程度的召回已经不仅限于企业的经济损失,更关乎到用户本身的生命健康,还要考虑召回的难度问题。随着工业 5.0 时代的到来,未来不光是医疗设备,植入式设备、VR/AR 设备的出现可能会越来越常见,比如可能出现植入人体的娱乐设备、传感器,那么当他们出现严重的安全问题时,我们又该何去何从?或许开发和安全的融合会是为数不多的一条思路,至少能够缓解安全问题。

*本文作者:欧阳洋葱,来源:爱活网,转载请注明来源和 FreeBuf.COM。

本文由 华盟网 作者:AlexFrankly 发表,其版权均为 华盟网 所有,文章内容系作者个人观点,不代表 华盟网 对观点赞同或支持。如需转载,请注明文章来源。

0

发表评论