22.1 Meltdown发生的背景

今天讲的是Meltdown,之所以我会读这篇论文,是因为我们在讲解如何设计内核时总是会提到安全。内核提供安全性的方法是隔离,用户程序不能读取内核的数据,用户程序也不能读取其他用户程序的数据。我们在操作系统中用来实现隔离的具体技术是硬件中的User/Supervisor mode,硬件中的Page Table,以及精心设计的内核软件,例如系统调用在使用用户提供的指针具备防御性。

但是同时也值得思考,如何可以破坏安全性?实际上,内核非常积极的提供隔离性安全性,但总是会有问题出现。今天的论文讨论的就是最近在操作系统安全领域出现的最有趣的问题之一,它发表于2018年。包括我在内的很多人发现对于用户和内核之间的隔离进行攻击是非常令人烦恼的,因为它破坏了人们对于硬件上的Page Table能够提供隔离性的设想。这里的攻击完全不支持这样的设想。

同时,Meltdown也是被称为Micro-Architectural Attack的例子之一,这一类攻击涉及利用CPU内隐藏的实现细节。通常来说CPU如何工作是不公开的,但是人们会去猜,一旦猜对了CPU隐藏的实现细节,就可以成功的发起攻击。Meltdown是可被修复的,并且看起来已经被完全修复了。然后它使得人们担心还存在类似的Micro-Architectural Attack。所以这是最近发生的非常值得学习的一个事件。

让我从展示攻击的核心开始,之后我们再讨论具体发生了什么。

这是论文中展示攻击是如何工作的代码的简化版。如果你是攻击者,出于某种原因你可以在计算机上运行一些软件,这个计算机上有一些你想要窃取的数据。虽然你不能直接访问这些数据,但是这些数据还是位于内存中,或许是内核内存,或许是另一个进程的内存。你可以在计算机上运行一个进程,或许因为你登录到了分时共享的机器,也或许你租用了运行在主机上的服务。你可以这样发起攻击:

  • 在程序中你在自己的内存中声明了一个buffer,这个buffer就是普通的用户内存且可以被正常访问。

  • 然后你拥有了内核中的一个虚拟内存地址,其中包含了一些你想要窃取的数据。

  • 这里的程序是C和汇编的混合,第3行代码的意思是你拥有了内核的虚拟内存地址,你从这个内存地址取值出来并保存在寄存器r2中。

  • 第4行获取寄存器r2的低bit位,所以这里这种特定的攻击只是从内核一个内存地址中读取一个bit。

  • 第5行将这个值乘以4096,因为低bit要么是1,要么是0,所以这意味着r2要么是4096,要么是0。

  • 第6行中,我们就是读取前面申请的buffer,要么读取位置0的buffer,要么读取位置4096的buffer。

这就是攻击的基本流程。

这里的一个问题是,为什么这里不能直接工作?在第3行,我们读取了内核的内存地址指向的数据,我们可以直接读取内核的内存地址吗?并不能,我们相信答案是否定的。如果我们在用户空间,我们不可能直接从内核读取数据。我们知道CPU不能允许这样的行为,因为当我们使用一个内核虚拟地址时,这意味着我们会通过Page Table进行查找,而Page Table有权限标志位,我们现在假设操作系统并没有在PTE中为内核虚拟地址设置标志位来允许用户空间访问这个地址,这里的标志位在RISC-V上就是pte_u标位置。因此这里的读取内核内存地址指令必然会失败,必然会触发Page Fault。实际中如果我们运行代码,这些代码会触发Page Fault。如果我们在代码的最后增加printf来打印r3寄存器中的值,我们会在第3行得到Page Fault,我们永远也走不到printf。这时我们发现我们不能直接从内核中偷取数据。

然而,如论文展示的一样,这里的指令序列是有用的。虽然现在大部分场景下已经不是事实了,但是论文假设内核地址被映射到了每个用户进程的地址空间中了。也就是说,当用户代码在运行时,完整的内核PTE也出现在用户程序的Page Table中,但是这些PTE的pte_u比特位没有被设置,所以用户代码在尝试使用内核虚拟内存地址时,会得到Page Fault。在论文写的时候,所有内核的内存映射都会在用户程序的Page Table中,只是它们不能被用户代码使用而已,如果用户代码尝试使用它们,会导致Page Fault。操作系统设计人员将内核和用户内存地址都映射到用户程序的Page Table中的原因是,这使得系统调用非常的快,因为这使得当发生系统调用时,你不用切换Page Table。切换Page Table本身就比较费时,同时也会导致CPU的缓存被清空,使得后续的代码执行也变慢。所以通过同时将用户和内核的内存地址都映射到用户空间可以提升性能。但是上面的攻击依赖了这个习惯。我将会解释这里发生了什么使得上面的代码是有用的。

学生提问:能重复一下上面的内容吗?

Robert教授:在XV6中,当进程在用户空间执行时,如果你查看它的Page Table,其中包含了用户的内存地址映射,trampoline和trap frame page的映射,除此之外没有别的映射关系,这是XV6的工作方式。而这篇论文假设的Page Table不太一样,当这篇论文在写的时候,大部分操作系统都会将内核内存完整映射到用户空间程序。所以所有的内核PTE都会出现在用户程序的Page Table中,但是因为这些PTE的pte_u比特位没有被设置,用户代码并不能实际的使用内核内存地址。

这么做的原因是,当你执行系统调用时,你不用切换Page Table,因为当你通过系统调用进入到内核时,你还可以使用同一个Page Table,并且因为现在在Supervisor mode,你可以使用内核PTE。这样在系统调用过程中,进出内核可以节省大量的时间。所以大家都使用这个技术,并且几乎可以肯定Intel也认为一个操作系统该这样工作。

在论文中讨论的攻击是基于操作系统使用了这样的结构。最直接的摆脱攻击的方法就是不使用这样的结构。但是当论文还在写的时候,所有的内核PTE都会出现在用户空间。

学生提问:所以为了能够攻击,需要先知道内核的虚拟内存地址?

Robert教授:是的。或许找到内存地址本身就很难,但是你需要假设攻击者有无限的时间和耐心,如果他们在找某个数据,他们或许愿意花费几个月的时间来窃取这个数据。有可能这是某人用来登录银行账号或者邮件用的密码。这意味着攻击者可能需要尝试每一个内核内存地址,以查找任何有价值的数据。

或许攻击者会研究内核代码,找到内核中打印了数据的地址,检查数据结构和内核内存,最后理解内核是如何工作的,并找到对应的虚拟内存地址。因为类似的攻击已经存在了很长的时间,内核实际上会保护自己不受涉及到猜内核内存地址的攻击的影响。论文中提到了Kernal address space layout randomization。所以现代的内核实际上会将内核加载到随机地址,这样使得获取内核虚拟地址更难。这个功能在论文发表很久之前就存在,因为它可以帮助防御攻击。

在这个攻守双方的游戏中,我们需要假设攻击者最后可以胜出并拿到内核的虚拟内存地址。所以我们会假设攻击者要么已经知道了一个内核虚拟地址,要么愿意尝试每一个内核虚拟内存地址。

我们会好奇,上面的代码怎么会对攻击者是有用的?如果CPU如手册中一样工作,那么这里的攻击是没有意义的,在第三行会有Page Fault。但是实际上CPU比手册中介绍的要复杂的多,而攻击能生效的原因是一些CPU的实现细节。

这里攻击者依赖CPU的两个实现技巧,一个是Speculative execution(预测执行),另一个是CPU的缓存方式。

Last updated