原来Intel CPU漏洞是它引起的!

导读 2017年6月1日,Google的安全团队向Intel、AMD、ARM报了一个硬件级的漏洞,造成的危害是内核数据泄露,修复该漏洞的代价是至少30%的性能损失。2017年末Linux内核社区推出了KPTI「Kernel Page Table Isolation」补丁,Linus Torvalds在内核邮件列表上毫不留情地抨击了Intel。

2017年6月1日,Google的安全团队向Intel、AMD、ARM报了一个硬件级的漏洞,造成的危害是内核数据泄露,修复该漏洞的代价是至少30%的性能损失。2017年末Linux内核社区推出了KPTI「Kernel Page Table Isolation」补丁,Linus Torvalds在内核邮件列表上毫不留情地抨击了Intel。原来Intel CPU漏洞是它引起的!

背景

安全人员将这两个漏洞命名为MeltdownSpectre;Meltdown目前只存在于Intel的处理器和部分ARM处理器,Spectre存在于一切有乱序执行的现代处理器架构里面,包括AMD。从原理上来说漏洞无法彻底修复。

本次的漏洞会对所有云厂商造成较大影响,已经有迹象表明有黑客在利用漏洞攻击云系统。Microsoft Azure中国区已发布公告称,将于北京时间2018 年 1 月 4 日上午 11:30 开始自动重启受影响的虚拟机,并全部关闭向部分客户开放的自助维护窗口;AWS也发送了通知邮件声称本周五将进行重大安全更新。

原因

一切还是要从CPU指令执行的框架——流水线说起。Intel当然不至于明知你要用一个用户态的进程读取Kernel内存还会给你许可。但现代CPU流水线的设计,尤其是和性能优化相关的流水线的特性,让这一切充满了变数。

给所有还没有看过云杉网络连载的系列文章《x86高性能编程笺注系列》的读者一点背景知识的介绍:

x86 CPU为了优化性能,在处理器架构方面做了很多努力。诸如“多级缓存”这一类的特性,是大家都比较熟悉的概念。还有一些特性,比如分支预测和乱序执行,也都是一些可以从并行性等方面有效提升程序性能的特性,并且它们也都是组成流水线的几个关键环节。即便你暂时还不能准确理解其含义,但望文生义,也能看出来这肯定是两个熵增的过程。熵增带来无序,无序就会带来更多漏洞。

缓存的困境

讲缓存,必然先挂一张memory hierarchy镇楼:

原来Intel CPU漏洞是它引起的!

不过我要说的和这个没太大关系。现在需要考虑的是,如果能读取到内核地址的内容,那这部分内容最终肯定是跑到缓存中去了,因为真正直接和CPU核心交互的存储器,就是缓存。这对一级缓存(L1 Cache,业内也常用缩写L1$,取cash之音)提出的要求就是,必须要非常快,唯有如此才能跟上CPU处理核心的速度。

ide Notes: 为什么在不考虑成本的情况下缓存不是越大越好,也是因为当缓存规模越大,查找某一特定数据就会越慢。而缓存首先要满足的要求就是快,其他的都是次要的。

根据内核的基本知识我们知道,进程运行时都有一个虚拟地址「Virtual address」和其所对应的物理地址「physical address」。

从虚拟地址到物理地址的翻译转换也由CPU通过page table完成。Page table并不储存在CPU里,但近期查找到的Page table entry「PTE」都像数据一样,缓存在了CPU中的translation lookaside buffer「TLB」里。为了不再过多堆砌术语和名词,画张图说明一下:

原来Intel CPU漏洞是它引起的!

当CPU根据程序要求需要读取某个地址上的数据时,首先会在L1 Cache中查找。为了适应CPU的速度,L1缓存实现为Virtually indexed physically tagged「VIPT」的形式,即用虚拟地址即可直接读取该虚拟地址对应的物理地址的内容,而不再需要多加一道转换的工序。

如果L1 Cache miss,则会在下级缓存中查找。但越过L1 Cache之后,对L2$和L3$的速度要求就不再这么严苛。此时CPU core给出的虚拟地址请求会先通过TLB转换为物理地址,再送入下级缓存中查找。而检查进程有没有权限读取某一地址这一过程,仅在地址转换的时候发生,而这种转换和检查是需要时间的,所以有意地安排在了L1 Cache之后。

L1缓存这种必须求“快”的特性,成了整个事件的楔子。

分支预测

分支预测是一种提高流水线执行效率的手段。在遇到if..else..这种程序执行的分支时,可以通过以往的历史记录判断哪一分支是最可能被执行的分支,并在分支判断条件真正返回判断结果之前提前执行分支的代码。详情可以在上面提到的连载文章中阅读。

需要强调的是,提前执行的分支代码,即便事后证明不是正确的分支,其执行过程中所读取的数据也可以进入L1缓存。在Intel的官网文档《Intel® 64 and IA-32 Architectures Optimization Reference Manual》第2.3.5.2节中指:

L1 DCache Loads:

– Be carried out speculatively, before preceding branches are resolved.

– Take cache misses out of order and in an overlapped manner.

Show you the [伪] code:

if (likely(A < B)) {    value = *(kernel_address_pointer);}

当分支判断条件A < B被预测为真时,CPU会去提前执行对内核地址的读取。当实际条件为A > B时,虽然内核的值不会真正写入寄存器(没有retire),但会存入L1 Cache,再加之上一节介绍的,获取L1 Cache的值毋须地址转换,毋须权限检查,这就为内核信息的泄漏创造了可能。

从理论上来讲,如果可以控制程序的分支判断,并且可以获取L1缓存中的数据(这个没有直接方法,但可以通过其他间接手法)的话,就完全可以获取内核信息。而分支预测这种特性是不能随随便便就关闭的,这也就是这次问题会如此棘手的原因。

乱序执行

还有一个原因是乱序执行,但原理大致类似。乱序执行是Intel在1995年首次引入Pentium Pro处理器的机制。其过程首先是将我们在汇编代码中看到的指令“打散”,成为更细粒度的微指令「micro-operations」,更小的指令粒度将会带来更多的乱序排列的组合,CPU真正执行的是这些微指令。

没有数据依赖的微指令在有相应执行资源的情况下乱序并行执行,进而提升程序的并行程度,提高程序性能。但引入的问题是,读取内核数据的微指令可能会在流水线发出exception之前将内核数据写入L1 Cache。与分支选择一样,为通过用户态进程获取内核代码提供了可能。

限于篇幅,更详细的内容读者可以在国外安全团队发布的消息中获取。

后续

刚刚查阅之前连载中的一些细节的时候,看到在“流水线”那一章里写过这样一段话:

在面对问题的时候,人总是会倾向于引入一个更复杂的机制来解决问题,多级流水线就是一个例子。复杂可以反映出技术的改良,但“复杂”本身就是一个新的问题。这也许就是矛盾永远不会消失,技术也不会停止进步的原因。但“为学日益,为道日损”,愈发复杂的机制总会在某个时机之下发生大破大立,但可能现在时机还没有到来:D

很难讲现在是不是就是所谓的那个“时机”。虽然对整个行业都产生了负面影响,但我对此仍保持乐观。因为这就是事物自然发展的一个正常过程。性能损失并不是一件坏事,尤其是对牙膏厂的用户来说。

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/116322.html

(0)
上一篇 2021年8月27日
下一篇 2021年8月27日

相关推荐

发表回复

登录后才能评论