首先我们先来解读一下CPU漏洞:熔断和幽灵
Meltdown(熔断),应对漏洞CVE-2017-5754;
Spectre(幽灵),对应漏洞CVE-2017-5753/CVE-2017-5715。
利用Meltdown漏洞,低权限用户可以访问内核的内容,获取本地操作系统底层的信息;当用户通过浏览器访问了包含Spectre恶意利用程序的网站时,用户的如帐号,密码,邮箱等个人隐私信息可能会被泄漏;在云服务场景中,利用Spectre可以突破用户间的隔离,窃取其他用户的数据。
Meltdown漏洞影响几乎所有的Intel CPU和部分ARM CPU,而Spectre则影响所有的Intel CPU和AMD CPU,以及主流的ARM CPU。从个人电脑、服务器、云计算机服务器到移动端的智能手机,都受到这两组硬件漏洞的影响。
这两组漏洞来源于芯片厂商为了提高CPU性能而引入的新特性。现代CPU为了提高处理性能,会采用乱序执行(Out-of-Order Execution)和预测执行(Speculative Prediction)。乱序执行是指CPU并不是严格按照指令的顺序串行执行,而是根据相关性对指令进行分组并行执行,最后汇总处理各组指令执行的结果。预测执行是CPU根据当前掌握的信息预测某个条件判断的结果,然后选择对应的分支提前执行。
乱序执行和预测执行在遇到异常或发现分支预测错误时,CPU会丢弃之前执行的结果,将 CPU的状态恢复到乱序执行或预测执行前的正确状态,然后选择对应正确的指令继续执行。这种异常处理机制保证了程序能够正确的执行,但是问题在于,CPU恢复状态时并不会恢复CPU缓存的内容,而这两组漏洞正是利用了这一设计上的缺陷进行测信道攻击。
漏洞原理解析
Meltdown漏洞原理
乱序执行可以简单的分为三个阶段,如下图所示:
每个阶段执行的操作如下:
1)获取指令,解码后存放到执行缓冲区Reservations Stations
2)乱序执行指令,结果保存在一个结果序列中
3)退休期Retired Circle,重新排列结果序列及安全检查(如地址访问的权限检查),提交结果到寄存器
结合Meltdown利用的代码片段来看:
Meltdown漏洞的利用过程有4个步骤:
1) 指令获取解码
2) 乱序执行3条指令,指令2和指令3要等指令1中的读取内存地址的内容完成后才开始执行,指令3会将要访问的rbx数组元素所在的页加载到CPU Cache中。
3) 对2)的结果进行重新排列,对1-3条指令进行安全检测,发现访问违例,会丢弃当前执行的所有结果,恢复CPU状态到乱序执行之前的状态,但是并不会恢复CPU Cache的状态
4) 通过缓存测信道攻击,可以知道哪一个数组元素被访问过,也即对应的内存页存放在CPU Cache中,从而推测出内核地址的内容
Spectre漏洞原理
与Meltdown类似,Spectre的原理是,当CPU发现分支预测错误时会丢弃分支执行的结果,恢复CPU的状态,但是不会恢复CPU Cache的状态,利用这一点可以突破进程间的访问限制(如浏览器沙箱)获取其他进程的数据。
Spectre的利用代码片段:
具体攻击过程可以分为三个阶段:
1)训练CPU的分支预测单元使其在运行利用代码时会进行特定的预测执行
2)预测执行使得CPU将要访问的地址的内容读取到CPU Cache中
3) 通过缓存测信道攻击,可以知道哪一个数组元素被访问过,也即对应的内存页存放在CPU Cache中,从而推测出地址的内容。
漏洞验证
目前开源社区github已经放出来了漏洞的验证代码(PoC),如下:
https://github.com/Eugnis/spectre-attack
https://github.com/feruxmax/meltdown
https://github.com/gkaindl/meltdown-poc
https://github.com/turbo/KPTI-PoC-Collection
经过我们和其他安全研究人员实际验证,漏洞可在Windows、Linux、Mac-OS等操作系统下,成功读取任意指定内存地址的内容,如下图所示:
Windows:
Ubuntu 16.04:
针对这两组漏洞,各家芯片厂商,操作系统厂商,浏览器厂商,以及云服务厂商,都积极采取措施,发布安全公告,并及时推出缓解措施和修复补丁。
CPU作为核心部件,在计算机历史上可谓是突飞猛进发展,而我们知道,一台电脑除了cpu,还需要内存和硬盘(ram和rom(固态硬盘也是rom,eeprom))
当cpu开始工作时,硬盘的数据会经过PCH(所谓的南桥),然后再经过CPU,之后才被加载进内存开始执行。
虽然cpu的执行速度十分的快,但由于半导体企业的综合考量,因此主内存(也就是我们所说的内存条和手机运存)被设计成了动态的(与cpu内部静态的寄存器相对)。
我们称之为DRAM,也就是动态随机存取存储器,原理是采用电容存储,通过间隔一段时间,就重新刷新内存,没有被刷新的电容就会丢失存储的数据。
这种简单高效的dram,一个bit只需要一个晶体管和一个电容即可完成,非常的节省成本,现代的内存很容易就能做到16GB,32GB,128GB的高容量,为容纳Chrome提供了充实的条件。
无论多大的内存在我面前一律占用80%,而cpu内部的寄存器就需要6个甚至更多的晶体管,成本是极其高昂的,并且功耗也比dram来的高,因此寄存器的数量很少,用来暂时保存程序运算。借此,我们就能得出一个存储器结构,如下。
这是存储器结构,这个结构看上去似乎没有问题,但由于现代的处理器工作速度十分迅速,而主内存的速度却没有这么快,只有0~60GB/s的速度,并且容量又小,现代的操作系统,大型游戏动辄就是几十GB,而且,在一些有大量执行重复任务的程序里,显然对又慢又小的主内存来回写是浪费处理器的性能,借此,上世纪90年代,随着奔腾cpu的横空出世,介于主内存和处理器之间的缓冲区-缓存诞生了。
缓存,顾名思义,是作为缓冲区的一块内存,它虽然没有cpu的寄存器来的高速,但肯定要比主内存来的快,因此,cpu执行程序,不再是取主内存所存储的代码和数据,而是从缓存里取,如图↓
(Cache即缓存),缓存基于局部性原理,什么是局部性原理呢?
处理器要执行程序,首先他会从缓存里寻找指令和数据,如果没有,就去内存读取,如果内存也没有,就去硬盘读取,如果硬盘也没有,就去远程服务器存储空间读取(当然,这需要更高级的操作系统的配合),如果都没有找到数据,那就不执行程序
1.处理器内部的寄存器,速度最快,处理器主要的数据处理场所也是寄存器,造价高昂,容量小(每一个通用寄存器只有8字节[四字]的大小)
2.处理器内部的Level 1缓存,通常被访问次数小于寄存器,简称L1缓存,L1缓存又分成两个部分,一个部分存储指令,一个部分存储数据,现在的x86处理器一般都是一个核心搭配32KB的L1缓存,指令和数据各一个。
3.L2缓存,用来做更大的数据缓冲区,容量更大,大小在2MB~8MB左右,被访问次数通常更低,并且速度更慢。
4.L3缓存,用于多个核心共享使用,做更大的数据缓冲区,大小在8MB~80MB都有,处理器厂商一般用产品线划分来决定一个处理器的L3缓存,同时L3缓存的大小也十分影响运行性能。
5.内存,硬盘,这些不用我多说了。
上面都是废话,下面才是重点:
程序想要访问某一部分的内存的时候,cpu首先会看这部分内存在不在缓存里面,如果有,那么就能快速得到数据,如果没有,就需要去内存里面找,速度就会慢一些,而这个时间差,是可以被利用来获取数据的!
这就得了解另一个机制,叫内存分页,一些学过计算机的可能知道,计算机里面的基本单位是字节(Byte),千字节就是KiloByte(KB),百万字节就是MegaByte(MB),十亿字节就是GigaByte(GB)。。。。
如上面例子所说,当cpu在缓存里找不到数据时,就会从内存里,先获得数据,再放进缓存(方便程序接着继续用),而cpu为了加快放进缓存的进度,就把内存以4KB,1MB等容量,划分为一个单位,把数据放进缓存的时候,不是一个字节一个字节的放,而是一次放4kb,或者一次放1mb等等。。
以上,是熔断漏洞的几个要素罢了,而核心要素,在于cpu一个提升速度的根本要素:乱序执行!
现代cpu执行程序的代码的时候,其实并不是一条一条顺序执行(这样安全,但是不够快),为了提升速度,都是以三条代码,四条代码为单位,一批一批的丢进cpu,(Cpu会自动处理代码之间的先后关系,所以不会错乱的),不过有些时候,速度太快了,会执行一些涉及系统安全的非法代码,这些代码本身并不能被执行,但是cpu的执行逻辑是:先执行,后判断!
于是,结合以上因素,我们就得到了这段非常厉害的代码:
MOV RAX Byte PTR [Kernel]
注释:把Kernel(中文名内核),也就是系统内核的一个地址,里面的数据,以一个字节(Byte PTR),放到一个叫 RAX的寄存器
这段代码本来不应该被执行(因为涉及系统保护),但是因为cpu的乱序执行,导致这个代码会被执行
SHL RAX 0xC
注释:这是最重要的一行代码,意思是,把RAX这个寄存器的数据,向左移动0xC(12)个比特位,相当于是把这个寄存器的数据乘4096(2的12次方)
4096其实就是一个内存页的大小,从这里看出,攻击者要把数据转化为另一种形式
MOV RBX Qword PTR [RAX+RBX]
注释:RBX是一个可用的寄存器,存放的数据是合法的,不涉及系统隐私。
[RAX+RBX]的意思是什么呢?就是把RAX和RBX相加,把这个相加后的数值,转化为一个地址,并将这个地址里面的数据,以4个字(一个字是2字节,4字就是8字节)为单位,放进一个叫RBX的寄存器
这时候,核心的要点来了,如果RAX+RBX作为一个地址,这个地址不在内存里,那么CPU就会从内存里去找!并且放在缓存里面!!!!!!
MOV RAX Byte PTR [Kernel]
SHL RAX 0xC
MOV RBX Qword PTR [RAX+RBX]
上面三行代码,就是攻击的全部。
在执行后,CPU发现自己执行了涉及系统隐私的代码,会自动回滚之前的操作,把RAX RBX的数据都给摧毁掉,然而,这两个寄存器的值,虽然不以数据的形式存在了,但是还以一个内存页的形式存在于缓存里面,cpu不会去回滚缓存
接着是最后一步,挨个访问内存页,因为我们知道,如果一个内存页放在缓存里,那么cpu就可以更快得到数据,耗时会更短,这时候,只需要记录访问每一个内存页所需的时间,那么,所需时间最短的那个内存页的内存地址,就是RAX+RBX的值!!!
访问页所需时间,这时候,假设我们得到第X个内存页耗时间最短,如何还原出最先我们从系统里面获得的数据呢?
很简单,因为这个内存页的地址是RAX寄存器+RBX寄存器的值,那么我们只要知道RBX最开始的数值,和这个内存页的地址,我们就可以还原出RAX寄存器的值,用公式表达就是:
地址X - RBX的值 = RAX的值
后来又陆陆续续发现了很多CPU漏洞,这些漏洞都是上面所说漏洞的变种,幽灵漏洞原理相似。