云计算百科
云计算领域专业知识百科平台

TLPI 第39章 读书笔记:Capabilities

笔记和练习博客总目录见:开始读TLPI。

本章介绍 Linux 权能(capabilities)机制。该机制将传统 UNIX「要么全部权限、要么没有权限」的特权模型,拆分为若干可独立启用或禁用的单项权能。借助权能机制,程序可以执行部分特权操作,同时被禁止执行其余特权操作。

39.1 Rationale for Capabilities

传统 UNIX 特权机制将进程分为两类:一类是有效用户 ID 为 0 的进程(超级用户),这类进程会绕过所有权限检查;其余所有进程则需要根据自身用户 ID 与组 ID 接受权限校验。

该机制的权限粒度过粗,这是一个突出问题。如果我们希望允许某个进程执行仅超级用户才能执行的操作(例如修改系统时间),就必须以有效用户 ID 为 0 的身份运行该进程。(非特权用户需要执行这类操作时,一般通过 set-user-ID-root 程序实现。)但这样做会赋予该进程大量其他操作权限 —— 例如访问文件时绕过全部权限校验。一旦程序出现非预期行为(可能由意外场景触发,或是遭到恶意用户蓄意利用),就会引发各类安全漏洞。第 38 章介绍过处理该问题的传统方案:下放有效特权(即把有效用户 ID 从 0 修改为其他值,同时将保存的设置用户 ID 保持为 0),仅在必要时临时重新获取特权。

Linux 权能机制优化了该问题的处理方式。内核在执行安全检查时,不再使用单一特权(即有效用户 ID 为 0),而是将超级用户特权拆分为多个独立单元,这些单元就称为权能(capabilities)。每项特权操作都关联一项特定权能;进程只有持有对应的权能,才能执行该操作(与进程的有效用户 ID 无关)。换句话说,本书中但凡提到 Linux 下的特权进程,其真实含义是:该进程拥有执行某项特定操作所需的相关权能。

大多数场景下,Linux 权能机制对使用者是透明的。原因在于:当不感知权能机制的应用程序假定自身有效用户 ID 为 0 时,内核会向该进程授予全部权能。

💡 Linux 引入 capability 是为了拆分 root 权限,但是为了兼容几十年的旧 root 程序,做了以上兼容规则。

