Linux内核分析之安全模块-03

55.1 Linux Capability 机制详解

Capability 把 root 的"全部权限"拆成编号的能力位(kernel_cap_t 是两个 64 位字的位图,52.3.1 节),分布在 cred 的五个集合中,exec 时按一组确定性规则计算新集合。本节解剖位表、五集合语义与 exec 计算式。


55.1.1 能力位表与五集合

能力位选摘 (include/uapi/linux/capability.h:15-423):

  0 CAP_CHOWN            改文件属主
  1 CAP_DAC_OVERRIDE     绕过文件权限位
  3 CAP_FOWNER           以非属主操作文件
  5 CAP_KILL             发信号给任意进程
  6 CAP_SETGID/SETUID    切换 UID/GID
  8 CAP_SETPCAP          转移能力给他人
 10 CAP_NET_BIND_SERVICE 绑 <1024 端口
 12 CAP_NET_ADMIN        网络配置 (48.2 节 rtnetlink)
 14 CAP_NET_RAW          原始套接字
 16 CAP_SYS_MODULE       加载内核模块
 17 CAP_SYS_RAWIO        原始 IO/ioperm
 18 CAP_SYS_CHROOT       chroot
 19 CAP_SYS_PTRACE       调试任意进程
 21 CAP_SYS_ADMIN        万能位 (挂载/容器/...)
 22 CAP_SYS_BOOT         重启
 24 CAP_SYS_RESOURCE     超限额
 27 CAP_MKNOD            建设备文件
 38 CAP_BPF              BPF 编程 (52.2 节)
 40 CAP_CHECKPOINT_RESTORE (LAST_CAP, :423)

 五个集合 (cred.h:124-128 区):
  cap_inheritable (i)  exec 后子进程可继承的
  cap_permitted (p)    该进程允许拥有的上限池
  cap_effective (e)    当前生效 (内核检查的就是它!)
  cap_bset (b)         边界集: p 永远不能超出 b
                       (只减不增的"天花板")
  cap_ambient (a)      非特权 exec 也能保留的能力
                       (i 的子集 — 解决"能力随
                        setuid 链掉线"的工程痛点)

55.1.2 exec 时的能力计算式

新进程能力的确定性计算 (capability.h 的 CAP_BPRP 注释):

 P'(permitted) = (P(inheritable) & F(inheritable)) |
                 (F(permitted) & P(bset))
 P'(effective) = F(setuid root) ? P'(permitted) : P(inheritable)...
 P'(ambient)   = P(ambient) & P'(permitted) & P'(inheritable)
 P'(bset)      = P(bset)

 (P=进程旧集, F=文件能力 xattr —
  setcap cap_net_bind_service+ep /usr/bin/myapp 的存储处)

 三种典型场景:

 [1] root exec 普通程序: F 无能力 xattr
     P'(p) = 全集, e 因非 setuid 清空 → 能力掉线
     (root 跑个 ls 也不再全能 — CAP_AUDIT_WRITE 等
      ambient 兜底的历史细节)
 [2] setuid-root 程序: e = p' 全开 (传统语义保持)
 [3] setcap 授权的非 root 程序: 文件 xattr 带来的
     能力经 bset 过滤进入 p'/e' — 无 root 绑 80 端口

55.1.3 操纵接口与守则

用户态接口:

 capset(2) / libcap (cap_set_proc): 运行时改集合
 capsh --print        现状诊断
 prctl(PR_CAPBSET_DROP, cap)  收紧 bset (永失该能力)

 驱动/子系统守则 (全书对照):
 新代码用 ns_capable(ns, 具体位) — 永不 capable(
  CAP_SYS_ADMIN) (52.3 节"万能位"收敛)
 文件类操作用 file_ns_capable (capability.c:433)
   (以打开时的凭证判, 防"打开后换身份"TOCTOU)

 授权的反面清单 (容器运行时默认 drop):
  CAP_SYS_ADMIN (挂载=容器逃逸主通道)
  CAP_SYS_PTRACE / SYS_MODULE / SYS_RAWIO /
  NET_ADMIN / MKNOD ...

小结

Capability 把 root 特权拆为约 40 个位并放进 cred 五集合(i/p/e/b/a),exec 的确定性计算式保证能力沿进程树的可预测流动(文件 xattr 授予、bset 天花板、ambient 继承);内核检查一律走 ns_capable(具体位)——CAP_SYS_ADMIN 的滥用是攻击面,细粒度位与命名空间限定(55.3 节)是现代答案。下一节看判定的完整决策序。

55.2 cred 结构与权限检查

权限判定的完整决策序是"DAC → 能力 → LSM → seccomp"四道门(52.3.3 节总览),本节解剖前两道的实现细节:cred 快照的复制/提交协议、四种 UID 的使用点、文件能力 xattr,以及 file_ns_capable 的 TOCTOU 防御。


55.2.1 cred 快照协议

prepare/commit 的不可变快照 (52.3.1 节的展开):

 修改身份的内核路径 (setuid/setresuid/...):
   new = prepare_creds()      复制当前 cred (usage++)
   改 new->euid 等            (只改副本!)
   commit_creds(new)          task->cred 原子切换 (RCU)
                              旧 cred usage-- → 归零释放

 读者纪律: 权限检查路径
   const struct cred *cred = current_cred()
   (可能持引用, 期间 cred 不会变 — 快照一致)
 fork: copy_creds → 子继承 (引用)

 cred 的共享优化: 无修改的 fork/clone 共享同一
   cred 对象 (usage++ 而非复制) — 千线程进程
   的内存账 (对照 21 章 slab: cred_cache)

55.2.2 DAC 判定:四种 UID 的使用点

一次 open 的判定序列 (33 章路径查找的末段):

 inode_permission(inode, MAY_READ|MAY_WRITE):
   ├─ sb/s_isuid 特例 (只读挂载/不可变文件)
   ├─ exec 层: security_inode_permission (52 章 LSM)
   ├─ DAC 主体: fsuid/fsgid (VFS 专用 — 52.3.1 节
   │   与 euid 分离: NFS 代理性服务可代查而
   │   不动进程身份)
   ├─ 三级匹配: owner (fsuid==i_uid) →
   │   group (组成员) → other
   │   匹配级权限位含需? → 放行
   └─ 不放行 → CAP_DAC_OVERRIDE / CAP_DAC_READ_SEARCH
       (能力可以"盖过" DAC — 55.1 节位 1/3 的用途)

 setuid 程序的四 UID 舞蹈 (passwd 例):
   exec(setuid-root): ruid=user, euid=0, suid=0
   读 /etc/shadow: 用 euid=0 → DAC 过
   降权算哈希: seteuid(user) — euid 回 user
     (suid=0 保存着, 可提回)
   提回写 shadow: seteuid(0)
   全程 ruid=user — 进程的"真实身份"从未是 root

55.2.3 文件能力与 TOCTOU 防御

文件能力 (security.capability xattr, 55.1.2 的 F 集):

 setcap cap_net_bind_service=ep /usr/bin/myapp
 exec 时经 55.1.2 计算式进入进程集
 (insecure 状态标记: fd 再变文件系统属主等
  边界会清 e — 内核置 fscap 安全旗)

 TOCTOU 防御 (file_ns_capable, capability.c:433):
 场景: 进程 A 以特权打开设备 fd, 之后降权,
   再把 fd 传给无权进程 B — B 能用 fd 干特权事吗?
 file_ns_capable(file, ns, cap):
   判定的是 file->f_cred (打开时快照!) —
   B 经此 fd 的操作以"打开者的能力"为准
   (对照 33.1.4 节 f_path/f_cred 的设计: 会话
    状态在打开时刻固化)

检查入口清单(本卷的汇总):capable/init(宿主全权)、ns_capable(ns 限定)、file_ns_capable(打开时快照)、capable_wrt_inode_uidgid(+ 文件属主 ns——跨 userns 的 DAC 与能力联合判定,9/33/55 三章在此合流)。


小结

cred 的快照协议(prepare/commit+RCU)保证身份切换的原子性与检查者一致性;DAC 判定用 fsuid 走三级匹配、失败由 CAP_DAC_* 盖章,setuid 程序的四 UID 舞蹈(r/e/s 分离)是 suid 语义的内核骨架;文件能力经 xattr 授予非 root 程序特权,file_ns_capable 以"打开时快照"封死换身份 TOCTOU。能力与 cred 的最后一块拼图是命名空间——下一节看容器 root 的映射真相。

55.3 User Namespace 与 Capability 映射

user namespace 让"UID/GID/能力检查全部按 namespace 重新解释":容器内进程可以拥有 uid=0(容器内 root)而宿主视角是 uid=100000——因为它的 cred.user_ns 指向容器 ns,其内 0 映射到宿主 100000。能力检查 ns_capable(user_ns, cap) 因此"容器内全能、宿主外无权"。本节解剖映射机制、创建流程与逃逸防线。


55.3.1 映射机制:uid_gid_map

// include/linux/user_namespace.h:25-33
struct uid_gid_map { /* 64 bytes -- 1 cache line */
    u32 nr_extents;
    union {
        struct {        /* 线性写法 ≤5 段 */
            u32 lower;  /* ns 内 id */
            u32 upper;  /* 宿主 id */
            u32 count;
        } extent[5];
        struct {        /* >5 段: 堆上反向树 */
            struct uid_gid_extent *forward;
            struct uid_gid_extent *reverse;
        } forward_reverse;
    };
};
// include/linux/user_namespace.h:76
struct user_namespace { ... /* 映射 + 属主 + 层级 */ };
// kernel/user_namespace.c(map_write: 写 /proc/self/uid_map 的实现)
映射的三段式表示 (容器常见配置):

 宿主视角          容器 ns 内视角
 100000-165535  ↔  0-65536        (一段: lower=0,
                                    upper=100000, count=65536)
 多段也支持 (如再映射 gid 独立段)

 写入协议 (/proc/self/uid_map):
 [1] 创建 ns: unshare(CLONE_NEWUSER) 或 clone 标志
     → 新 ns 无映射 (nobody 状态)
 [2] 父进程写 /proc/<child>/setgroups "deny"
     (安全序: 防 gid 映射绕过辅助组检查)
 [3] 父进程写 uid_map: "0 100000 65536"
     权限检查: 写者须对父 ns 有 CAP_SETUID,
     且映射的宿主段不与其已有映射重叠
 [4] 同法写 gid_map → 映射生效

 判定转换 (kernel/userns 的 from_kuid/make_kuid):
 每个跨 ns 边界显示/比较 UID 都经映射翻译 —
 ls 显示 0, /proc 的统计按宿主 100000 记账
 (12 章 cgroup 限额算宿主视角 — 两套视角并存)

55.3.2 容器 root 的能力真相

容器内 root 的权限边界:

 容器内进程: uid=0, cred->user_ns = 容器 userns
   ns_capable(容器 userns, CAP_SYS_ADMIN) → TRUE!
   → 容器内可以: mount(受限类)、chroot、
     设备 mknod(映射内)、设自己 ns 的主机名...
 ns_capable(init_user_ns, CAP_SYS_ADMIN) → FALSE!
   → 对宿主: 不能挂宿主 fs、不能加载模块、
     不能碰宿主进程 (kill/ptrace 跨 ns 被拒)、
     不能建真实网络接口...

 能力的"ns 相对性"对照表:

 操作                  容器内 root   判定的 ns
 mount --bind 自己的目录   可         容器 ns (MS_PRIVATE 等)
 mount -t ext4 /dev/sda   不可        需要 init ns 能力
 kill 宿主进程            不可        ptrace/信号跨 ns 检查
 改自己 ns 内主机名        可          UTS ns (9 章)
 加载内核模块             不可        init_user_ns

"capability 相对化"的实现原理:能力检查从来不是"是否 uid=0",而是"cred->user_ns(或其祖先链)中是否有该能力位"——创建 user ns 时新 ns 的父进程在父 ns 中有 CAP_SYS_ADMIN,新 ns 内自动获得全集能力(相对新 ns)。祖先链遍历(ns_capable 沿 user_ns->level 上溯)让父容器/宿主保留管理权。

55.3.3 逃逸防线与残余风险

userns 是隔离而非沙箱 — 已知攻击面:

 [1] 内核漏洞面扩大: userns 让非特权用户可以
     触达"需要 CAP_SYS_ADMIN 的代码路径"
     (大量 CVE 的前置条件是"unshare 后可达")
     → 缓解: user.max_user_namespaces=0/限发/
       unprivileged_userns_clone 策略 (发行版分歧)
 [2] setgroups 漏洞史 (CVE-2014-8989): 映射写入
     顺序强制的由来
 [3] 特权文件/设备: 容器内 root + mknod 白名单
     泄漏 → 设备是全局资源 (ns 不隔离设备号)

 纵深组合 (本卷收束):
   userns (55) + seccomp (56, 拒危险 syscall)
   + AppArmor/SELinux (53/54, 拒敏感路径)
   + cgroup (12, 资源上限)
   = 容器安全 = 多层各管一维, 单层皆可绕

小结

user namespace 以 uid_gid_map(≤5 段线性/多段树)实现 UID/GID 的 ns 内外翻译:容器内 root 经 ns_capable(容器 ns) 获得相对全能、对宿主 init ns 全无——能力检查的"ns 相对性"是容器权限模型的根基;映射写入的 setgroups 先行协议与祖先链遍历是安全与管理的细节;unshare 扩大内核攻击面的代价由 seccomp/MAC/cgroup 纵深补偿。第十部分的各章在此互相引用成网。下一章看系统调用级的最后防线——seccomp。