使用内核安全模块接口

作者:Greg Kroah-Hartman

在 2001 年 Linux 内核峰会上,NSA 的开发人员介绍了他们在 Security-Enhanced Linux (SELinux) 上的工作,并强调了在主 Linux 内核中增强安全支持的必要性。在随后的讨论中,与会者达成共识,认为 Linux 内核需要一个通用的访问控制框架。这种方法将允许不同的安全模型在不修改主内核代码的情况下工作。

Linux 安全模块项目 (LSM) 由此讨论发展而来。许多开发人员共同努力,创建了一个内核钩子框架,该框架允许许多安全模型作为可加载的内核模块工作。在 2002 年 USENIX 安全会议上发表的一篇论文中,详细描述了这种通用系统的设计 (lsm.immunix.org/docs/lsm-usenix-2002/html/),并在 2002 年渥太华 Linux 研讨会上发表的一篇论文中,介绍了 LSM 接口如何工作的技术描述 (www.linux.org.uk/~ajh/ols2002_proceedings.pdf.gz)。

在 2002 年 Linux 内核峰会期间,介绍了该项目的技术描述,LSM 框架的第一部分出现在 2.5.29 内核版本中。随后的内核版本包含更多 LSM 框架的部分,希望在 2.6.0 版本发布时,整个补丁将被包含在内。

本文不试图描述 LSM 框架如何工作或创建它时做出的设计决策;前面提到的参考文献在这方面做得非常出色。相反,本文展示了创建一个使用 LSM 框架的简单内核模块是多么容易。

Root Plug

我们的示例使用 2.5.31 内核版本,其中包含足够多的 LSM 接口,供我们创建一个有用的模块。在我们的模块中,我们希望防止任何组 ID 为 0(root)的程序在特定 USB 设备未插到机器上的情况下运行。这为我们提供了一种简单的方法,可以防止 root 漏洞在我们的机器上运行,或者防止新用户在我们不在场时登录。

此示例创建了一个名为 root_plug 的内核模块,该模块可作为针对来自 Linux Journal FTP 站点的干净 2.5.31 内核树的补丁提供 [ftp.linuxjournal.com/pub/lj/listings/issue103/6279.tgz]。

有关处理用户和组 ID 值以及它们如何与 setuid 类系统调用交互的 UNIX 系统的描述,请参阅 Hao Chen、David Wagner 和 Drew Dean 撰写的优秀论文“Setuid Demystified”,该论文在 2002 年 USENIX 安全会议上发表 (www.cs.berkeley.edu/~daw/papers/setuid-usenix02.ps)。

LSM 接口是四个简单的函数

int register_security
    (struct security_operations *ops);
int unregister_security
    (struct security_operations *ops);
int mod_reg_security (const char *name,
                  struct security_operations *ops);
int mod_unreg_security  (const char *name,
                  struct security_operations *ops);

安全模块通过调用函数 register_security() 向内核注册一组 security_operations 函数回调。如果注册失败,则意味着可能已经加载了其他安全模块,因此调用 mod_reg_security() 函数尝试向此安全模块注册。这可以在以下代码中看到

