KPTI:内核页表隔离的当前的发展

KPTI:内核页表隔离的当前的发展

英特尔处理器曝出了一个严重的硬件设计漏洞,迫使包括 Linux、Windows 在内的主要操作系统和各大云计算服务商都忙着打补丁。因为漏洞信息没有解密,所以目前只能通过已发布的补丁反推这个漏洞

这里是一篇对该漏洞的技术分析文章。

在十月底的时候,KAISER 补丁集被披露了;它做了一项工作,将内核空间与用户空间使用的 页表 page tables 进行了隔离,以解决 x86 处理器上向攻击者透露内核布局的安全漏洞。这些补丁是自它们被公布以来,这一星期中最值关注的事情,但是,它们似乎正在接近最终的状态。这应该是再次审视它们的合适机会。

这项工作被重命名为 “ 内核页表隔离 kernel page-table isolation ” (KPTI),但是目的是一样的:分割页表,将现在被用户空间和内核空间共享使用的这张表分成两套,内核空间和用户空间各自使用一个。这对内核的内存管理产生了根本性的变化,并且,这是本来人们期望先争论多年再做决定的,尤其是考虑到它的性能影响的时候。不过,KPTI 仍然处于快速发展的轨道上。一组预备补丁 已被被合并到 4.15 - rc4 之后的主版本线上了 — 一般情况下仅重要的修复才被允许这样做 — 并且其余的似乎被确定进入 4.16 版的合并窗口中。许多内核开发者都在这项工作上投入了大量的时间,并且 Linus Torvalds 要求 将这项工作 回迁 ( backport ) 到长期稳定内核中。

也就是说,KPTI 已经在最后期限的压力下安全补丁的所有标记都已经就绪了。对于任何基于 ARM 的读者,在这里值的注意的是,在这项工作中有一个 为 arm64 的等效补丁集

51 个补丁乃至更多

在这篇文章中,x86 补丁系列正处在 163 版本。它包含 51 个补丁,因此,我们应该感谢那些没有公开的版本。最初的补丁集,由 Dave Hansen 发布,由 Thomas Gleixner、Peter Zijlstra、Andy Lutomirski、和 Hugh Dickins 根据许多其它人的建议,做了大量的修订。任何还存在的缺陷都不是由于没有足够多的有经验的开发人员过目所导致的。

在现代系统中,页表是以一个树形结构进行组织的,这样可以高效地存储稀疏内存映射和支持巨页特性;可以查看这篇 2005 年的文章 了解更多细节以及它是怎么工作的示意图。在一个有四级页面表的系统上(目前的大多数大型系统都是这样),顶级是页面全局目录(PGD)。紧接着是页面上层目录(PUD)、页面中层目录(PMD)和页面表条目(PTE)。有五级页面表的系统是在 PGD 下面插入了一层(称为 P4D)。

页面故障解析通常遍历整个树去查找所需的 PTE,但是,巨页可以被更高层级的特定条目所表示。例如,一个 2MB 的内存 块 chunk 既可以由 PMD 层级的一个单个的巨页条目表示,也可以由一个单页 PTE 条目的完整页面表示。

在当前的内核中,每个处理器有一个单个的 PGD;在 KPTI 系列补丁中所采取的第一步的其中一个措施是,去创建一个第二个 PGD。当内核运行时,原来的仍然在使用;它映射所有的地址空间。当处理器运行在用户空间时,(在打完该系列补丁之后)第二个被激活。它指向属于该进程的页面的相同目录层次,但是,描述内核空间(位于虚拟地址空间的顶端)的部分通常都不在这里。

页表条目包含权限描述位,它们记录了内存该如何被访问;不用说都知道,这些位是用来设置阻止用户空间访问内核页面的,即便是通过那些被映射到该地址空间的页面访问。不幸的是,一些硬件级的错误允许用户空间的攻击者去确定一个给定的内核空间地址是否被映射,而不管那个页面上映射的地址是否被允许访问。而那个信息可以被用于击败内核地址空间布局随机化,可以使一个本地的攻击者更易于得逞。在 KPTI 背后的核心思想是,切换到一个没有内核空间映射的 PGD,将会使基于这个漏洞的攻击失效,而到现在为止,我们还没有看到这些攻击。

细节

这个想法很简单,但是,就像经常发生的那样,有各种各样麻烦的细节使得这个简单的想法变成了一个由 51 个部分构成的补丁集。其中最初的一个是,如果处理器在用户模式运行时响应一个硬件中断,处理中断需要的内核代码将在地址空间中不存在。因此,必须有足够的内核代码映射在用户模式中,以能够切换回到内核 PGD,使剩余的代码也可用。对于 traps、非屏蔽中断、和系统调用,也存在一个相似的情况。这个代码很小而且可以与其它部分隔离,但是,在处理安全且有效地切换时,涉及到一些很复杂的细节。

另一个难题来自 x86 本地描述符表(LDT)的构成,它可以被用于去改变用户空间的内存布局。它可以使用鲜为人知的 modify_ldt() 系统调用来做微调。例如,在 Linux 上早期的 POSIX 线程实现,使用了 LDT 去创建一个本地线程存储区域。在现在的 Linux 系统上,LDT 几乎不再使用了,但是,一些应用程序(比如,Wine)仍然需要它。当它被使用时,LDT 必须能够被用户空间和内核空间都可以访问到,但是,它必须一直处于内核空间中。KPTI 补丁集打乱内核附近的内存,在 PGD 级别上为 LDT 保留一个完全的条目;因此,vmalloc() 调用的可用空间收缩到仅有 12,800TB。那是一个非常巨大的 LDT 空间数,可以满足有很多 CPU 系统的需要。这种变化的其中一个结果是,LDT 的位置将是固定的,并且已知道用户空间 ——因此这将是个潜在的问题,由于覆写 LDT 的能力很容易被用来破坏整个系统。在这个系列的最终的补丁是映射为只读 LDT,以阻止此类攻击。

另一个潜在的安全缺陷是,如果内核可以一直被操纵直至返回用户空间,以至于不切换回经过过滤的 PGD。因为内核空间 PGD 也映射用户空间内存,这种疏忽可能在一段时间内不会被察觉到。对此问题的应对方法是将虚拟内存空间中用户空间的部分以非可执行的方式映射到内核的 PGD。只要用户空间(的程序)开始从一个错误的页面表开始执行,它将会立即崩溃。

最后,虽然所有已存在的 x86 处理器似乎都会受到这个信息泄露的漏洞影响,但是,以后的处理器可能不会受此影响。KPTI 有一个可测量的运行时成本,估计在 5%。有些用户也许不愿意为这些成本埋单,尤其是他们拿到了不存在这个问题的新处理器之后。为此将会有一个 nopti (内核)命令行选项来在启动的时候禁用这个机制。这个补丁系列也增加了一个新的“特性”标识(X86_BUG_CPU_INSECURE)去标识有漏洞的 CPU;它被设置在现在所有的 x86 CPU 上(LCTT 译注:AMD 表示不背这锅),但是在以后的硬件上可能没有。如果没有该特性标识,页面隔离将自动被关闭。

在 4.16 版的合并窗口打开之前剩下将近一个月。在这期间,针对一些新发现而不可避免的小毛病,KPTI 补丁集毫无疑问的将迎来一系列的小修订。一旦所有的事情都敲定了,看起来这些代码将会被合并同时以相对快的速度迁回到稳定版本的内核。显而易见的是,我们将会收到一个更慢,但是更安全的内核作为一个新年礼物。


via: https://lwn.net/SubscriberLink/741878/eb6c9d3913d7cb2b/

作者:Jonathan Corbet 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出