Linux内核分析之安全模块-04
This language version is unavailable; showing the other language.
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 的每一层都已解剖。