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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec22-meltdown-robert/22.6-meltdown-attack.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
