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

56.1 Seccomp 原理 —— strict 模式与 filter 模式

seccomp 有两代:strict 模式(一代,只有 read/write/exit/sigreturn 四个调用可用,违约 SIGKILL)与 filter 模式(二代,用户态提交 cBPF 过滤器按调用号/参数裁决)。共同的不变式是单向门:一旦启用不能关闭(除非 CAP_SYS_ADMIN 且未 no_new_privs),进程与其全部后代终身受限。本节解剖两模式与动作语义。


56.1.1 单向门与状态结构

// include/linux/seccomp.h(进程内状态, 节选要点)
struct seccomp {
    int mode;           /* SECCOMP_MODE_* (uapi:10-12) */
    union {
        struct seccomp_filter *filter;  /* 过滤器链头 */
        ...
    };
};
// include/uapi/linux/seccomp.h:10-12
#define SECCOMP_MODE_DISABLED   0 /* seccomp is not in use. */
#define SECCOMP_MODE_STRICT 1 /* uses hard-coded filter. */
#define SECCOMP_MODE_FILTER 2 /* uses user-supplied filter. */
单向门的两把锁:

 [1] prctl(PR_SET_SECCOMP, mode) 或 seccomp(2):
     非 CAP_SYS_ADMIN 者须先 prctl(PR_SET_NO_NEW_PRIVS, 1)
     (no_new_privs: 本进程及后代 exec 不得通过
      setuid/文件能力"升权" — 防"沙箱内程序借
      suid 逃出", 55.2 节能力语义的沙箱配对)
 [2] mode 只增不减: DISABLED → STRICT/FILTER,
     FILTER 可再叠新过滤器 (链头插, 56.2 节) —
     不可回退 (无 CAP 时)

 继承: fork/clone 自然继承 (task->seccomp 复制) —
 容器 init 设置后全容器生效

56.1.2 strict 与 filter 两模式

SECCOMP_MODE_STRICT (一代):
 可用 syscall: read/write/exit(_group)/sigreturn
 其余: 进程 SIGKILL (不可捕获!)
 用途: 计算/密码学类纯计算进程 (历史意义>
      实用) — "如果用不到就一个都不给"的极致

SECCOMP_MODE_FILTER (二代, 现代全部用法):
 seccomp(SECCOMP_SET_MODE_FILTER, flags, &prog)
   prog = cBPF 过滤器 (56.2 节)
   每次 syscall 入口执行过滤器 → 返回动作:
   SECCOMP_RET_ALLOW       放行
   SECCOMP_RET_ERRNO       返回固定 errno (伪装不存在)
   SECCOMP_RET_KILL_*      杀进程/线程 (不可捕)
   SECCOMP_RET_TRACE       通知 ptracer (沙箱调试)
   SECCOMP_RET_LOG         放行+记录 (策略生成期)
   SECCOMP_RET_USER_NOTIF  交给用户态监督者裁决
                           (56.2.3 节, 8.2 新特性)
一个最小过滤器 (语言无关的 cBPF 伪码):

 # 架构检查 (必须第一! 防 32 位调用号混淆):
 A = arch; if (A != AUDIT_ARCH_X86_64) KILL
 # 危险调用直接杀:
 if (nr == execve || nr == mount ||
     nr == keyctl || nr == bpf) KILL
 # 文件写入禁止 → 伪装 EACCES:
 if (nr == openat && arg1 & O_WRONLY) ERRNO(EACCES)
 # 其余放行:
 ALLOW

 加载:
 prctl(PR_SET_NO_NEW_PRIVS, 1, 0,0,0);
 seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog);

56.1.3 与其他门的时序对照

门 时机 可见信息 拒绝方式
seccomp syscall 入口最前 调用号+参数 KILL/ERRNO/NOTIF
DAC (33 章) VFS 判定点 路径/身份/权限位 EACCES
capability (55) 各操作点 能力位 EPERM
LSM (52-54) 250 个钩子点 完整内核上下文 EACCES+审计

seccomp 只见"号与裸参数"(struct seccomp_data:nr/arch/ip/args[6],uapi:62)——不看路径不看身份,因此便宜(纳秒级)但粗;它的价值是缩小攻击面而非精确授权。四道门是递进的漏斗,容器沙箱全部启用(56.3 节)。


小结

seccomp 的核心是"单向门 + 入口最早裁决":strict 模式(四调用极限)是历史起点,filter 模式以 cBPF 过滤器按调用号/参数返回 ALLOW/ERRNO/KILL/TRACE/LOG/USER_NOTIF 六种动作;no_new_privs 与不可回退保证沙箱不可自逃,fork 继承保证覆盖全部后代。它以纳秒级成本换取攻击面收缩——"没用到的 syscall 就不该存在"。下一节深入过滤器本体与用户态通知。

56.2 BPF 过滤器与系统调用审计

filter 模式的过滤器是 cBPF(经典 BPF,非 eBPF 指令集):输入是固定的 struct seccomp_data(调用号/架构/指令指针/六参数),输出是动作码。过滤器链式叠加(最新优先),配合 SECCOMP_RET_USER_NOTIF 可把裁决权交给用户态监督者。本节解剖数据结构、链式求值与通知协议。


