> For the complete documentation index, see [llms.txt](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec22-meltdown-robert/22.6-meltdown-attack.md).

# 22.6 Meltdown Attack

接下来让我们回到Meltdown。

![](/files/-MYjSV7ePTdr8TyqojC4)

这段代码比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行被预测执行，这样才能完成攻击。

![](/files/-MYjY21ZwnaKUdrZwMg2)

所以我们希望代码第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教授：是的，你是对的。

![](/files/-MYlvBz7RXqzc8M3ZkWM)

> 学生提问：在第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并不总是能生效，具体的原因我认为论文作者并没有解释或者只是猜测了一下。如果你查看论文的最后一页，

![](/files/-MYogM9outosp5CkFKqZ)

你可以看到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的密码管理器中的密码，你该怎么找出内存地址，以及一个攻击的完整流程，我的意思是由学院派而不是实际的黑客完成的一次完整的攻击流程。尽管如此，这里也包含了很多实用的细节。
