概述
内核的模块机制允许构建具有广泛软硬件支持的内核,而无需将所有的代码实际加载到任何给定的运行系统中。在一个典型的分配器内核中,所有这些模块的可用性意味着非常多的功能是可用的,但是也可能存在许多可以被利用的bug。在许多情况下,内核的自动模块加载器已被用于将错误代码带入正在运行的系统当中。减少内核暴露于有缺陷的模块的尝试表明,某些类型的加固工作是多么困难。
模块自动加载
两种方法可以将模块加载到Linux内核中,无需管理员进行明确的操作。在大多数的现有系统上,它发生在硬件被发现时,无论是通过总线驱动程序(在受支持被发现的总线上)还是通过设备树等外部描述。这个发现将导致一个事件被发送到用户空间像udev这样的已经被配置的守护进程应用和被加载的适当的模块。这种机制由可用的硬件驱动,并且相对难以受攻击者影响。
然而在内核中,潜伏着一种较旧的机制,即request_module()函数的形式。当内核函数确定缺少所需的模块时,它可以调用request_module()函数向用户空间发送请求以加载相关的模块。例如,如果应用程序使用给定的主要和次要编号打开一个char设备,并且这些数字不存在驱动程序,则char设备代码将尝试通过调用来定位驱动程序:
1 | request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev)); |
如果驱动模块声明了一个具有匹配编号的别名,他将自动加载到内核中以处理打开请求。
内核中有数百个request_module()调用。有些是非常具体的;如果用户不幸拥有这样一个设备,就会自动加载ide-tape模块。其他的更为普遍;例如,在网络子系统中有许多调用,以找到实现特定网络协议或数据包过滤机制的模块。虽然特定设备的调用已经大部分被udev机制所取代,但是像网络协议这样的模块仍然依赖于request_module()来实现用户透明的自动加载。
自动加载方便系统管理,但是它也可以方便于系统的开发。如果DCCP模块未被加载到内核中,则2月份发布的DCCP协议漏洞是不可利用的。通常情况下,因为DCCP的用户很少。但是自动加载机制允许任何用户通过创建一个DCCP套接字来强制加载该模块。因此,自动加载扩大了内核的攻击面,将非特权用户可能导致加载的任何模块包含在模块中——典型的分发器内核中有很多模块。
收紧系统
Djalal Harouni一直在开发一个补丁集,旨在减少自动加载的风险;最新版本是在11月27日发布的。Harouni的工作从grseurity补丁集的加固中获得了灵感,但没有从那里获得代码。在这个版本中(随着时间的推移,它有一些变化),它增加了一个新的sysctl的按钮(/proc/sys/kernel/module_autoload_mode),用来限制内核的自动加载机制。如果这个按钮被设置为零(默认值),自动加载就会像在当前内核一样工作。把它设置为1,就可以把自动加载限制在具有特定功能的进程中:具有CAP_SYS_MODULE的进程就可以使任何模块被加载,而具有CAP_NET_ADMIN的进程可以加载任何以netdev-开头的模块。将这个按钮设置为2可以完全禁用自动加载功能。一旦这个值被提高到0以上,在系统的生命周期内就不能再降低了。
补丁集还实现了可以使用prctl()系统调用设置每个进程的标志。该标志(与全局标志具有相同的值)可以限制特定进程及其所有后代的自动加载,而不会改变整个系统中的模块加载行为。
可以肯定地说,这个补丁集不会以其当前形式合并,原因很简单:Linus Torvalds非常不喜欢它。禁用自动加载可能破坏许多系统,这意味着分销商将不愿意启动这个选项,他也不会有太多的用途。“人们在不破坏系统的情况下无法使用的安全选项毫无意义”,他说。讨论有时候会变得激烈,但是Linus并不反对减少内核暴露于自动加载漏洞的想法。这只是找到正确解决方案的问题。
每个进程标志看起来是解决该问题方案的一部分。例如,它可以用于限制在容器内运行的代码的自动加载,同时保持整个系统不变。在容器中创建具有CAP_NET_ADMIN功能的进程来配置该容器的网络并希望容器中运行的大多数代码无法强制加载模块的情况并不少见。
但是Linus说,一个标志永远无法正确控制自动加载发挥作用的所有情况。一些模块可能始终是可加载的,而其他模块可能需要特定的功能。因此他建议保留Harouni的补丁集添加的request_module_cap()函数(仅在存在特定功能时才执行加载)并更广泛使用它。但他确实有一些更改要求。
首先是request_module_cap()不应该在所需的功能不存在的情况下实际阻止模块的加载-至少在开始时不应该。相反,它应该记录一条信息。这将允许对实际需要模块自动加载的地方进行研究,如果幸运的话,将指出可以限制自动加载而不破坏现有系统的地方。他还建议,能力检查过于简单。例如,上面描述的“char-major-”自动加载在进程能够打开具有给定的主要和次要编号的设备节点时发生。在这种情况下,权限测试(打开该特殊文件的能力)已经通过,模块应该无条件加载。因此,可能需要request_module()的其他条件来描述功能不实用的设置。
最后Linus有另一个想法,即最糟糕的错误往往潜伏在维护不善的模块中。例如,上面提到的DCCP模块很少被使用并且无人维护。如果维护良好的模块标有特殊标志,则可以将非特权自动模块限制为仅对这些模块。这将阻止一些更复杂的模块的自动加载。不过这个想法确实提出了一个没人问的问题:当一个模块不在被维护时,谁会很好的维护它以去除“维护良好”的标志。
无论如何,如果Kees Cook提出的计划成立,该标志可能不会立即添加。他建议从启用警告的request_module_cap()方法开始。将为那些可以使用它的人添加每个进程的标志,但限制自动加载的全局按钮不会。最终有可能摆脱非特权模块加载,但这将是未来的目标。短期利益有关如何实际自动加载的更好信息,以及现在想要收紧的管理员提供每个进程的选项。
这段话强调了围绕内核强化工作的基本张力之一。很少有人反对更安全的内核但是一旦加固工作可以破坏现有系统,事情就会变得困难——而且情况通常如此。面向安全的开发人员经常对内核社区抵制对用户可见的影响的加固工作而感到沮丧,而内核开发者对那些会导致错误报告和用户不高兴的改动却没有什么同情心。这些挫折感在这次讨论的开发者都对达成一个所有人都有效的解决方案感兴趣。