56.2.1 seccomp_data 与过滤器链

// include/uapi/linux/seccomp.h:62(过滤器可见的全部输入)
struct seccomp_data {
    int nr;                 /* 系统调用号 */
    __u32 arch;             /* AUDIT_ARCH_* (必须检查!) */
    __u64 instruction_pointer;      /* 触发点的用户 IP */
    __u64 args[6];              /* 六个裸参数 */
};
// kernel/seccomp.c:224
struct seccomp_filter {
    refcount_t usage;           /* 引用计数 (进程/后代共享) */
    bool log;               /* 是否记审计 */
    bool early:1;
    struct action_cache;            /* 调用号→动作缓存 (快路径) */
    struct seccomp_filter *prev;        /* 链: 越新越前 */
    struct bpf_prog *prog;          /* cBPF 编译产物 */
    ...
};
入口求值 (seccomp_run_filters, kernel/seccomp.c:404):

 syscall 入口 → __seccomp_filter(sd, match):
   从最新 filter 向 prev 遍历:
     action = BPF_PROG_RUN(filter->prog, sd)   (:700-728)
       (cBPF JIT 后的机器码 — 56.1 节六字段输入)
     非 ALLOW → 立即按动作处置 (新过滤器优先权)
     ALLOW → 继续查更老的过滤器 (全体放行才真放行)
   全部 ALLOW → 走正常 syscall

 action_cache: 热调用号的动作缓存 —
   命中免 BPF 执行 (纳秒级)
 链式语义: "各层都要放行" — 容器运行时先挂
   宽过滤器, 应用层再挂严过滤器, 交集生效

56.2.2 SECCOMP_RET_USER_NOTIF:委托裁决

// include/uapi/linux/seccomp.h:73 区(通知结构)
struct seccomp_notif {
    __u64 id;           /* 拦截事件 id */
    __u32 pid;          /* 触发进程 */
    __u32 flags;
    struct seccomp_data data;   /* 完整上下文 (56.1.2) */
};
USER_NOTIF 协议 (监督者模式):

 被沙箱进程: ioctl(fd, SECCOMP_SET_MODE_FILTER)
   挂含 SECCOMP_RET_USER_NOTIF 的过滤器 +
   SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP 等 flag
 监督者: ioctl(notif_fd, SECCOMP_IOCTL_NOTIF_RECV)
   → 收到 {id, data} (如一个 openat 调用)
   监督者代替内核回答:
     - 检查参数 (路径在容器内? 目标安全?)
     - ioctl(NOTIF_ADDFD): 代为打开并把 fd 注回
       (监督者持更高权 — "提权代理"模式)
     - 或 NOTIF_SEND 返回 ERRNO/ALLOW
 语义要点: 拦截时 syscall 已"暂停在门口" —
   监督者裁决前进程不动 (与 ptrace 拦截对照:
   seccomp 不用信号停启, 便宜且无竞态窗口)

 应用: 无特权容器挂载 FUSE/打开宿主授权文件 —
   "缺能力就问监督者"的可编程模式
 安全要点: 监督者必须验证 arg 指针指向的路径
   (借 /proc/<pid>/fd 反查 — TOCTOU 是该协议
   的经典陷阱, 55.2.3 节快照思想的镜像)

56.2.3 审计与观测

过滤器的可见性:

 /proc/<pid>/status 的 Seccomp: 0/1/2 模式
 /proc/<pid>/status 的 NoNewPrivs: 0/1
 seccomp 过滤器本体: /proc/<pid>/seccomp (仅自见)
 SECCOMP_RET_LOG + audit: 被拦调用经 48.2.2 节
   audit 总线出 — "策略生成期"的采集器
 permissive 工具: strace 先行 → 按 trace 结果
   生成最小 allow 集 (docker/workspace 工具链)

与 LSM 的分工再对照(52-56 章的最终定位):LSM 判定"这个操作对不对"(语义级、有上下文);seccomp 判定"这个调用根本不许发生"(面级、无上下文)——前者精确贵、后者粗便宜,先 seccomp 砍面、再 LSM 管点。


小结

filter 模式的过滤器是"以 seccomp_data 为输入的 cBPF 程序",链式叠加最新优先、全体放行才放行,action_cache 把热调用压到纳秒级;SECCOMP_RET_USER_NOTIF 引入"监督者代答"协议(NOTIF_RECV/ADDFD/SIGND),把无特权沙箱的缺能力操作变成可编程的委托请求——路径校验的 TOCTOU 是其安全要点。观测面从 status 位到 audit 流完整。下一节把 seccomp 放进容器沙箱的完整纵深。

56.3 Seccomp 与容器安全

容器的安全不是单一机制而是多层各管一维的纵深:userns 管"身份相对化"(55.3)、capabilities 管"权限粒度"(55.1)、MAC 管"路径/类型访问"(53/54)、cgroup 管"资源上限"(12 章)、seccomp 管"syscall 面"(56.1-56.2)。本节以 Docker/runc 的默认 seccomp profile 为解剖对象,拼装完整纵深并分析逃逸案例。


