22.6 Meltdown Attack

接下来让我们回到Meltdown。

这段代码比22.1里面的代码更加完整,这里是一个更完整的Meltdown攻击代码,这里我们增加了Flush and Reload代码。

首先我们声明了一个buffer,现在我们只需要从内核中窃取1个bit的数据,我们会将这个bit乘以4096,所以我们希望下面的Flush and Reload要么看到buffer[0]在cache中,要么看到buffer[4096]在cache中。为什么要有这么的大的间隔?是因为硬件有预获取。如果你从内存加载一个数据,硬件极有可能会从内存中再加载相邻的几个数据到cache中。所以我们不能使用两个非常接近的内存地址,然后再来执行Flush and Reload,我们需要它们足够的远,这样即使有硬件的预获取,也不会造成困扰。所以这里我们将两个地址放到了两个内存Page中(注,一个内存Page 4096)。

现在的Flush部分直接调用了clflush指令(代码第4第5行),来确保我们buffer中相关部分并没有在cache中。

代码第7行或许并不必要,这里我们会创造时间差。我们将会在第10行执行load指令,它会load一个内核内存地址,所以它会产生Page Fault。但是我们期望能够在第10行指令Retired之前,也就是实际的产生Page Fault并取消这些指令效果之前,再预测执行(Speculative execution)几条指令。如果代码第10行在下面位置Retired,那么对我们来说就太早了。实际中我们需要代码第13行被预测执行,这样才能完成攻击。

所以我们希望代码第10行的load指令尽可能晚的Retired,这样才能推迟Page Fault的产生和推迟取消预测执行指令的效果。因为我们知道一个指令只可能在它之前的所有指令都Retired之后,才有可能Retired。所以在代码第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(注,详见Lec17),并且在Page Fault之后重新获得控制。论文还讨论了一些其他的方法使得发生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。如果我们重复几百万次,我们可以扫描出所有的内核内存。

学生提问:在这里例子中,如果b-a<c-b,是不是意味着buffer[0]在cache中?

Robert教授:是的,你是对的。

学生提问:在第9行之前,我们需要if语句吗?

Robert教授:并不需要,22.2中的if语句是帮助我展示Speculative execution的合理理由:尽管CPU不知道if分支是否命中,它还是会继续执行。但是在这里,预测执行的核心是我们并不知道第10行的load会造成Page Fault,所以CPU会在第10行load之后继续预测执行。理论上,尽管这里的load可能会花费比较长的时间(例如数百个CPU cycle),但是它现在不会产生Page Fault,所以CPU会预测执行load之后的指令。如果load最终产生了Page Fault,CPU会回撤所有预测执行的效果。

预测执行会在任何长时间执行的指令,且不论这个指令是否能成功时触发。例如除法,我们不知道是否除以0。一旦触发预测执行,所有之后的指令就会开始被预测执行。

不管怎样,真正核心的预测执行从第10行开始,但是为了让攻击更有可能成功,我们需要确保预测执行从第7行开始。

学生提问:在这个例子中,我们只读了一个bit,有没有一些其他的修改使得我们可以读取一整个寄存器的数据?

Robert教授:有的,将这里的代码运行64次,每次获取1个bit。

学生提问:为什么不能一次读取64bit呢?

Robert教授:如果这样的话,buffer需要是2^64再乘以4096,我们可能没有足够的内存来一次读64bit。或许你可以一次读8个bit,然后buffer大小是256*4096。论文中有相关的,因为这里主要的时间在第17行到第24行,也就是Flush and Reload的Reload部分。如果一次读取一个字节,那么找出这个字节的所有bit,需要256次Reload,每次针对一个字节的可能值。如果一次只读取一个bit,那么每个bit只需要2次Reload。所以一次读取一个bit,那么读取一个字节只需要16次Reload,一次读取一个字节,那么需要256次Reload。所以论文中说一次只读取一个bit会更快,这看起来有点反直觉,但是又好像是对的。

学生提问:这里的代码会运行在哪?会运行在特定的位置吗?