Linux 权能的实现基于 POSIX 1003.1e 草案标准(http://wt.tuxomania.net/publications/posix.1e/)。这项标准化工作在 20 世纪 90 年代后期尚未完成就宣告搁置,但各类权能实现依然以这份草案为基础。(表 39-1 所列的部分权能在 POSIX.1e 草案中定义,但大量权能属于 Linux 扩展。)

少数其他 UNIX 实现也提供了权能机制,例如 Sun 的 Solaris 10 以及更早的可信 Solaris 版本、SGI 的 Trusted Irix,还有 FreeBSD 的 TrustedBSD 项目(参见 [Watson, 2000])。其他一些操作系统中也存在类似机制;例如Digital VMS 系统中的特权机制。

39.2 The Linux Capabilities

表 39-1 列出了 Linux 的各项权能,并简要(且非完整)说明了这些权能对应的适用操作。

Table 39-1: Operations permitted by each Linux capability

Linux Capabilities 权能对照表(中英双语 Markdown 表格)

权能允许进程执行的操作
CAP_AUDIT_CONTROL (自 Linux 2.6.11)启用、禁用内核审计日志;修改审计过滤规则;获取审计状态与过滤规则
CAP_AUDIT_WRITE (自 Linux 2.6.11)向内核审计日志写入记录
CAP_CHOWN 修改文件用户ID(属主);或将文件组ID修改为该进程并非其成员的用户组(chown())
CAP_DAC_OVERRIDE 绕过文件读、写、执行权限检查(DAC 即自主访问控制 discretionary access control);读取 /proc/PID 下 cwd、exe、root 符号链接内容
CAP_DAC_READ_SEARCH 绕过文件读权限检查,以及目录读、执行(检索)权限检查
CAP_FOWNER 通常可忽略这类操作的权限校验:操作一般要求进程文件系统UID与文件UID匹配(chmod()、utime());可对任意文件设置inode标志;对任意文件设置、修改ACL;删除文件时忽略目录粘滞位作用(unlink()、rmdir()、rename());在open()与fcntl(F_SETFL)中,可对任意文件指定O_NOATIME标志
CAP_FSETID 修改文件时,内核不会清除set-user-ID和set-group-ID位(write()、truncate());当文件组ID与进程文件系统GID或附加组ID不匹配时,仍可启用该文件的set-group-ID位(chmod())
CAP_IPC_LOCK 解除内存锁定限制(mlock()、mlockall()、shmctl(SHM_LOCK)、shmctl(SHM_UNLOCK));在shmget()中使用SHM_HUGETLB标志,在mmap()中使用MAP_HUGETLB标志
CAP_IPC_OWNER 绕过System V IPC对象操作的权限检查
CAP_KILL 绕过发送信号的权限检查(kill()、sigqueue())
CAP_LEASE (自 Linux 2.4)在任意文件上建立文件租约(fcntl(F_SETLEASE))
CAP_LINUX_IMMUTABLE 设置inode的追加(append)与不可变(immutable)标志
CAP_MAC_ADMIN (自 Linux 2.6.25)配置或修改强制访问控制MAC状态(由部分Linux安全模块实现)
CAP_MAC_OVERRIDE (自 Linux 2.6.25)覆盖强制访问控制MAC限制(由部分Linux安全模块实现)
CAP_MKNOD (自 Linux 2.4)使用mknod()创建设备文件
CAP_NET_ADMIN 执行各类网络相关操作(例如:设置特权socket选项、启用组播、配置网络接口、修改路由表)
CAP_NET_BIND_SERVICE 绑定到特权端口
CAP_NET_BROADCAST (未使用)执行socket广播,监听组播
CAP_NET_RAW 使用原始套接字与数据包套接字
CAP_SETGID 任意修改进程组ID(setgid()、setegid()、setregid()、setresgid()、setfsgid()、setgroups()、initgroups());通过UNIX域套接字传递凭证(SCM_CREDENTIALS)时伪造组ID
CAP_SETFCAP (自 Linux 2.6.24)设置文件权能
CAP_SETPCAP 如果不支持文件权能,可以对任意其他进程(包括自身)增删进程允许集中的权能;如果支持文件权能,可将进程权能边界集中的任意权能添加到可继承集、从边界集中移除权能,并修改安全位标志(securebits flags)
CAP_SETUID 任意修改进程用户ID(setuid()、seteuid()、setreuid()、setresuid()、setfsuid());通过UNIX域套接字传递凭证(SCM_CREDENTIALS)时伪造用户ID
CAP_SYS_ADMIN 在打开文件的系统调用(如open()、shm_open()、pipe()、socket()、accept()、exec()、acct()、epoll_create())中突破 /proc/sys/fs/file-max 限制;执行各类系统管理操作,包括quotactl()(磁盘配额管理)、mount()与umount()、swapon()与swapoff()、pivot_root()、sethostname()和setdomainname();执行各类syslog(2)操作;覆盖RLIMIT_NPROC资源限制(fork());调用lookup_dcookie();设置trusted与security扩展属性;对任意System V IPC对象执行IPC_SET、IPC_RMID操作;通过UNIX域套接字传递凭证(SCM_CREDENTIALS)时伪造进程ID;使用ioprio_set()设置IOPRIO_CLASS_RT调度类别;使用TIOCCONS ioctl;在clone()与unshare()中使用CLONE_NEWNS标志;执行KEYCTL_CHOWN、KEYCTL_SETPERM等keyctl()操作;管理random(4)设备;以及各类设备专属操作
CAP_SYS_BOOT 使用reboot()重启系统;调用kexec_load()
CAP_SYS_CHROOT 使用chroot()设置进程根目录
CAP_SYS_MODULE 加载、卸载内核模块(init_module()、delete_module()、create_module())
CAP_SYS_NICE 提升nice值(nice()、setpriority());修改任意进程的nice值(setpriority());为调用进程设置SCHED_RR、SCHED_FIFO实时调度策略;重置SCHED_RESET_ON_FORK标志;为任意进程设置调度策略与优先级(sched_setscheduler()、sched_setparam());为任意进程设置I/O调度类别和优先级(ioprio_set());设置任意进程的CPU亲和性(sched_setaffinity());使用migrate_pages()迁移任意进程,并允许进程迁移到任意NUMA节点;对任意进程使用move_pages();在mbind()与move_pages()中使用MPOL_MF_MOVE_ALL标志
CAP_SYS_PACCT 使用acct()启用或禁用进程记账
CAP_SYS_PTRACE 使用ptrace()跟踪任意进程;读取任意进程的 /proc/PID/environ;对任意进程调用get_robust_list()
CAP_SYS_RAWIO 通过iopl()、ioperm()对I/O端口执行操作;访问/proc/kcore;打开/dev/mem和/dev/kmem
CAP_SYS_RESOURCE 使用文件系统预留空间;调用ioctl控制ext3日志;突破磁盘配额限制;提升硬资源限制(setrlimit());覆盖RLIMIT_NPROC资源限制(fork());将System V消息队列msg_qbytes上限提升至 /proc/sys/kernel/msgmnb 之上;绕过 /proc/sys/fs/mqueue 下文件定义的各类POSIX消息队列限制
CAP_SYS_TIME 修改系统时钟(settimeofday()、stime()、adjtime()、adjtimex());设置硬件时钟
CAP_SYS_TTY_CONFIG 使用vhangup()对终端或伪终端执行虚拟挂断操作

如果你想要原版英文表格(同样把版本信息移到第二列),我也一并生成。

39.3 Process and File Capabilities

每个进程关联三组权能集,分别称为允许集(permitted)、有效集(effective)与可继承集(inheritable),每组权能集可包含表 39-1 中一项或多项权能。

每个文件同样可以附带三组同名的权能集。(后文会说明缘由:文件有效权能集实际上仅是一个单独比特位,只有启用或禁用两种状态。)我们将在后续小节详细介绍每一类权能集。

39.3.1 Process Capabilities

内核为每个进程维护三组权能集(以位掩码实现),其中可启用表 39-1 中一项或多项权能。这三组集合说明如下:

  • 允许集(Permitted):进程可以使用的权能。允许集是一个上限超集,决定了可以添加到有效集与可继承集中的权能范围。如果进程从自身允许集中移除某项权能,那么该进程将永远无法重新获取这项权能(除非通过 exec 执行另一个能重新赋予该权能的程序)。
  • 有效集(Effective):内核针对该进程执行权限检查时所使用的权能。只要某项权能仍保留在允许集中,进程就可以将它从有效集中移除,临时禁用该权能,后续还能再把这项权能恢复到有效集中。
  • 可继承集(Inheritable):当该进程调用 exec 加载新程序时,可以传递给新进程允许集的权能。

我们可以在 Linux 特有的 /proc/PID/status 文件的 CapInh、CapPrm、CapEff 这三个字段中,查看任意进程三组权能集的十六进制表示。

getpcap 工具(属于 39.7 节介绍的 libcap 软件包)能够以更易读的格式展示进程的权能。

通过 fork() 创建的子进程,会继承父进程权能集的副本。我们将在 39.5 节介绍执行 exec() 时权能集的处理规则。

实际上,权能是按线程的属性,进程内的每一个线程都可以独立调整自身权能。多线程进程中某个特定线程的权能,记录在 /proc/PID/task/TID/status 文件;而 /proc/PID/status 文件展示的是主线程的权能。

在内核 2.6.25 版本之前,Linux 使用 32 位来表示权能集。2.6.25 内核新增更多权能,因此权能集升级为 64 位。

39.3.2 File Capabilities

如果文件绑定了权能集,那么当进程通过exec执行该文件时,就会依据这些权能集确定赋予该进程的权能。文件权能集共分为三组:

  • 允许集(Permitted):在exec()执行期间,可以添加到进程允许集的一组权能,不受进程原有权能影响。
  • 有效集(Effective):它仅为单个比特标志位。若该位启用,那么exec()执行时,进程新允许集中生效的权能,也会在进程新有效集中启用;若文件有效位未启用,则exec()之后,进程新有效集初始为空。
  • 可继承集(Inheritable):该集合会与进程的可继承集做掩码运算,以此确定exec()执行后需要在进程允许集中启用的权能。

39.5 节将详细介绍exec()过程中文件权能的使用规则。

文件允许权能集与可继承权能集过去被称为强制集(forced)和许可集(allowed)。这些术语现已废弃,但仍有助于理解概念。文件允许权能,是在exec()期间强制写入进程允许集的权能,不受进程原有权能影响。文件可继承权能,则是文件允许纳入进程允许集的权能,前提是这些权能同时在进程的可继承权能集中处于启用状态。

与文件绑定的权能存放在名为security.capability的安全扩展属性中(参见 16.1 节)。修改该扩展属性需要拥有CAP_SETFCAP权能。

39.3.3 Purpose of the Process Permitted and Effective Capability Sets

进程允许权能集规定了进程能够使用的权能。进程有效权能集代表进程当前生效的权能 —— 也就是内核用来校验进程是否具备执行特定操作所需特权的权能集合。

允许权能集对有效集划定了上限。进程仅当某项权能存在于允许集中时,才可以在有效集中启用该权能。(术语 add to、set 有时和 raise 同义;对应的反向操作称为 drop,也可用 remove 或 clear 表达。)

有效权能集与允许权能集之间的关系,类似于 setuid-root 程序中有效用户 ID和保存的设置用户 ID之间的关系。从有效集中移除某项权能,就好比临时将有效用户 ID 从 0 下放,同时保存的设置用户 ID 仍保留为 0。同时从有效集和允许集中移除某项权能,则类似于将有效用户 ID 与保存的设置用户 ID 都改为非 0 值,永久放弃超级用户特权。

39.3.4 Purpose of the File Permitted and Effective Capability Sets

文件允许权能集提供了一种机制,可让可执行文件向进程赋予权能。它指定一组权能,在调用exec()时分配给进程的允许权能集。

文件有效权能集是一个仅可启用或禁用的标志位。想要理解为什么该集合只有单个比特位,我们需要分析执行程序时会出现的两种场景:

  • 程序不感知权能(capability-dumb):即程序不了解权能机制(属于传统 set-user-ID-root 程序)。这类程序不知道需要在有效集中启用权能才能执行特权操作。对于这类程序,执行exec()时,应当把进程新允许集中的全部权能自动赋予有效集。该效果通过启用文件有效位实现。
  • 程序感知权能(capability-aware):程序在设计时就考虑了权能框架,会调用相应系统调用(后文介绍)来启用、关闭有效集中的权能。基于最小权限原则,这类程序在exec()之后,进程有效权能集内所有权能初始都应处于禁用状态。该效果通过关闭文件有效位实现。

39.3.5 Purpose of the Process and File Inheritable Sets

乍一看,进程与文件各自使用允许集和有效集,似乎就足以构成一套完整的权能体系。但在部分场景下,仅依靠这两组集合是不够的。举个例子:如果进程调用exec()时,希望在执行 exec 之后保留当前拥有的部分权能,该如何处理?直观上权能机制似乎可以直接在 exec 过程中保留进程的允许权能,以此实现该需求。但这种方案无法处理下面两类场景:

  • 执行 exec 操作本身可能需要某些特权(例如CAP_DAC_OVERRIDE),而我们并不希望这些特权在 exec 之后继续保留。
  • 假设我们已经主动丢弃了一部分不想在 exec 后保留的允许权能,但随后 exec 调用失败。此时程序可能又需要那些已经不可恢复地丢弃的允许权能。

出于上述原因,进程的允许权能集不会在 exec 调用后自动保留。为此引入了另一组权能集:可继承集(inheritable set)。可继承集提供一种机制,让进程能够跨 exec 保留部分权能。

进程可继承权能集指定了一组权能,这些权能可以在 exec 执行期间分配给进程的允许权能集。对应的文件可继承集会与进程可继承集做掩码按位与(AND)运算,以此确定 exec 过程中实际添加到进程允许权能集的权能。

不直接跨 exec 保留进程允许权能集,还有一层设计理念上的原因。权能机制的核心思想是:赋予进程的所有特权,都由进程所执行的文件来授予和管控。虽然进程可继承集指定了可以跨 exec 传递的权能,但这些权能还要经过文件可继承集的掩码筛选。

39.3.6 Assigning and Viewing File Capabilities from the Shell

setcap(8) 和 getcap(8) 命令包含在 39.7 节介绍的 libcap 软件包中,可用于操作文件权能集。我们以标准date(1)程序为例,简单演示这两条命令的用法。(按照 39.3.4 节中的定义,该程序属于不感知权能的应用。)在特权状态下运行时,date(1)可修改系统时间。date 程序并未设置 set-user-ID-root 位,因此通常情况下,唯一以特权运行它的方式是切换为超级用户。

我们先查看当前系统时间,再尝试以非特权用户修改系统时间:

$ date
Mon Sep 28 03:08:38 AM UTC 2026
$ date -s '2018-02-01 21:39'
date: cannot set date: Operation not permitted
Thu Feb 1 09:39:00 PM UTC 2018

从上面的输出可以看到:date 命令修改系统时间的操作失败了,但仍然按照标准格式输出了传入的参数。

接下来切换为超级用户,此时就可以成功修改系统时间:

$ sudo date -s '2026-09-28 03:11:30'
Mon Sep 28 03:11:30 AM UTC 2026
$ date
Mon Sep 28 03:11:32 AM UTC 2026

现在复制一份 date 程序,并为其赋予所需的权能:

$ pwd
/home/vagrant/test
$ whereis -b date
date: /usr/bin/date
$ cp /usr/bin/date .
$ sudo setcap "cap_sys_time=pe" date
$ getcap date
date cap_sys_time=ep

上面这条 setcap 命令,将 CAP_SYS_TIME 权能赋予该可执行文件的允许集(p)与有效集(e)。随后我们使用 getcap 命令验证赋予该文件的权能。(setcap 和 getcap 用于表示权能集的语法,在 libcap 软件包中的 cap_from_text (3) 手册页里有说明。)

此 date 程序副本上挂载的文件权能,使得非特权用户也能借助该程序修改系统时间:

$ ./date -s '2026-09-28 03:16:30'
Mon Sep 28 03:16:30 AM UTC 2026
$ date
Mon Sep 28 03:16:32 AM UTC 2026

39.4 The Modern Capabilities Implementation

一套完整的权能实现,需要具备以下要素:

  • 对于每一项特权操作,内核应当检查进程是否拥有对应的权能,而不是校验有效用户 ID(或文件系统用户 ID)是否为 0。
  • 内核必须提供系统调用,支持获取和修改进程的权能。
  • 内核必须支持将权能附着在可执行文件上,使得执行该文件时,进程能够获得对应的权能。这类似于 set-user-ID 位,但可以对可执行文件独立指定各项权能。除此之外,系统还需要提供一组编程接口与命令,用于设置和查看附着在可执行文件上的权能。

2.6.23 及更早版本的 Linux 内核,仅满足上述前两项要求。自 2.6.24 内核起,Linux 支持向文件附加权能。内核 2.6.25 与 2.6.26 版本陆续增加各类特性,从而完成整套权能机制的实现。

在权能相关的大部分讨论中,我们将聚焦现代实现方案。39.10 节会介绍文件权能引入之前的实现差异。此外,在现代内核中文件权能属于可选内核组件;但在主体内容的讨论里,我们假定该组件已开启。后文会介绍文件权能未启用时产生的行为差异。(很多方面,该行为和 2.6.24 之前、尚未实现文件权能的 Linux 内核相近。)

接下来的章节,我们会深入讲解 Linux 权能机制的实现细节。

39.5 Transformation of Process Capabilities During exec()

在执行exec()期间,内核会根据进程当前的权能,以及待执行文件的权能集,为该进程设置新的权能。内核通过下面这套规则计算进程的新权能:

P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset)
P'(effective) = F(effective) ? P'(permitted) : 0
P'(inheritable) = P(inheritable)