56.3.1 默认 profile:删掉 44 个调用号

Docker/moby 默认 seccomp profile 的结构 (概化):

 默认动作: SCMP_ACT_ERRNO (EPERM)     ← 未列入者一律拒
 白名单: ~300 个允许的调用号 (名单制!)
 显式拒绝组 (即使列入也单独拦):
   clone/fork 带 CLONE_NEWUSER 标志 → EPERM
     (防容器内自建 userns — 55.3.3 节攻击面关闭)
   keyctl / ptrace / bpf / perf_event_open / kexec_*
     module_* / swapon / setns(跨) / unshare(某些)
     (历史逃逸 CVE 的调用族 — 按攻击史累积)
 允许但降级组: 
   clone: 允许但清 CLONE_NEWUSER 位后再放行
   (动作组合: SCMP_ACT_ERRNO + 参数过滤)

 三大容器运行时的差异:
 runc/docker: 白名单 profile (上述)
 gVisor:     自己实现 ~200 个 syscall (拦截后
             用户态内核执行 — seccomp 全拒后自供)
 Kata:       轻量 VM (49 章) — seccomp 需求最小
             (边界是 VM 不是 syscall)

56.3.2 纵深拼装:五层各管一维

一次"容器内攻击者尝试逃逸"的五层拦截:

 攻击: 拿到容器内 root shell (应用被攻破)

 [1] seccomp: mount/bpf/ptrace/kexec 全被 ERRNO
     → 大部分内核漏洞利用的载荷 syscall 不可用
 [2] capabilities: 全部能力被 drop (只留 5 个默认)
     → 即便 syscall 可用, 无 CAP_SYS_ADMIN 等授权
 [3] MAC (AppArmor docker-default / SELinux
     container_t:s0:c1,c2): 写宿主敏感路径被拒
     → 文件系统侧逃逸被路径/标签闸住 (53.3 节)
 [4] namespaces (55.3): PID/NET/MNT/USER ns —
     看不到宿主进程/网络/挂载 (9 章)
 [5] cgroups (12 章): 资源上限 — 即便逃逸成功
     也被 memory.max/pids 限死爆破空间

 各层"管一维": syscall 面 / 权限粒度 / 路径标签 /
   可见性 / 资源 — 单层皆可绕, 组合才是沙箱

56.3.3 逃逸案例与经验

三个历史逃逸的机制复盘:

 [1] Dirty COW 类 (内核 UAF/COW 竞态, 25.3):
     写 /etc/passwd → 容器内 root 改宿主文件
     教训: 内核内存安全是根 — seccomp 挡不住
     "允许的 syscall 上的内核 bug"; 缓解: 及时
     打补丁 + 只读宿主挂载
 [2] CVE-2019-5736 (runc 自覆写):
     容器内恶意进程改写宿主 /proc/self/exe
     (runc 二进制) → 下次 exec 执行恶意代码
     缓解: runc 以 O_PATH/fd 化自引用 + seccomp
     拒 /proc/self/exe 写
 [3] 内核 keyring/bpf 系列: 容器内调用 keyctl/bpf
     触达未授权内核代码
     缓解: profile 显式拦 (56.3.1 节拒绝组) +
     CAP_BPF 收紧 (55.1)

 经验提炼:
 - seccomp profile 按"攻击史"持续累积拒绝项
 - 内核自身缺陷只能靠打补丁/最小 syscall 面/
   gVisor/Kata 换边界
 - "容器 ≠ 虚拟机": 内核共享是性能来源也是
   风险来源 — 多租户强隔离选 Kata/VM (49 章)

56.3.4 沙箱机制的全景定位

第十分部安全机制的最终定位图:

 主体身份:  cred 四 UID + 能力五集 (55 章)
 命名空间:  8 种 ns (9 章) — 可见性隔离
 资源:      cgroup (12 章) — 用量隔离
 syscall 面: seccomp (56 章) — 入口最小化
 路径/类型:  MAC: SELinux(53)/AppArmor(54)
 框架:      LSM (52 章) — 上两者的插槽
 内核完整性: 静态: W^X/KASLR/锁 (1.6, 20.5 节)
            动态: lockdown 模式

 沙箱 = 以上全部叠加; 虚拟机 (49-51 章) =
 另一个维度 — "内核边界"整体移动

小结

容器沙箱是五层纵深的组合:默认 seccomp profile 以"白名单+拒绝组+参数过滤"删掉约 44 个危险调用号(含 CLONE_NEWUSER 拦截),capabilities/MAC/namespaces/cgroups 各管一维;三个历史逃逸(COW 写穿、runc 自覆写、keyring 族)证明内核内存安全与运行时自身才是根,seccomp 的价值是持续收缩攻击面。当隔离要求超越"共享内核"时,gVisor/Kata 把边界换到用户态内核或轻量 VM——第九部分的虚拟化与第十部分的安全在此汇合。全书至此:从寄存器到沙箱,Linux 7.0 的每一层都已解剖。