地球上有没有一种安全漏洞,广泛存在于各种计算设备之中,并且难以被检测,难以被修复?在2018年的第一个月里,Meltdown与Spectre两大处理器安全漏洞给出了这个问题的部分答案。
边信道攻击进入热核时代
信息安全行业有一类很高端的攻击方法,叫做边信道攻击(side channel,也有译作侧信道攻击,或旁路攻击的),听着就感觉很高级有没有。这种攻击出现于上世纪90年代,以色列的高等学府似乎经常研制边信道攻击的奇技淫巧——所谓的边信道,就是不从正面进攻,而是从侧面窃取或传递信息的方式,比如利用设备运行时产生的电磁辐射,或者电流导致的机械波动噪声,来分析破解出密钥,甚至还能利用散热风扇产生的噪声、机箱上的LED灯闪烁变化来将信息传出。
许多边信道攻击条件都非常苛刻,而且攻击噪声比较大,情报获取的正确率有时很堪忧,所以边信道攻击在此之前只有2016年精妙的Linux TCP漏洞(CVE-2016-5696)造成广泛的影响[1]。
如果将各种漏洞比作攻击武器,那Linux TCP漏洞之前的各种边信道攻击类似冷兵器——致命但难以造成广泛破坏。Linux TCP漏洞攻击思路则将边信道攻击水平提升到了热兵器时代,1年之后,Meltdown和Spectre漏洞正式开启了热核时代(即便边信道攻击本身严格意义上并不需要漏洞的介入)。
核武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,也需要持续的冲刷,这给此类攻击制造了大量机会。
这次有关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命中率有多高。
当代处理器都采用多层级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,末级缓存)中的所有数据,这也间接降低了攻击实施的难度。
假设有这样一个目标进程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的时间就很短了)——撞上了!
如此循环往复,如上图所示,这样一来就能得知哪些数据被存取过。这就是所谓的FLUSH RELOAD L3 cache攻击。当然了,这种攻击还是存在精度问题的,比如说上图C的情况,恶意进程请求载入某条line,在极短时间内目标进程也恰巧请求载入这条line,这样观测到的结果就会有问题;另外还需要考虑一些处理器的性能优化方案的噪声干扰,比如说现在的处理器的空间局部性实现的数据预取(prefetch),还有像是分支预测,这要求在设计攻击程序(建议采用循环体之类以内存访问相对频繁的为目标,如上图E)和对结果进行分析的时候有过滤策略。
比如像上面这样,其中第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]。
就Meltdown而言,这种攻击方式主要利用的是当代处理器的乱序执行特性,可以在不需要进行系统提权的情况下,就读取任意内核内存位置,包括敏感数据和密码,甚至拿下整个内核地址空间。不过它对AMD和ARM的处理器也是无效的,但原因似乎并不像上面这样。研究人员在paper中提到[6],Meltdown在AMD CPU上的攻击复现并不成功,但可能只需要对攻击进行一定优化,深入挖掘依然可能会成功,比如对竞争条件进行一些调整,所以Meldown攻击或许也并不仅限于Intel。
“乱序执行”本身不是个生词了,当代的高性能处理器都有乱序执行特性:早些年CPU性能每年翻番都并不稀罕,通过增加核心数、时钟频率,加宽管线外加工艺迭代就能实现。但是在顺序执行架构达到瓶颈之后,架构的优化就成为一个重要方向:顺序执行架构中,指令完全按照一个不变的顺序执行,就算CPU运算单元的执行速度很快,CPU却浪费大量时间在等待,许多单元处在闲置状态,所以乱序执行成为提升效率的重要解决方案。好比你攒台PC,如果显卡还没到货,肯定不会守在门口傻等,而是把其余部分先组装好。
1967年,Tomasulo最早开发出了可用于动态规划指令乱序执行的算法,当时他就提出了一个叫 Unified Reservation Station的东西。CPU在不需要将某个值存储到寄存器并读取的情况下,就可以在这里使用值。此外,Unified Reservation Station还通过一个CDB(共用数据总线)把所有执行单元连接起来。如果某个操作数尚未准备就绪,URS单元可以监听CDB,获取里面的数据,然后就可以直接执行指令了。(请注意,这一段对于理解后面的Meltdown攻击流程很重要)
在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运行在特权模式下才可以访问。这一步是由操作系统决定的,操作系统在相应的转换表中禁用用户可访问属性即可。
实际上内核地址空间不仅包含内核自己用的部分,也需要在用户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就需要获取到这种随机偏移量。但要做到这一点也并不难。
这里我们看一个乱序执行的简单例子。这段代码第一行就产生了异常(不用管是怎么产生异常的),按照控制流来说,发生异常就该跳到操作系统的异常处理程序,应用终止,后面的代码就不会继续执行了。但因为有乱序执行的存在,第三行指令可能已经部分被执行了,只不过没有retire,所以实际上并不会在架构层面产生可见的影响,即它对寄存器、内存都不会有影响。
这部分执行最终会被丢弃(恢复状态),寄存器和内存的内容也不会执行commit。但它对微架构层面有影响——这里所谓的微架构也就是cache部分了。在乱序执行(第三行指令)过程中,引用的内存读取到寄存器上,也存储到了cache里面,即便最终CPU发现异常以后清空整条管线,cache中的内容也依然会保留。这样一来,利用本文第一部分提到的cache边信道攻击就可以获知其中的信息了(此处是要提取 data 的值):也就是不停探测某个内存位置是否进入cache。
完整的Meltdown攻击主要分成两个组成部分,第一部分就是让CPU执行某个永远不会在路径中被执行的指令,如上例中的第三行代码——我们将这种指令称作 transient instruction。真正要将这种transient instruction应用到实践攻击中,就要求指令序列中包含密钥之类的东西——就是攻击者想要窃取的数据。
Meltdown的第二部分主要就是我们这篇文章第一部分提到的那种基于时间的边信道攻击了,把密钥给恢复出来,或者说对L3 cache进行攻击获取数据的方法。这样我们基本上已经把Meltdown的攻击过程给说清楚了。但实际操作中可能会碰到很多复杂的情况,如攻击者要获取密钥,那么就意味着要访问用户不可访问的page,比如说内核页——访问这样的位置,由于没有权限,所以会导致异常。攻击者需要去想办法处理这样的异常,否则的话进程就要被终止了。处理方法可以是把攻击程序分成不同部分,只在子进程中执行后面的transient instruction序列,而父进程通过后续的边信道攻击来恢复密钥。
另外,在第二部分的边信道攻击,也就是上图中所谓构建covert channel秘密通道,悄悄把已经存在于cache中的密钥给窃取到了。在这部分里,我们可以把transient instruction序列看成是这个通道的发射端,而接收端可以是不同的进程(和第一部分的transient instruction可以是无关的),比如可以是前面提到的父进程。
如果你还是不明白,这里举个简单例子:有家餐厅(CPU),这家餐厅有个收银员,还有个厨师(执行引擎)。小明和小红每天都去这家餐厅吃东西,小红(内核)每次点东西的方法都是对收银员说:我要和昨天一样的东西——然后厨师把东西做出来,收银员给小红打包带走。小明很想知道小红吃的究竟是什么,所以他有一天跟在小红身后;小红点完以后,小明跟收银员说:我要和小红一样的东西。收银员说:你这是侵犯人家的隐私,滚出我们餐厅(发生异常)!于是小明就被人收银员一脚踢出了餐厅(管线清空)。
小明想了一个新的办法,第二天他带着小方一起去,他俩跟在小红后面。小红照常点餐(点的是汉堡),小明大声喊:我要和小红点一样的东西。餐厅的厨师听到了,于是就做了两个汉堡出来(执行引擎进行乱序执行,第二个汉堡进入了cache)。但小明再次因为侵犯隐私,被收银员扔出了餐厅。小方(cache边信道攻击)这个时候上前了,他和小明是串通一气的,他对收银员说:我要点汉堡、薯条、鸡腿、土豆泥、红豆派、可乐、鸡翅……最终小方发现,最快送上前来的是汉堡(因为在cache里面),于是就知道小红点的其实是汉堡。
Spectre,处理器背后的幽灵
Spectre的情况在这里只略作介绍,下文也不再进行展开。笔者认为,Spectre的利用难度要大很多(攻击成本更高),但牵涉面更广。Spectre基本思路和Meltdown是差不多的,最终也是通过FLUSH RELOAD这样的cache边信道攻击来获取内存里面的东西,只不过切入方式利用的是分支预测——就是我们前面提到的,比如If Then语句出现,处理器在还没有判断if条件是否满足的情况下,就会去预测后面的分支,如果预测正确就可以有效提升运算效率。对分支预测的利用,无论是分析成本,还是攻击成本,都大了不少,只不过包括AMD、ARM、Intel、等在内的处理器全部都中招。
Spectre攻击涉及到两个漏洞,分别是CVE-2017-5753和5715。针对CVE-2017-5753,如上面这个语句,绿色部分那一行是个if条件语句。上述代码,如果arr1->length, arr2->data[0x200]和arr2->data[0x300]都还没有进入cache,处理器都还没有判断条件语句,但其它数据都已经cache,而且分支条件预测为真,那么处理器在加载arr1->length之前就会加载arr1->data[untrusted_offset_from_caller]的值,并开始载入arr2->data数据依赖偏移,将相应cache line载入L1 cache。
但最后处理器发现,预测错误,此刻包含arr2->data[index2]的cache line已经位于L1 cache,那么此时由恶意程序去请求arr2->data[0x200]和arr2->data[0x300],测量请求载入的时间,就能判断index2的值是0x200还是0x300了,也就可以知道arr1->data[untrusted_offset_from_caller]&1是0还是1。
篇幅有限,我们无法再展开做更为具体的分析和讲解,不过这个逻辑本质上和Meltdown对于乱序执行的利用有些相似,都建基于处理器率先执行了后面的指令,而且还把数据放在了cache中,但明显更为复杂。最终也都通过恶意程序去检查读取加载指令所需时间,来推测某值。
要利用这样的行为,攻击者需要在目标环境中构造这样的代码模式执行。有两种思路,要么这种代码模式在现有代码中就有,要么需要有个解释器(interpreter)或者JIT引擎,最终可以生成这种代码模式。第一种攻击思路,对实际环境的要求太苛刻或者说很难达成;第二种是谷歌采用的方案,他们选择了eBPF字节码解释器和JIT引擎。
这种攻击还涉及一些复杂的问题,比如说需要对分支预测机制首先进行训练,令其足以产生错误的预测结果;然后操作目标进程,执行上述构造出来的代码模式;最后用上咱们文章第一部分提到的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就会生成异常,用户空间无法简单地读取到这类地址的内容。
不过乱序执行上线了,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里面还有后面乱序执行算出来的货呢,这些货没有清掉。
第二步:传输密钥
如果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写入时间最短),也就直接表示密钥的值了。
对256个page进行迭代,观察读取时间,仅有一个cache命中,这个命中的page就是data值啦!
这两部分是不是表示看不懂了?没关系,反正只要搞清楚,恶意进程依靠一些巧妙的算法,试出了这个敏感数据是什么——根据文章第一部分FLUSH RELOAD的思路,再用编程的一些技巧就可以搞定。
通过对前面三个步骤进行重复操作,对所有不同的地址进行迭代,攻击者是可以还原出整个内存样貌的。研究人员利用这套方案,在Intel酷睿i7-6700K平台,外加Ubuntu 16.10(Linux内核4.80)操作系统中进行攻击,能够从内存中获取及其向web服务器发出请求的HTTP header。另外,还能获取Firefox 56浏览器的内存部分,并找出存储在其内部密码管理器中的密码。
Firefox 56存储的密码一览无遗啊
Meltdown的这套攻击方案,利用的是漏洞CVE-2017-5754。实际上,这种攻击技术的提出并不是近期才有的,去年7月份就有人写过一篇警告文《Negative Result: Reading Kernel Memory From User Mode》[10],不过此人当时未能完美复现Meltdown造成的问题,但他自己说似乎是“开启了潘多拉魔盒”。
这是个实实在在的硬件级漏洞,属于CPU微架构实施层面的漏洞。鉴于硬件产品的特殊性,让Intel召回这20年的产品是不现实的。不过封堵这种程度的漏洞,最佳方案难道不是禁用乱序执行吗?但这对当代CPU性能影响将会是灾难性的。
硬件层面实际上还可以对权限检查和寄存器读取,实施一个序列化过程——如果权限检查失败,则不读取该内存地址。但这么做的代价也会非常大,每次内存读取都要进行权限检查。更加现实的方案应该是从硬件层面实现对用户空间和内核空间的隔离:设立分隔比特位,内核必须位于地址空间上部,用户空间则必须位于地址空间下半部。这样一来,内存读取即便通过虚拟地址都能发现读取目标是否违反安全边界。
熔断易挡,幽灵难防
硬件层面的修复对我们来说都是空谈。操作系统层面,各主流操作系统这两天都已经陆续推了针对Meltdown的修复补丁。主要是一种研究人员推荐叫做KAISER的方案,在Linux内核更新中这项特性叫KPTI(内核页表隔离)——其实现方法是,让内核不要映射到用户空间中。这种方案其实早两年就有了,当时是为了杜绝攻破KASLR的边信道攻击提出的。
国外网友最早发布的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设备、网络互联设备有明显的性能下降。
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管理员给出这样的说辞。
值得一提的是,KAISER本身也存在一定的局限性,鉴于x86架构的设计,某些高权限内存位置必须映射到用户空间。这仍然留下了一小撮攻击面,这些内存位置仍然可从用户空间读取,只不过这些内存位置并没有什么敏感信息——但善加利用,比如这些位置仍然可以包含指针,打破KASLR也就成为可能了。
另外,针对Spectre,目前并没有真正行之有效的根治方案,包括KAISER对Spectre也是没用的。原paper提供的一些缓解思路并不能从根本上杜绝Spectre攻击。Meltdown与Spectre官网在Q&A中表态说[16],修复Spectre并不容易,未来可能会困扰我们很长一段时间;补丁会让攻击变得更困难。
性能与安全的割裂是否将永远持续?
Meltdown和Spectre两者几乎影响到当代所有的电子设备,包括笔记本、笔记本、智能手机,以及对企业而言悲剧的云服务器。但凡操作系统(微软、苹果、谷歌、Linux)、芯片制造商(苹果、三星、Intel、AMD等)似乎都在慌忙地对这次事故做响应,尤其Intel公关,以为去年忙完Zen的灭火之后今年得交好运了,没想到碰上这么个事儿。但从微软去年11月的动作来看,大约Meltdown的修复从来不是最近才开始的。
不过对个人设备的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设备的出现可能会越来越常见,比如可能出现植入人体的娱乐设备、传感器,那么当他们出现严重的安全问题时,我们又该何去何从?或许开发和安全的融合会是为数不多的一条思路,至少能够缓解安全问题。
参考资料:
[1]Linux设备TCP连接的有趣漏洞:传说中的Off-Path劫持 [2]Reading privileged memory with a side-channel [3]FLUSH RELOAD: a High Resolution, Low Noise, L3 Cache Side-Channel Attack [4]Spectre Research Paper [5]Cache Template Attacks: Automating Attacks on Inclusive Last-Level Caches [6]Meltdown Research Paper [7]Retpoline: a software construct for preventing branch-target-injection [8]Mitigations landing for new class of timing attack [9]Site Isolation–The Chromium Projects [10]Negative Result: Reading Kernel Memory From User Mode [11]KASLR is Dead: Long Live KASLR [12]Initial Benchmarks Of The Performance Impact Resulting From Linux’s x86 Security Changes [13]Intel Offers Security Issue Update [14]Blog Benchmark Table–Intel [15]Understanding the performance impact of Spectre and Meltdown mitigations on Windows Systems [16]Meltdown and Spectre–Vulnerabilities in modern computers leak passwords and sensitive data [17]The CPU catastrophe will hit hardest in the cloud [18]Timing Attacks on Implementations of Diffe- Hellman, RSA, DSS, and Other Systems