第 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 概念。一个进程可只保留 |
一、核心概念
本章围绕 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;见 |
文件 capability 三套集 |
文件 permitted(强制给新进程 permitted)、file effective(单 bit,决定 permitted 是否自动转 effective)、file inheritable(与进程 inheritable AND 后转 permitted)。 |
§39.3.2;存在 |
exec capability 转换规则 |
|
§39.5; |
capability bounding set (cap_bset) |
每线程一个上界集合;init 起步含全部能力; |
§39.5.1;2.6.25 起 per-thread;见 CapBnd;永久防止进程拿到某 capability 是隔离服务最严的拦截。 |
securebits(pure-cap 开关) |
线程级 flag: |
§39.8;配 |
二、详细笔记
39.1 能力的动机
What:传统 UNIX 二元权限(root vs 非 root)粒度太粗——任何 root 进程都能做任意事。Capabilities 把 root 权限拆为独立的单位(CAP_DAC_OVERRIDE、CAP_NET_ADMIN 等),进程可分别启用/禁用。
Why:要让程序(如 date 改时间)只需一项特权,却不必给整个 root 凭证;这能大幅缩小被攻陷时的爆炸半径。
How:
-
起源:POSIX 1003.1e 草案(1990s 流产),Linux 实现 + 大幅扩展;Solaris 10、Trusted IRIX、TrustedBSD 也有类似机制。
-
Linux 现状:默认 euid==0 即「全能力」;用 capabilities 可以只保留单个能力。
-
按能力粒度保护资源:表 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:
-
permitted:上限集。drop 后除非 exec 重新拿到,否则永久失去。
P'(permitted)也以此为输入。 -
effective:真正被内核拿来检查的能力。从 permitted 取子集,临时禁用/启用。
-
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:
-
文件 permitted:
setcap("cap_xxx=p")——该 capability 会*强制*加入执行该文件后进程的 permitted。 -
文件 effective 位:单 bit。=1 → permitted 转 effective(capability-dumb 程序期望的语义);=0 → 程序必须自己 raise(capability-aware 程序)。
-
文件 inheritable:与进程 inheritable AND 后,加入新进程 permitted。
-
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_bset、P'(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:
-
控制:
prctl(PR_CAPBSET_DROP, capability)单向删除(不可逆);prctl(PR_CAPBSET_READ, capability)查询。 -
作用:
P'(permitted) = … | (F(permitted) & cap_bset)——文件 permitted 中超 cap_bset 之外的能力被屏蔽。 -
继承:
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:进入纯能力模式
What:securebits 是每线程一组 flag,控制 root 自动获能力的几条传统路径。SECBIT_NOROOT 关掉「root exec 拿全部能力」;SECBIT_NO_SETUID_FIXUP 关掉「set-user-ID 触发的能力变化」。
Why:让 root 进程*不*像传统那样拿全部能力——纯 capability-only 系统;只有用 file capabilities 才能获得能力。
How:
-
配对:每 base flag 有对应
_LOCKED后缀,LOCKED 后就不能再改了。 -
SECBIT_KEEP_CAPS:特例——让「全 ID 都变非 0 时」不清空 permitted;与旧的
prctl(PR_SET_KEEPCAPS)等效(被 exec 清掉)。 -
进入纯模式:
// 摘自《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); -
权限:需要
CAP_SETPCAP才能改 securebits。
When:写一个「无论 root 都不会拿到所有能力」的 daemon 时——配 cap_bset drop + securebits lock。
Example:配 above 调用后,进程和后代必须通过 file capabilities 才能获得任何 capability。
39.7 用 libcap 修改 capability
What:libcap 提供 cap_get_proc() / cap_set_proc() / cap_set_flag() / cap_free() 等函数——推荐使用,避免直接用 capset() 系统调用(语义复杂,跨实现差异大)。
Why:libcap 隐藏了 bitmask 细节;很多语言只有 libcap binding。
How——四步流程(来自 §39.7):
-
cap_get_proc()读当前 capability 集到cap_t结构;或cap_init()新建空集。 -
cap_set_flag(caps, CAP_EFFECTIVE, n, capList, CAP_SET/CLEAR)改某集。 -
cap_set_proc(caps)写回内核。 -
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:
-
strace:
strace -f -e trace=all program 2>&1 | grep EPERM—— 看哪些 syscall 因 EPERM 失败;查 syscall man page 即可知需要哪个 capability。注意有些 EPERM 不是 capability 缺失(如文件权限)。 -
kernel probe:[Hallyn, 2007] 的 systemtap 脚本在 capability 检查时打印调用方和所需能力——更准确,但需要 systemtap 编译内核模块。
When:接手老程序想最小化所需能力时。
三、关键图表
|
非可视化条目(capability 对照表)
|
|
进程 / 文件 capability 集对照
|
四、思维导图
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
五、重点与易错点
-
root 默认拿全部 capability——所以传统 set-UID-root 程序即使不知道自己用 capabilities,也都「全权」运行;用 file capabilities 才能给单能力。
-
permitted 是上限:effective 永远 ≤ permitted;可以从 effective 临时 drop 但保留 permitted;drop 出 permitted = 永久失去(除非 exec 重新拿到)。
-
effective ≠ permitted:effective 是当前生效;permitted 是上限。一个进程
getcap看 permitted,但内核检查 effective。 -
drop permitted 是不可逆的:下个
cap_set_proc()不能再加上;除非 exec 一个有 file permitted 的新可执行文件。这正是设计意图。 -
文件 effective 位是单 bit:不是因为「不想存 N 个」,而是因为它只决定 自动迁移——「设为 1」让 permitted 转 effective(capability-dumb 程序期望),「设为 0」要程序自己 raise(capability-aware 期望)。
-
exec 转换公式易记:
P'(perm) = (P(inh) & F(inh)) | (F(perm) & bset);P'(eff) = F(eff) ? P'(perm) : 0;P'(inh) = P(inh)。 -
bounding set 与 permitted 不同:bounding 是「exec 后能注入什么」;permitted 是「现在有什么」。
-
CAP_SETPCAP 语义依赖内核版本:2.6.24 之前能让进程给别人赋能力;现代内核(file capabilities 启用时)只允许改自己的 securebits 和 bounding。
-
securebits 在 exec 后大多保留:除
SECBIT_KEEP_CAPS(与旧 PR_SET_KEEPCAPS 兼容,特例清掉)。这是纯 cap 系统的入口。 -
fork()继承 bounding set;子进程可继续 drop bounding——但不能加回(drop 不可逆)。
-
libcap API 比直接 syscall 简洁:
cap_get_proc/cap_set_proc/cap_set_flag/cap_free四步流程;推荐只用此 API。 -
cap-bounding-drop 后不要 fork 还能交还——一旦 drop,某些能力永远拿不回(除非 exec 新可执行文件带 file permitted)。
-
推断能力需求用 strace 多次运行——单次可能漏(条件路径)。
-
capability-dumb 程序期望 file effective = 1;在 cap-aware 程序(用 libcap 显式 raise)上设 effective = 0 更安全。
-
不要依赖 /proc/PID/status 的 CapEff 而改逻辑——它是 per-thread 视图,不同线程可能不同。
-
跨章衔接:与第 9 章用户/组 ID、SUID;第 38 章 set-UID 安全的对比;第 17 章 extended attributes(capability xattr 在那里);第 35 章调度;第 15 章 ACL。