/* register ourselves with the security framework */
if (register_security (&rootplug_security_ops)) {
   printk (KERN_INFO
       "Failure registering Root Plug module "
       "with the kernel\n");
   /* try registering with primary module */
   if (mod_reg_security (MY_NAME,
                         &rootplug_security_ops)) {
       printk (KERN_INFO "Failure registering "
               "Root Plug module with primary "
               "security module.\n");
       return -EINVAL;
   }
   secondary = 1;
}
当模块想要卸载自身时,必须发生相反的过程。如果我们使用 mod_reg_security() 注册了自身,则应调用 mod_unreg_security() 函数,否则应调用 unregister_security() 函数。以下代码显示了此逻辑
/* remove ourselves from the security framework */
if (secondary) {
   if (mod_unreg_security (MY_NAME,
                           &rootplug_security_ops))
      printk (KERN_INFO
              "Failure unregistering Root Plug "
              " module with primary module.\n");
} else {
   if (unregister_security (
       &rootplug_security_ops)) {
      printk (KERN_INFO "Failure unregistering "
              "Root Plug module with the kernel\n");
   }
}
rootplug_security_ops 是一个大型函数指针结构,当内核中发生各种事件时会调用这些函数指针。这包括每当访问 inode、加载模块或创建任务时。截至 2.5.31 内核,需要 88 个不同的函数指针。大多数安全模型不需要这些函数,但必须实现它们,否则内核将无法正常工作。如果安全模块不需要对特定钩子执行任何操作,则需要向内核返回一个“good”值。以下函数中可以看到一个示例
static int rootplug_file_permission
    (struct file *file, int mask)
{
    return 0;
}
每当内核想要确定此时是否可以访问特定文件时,都会调用此函数。安全模块可以查看该文件,检查当前用户是否具有适当的权限,并可能拒绝授予该权限。
要使用哪个钩子

对于我们的示例模块,我们希望能够阻止在我们的 USB 设备未连接的情况下运行新程序。这可以通过使用 bprm_check_security 钩子来完成。当进行 execve 系统调用时,就在内核尝试启动任务之前调用此函数。如果从此函数返回错误值,则任务将不会启动。这是我们的钩子函数

static int rootplug_bprm_check_security
    (struct linux_binprm *bprm)
{
    if (bprm->e_gid == 0)
        if (find_usb_device() != 0)
            return -EPERM;
    return 0;
}

此函数检查程序要运行的有效组 ID 值。如果为零,则调用函数 find_usb_device()。如果在系统中未找到 USB 设备,则返回 -EPERM,这将阻止任务启动。

查找 USB 设备

find_usb_device() 函数只是遍历系统中的所有 USB 设备,并查看用户指定的设备是否存在。USB 设备保存在一个树中,从根集线器设备开始。不同的根集线器保存在总线列表中。在 find_usb_device() 函数中按顺序检查这些总线

static int find_usb_device (void)
{
    struct list_head *buslist;
    struct usb_bus *bus;
    int retval = -ENODEV;
    down (&usb_bus_list_lock);
    for (buslist = usb_bus_list.next;
         buslist != &usb_bus_list;
         buslist = buslist->next) {
        bus = container_of (buslist,
                            struct usb_bus,
                            bus_list);
        retval = match_device(bus->root_hub);
        if (retval == 0)
            goto exit;
    }
exit:
    up (&usb_bus_list_lock);
    return retval;
}

match_device() 函数查看传递给它的设备。如果它与预期设备匹配,则返回成功。否则,它会查看此设备的子设备,递归调用自身

static int match_device (struct usb_device *dev)
{
   int retval = -ENODEV;
   int child;
   /* see if this device matches */
   if ((dev->descriptor.idVendor == vendor_id) &&
       (dev->descriptor.idProduct == product_id)) {
       /* we found the device! */
       retval = 0;
       goto exit;
   }
   /* look at all of the children of this device */
   for (child = 0; child < dev->maxchild; ++child) {
       if (dev->children[child]) {
           retval =
               match_device (dev->children[child]);
           if (retval == 0)
               goto exit;
       }
   }
exit:
   return retval;
}
指定 USB 设备

由于每个用户都有不同类型的 USB 设备,因此必须以简单的方式指定要查找的设备。所有 USB 设备都有特定的供应商 ID 和产品 ID。当您的系统连接了一些 USB 设备时,您可以使用 lsusb 或 usbview 程序查看这些值。此信息也显示在 /proc/bus/usb/devices 文件中,在以“P:”开头的行中。有关此文件中数据的呈现方式的更多信息,请参阅 Documentation/usb/proc_usb_info.txt 文件。

match_device() 函数查看特定设备的值是否与 vendor_id 和 product_id 变量匹配。这些变量在代码中定义为