在上述规则中:P 代表exec 执行之前的权能集;P' 代表exec 执行之后的权能集;F 代表文件权能集。标识符cap_bset代表权能边界集(capability bounding set)。请注意:exec()不会修改进程的可继承权能集。

39.5.1 Capability Bounding Set

权能边界集(capability bounding set)是一种安全机制,用于限制进程在exec()过程中能够获取到的权能。该集合的作用规则如下:

  • 在exec()执行期间,权能边界集会与文件允许权能做按位与运算,以此确定赋予新程序的允许权能。换句话说,如果某项权能不在边界集中,那么即便可执行文件的允许权能集包含该项权能,也无法把它授予进程。
  • 权能边界集是进程可继承权能集可添加权能的上限超集。也就是说,如果某项权能不在边界集中,进程就无法将自身允许集中的该项权能添加到可继承集;进而,即便待执行文件的可继承集包含该权能,也无法依靠前面提到的权能转换规则,在 exec 之后把这项权能保留在进程允许集中。

权能边界集是按进程的属性,通过fork()创建的子进程会继承该集合,并且在exec()执行后仍然保留。在支持文件权能的内核上,所有进程的始祖init进程启动时,其权能边界集包含全部权能。

进程如果拥有CAP_SETPCAP权能,就可以通过prctl()的PR_CAPBSET_DROP操作,不可撤销地从自身边界集中移除权能。(从边界集中移除某项权能,不会影响该进程的允许、有效与可继承权能集。)进程可使用prctl()的PR_CAPBSET_READ操作,判断某权能是否存在于自身边界集中。

