关注了就能看到更多这么棒的文章哦~
mimmutable() for OpenBSD
By Jonathan Corbet
December 9, 2022
DeepL assisted translation
https://lwn.net/Articles/915640/
虚拟内存(virtual-memory)系统在映射和保护内存方面提供了很多灵活性。不幸的是,内存管理越灵活,那些一心想破坏系统的攻击者来说也就越高兴。在 OpenBSD 的世界里正在添加一个新的系统调用,希望能减少这种灵活性;不过这是一个几乎没有代码会使用的系统调用。
OpenBSD 创始人 Theo de Raadt 在 9 月初首次提出了一个新的系统调用,叫做 mimmutable()。经过多次修改之后,该系统调用看起来准备用下面的形式来完成合入:
int mimmutable(void *addr, size_t len);
调用 mimmutable() 会使得从 addr 开始的 len 字节长的内存映射变得不可再被改动,这意味着内核将不允许对该范围内的内存保护或映射做出任何改动。因此,像 mmap()或 mprotect()这样的系统调用如果牵涉到这个范围的话,就会失败。
乍一看,mimmutable()与 OpenBSD 的 pledge() 本质上很类似,后者是限制了调用进程中可以使用的系统调用。但是,虽然 pledge() 调用已经在 OpenBSD 仓库中的许多程序中使用起来了,但是使用 mimmutable() 的地方确实很少。大多数开发者对他们的程序的内存布局缺乏详细的了解,因此不能很好地把自己地址空间的一部分配置成不可改变的,但内核和链接器(linker)是有相关信息的。
关于如何使用 mimmutable()的细节,在 De Raadt 的一封邮件中做了详细描述。简而言之,当内核加载一个新的可执行映像文件时,它就开始生效了;只要有 text、stack 和 data section 被映射,在程序开始运行之前,它们就会变成不可更改状态。对于静态二进制文件,C runtime 会做一些 fixup 工作,然后使用 mimmutable() 让映射好的地址空间的其余部分中大部分也变成不可更改的。对于动态链接的二进制文件,共享库链接器(ld.so,shared-library linker)会做一系列类似的动作,将每个库都映射到地址空间中,然后让这些映射的大部分内容变得不可更改。
所有这些都将自动发生,而不需要被加载的程序意识到这些。最终的结果将是这个进程几乎无法对其映射的地址空间进行修改(尽管它总是可以利用尚未映射的地址空间区域来创建新的映射)。这里有一个小小的例外:
所以这个 static executable 是完全不可改变的,除了 OPENBSD_MUTABLE 区域之外。这个标注现在只用在一个地方,就是在 libc 的 malloc(3)代码之内,其中有一段代码会将一个数据结构在 readonly 和 read-write 之间切换,这是一种安全措施。这部分区域就没有被设置为不可变的。
要使整个计划能顺利进行的话,不仅仅需要改变 OpenBSD kernel;尤其是还需要修改编译器工具链(compiler toolchain),需要加强对程序加载时必须保持 mutable(可以修改)的部分的标记。显然会有一些程序需要特殊调整才能在这种环境下正常地工作起来;由于 OpenBSD 同时管控着 kernel 和用户空间的程序,它能够做出 Linux 所不能做出的一些改动,不用担心会造成用户空间的 regression 问题。
即便如此,实现 mimmutable() 也涉及到相当多的很繁琐的工作;我们可以理解,OpenBSD 的开发者希望看到这么做能带来的好处。一个明显的好处就是在可执行内存方面。OpenBSD 一直在努力防止内存既是可写的,同时又是可执行的,但如果攻击者能够将恶意代码加载到可写区域,然后改变权限,那么这种限制所带来的保护就会失效。对进程的数据区域进行严格的保护会使得这种攻击变得不再可行。
除此之外,OpenBSD 还使用了一些 Linux 中没有的内存保护措施。其中之一是对那些可以支持调用到 kernel 的可执行内存进行标记;在 OpenBSD 系统中,只有 C 库才有这种能力。这可以防止从其他地方加载的恶意代码直接进行系统调用;用 mimmutable() 来保护进程的其他部分,就可以避免这个保护措施被修改成允许从其他地方进行系统调用(在 OpenBSD 上,可以通过 msyscall()来进行这种改动)。
OpenBSD 也会对用于存放 stack 的内存区域进行标记。每当一个进程进入内核态时,它的 stack 指针就会被检查,看它是否真的指向了一个 stack 区域;如果不是的话,会把这个进程直接 kill。这种检查可以阻止 "stack pivot" 攻击,即攻击者将堆栈指针重定向到一个更有利于执行攻击的内存区域中。同样,mimmutable() 可以阻止攻击者将普通的数据区域变成支持防治 stack 的区域。
类似 mimmutable()这样的系统调用有可能被用来提高 Linux 系统的安全性,但这将会是一个更难的任务。Linux 内核开发者无法同步修改用户空间程序,所以在希望不破坏用户空间的代码的情况下做出这种改动是很困难的。例如,增加一个 "允许直接系统调用" 的保护性 bit 会很容易就导致 Linux 下的许多程序无法使用,这些程序出于各种原因没有使用 C-library wrapper,而是直接使用系统调用进入内核。
还有一些类似的障碍也同样适用于对 stack 指针的限制措施。内核确实有一个 "grows down" bit,可以用来识别 stack 区域,但是只适用于那些可以增长的 stack。多线程程序经常会创建具有固定长度的 stack 的线程,这些线程都没有设置这个 bit。因此,任何为线程创建 stack 的用户空间程序都需要修改,从而明确设置这个 bit,而内核开发者不能进行这样的修改。因此,stack 指针的检查功能就不可能很容易地加到 Linux 中来。
不过,让内存映射变得不可改变的这个系统调用可能是有价值的。要让这样的东西进入 Linux,需要一个有兴趣实现此功能的开发者,需要证明用户空间的代码会使用它,还需要有某种令人信服的场景来描述会被它所阻挡的攻击。可能还需要对 toolchain 进行修改以支持这一功能。这是一个很高的标准,就跟通常添加新的系统调用差不多。但也许有人最终会受到启发,尝试开发一个 patch 来支持这个功能的。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~