Robert教授:这取决于你对于机器有什么样的权限,并且你想要窃取的数据在哪了。举个例子,你登录进了Athena(注,MIT的共享计算机系统),机器上还有几百个其他用户 ,然后你想要窃取某人的密码,并且你很有耐心。在几年前Athena运行的Linux版本会将内核内存映射到每一个用户进程的地址空间。那么你就可以使用Meltdown来一个bit一个bit的读取内核数据,其中包括了I/O buffer和network buffer。如果某人在输入密码,且你足够幸运和有耐心,你可以在内核内存中看见这个密码。实际中,内核可能会映射所有的物理内存,比如XV6就是这么做的,这意味着你或许可以使用Meltdown在一个分时共享的机器上,读取所有的物理内存,其中包括了所有其他进程的内存。这样我就可以看到其他人在文本编辑器的内容,或者任何我喜欢的内容。这是你可以在一个分时共享的机器上使用Meltdown的方法。其他的场景会不太一样。

分时共享的机器并没有那么流行了,但是这里的杀手场景是云计算。如果你使用了云服务商,比如AWS,它会在同一个计算机上运行多个用户的业务,取决于AWS如何设置它的VMM或者容器系统,如果你购买了AWS的业务,那么你或许就可以窥探其他运行在同一个AWS机器上的用户软件的内存。我认为这是人们使用Meltdown攻击的方式。

另一个可能有用的场景是,当你的浏览器在访问web时,你的浏览器其实运行了很多不被信任的代码,这些代码是各种网站提供的,或许是以插件的形式提供,或许是以javascript的形式提供。这些代码会被加载到浏览器,然后被编译并被运行。有可能当你在浏览网页的时候,你运行在浏览器中的代码会发起Meltdown攻击,而你丝毫不知道有一个网站在窃取你笔记本上的内容,但是我并不知道这里的细节。

学生提问:有人演示过通过javascript或者WebAssembly发起攻击吗?

Robert教授:我不知道。人们肯定担心过WebAssembly,但是我不知道通过它发起攻击是否可行。对于javascript我知道难点在于时间的测量,你不能向上面一样获取到纳秒级别的时间,所以你并不能使用Flush and Reload。或许一些更聪明的人可以想明白怎么做,但是我不知道。

实际中Meltdown Attack并不总是能生效,具体的原因我认为论文作者并没有解释或者只是猜测了一下。如果你查看论文的最后一页,

你可以看到Meltdown Attack从机器的内核中读取了一些数据,这些数据里面有一些XXXX,这些是没能获取任何数据的位置,也就是Meltdown Attack失败的位置。论文中的Meltdown Attack重试了很多很多次,因为在论文6.2还讨论了性能,说了在某些场景下,获取数据的速率只有10字节每秒,这意味着代码在那不停的尝试了数千次,最后终于获取到了数据,也就是说Flush and Reload表明了两个内存地址只有一个在Cache中。所以有一些无法解释的事情使得Meltdown会失败,从上图看,Meltdown Attack获取了一些数据,同时也有一些数据无法获得。据我所知,人们并不真的知道所有的成功条件和失败条件,最简单的可能是如果内核数据在L1 cache中,Meltdown能成功,如果内核数据不在L1 Cache中,Meltdown不能成功。如果内核数据不在L1 cache中,在预测执行时要涉及很多机制,很容易可以想到如果CPU还不确定是否需要这个数据,并不一定会完成所有的工作来将数据从RAM中加载过来。你可以发现实际中并没有这么简单,因为论文说到,有时候当重试很多次之后,最终还是能成功。所以这里有一些复杂的情况,或许在CPU内有抢占使得即使内核数据并不在Cache中,这里的攻击偶尔还是可以工作。

论文的最后也值得阅读,因为它解释了一个真实的场景,比如说我们想要通过Meltdown窃取Firefox的密码管理器中的密码,你该怎么找出内存地址,以及一个攻击的完整流程,我的意思是由学院派而不是实际的黑客完成的一次完整的攻击流程。尽管如此,这里也包含了很多实用的细节。

Last updated