更准确地说,权能边界集属于按线程的属性。自 Linux 2.6.26 起,该属性在 Linux 特有的/proc/PID/task/TID/status文件中以CapBnd字段展示;而/proc/PID/status文件展示的是进程主线程的边界集。

39.5.2 Preserving root Semantics

为保留 root 用户的传统语义(即 root 拥有全部特权),当 root 执行文件时,会忽略该文件上绑定的所有权能集。取而代之,在 39.5 节所述算法中,exec()执行期间文件权能集按虚拟定义处理,规则如下:

  • 如果执行的是 set-user-ID-root 程序,或者调用exec()的进程的真实用户 ID、有效用户 ID 为 0,则文件可继承集与允许集被虚拟定义为全部置 1(包含所有权能)。
  • 如果执行的是 set-user-ID-root 程序,或者调用exec()的进程有效用户 ID 为 0,则文件有效位被虚拟定义为已开启。

假设当前执行的是 set-user-ID-root 程序,基于文件权能集的这套虚拟定义,39.5 节中进程新允许权能集与有效权能集的计算可简化为:

P'(permitted) = P (inheritable) | cap_bset
P'(effective) = P'(permitted)

39.6 Effect on Process Capabilities of Changing User IDs

为保持用户 ID 在 0 与非 0 之间切换时与传统行为兼容,内核在修改进程用户 ID(通过setuid()等系统调用)时执行如下逻辑:

  • 若真实用户 ID、有效用户 ID 或保存的设置用户 ID之前为 0,并且经过本次用户 ID 修改后,这三者全部变为非 0 值,则清空允许权能集与有效权能集(即永久丢弃所有权能)。
  • 若有效用户 ID 从 0 修改为非 0 值,则清空有效权能集(即关闭有效权能,但允许集中的权能仍可重新启用)。
  • 若有效用户 ID 从非 0 值修改为 0,则将允许权能集原样复制到有效权能集(即所有允许权能全部变为生效状态)。
  • 如果文件系统用户 ID 从 0 修改为非 0 值,则会从有效权能集中清除以下和文件操作相关的权能:CAP_CHOWN、CAP_DAC_OVERRIDE、CAP_DAC_READ_SEARCH、CAP_FOWNER、CAP_FSETID、CAP_LINUX_IMMUTABLE(Linux 2.6.30 起)、CAP_MAC_OVERRIDE以及CAP_MKNOD(Linux 2.6.30 起)。 与之相反,如果文件系统用户 ID 从非 0 值修改为 0,那么允许集中已启用的上述权能,会在有效集中一并启用。进行这些操作,是为了维持 Linux 特有的文件系统用户 ID 在权限变更时的传统语义。
  • 39.7 Changing Process Capabilities Programmatically

    进程可通过capset()系统调用,或者更推荐使用下文介绍的 libcap API,来启用或移除自身权能集中的权能。修改进程权能需要遵守如下规则:

  • 若进程的有效权能集中不具备CAP_SETPCAP权能,则新的可继承集必须是原有可继承集与允许集的并集的子集。
  • 新的可继承集必须是原有可继承集与权能边界集的并集的子集。
  • 新的允许集必须是原有允许集的子集。换言之,进程不能自行赋予自身原本不存在的允许权能。换句话说,一旦某项权能从允许集中移除,就无法再次获取。
  • 新的有效集仅能包含同时存在于新允许集中的权能。
  • The libcap API 到这里为止,我们刻意没有给出capset()系统调用,以及用于获取进程权能的配套调用capget()的函数原型。原因是应当尽量避免直接使用这两个原始系统调用,而改用 libcap 库提供的函数。这些库函数提供了一套接口,兼容已撤回的 POSIX 1003.1e 草案标准,并附带部分 Linux 扩展。

    受篇幅限制,我们不详细描述 libcap API。作为概览,使用这套库函数的程序一般遵循以下步骤:

  • 调用cap_get_proc(),从内核读取进程当前权能集的副本,存入该函数在用户空间分配的结构体。(也可以调用cap_init()创建全新的空权能集结构体。)在 libcap API 中,cap_t是指向这类结构体的指针类型。
  • 调用cap_set_flag(),更新用户空间结构体,对前一步拿到的允许、有效、可继承权能集执行启用(CAP_SET)与清除(CAP_CLEAR)操作。
  • 调用cap_set_proc()函数,将用户空间结构体传回内核,以此修改进程的权能。
  • 调用cap_free()函数,释放第一步中由 libcap API 分配的结构体。
  • 撰写本书时,新版改进型权能库 API libcap-ng仍在开发中,详情可参见 https://github.com/stevegrubb/libcap-ng。

    💡 2.6.24之后的linux都支持libcap-ng:

    $ ldconfig -p | grep libcap-ng
    libcap-ng.so.0 (libc6,x86-64) => /lib64/libcap-ng.so.0

    Example program 本书 164 页的清单 8-2中,我们给出过一个程序,该程序依据标准密码数据库校验用户名与密码。当时我们提到,读取影子密码文件需要特权;该文件受保护,仅允许 root 用户或 shadow 用户组的成员读取。传统方案要给这个程序赋予所需权限,要么以 root 账号运行,要么将其设置为 set-user-ID-root 程序。下面我们给出该程序的修改版本,使用权能机制与 libcap API 实现。

    普通用户想要读取影子密码文件,需要绕过标准文件权限检查。查阅表 39-1 所列权能,可以找到适用权能:CAP_DAC_READ_SEARCH。这份修改后的密码认证程序见清单 39-1。该程序利用 libcap API,在访问影子密码文件之前,在有效权能集中启用CAP_DAC_READ_SEARCH;访问完成后立刻移除这项权能。要让非特权用户可以运行此程序,必须在文件的允许权能集中配置该项权能,如下例 shell 会话所示:

    # 此时check_password_caps为绿色,表示其为可执行文件
    $ ls -l check_password_caps
    -rwxr-xr-x. 1 vagrant vagrant 33792 Mar 31 07:26 check_password_caps
    $ sudo setcap "cap_dac_read_search=p" check_password_caps
    # 此时check_password_caps为红底黑字,表示其启用了文件权能
    $ ls -l check_password_caps
    -rwxr-xr-x. 1 vagrant vagrant 33792 Mar 31 07:26 check_password_caps
    $ getcap check_password_caps
    check_password_caps cap_dac_read_search=p
    $ ./check_password_caps
    Username: vagrant
    Password:
    Successfully authenticated: UID=1000

    Listing 39-1: A capability-aware program that authenticates a user

    // cap/check_password_caps.c
    略。

    39.8 Creating Capabilities-Only Environments

    在前述章节中,我们介绍了**用户 ID 为 0(root)的进程在权能(capabilities)**方面受到特殊处理的多种情形:

    • 若一个进程存在至少一个等于 0 的用户 ID,当它将自身所有用户 ID 全部设置为非 0 值时,该进程的**许可权能集(permitted capability set)与有效权能集(effective capability set)**会被清空。(参见 39.6 节)
    • 若进程的**有效用户 ID(effective user ID)为 0,当该 ID 修改为非 0 值时,进程会丢失其有效权能。反之,若将有效用户 ID 改回 0,则会把许可权能集复制到有效权能集。当进程的文件系统用户 ID(file-system user ID)**在 0 与非 0 值之间切换时,针对一部分权能也会执行类似逻辑。(参见 39.6 节)
    • 如果真实用户 ID或有效用户 ID为 root 的进程执行新程序,或是任意进程执行设置用户 ID 为 root(set-user-ID-root)的程序,则文件可继承权能集与文件许可权能集在概念上被定义为全 1。若进程的有效用户 ID 为 0,或是正在执行 set-user-ID-root 程序,则文件有效位在概念上被定义为 1。(参见 39.5.2 节)在常规场景下(即真实用户 ID 与有效用户 ID 均为 root,或是正在执行 set-user-ID-root 程序),这意味着该进程的许可权能集与有效权能集将获得全部权能。

    在一套完全基于权能的系统中,内核无需针对 root 执行上述任何特殊处理。系统中将不再存在 set-user-ID-root 程序,而是通过文件权能,仅授予程序运行所需的最小权能。

    由于现有应用程序在设计时并未考虑使用文件权能这套底层机制,内核必须保留对用户 ID 为 0 的进程的传统处理逻辑。尽管如此,我们有时仍希望应用运行在纯基于权能的环境中:在该环境下,root 不再享有上文所述的任何特殊特权。自内核 2.6.26 版本起,若内核开启文件权能支持,Linux 提供了安全位(securebits)机制。该机制管理一组按进程设置的标志位,用来启用或禁用针对 root 的三项特殊处理规则。(严格来说,securebits 标志实际上是按线程的属性。)

    securebits 机制管控的标志位如表 39-2 所示。这些标志成对存在:基础标志以及对应的锁定标志。每个基础标志控制上述 root 特殊处理规则中的一项。设置对应的锁定标志属于一次性操作,它会阻止后续修改关联的基础标志 —— 锁定标志一旦置位,便无法清除。

    Table 39-2: The securebits flags

    标志位置位后的含义
    SECBIT_KEEP_CAPS 当进程存在一个或多个值为0的用户ID,随后将其所有用户ID设置为非0值时,不清空许可权能集。该标志仅在未同时设置 SECBIT_NO_SETUID_FIXUP 时生效。在调用 exec() 时此标志会被清除。
    SECBIT_NO_SETUID_FIXUP 在有效用户ID或文件系统用户ID于0与非0值之间切换时,不修改权能。
    SECBIT_NOROOT 若真实用户ID或有效用户ID为0的进程执行 exec(),或是执行 set-user-ID-root 程序,不自动赋予权能(除非该可执行文件本身带有文件权能)。
    SECBIT_KEEP_CAPS_LOCKED 锁定 SECBIT_KEEP_CAPS 标志。
    SECBIT_NO_SETUID_FIXUP_LOCKED 锁定 SECBIT_NO_SETUID_FIXUP 标志。
    SECBIT_NOROOT_LOCKED 锁定 SECBIT_NOROOT 标志。

    由fork()创建的子进程会继承 securebits 标志的设置。执行exec()期间,绝大多数标志配置都会保留,但SECBIT_KEEP_CAPS例外:为了兼容下文所述PR_SET_KEEPCAPS的历史机制,该标志会被清除。

    进程可通过prctl()的PR_GET_SECUREBITS操作读取 securebits 标志。 若进程拥有CAP_SETPCAP权能,则可以通过prctl()的PR_SET_SECUREBITS操作修改 securebits 标志。 纯基于权能的应用程序,可通过如下调用,不可逆地关闭调用进程及其所有后代进程中 root 的特殊处理逻辑:

    if (prctl(PR_SET_SECUREBITS,
    /* SECBIT_KEEP_CAPS off */
    SECBIT_NO_SETUID_FIXUP | SECBIT_NO_SETUID_FIXUP_LOCKED |
    SECBIT_NOROOT | SECBIT_NOROOT_LOCKED)
    == –1)
    errExit("prctl");

    执行该调用之后,此进程及其后代进程获取权能的唯一途径,就是运行带有文件权能的可执行程序。

    SECBIT_KEEP_CAPS and the prctl() PR_SET_KEEPCAPS operation SECBIT_KEEP_CAPS 标志的作用:当进程存在一个或多个值为 0 的用户 ID,随后将全部用户 ID 修改为非 0 值时,阻止内核清空进程的权能。

    粗略来说,SECBIT_KEEP_CAPS 只提供了 SECBIT_NO_SETUID_FIXUP 一半的功能。(如表 39-2 所述,仅当未设置 SECBIT_NO_SETUID_FIXUP 时,SECBIT_KEEP_CAPS 才会生效。)引入该标志,是为了在 securebits 中提供一个和旧版 prctl () 的 PR_SET_KEEPCAPS 操作功能对等的标志,二者控制同一属性。 (两种机制有一处差别:进程调用 prctl () 的 PR_SET_KEEPCAPS 操作时,不需要拥有 CAP_SETPCAP 权能。)

    前文提到过:exec () 执行时,除 SECBIT_KEEP_CAPS 以外,其余所有 securebits 标志都会保留。SECBIT_KEEP_CAPS 的处理逻辑和其他 securebits 标志相反,目的是和 prctl () 的 PR_SET_KEEPCAPS 所设置属性的行为保持一致。

    prctl () 的 PR_SET_KEEPCAPS 操作,是为那些运行在不支持文件权能的旧内核上的 set-user-ID-root 程序设计的。这类程序依旧可以按需通过代码主动丢弃、重新启用权能,以此提升安全性(参见 39.10 节)。

    然而,即便这类 set-user-ID-root 程序丢弃了除自身所需之外的全部权能,它仍然保有两项重要特权:能够访问 root 属主的文件,并且可以通过执行新程序重新获取权能(见 39.5.2 节)。想要永久去除这些特权,唯一办法是把进程的所有用户 ID 都设置为非 0 值。但通常情况下,执行该操作会清空许可权能集与有效权能集(参见 39.6 节中介绍用户 ID 变更对权能影响的四点内容),这就违背了初衷:即永久脱离 UID 0 身份,同时保留部分权能。为支持该场景,可以使用 prctl () 的 PR_SET_KEEPCAPS 操作设置进程属性,使得在全部用户 ID 修改为非 0 值时,许可权能集不会被清空。(但在此场景下,无论 “保留权能” 属性如何设置,进程的有效权能集总会被清空。)

    39.9 Discovering the Capabilities Required by a Program

    假设有这样一个程序:它并不感知权能机制,且仅提供二进制可执行文件;或是程序源码体量过大,难以通过阅读源码判断运行它需要哪些权能。如果该程序需要特权,但又不适合做成 set-user-ID-root 程序,那么我们该如何通过 setcap(8) 确定要赋予这个可执行文件的许可权能?有两种解决思路:

    • 使用 strace(1)(附录 A)观察哪些系统调用返回 EPERM 错误,该错误代表缺少必需的权能。随后查阅系统调用手册页或内核源码,就能推断出所需权能。这种方法并不完美:EPERM 错误偶尔也会由其他原因触发,其中部分原因和程序的权能需求毫无关系。此外,程序可能会正常调用需要特权的系统调用,在发现自身没有对应操作权限后再改变执行逻辑。在确定可执行文件真实所需权能时,有时很难区分这类误报。
    • 在内核执行权能校验时,可借助内核探针生成监控输出。文件权能的开发者之一撰写的文献 [Hallyn, 2007] 给出了实现示例。文中的探针会记录每一次权能校验请求:被调用的内核函数、被校验的权能,以及发起请求的程序名称。虽然这种方法相比 strace (1) 工作量更大,但可以更精准地判断程序所需的权能。

    39.10 Older Kernels and Systems Without File Capabilities

    本节将介绍旧版内核中权能机制实现上的各类差异,同时说明在不支持文件权能的内核上存在的区别。Linux 在两种场景下不支持文件权能:

    • Linux 2.6.24 版本之前,尚未实现文件权能。
    • 自 Linux 2.6.24 版本起,若编译内核时未开启 CONFIG_SECURITY_FILE_CAPABILITIES 配置项,则可以禁用文件权能。

    尽管 Linux 从 2.2 版内核就引入了权能机制,并支持将权能关联到进程,但文件权能的实现却是数年之后才完成。文件权能迟迟没有落地,其原因在于策略层面考量,而非技术难题。(实现文件权能所依赖的扩展属性,见第 16 章,自 2.6 内核起就已经可用。)内核开发者群体的主流看法是:要求系统管理员为每一个特权程序设置并维护多套不同的权能集合,而部分权能的影响微妙且影响深远,这会带来难以管控的复杂运维工作。与之相对,系统管理员早已熟悉传统 UNIX 权限模型,知道要审慎对待 set-user-ID 程序,并且可以通过简单的 find 命令,找出系统中所有 set-user-ID 与 set-group-ID 程序。尽管如此,文件权能的开发者论证该机制在运维上具备可行性,最终提出了足够有说服力的观点,使得文件权能被并入内核。

    The CAP_SETPCAP capability 在不支持文件权能的内核上(即 2.6.24 之前的所有内核,以及 2.6.24 之后但禁用文件权能的内核),CAP_SETPCAP权能的语义有所不同。在遵循与 39.7 节所述类似规则的前提下,若进程的有效权能集中有CAP_SETPCAP,理论上它可以修改其他进程的权能。修改对象可以是单个其他进程、指定进程组内的全部进程,或是系统上除 init 进程与调用进程自身之外的所有进程。最后一种场景排除 init 进程,因为它是系统运行的基础;同时排除调用进程,是考虑调用者可能试图清除系统内所有其他进程的权能,而我们不希望调用进程自身的权能也被一并清除。

    不过,修改其他进程权能仅停留在理论可行。在旧内核,以及禁用文件权能的现代内核中,权能边界集(下一节讨论)总会屏蔽掉CAP_SETPCAP权能。

    The capability bounding set 自 Linux 2.6.25 起,权能边界集是按进程的属性。但在更早的内核中,权能边界集属于全局系统属性,会作用于系统上所有进程。全局权能边界集在初始化时就会屏蔽 CAP_SETPCAP(上文所述)。

    在 2.6.25 之后的内核上,仅当内核开启文件权能支持时,才允许从进程独立的边界集中移除权能。此种情况下,所有进程的祖先 init 进程启动时,其边界集包含全部权能;系统后续创建的其他进程会继承该边界集的副本。若文件权能被禁用,由于前面提到的 CAP_SETPCAP 语义差异,init 启动时的边界集包含除 CAP_SETPCAP 以外的全部权能。

    Linux 2.6.25 中权能边界集的语义还有另一处改动。如前文(39.5.1 节)所述:在 Linux 2.6.25 及更高版本中,进程级权能边界集作为一个上限超集,限制可添加到进程可继承权能集中的权能。而在 Linux 2.6.24 及更早版本里,系统全局权能边界集并不具备这种掩码限制作用。(该机制在这类内核中并无必要,因为它们不支持文件权能。)

    可通过 Linux 特有的/proc/sys/kernel/cap-bound文件访问系统全局权能边界集。进程需要拥有CAP_SYS_MODULE权能,才能够修改cap-bound的内容。但只有 init 进程可以开启该掩码中的标志位;其他特权进程只能关闭标志位。这些限制带来的结果是:在不支持文件权能的系统上,我们永远无法为某个进程赋予CAP_SETPCAP权能。这样设计是合理的,因为该权能可被用来破坏整套内核权限校验机制。(如果在极少数场景下需要修改该限制,则必须加载内核模块修改集合的值、修改 init 程序源码,或是修改内核源码中权能边界集的初始化代码并重新编译内核。)

    容易令人混淆的是:尽管全局 cap-bound 的值是位掩码,但该文件中的内容是以有符号十进制数展示的。例如,该文件初始值为 -257。这是对一个位掩码做二进制补码解读得到的结果:除 (1 << 8) 对应的位以外,其余所有位全部置 1(即二进制:11111111 11111111 11111110 11111111);而 CAP_SETPCAP 的编号为 8。

    Using capabilities within a program on a system without file capabilities 即便在不支持文件权能的系统上,我们依然可以使用权能机制提升程序的安全性,操作步骤如下:

  • 在有效用户 ID 为 0 的进程中运行程序(通常是 set-user-ID-root 程序)。这类进程会在其许可权能集与有效权能集中获得全部权能(前文提及的 CAP_SETPCAP 除外)。
  • 在程序启动阶段,调用 libcap API,清空有效权能集中的所有权能;并从许可权能集中剔除除后续运行必需之外的所有权能。
  • 设置 SECBIT_KEEP_CAPS 标志(或调用 prctl () 的 PR_SET_KEEPCAPS 操作实现同样效果),保证下一步操作不会丢弃权能。
  • 将所有用户 ID 设置为非 0 值,避免该进程访问 root 属主的文件,或是通过 exec () 重新获取权能。 如果我们仅希望阻止进程在执行 exec () 时重新获得特权,但仍需要允许它访问 root 属主的文件,就可以把上面两步合并,只设置 SECBIT_NOROOT 标志。(当然,允许访问 root 属主文件本身会带来安全漏洞风险。)
  • 在程序后续整个运行生命周期内,可通过 libcap API,按需将许可权能集中保留的权能在有效权能集中启用或清除,以此完成需要特权的任务。
  • 一些为 2.6.24 版本之前的 Linux 内核编译的应用程序就采用了这种方案。

    在内核开发者群体中,有部分人当初反对为可执行文件实现文件权能。他们认为,上文介绍的这种方案有一个明显优点:应用开发者清楚该可执行程序需要哪些权能。与之相对,系统管理员往往难以轻易确定这一信息。

    39.11 Summary

    Linux 权能机制将特权操作划分成相互独立的类别,允许为进程授予部分权能,同时拒绝授予另一些权能。该机制相比传统的全有或全无特权模型是一大改进:传统模型下,进程要么拥有全部操作特权(用户 ID 为 0),要么完全没有特权(非 0 用户 ID)。自内核 2.6.24 起,Linux 支持将权能附加到文件上,这样进程就可以通过执行程序来获得选定的权能。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » TLPI 第39章 读书笔记:Capabilities
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!