static int vendor_id = 0x0557;
static int product_id = 0x2008;
MODULE_PARM(vendor_id, "h");
MODULE_PARM_DESC(vendor_id,
            "USB Vendor ID of device to look for");
MODULE_PARM(product_id, "h");
MODULE_PARM_DESC(product_id,
           "USB Product ID of device to look for");

这允许在命令行上指定供应商和产品 ID 的情况下加载模块。例如,如果您想指定供应商 ID 为 0x04b4 和产品 ID 为 0x0001 的 USB 鼠标,则可以使用以下命令加载模块

modprobe root_plug vendor_id=0x04b4 \
product_id=0x0001
如果在模块加载命令行上未指定供应商或产品 ID,则代码默认查找供应商 ID 为 0x0557 和产品 ID 为 0x2008 的通用 USB 转串口转换器。
构建模块

最后,我们需要将我们的模块添加到内核构建过程中。这是通过将以下行添加到 security/Config.in 文件中完成的

tristate 'Root Plug Support'
CONFIG_SECURITY_ROOTPLUG

并将以下行添加到 security/Makefile 文件中

obj-$(CONFIG_SECURITY_ROOTPLUG)    += root_plug.o
这些更改允许用户选择将此内核模块直接构建到内核中,还是作为模块构建。运行您喜欢的 *config 选项以选择“Root Plug Support”(make oldconfig 在这里可以很好地工作,因为如果您已经为您的内核设置了工作正常的 .config 文件,则只会询问新选项)。然后像往常一样构建内核。

在您的内核构建并运行后,通过键入(以 root 身份)加载 root_plug 模块

modprobe root_plug vendor_id=<YOUR_VENDOR_ID> \
product_id=<YOUR_PRODUCT_ID>

现在尝试在您的指定 USB 设备已插入系统的情况下以 root 身份运行程序,然后在未插入的情况下尝试。在加载模块且设备已移除的情况下,我的机器上会发生以下错误

$ sudo ls
sudo: unable to exec
/bin/ls: Operation not permitted
重新插入设备,一切应该正常工作。
但这安全吗?

此示例展示了 LSM 接口的功能有多强大和简单。使用一个钩子,除非设备物理存在于系统中,否则任何具有 root 组 ID 的程序都将被阻止运行。

使用此代码,如果设备不存在,则不允许用户登录控制台,因为 mingetty 传统上以 root 身份运行。但是用户可以通过 SSH 以普通用户身份正常登录,因为 sshd 在设备移除之前已经运行。网页也可以提供服务,其他不以 root 身份运行的服务(您的邮件服务器、数据库服务器等)也将正常运行。如果其中一个服务器程序被攻破,并且它们试图生成 root shell,则该 root shell 将不允许运行。

此模块不会阻止任何已作为 root 运行的程序克隆自身,也不会阻止程序尝试更改当前分配给它的权限。要检查这些内容,应使用 security_operations 结构中的 task_* 函数。这些函数的实现将非常类似于 bprm_check_security 函数,但传递给函数的参数将不同,因此需要以不同的方式确定 egid。

可能还有其他方法可以获取现有正在运行的程序并生成此模块无法捕获的 root 进程。请不要在生产环境中使用它,而是将其作为学习练习,了解如何创建其他 LSM 示例代码。

致谢

我要感谢 Chris Wright、Stephen Smalley、James Morris 以及所有其他帮助创建 LSM 接口并使其被主内核树接受的程序员。由于他们的辛勤工作,Linux 现在拥有了一个灵活的安全模型,使日常用户能够轻松访问不同的安全模型。我还要感谢 Alan Cox 提出最初的想法,从而催生了这个例子。

有关 LSM 项目、开发邮件列表、文档和不同内核版本的补丁的更多信息,请访问网站 lsm.immunix.org

Using the Kernel Security Module Interface
Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护者。他在 IBM 工作,从事各种与 Linux 内核相关的工作,可以通过 greg@kroah.com 联系到他。
加载 Disqus 评论