第 39 章 能力 (Capabilities)

      +

      核心结论

      • 能力的核心动机:传统 UNIX 特权是二元(euid == 0 全权,否则靠权限位检查)——粒度太粗。Capabilities 把 root 特权拆成几十个独立能力(CAP_CHOWN、CAP_NET_ADMIN 等),进程可在 permitted/effective/inheritable 三套集合里独立启用/禁用每个能力。

      • 进程三套 capability 集:permitted(上限集,决定进程*可以*用哪些能力)、effective(真正起作用的能力,被内核用来做权限检查)、inheritable(exec 时可转入新 permitted 的能力)。drop 一项 permitted 能力就永远丢——除非 exec 拿到新 permitted。

      • 文件三套 capability 集:permitted (force 集)、effective (单个 bit)、inheritable (allowed 集)。文件能力存在安全扩展属性 security.capability 里,需要 CAP_SETFCAP 写。

      • exec 时的转换公式P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset)P'(effective) = F(effective) ? P'(permitted) : 0。这是理解「为什么 glibc 在不同版本里都保留向后兼容能力」的关键。

      • capability bounding set:每进程(thread)一个上界集,限制 exec 后能拿到哪些 permitted;init 起步包含所有能力;带 CAP_SETPCAP 可通过 prctl(PR_CAPBSET_DROP) 不可逆地删除。

      • securebits:每线程一组 flag,控制「root 是否享受特权空降」。SECBIT_NOROOT 让 exec 不自动赋全部能力;SECBIT_NO_SETUID_FIXUP 关掉 set-user-ID 触发的自动能力变化——是「纯能力系统」的开关。

      本章主旨

      Linux 把传统的 root-euid 方案替换成基于「能力的」方案——几十个独立 capability 取代单一的 root 概念。一个进程可只保留 CAP_DAC_READ_SEARCH 读 /etc/shadow 而不持其他任何 root 能力,极大缩小爆炸半径。本章先讲动机与传统根/能力对比,然后深入进程/文件的三套 capability 集、exec 转换规则、bounding set 和 securebits,最后介绍 libcap API 和发现未知程序所需能力的方法(strace / 内核 probe)。读者应掌握 (1) 三套集的语义和 (2) exec 转换公式,这是写「最小特权」程序的核心。

      一、核心概念

      本章围绕 6 个核心概念展开:从能力方案的动机出发,深入进程三套集、文件三套集、exec 转换规则、bounding set、libcap API、pure-capability 模式。

      概念 定义 + 重要性 实现提示

      能力 vs 传统 root 二元模型

      root 的特权被拆为约 40 个 capability(CAP_CHOWN、CAP_NET_ADMIN、CAP_SYS_ADMIN…)——表 39-1 列全。每个特权操作都对应一个 capability;进程可独立启用/禁用。

      §39.1;root 进程默认继承全部能力 (euid=0 → 全 capabilities);POSIX 1003.1e 草案 (完成前流产) 是基础,Linux 扩展之。

      进程 capability 三套集

      permitted:上限,决定进程可用能力;effective:当前生效;inheritable:exec 时可传递。drop 出 permitted = 永久丢失(除非 exec 拿回)。

      §39.3.1;见 /proc/PID/status 的 CapInh/CapPrm/CapEff;2.6.25 改 64-bit;多线程时每线程独立。

      文件 capability 三套集

      文件 permitted(强制给新进程 permitted)、file effective(单 bit,决定 permitted 是否自动转 effective)、file inheritable(与进程 inheritable AND 后转 permitted)。

      §39.3.2;存在 security.capability xattr;CAP_SETFCAP 写;用 setcap(8)/getcap(8) 操纵。

      exec capability 转换规则

      P'(permitted) = (P(inh) & F(inh)) ∪ (F(perm) & cap_bset)P'(effective) = F(eff) ? P'(perm) : 0P'(inh) = P(inh)。exec 不传 inheritable 给 inheritable。

      §39.5;set-user-ID-root 文件 + 进程有 0 用户 ID → 文件集视为全 1

      capability bounding set (cap_bset)

      每线程一个上界集合;init 起步含全部能力;prctl(PR_CAPBSET_DROP) 不可逆删除能力。

      §39.5.1;2.6.25 起 per-thread;见 CapBnd;永久防止进程拿到某 capability 是隔离服务最严的拦截。

      securebits(pure-cap 开关)

      线程级 flag:SECBIT_NOROOT 关掉「exec 时 root 自动获全部能力」;SECBIT_NO_SETUID_FIXUP 关掉「set-user-ID 触发的能力变更」;加 LOCKED 后缀则永久锁定。

      §39.8;配 CAP_SETPCAP 修改;prctl(PR_SET_SECUREBITS,…​) 三步锁。

      二、详细笔记

      39.1 能力的动机

      What:传统 UNIX 二元权限(root vs 非 root)粒度太粗——任何 root 进程都能做任意事。Capabilities 把 root 权限拆为独立的单位(CAP_DAC_OVERRIDE、CAP_NET_ADMIN 等),进程可分别启用/禁用。

      Why:要让程序(如 date 改时间)只需一项特权,却不必给整个 root 凭证;这能大幅缩小被攻陷时的爆炸半径。

      How

      1. 起源:POSIX 1003.1e 草案(1990s 流产),Linux 实现 + 大幅扩展;Solaris 10、Trusted IRIX、TrustedBSD 也有类似机制。

      2. Linux 现状:默认 euid==0 即「全能力」;用 capabilities 可以只保留单个能力。

      3. 按能力粒度保护资源:表 39-1 列约 40 个能力——每个特权内核操作都映射一个;检查时看 effective 集是否有该能力。

      When:写新的「需要特权的 daemon」时——采用 capabilities + 文件能力比 set-UID-root 安全得多。

      Example

      // 摘自《The Linux Programming Interface》 第 39 章 §39.3.6
      // 给 /bin/date 副本赋 CAP_SYS_TIME,普通用户就能改系统时间
      $ cp /bin/date ./date
      $ sudo setcap "cap_sys_time=pe" date          # p=permitted, e=effective
      $ getcap date
      date = cap_sys_time+ep
      $ ./date -s '2010-12-28 15:55'

      39.2 进程 capability 三套集

      What:每个进程(实际是 per-thread,2.6.25+)维护三套 capability bitmask:permitted、effective、inheritable。

      Why:分离「上限」「当前生效」「跨 exec 传递」三种语义,使最小特权管理精细化。

      How

      1. permitted:上限集。drop 后除非 exec 重新拿到,否则永久失去。P'(permitted) 也以此为输入。

      2. effective:真正被内核拿来检查的能力。从 permitted 取子集,临时禁用/启用。

      3. inheritable:跨 exec 传递时与文件 inheritable 共同决定进入新 permitted 的能力集合。

      伪代码操作(来自 §39.7 规则):

      // 摘自《The Linux Programming Interface》 第 39 章
      /* raise_cap(cap) —— 把 cap 加入 effective(须在 permitted 内) */
      /* drop_cap(cap)  —— 从 effective 删 cap(保留 permitted) */
      /* drop_all_caps —— 清空三套集(永久) */

      Why drop permitted:因为 drop permitted 才能真正限制未来行为(effective 只是临时禁用,攻击者改回即可);只要 permitted 包含某能力,进程随时可恢复 effective。

      When:写需要「临时提升能力做一件事、然后立刻关掉」的程序——raise → 做 → drop。

      Example

      // 摘自《The Linux Programming Interface》 第 39 章 Listing 39-1
      int modifyCap(int capability, int setting) {
          cap_t caps;
          cap_value_t capList[1];
          caps = cap_get_proc();                    /* 读当前 */
          if (caps == NULL) return -1;
          capList[0] = capability;
          cap_set_flag(caps, CAP_EFFECTIVE, 1,
                       capList, setting);           /* 改 effective */
          cap_set_proc(caps);                       /* 写回 */
          cap_free(caps);
          return 0;
      }

      39.3 文件 capability

      What:可执行文件也带三套 capability(permitted、effective 1-bit、inheritable),存在扩展属性 security.capability,通过文件属性赋予新进程能力。

      Why:让非 root 程序拥有特定 capability,无需 set-UID-root;类似 set-UID 但粒度更细。

      How

      1. 文件 permittedsetcap("cap_xxx=p")——该 capability 会*强制*加入执行该文件后进程的 permitted。

      2. 文件 effective 位:单 bit。=1 → permitted 转 effective(capability-dumb 程序期望的语义);=0 → 程序必须自己 raise(capability-aware 程序)。

      3. 文件 inheritable:与进程 inheritable AND 后,加入新进程 permitted。

      4. capability-dumb vs capability-aware:前者是「不知道 capabilities 的程序,希望和传统 set-UID 一样自动拿全」;后者会主动管 effective 集,期望最小特权。

      When:给自定义 daemon 副本加特定 capability 时——setcap "cap_x=p"(无 e 位,让 capability-aware 程序显式 raise);或者 "cap_x=pe"(让任何用户执行都拿到)。

      Example

      // 摘自《The Linux Programming Interface》 第 39 章
      $ setcap "cap_dac_read_search=p" check_password_caps
      $ getcap check_password_caps
      check_password_caps = cap_dac_read_search+p        /* 仅有 p,无 e */
      $ ./check_password_caps                                /* 程序自己 raise */

      39.4 exec 时 capability 的转换

      What:exec 时,新进程的 capability 集按规则从「进程旧集 + 文件集 + bounding set」推出。新 permitted = (旧 inheritable & 文件 inheritable) | (文件 permitted & bounding);新 effective = 文件 effective ? 新 permitted : 0;新 inheritable = 旧 inheritable。

      Why:让 inheritance 有更细的控制;只有 有意允许(文件 permitted)且 bounding 不限(cap_bset)的 capability 能转 permitted;只有 文件 effective bit 开启,新 permitted 才会自动转 effective。

      How——逐条规则(来自 §39.5):

      // 摘自《The Linux Programming Interface》 第 39 章 §39.5
      P'(permitted) = (P(inheritable) & F(inheritable))
                    | (F(permitted) & cap_bset);
      P'(effective) = F(effective) ? P'(permitted) : 0;
      P'(inheritable) = P(inheritable);                 /* 不变 */

      特殊规则(§39.5.2):对 set-user-ID-root 文件或 euid==0 进程——文件集视为全 1,导致 P'(permitted) = cap_bsetP'(effective) = cap_bset(保留传统 root 语义)。

      When:调试 capability 程序时盯 (a) 文件 effective 位,(b) bounding set,(c) 进程 permitted 边界。

      Example(概念):要保证新 permitted 自动转到 effective,文件必须有 e 位;要是 p 位单独在新 permitted,而程序 capability-dumb 又没主动 raise effective,就会失败。

      39.5 capability bounding set

      What:每线程一个上界集合,决定文件 permitted 能注入哪些能力到新进程。init 起步包含所有 capability。

      Why:让系统管理员或父进程永久「禁止」某些 capability 进入新进程——例如容器里不让 CAP_SYS_ADMIN

      How

      1. 控制prctl(PR_CAPBSET_DROP, capability) 单向删除(不可逆);prctl(PR_CAPBSET_READ, capability) 查询。

      2. 作用P'(permitted) = …​ | (F(permitted) & cap_bset)——文件 permitted 中超 cap_bset 之外的能力被屏蔽。

      3. 继承fork() 继承;exec() 保留。

      When:长期运行的 init / service manager 启动 daemon 前,先 prctl(PR_CAPBSET_DROP, CAP_SYS_ADMIN) 之类的能力,确保今后 fork 出来的进程都拿不到该能力。

      Example

      // 摘自《The Linux Programming Interface》 第 39 章
      /* 不可逆地丢弃 CAP_SYS_ADMIN */
      if (prctl(PR_CAPBSET_DROP, CAP_SYS_ADMIN, 0, 0, 0) == -1)
          errExit("prctl");
      /* 之后任何后代都拿不到 CAP_SYS_ADMIN 了 */

      39.6 securebits:进入纯能力模式

      Whatsecurebits 是每线程一组 flag,控制 root 自动获能力的几条传统路径。SECBIT_NOROOT 关掉「root exec 拿全部能力」;SECBIT_NO_SETUID_FIXUP 关掉「set-user-ID 触发的能力变化」。

      Why:让 root 进程*不*像传统那样拿全部能力——纯 capability-only 系统;只有用 file capabilities 才能获得能力。

      How

      1. 配对:每 base flag 有对应 _LOCKED 后缀,LOCKED 后就不能再改了。

      2. SECBIT_KEEP_CAPS:特例——让「全 ID 都变非 0 时」不清空 permitted;与旧的 prctl(PR_SET_KEEPCAPS) 等效(被 exec 清掉)。

      3. 进入纯模式

        // 摘自《The Linux Programming Interface》 第 39 章 §39.8
        prctl(PR_SET_SECUREBITS,
              SECBIT_NO_SETUID_FIXUP | SECBIT_NO_SETUID_FIXUP_LOCKED |
              SECBIT_NOROOT | SECBIT_NOROOT_LOCKED);
      4. 权限:需要 CAP_SETPCAP 才能改 securebits。

      When:写一个「无论 root 都不会拿到所有能力」的 daemon 时——配 cap_bset drop + securebits lock。

      Example:配 above 调用后,进程和后代必须通过 file capabilities 才能获得任何 capability。

      39.7 用 libcap 修改 capability

      Whatlibcap 提供 cap_get_proc() / cap_set_proc() / cap_set_flag() / cap_free() 等函数——推荐使用,避免直接用 capset() 系统调用(语义复杂,跨实现差异大)。

      Why:libcap 隐藏了 bitmask 细节;很多语言只有 libcap binding。

      How——四步流程(来自 §39.7):

      1. cap_get_proc() 读当前 capability 集到 cap_t 结构;或 cap_init() 新建空集。

      2. cap_set_flag(caps, CAP_EFFECTIVE, n, capList, CAP_SET/CLEAR) 改某集。

      3. cap_set_proc(caps) 写回内核。

      4. cap_free(caps) 释放。

      When:每次需要临时升能力做一件事的程序——参考 39.1 的 check_password_caps

      Example:见 39.2 节的 modifyCap(capability, CAP_SET) 函数。

      39.8 找出程序需要什么 capability

      What:对于「二进制或源码看不懂」的程序,可以用 (1) strace 看哪些 syscall 返回 EPERM;(2) kernel probe 监控 capability check。

      Why:决定 setcap 应该赋哪些 capability;多了攻击面太大,少了程序跑不起来。

      How

      1. stracestrace -f -e trace=all program 2>&1 | grep EPERM —— 看哪些 syscall 因 EPERM 失败;查 syscall man page 即可知需要哪个 capability。注意有些 EPERM 不是 capability 缺失(如文件权限)。

      2. kernel probe:[Hallyn, 2007] 的 systemtap 脚本在 capability 检查时打印调用方和所需能力——更准确,但需要 systemtap 编译内核模块。

      When:接手老程序想最小化所需能力时。

      三、关键图表

      非可视化条目(capability 对照表)
      能力 允许的操作 备注

      CAP_CHOWN

      chown 改 owner/group(即使非成员)

      §39.2 表

      CAP_DAC_OVERRIDE

      绕过文件读/写/执行权限

      第 9-15 章

      CAP_DAC_READ_SEARCH

      绕过读 + 目录读/exec 检查

      读 shadow 文件

      CAP_FOWNER

      绕过 fsuid == owner 的检查(chmod/utime);置 i-node flag;删 sticky 文件

      CAP_FSETID

      写时保留 SUID/SGID;set-GID when group mismatch

      CAP_KILL

      绕过发送信号的权限检查

      kill/sigqueue

      CAP_NET_ADMIN

      网络配置(路由、接口、组播、套接字选项)

      CAP_NET_BIND_SERVICE

      绑定特权端口 (< 1024)

      CAP_NET_RAW

      raw/packet 套接字

      ping/traceroute

      CAP_SETUID

      任意 set-user-ID 操作

      CAP_SETGID

      任意 set-group-ID;SCM_CREDENTIALS 伪造 group

      CAP_SETFCAP

      设文件 capability(xattr security.capability)

      §39.3.2

      CAP_SETPCAP

      修改 capability(4.4BSD 行为)

      CAP_SYS_ADMIN

      包罗万象——mount、quotactl、swapon、setns、keyctl 等

      「超级能力」,小心授

      CAP_SYS_PTRACE

      ptrace 任意进程;读 /proc/PID/environ

      CAP_SYS_RESOURCE

      setrlimit 提升 hard;覆写 RLIMIT_NPROC;ext3 ioctl

      CAP_SYS_TIME

      settimeofday / adjtime / 硬件时钟

      CAP_SYS_CHROOT

      chroot()

      进程 / 文件 capability 集对照
      集合 含义 操作

      P(permitted)

      进程可用 capability 的上限

      drop 后永久丢

      P(effective)

      当前生效;内核做权限检查

      可从 permitted 临时加减

      P(inheritable)

      exec 时可转入新 permitted 的能力

      通常较少使用

      F(permitted)

      文件强制给新进程 permitted

      setcap "cap=pe"

      F(effective)

      单 bit;决定新 permitted 是否自动转 effective

      setcap …​e…​

      F(inheritable)

      与 P(inh) AND 后转 permitted

      cap_bset

      每进程上限;init 起步 = 全

      prctl(PR_CAPBSET_DROP)

      四、思维导图

      mindmap
        root((第 39 章 能力))
          动机与背景
            二元 root 太粗
            POSIX 1e 草案
            Linux 扩展
          进程三套集
            permitted 上限
            effective 当前
            inheritable 传递
          文件三套集
            file permitted 强制
            file effective 单 bit
            file inheritable
          exec 转换
            三条公式
            set-UID-root 全 1
            cap_bset 上界
          bounding set
            init 起步全开
            PR_CAPBSET_DROP 不可逆
          securebits
            NOROOT 关 root 空降
            SETUID_FIXUP 关 set-UID 触发
            LOCKED 永久锁定
          libcap API
            cap_get_proc set_proc
            cap_set_flag
            cap_free
          推断能力需求
            strace EPERM
            kernel probe

      五、重点与易错点

      1. root 默认拿全部 capability——所以传统 set-UID-root 程序即使不知道自己用 capabilities,也都「全权」运行;用 file capabilities 才能给单能力。

      2. permitted 是上限:effective 永远 ≤ permitted;可以从 effective 临时 drop 但保留 permitted;drop 出 permitted = 永久失去(除非 exec 重新拿到)。

      3. effective ≠ permitted:effective 是当前生效;permitted 是上限。一个进程 getcap 看 permitted,但内核检查 effective。

      4. drop permitted 是不可逆的:下个 cap_set_proc() 不能再加上;除非 exec 一个有 file permitted 的新可执行文件。这正是设计意图。

      5. 文件 effective 位是单 bit:不是因为「不想存 N 个」,而是因为它只决定 自动迁移——「设为 1」让 permitted 转 effective(capability-dumb 程序期望),「设为 0」要程序自己 raise(capability-aware 期望)。

      6. exec 转换公式易记P'(perm) = (P(inh) & F(inh)) | (F(perm) & bset)P'(eff) = F(eff) ? P'(perm) : 0P'(inh) = P(inh)

      7. bounding set 与 permitted 不同:bounding 是「exec 后能注入什么」;permitted 是「现在有什么」。

      8. CAP_SETPCAP 语义依赖内核版本:2.6.24 之前能让进程给别人赋能力;现代内核(file capabilities 启用时)只允许改自己的 securebits 和 bounding。

      9. securebits 在 exec 后大多保留:除 SECBIT_KEEP_CAPS(与旧 PR_SET_KEEPCAPS 兼容,特例清掉)。这是纯 cap 系统的入口。

      10. fork()继承 bounding set;子进程可继续 drop bounding——但不能加回(drop 不可逆)。

      11. libcap API 比直接 syscall 简洁cap_get_proc/cap_set_proc/cap_set_flag/cap_free 四步流程;推荐只用此 API。

      12. cap-bounding-drop 后不要 fork 还能交还——一旦 drop,某些能力永远拿不回(除非 exec 新可执行文件带 file permitted)。

      13. 推断能力需求用 strace 多次运行——单次可能漏(条件路径)。

      14. capability-dumb 程序期望 file effective = 1;在 cap-aware 程序(用 libcap 显式 raise)上设 effective = 0 更安全。

      15. 不要依赖 /proc/PID/status 的 CapEff 而改逻辑——它是 per-thread 视图,不同线程可能不同。

      16. 跨章衔接:与第 9 章用户/组 ID、SUID;第 38 章 set-UID 安全的对比;第 17 章 extended attributes(capability xattr 在那里);第 35 章调度;第 15 章 ACL。

      Asciidoc lint check

      asciidoctor: 无警告。