Meltdown
Meltdown这一节主要介绍了一篇利用CPU工作的隐藏细节实现操作系统内核攻击的论文,也就是Micro-Architectural Attack。但这种攻击可以被修复,似乎也已被完全修复,并且也只是“学院派”的尝试,当然它确实是能造成攻击成功的。
内核提供安全性的方法是隔离,用户程序不能读取内核的数据,用户程序也不能读取其他用户程序的数据。操作系统中用来实现隔离的具体技术是硬件中的User/Supervisor mode,硬件中的页表。但是为了提高系统调用的性能(切换页表很慢,也会导致CPU的缓存被清空),操作系统同时将用户和内核的内存地址都映射到用户空间。也就是说,当用户代码在运行时,完整的内核PTE也出现在用户程序的页表中,但是这些PTE的pte_u比特位没有被设置,所以用户代码在尝试使用内核虚拟内存地址时,会得到Page Fault。而meltdown正是利用了这个细节以及CPU对指令执行的优化实现了攻击。
(这一节涉及到一些体系结构的知识,还好在研一学了体系结构 :),关于meltdown的前置知识就不说了,有相关基础还是比较好懂的
来看一下meltdown的具体攻击示例(最后一句那里是0):
首先是声明了一个8KB的buffer,这个buffer是我们用户可以访问的到的,正好是两个页表的内容。Meltdown的实验只是窃取一个bit,即0或1,然后将其乘以4096,然后对buffer[0]和buffer[4096]进行flush and reload,也就是刷新这两个页表,使得要么buffer[0]在cache中,要么看到buffer[4096]在cache中。为什么要有这么的大的间隔?是因为硬件有预获取。如果你从内存加载一个数据,硬件极有可能会从内存中再加载相邻的几个数据到cache中。所以我们不能使用两个非常接近的内存地址,然后再来执行Flush and Reload,我们需要它们足够的远,这样即使有硬件的预获取,也不会造成困扰。所以这里我们将两个地址放到了两个内存Page中。
第7行是一些非常费时的指令,它们需要很长时间才能完成。或许要从RAM加载一些数据,这会花费几百个CPU cycle,比如执行了除法,或者平方根等。这些指令花费了很多时间,并且很长时间都不会Retired,因此也导致代码第10行的load很长时间也不会Retired,并给第11到13行的代码时间来完成预测执行。
现在假设我们已经有了内核的一个虚拟内存地址,并且要执行代码第10行。我们知道它会生成一个Page Fault,但是它只会在Retired的时候才会真正的生成Page Fault。我们设置好了使得它要过一会才Retired。因为代码第10行还没有Retired,并且在Intel CPU上,即使你没有内存地址的权限,数据也会在预测执行的指令中被返回。这样在第11行,CPU可以预测执行,并获取内核数据的第0个bit。第12行将其乘以4096。第13行是另一个load指令,load的内存地址是buffer加上r2寄存器的内容。我们知道这些指令的效果会被取消,因为第10行会产生Page Fault,所以对于r3寄存器的修改会被取消。但是尽管寄存器都不会受影响,代码第13行会导致来自于buffer的部分数据被加载到cache中。取决于内核数据的第0bit是0还是1,第13行会导致要么是buffer[0],要么是buffer[4096]被加载到cache中。之后,尽管r2和r3的修改都被取消了,cache中的变化不会被取消,因为这涉及到Micro-Architectural,所以cache会被更新。
第15行表示最终Page Fault还是会发生,并且我们需要从Page Fault中恢复。用户进程可以注册一个Page Fault Handler,并且在Page Fault之后重新获得控制,也就是继续执行程序
现在我们需要做的就是弄清楚,是buffer[0]还是buffer[4096]被加载到了cache中。现在我们可以完成Flush and Reload中的Reload部分了。第18行获取当前的CPU时间,第19行load buffer[0],第20行再次读取当前CPU时间,第21行load buffer[4096],第22行再次读取当前CPU时间,第23行对比两个时间差。哪个时间差更短,就可以说明内核数据的bit0是0还是1。如果我们重复几百万次,我们可以扫描出所有的内核内存。
实际中Meltdown Attack并不总是能生效,比如前提是我们已经知道了有个有用的内核虚拟地址,而这个地址的获取很难,内核会保护自己不受涉及到猜内核内存地址攻击的影响,比如Kernal address space layout randomization。