一个轻量级的多层 Linux 沙箱,结合了命名空间、pivot_root、seccomp-bpf、能力降级以及基于证据的判决引擎(Truthimatics 公开版),用于安全、可审计的代码执行。
用于Linux上原生代码执行的多层沙箱。
七层独立防御——无外部依赖,约73 KiB的PIE二进制文件。
┌──────────────────────────────────────────────────────┐
│ Z-Jail │
├──────────────────────────────────────────────────────┤
│ Truthimatics PV (基于证据的裁决引擎) │
│ Namespaces (mount, pid, net, ipc, uts) │
│ pivot_root (加强版 chroot) │
│ Capabilities (丢弃全部,锁定 securebits) │
│ NO_NEW_PRIVS (禁止权限提升) │
│ seccomp-BPF (白名单:仅15个系统调用) │
│ Audit (JSON 日志 + BLAKE2b 哈希) │
└──────────────────────────────────────────────────────┘
git clone https://github.com/Division-36/Z-Jail.git
cd Z-Jail
make
sudo ./z_jail --root=/path/to/rootfs --seccomp-enforce -- /bin/ls
--root 目录应包含一个最小的文件系统,其中有目标二进制文件及其依赖(对于静态二进制文件,只需二进制文件本身即可)。
现有的沙箱解决方案需要做出权衡:
| Z-Jail | Firecracker | gVisor | bwrap | nsjail | |
|---|---|---|---|---|---|
| 外部依赖 | 零 | libc, seccomp | Go 运行时 | libc | libc, protobuf |
| 二进制大小 | ~73 KiB | 20+ MiB | 40+ MiB | ~70 KiB | ~1 MiB |
| 虚拟机隔离 | 否 | 是 (microVM) | 否 (沙箱) | 否 | 否 |
| seccomp 白名单 | 是 | 否 | 是 | 可选 | 是 |
| 内容哈希 | 是 | 否 | 否 | 否 | 否 |
| 审计 JSON | 是 | 否 | 是 | 否 | 部分 |
| 构建复杂度 | 一个 make | 复杂 | 复杂 | 简单 | 中等 |
Z-Jail 填补了 bwrap(最小化,默认无 seccomp)和 nsjail(功能丰富,依赖重)之间的空白。它专为 CI 流水线、CTF 监狱挑战和轻量级代码评估 而设计,无需引入容器运行时即可实现纵深防御。
flowchart LR
CLI[CLI 参数] --> P[parse_args]
P --> C{clone 命名空间}
C -->|子进程| CR[child_run]
C -->|父进程| W[waitpid]
CR --> RL[setrlimit]
RL --> FD[关闭 >=3 的文件描述符]
FD --> DUMP[PR_SET_DUMPABLE=0]
DUMP --> PV[pivot_root]
PV --> NNP[PR_SET_NO_NEW_PRIVS]
NNP --> CAP[丢弃 capabilities]
CAP --> SC[seccomp-BPF]
SC --> SIG[通知父进程]
SIG --> EX[execve 目标程序]
W --> A[审计 JSON]
A --> EXIT[退出]
每一层都经过排序,使得后一层无法被前一层撤销:
sequenceDiagram
participant P as 父进程
participant C as 子进程
P->>C: clone (NEWNS|NEWPID|NEWNET|NEWIPC|NEWUTS)
Note over C: setrlimit(CPU, AS, NOFILE, NPROC)
Note over C: close(所有 >2 的 fd)
Note over C: PR_SET_DUMPABLE=0
Note over C: pivot_root → chdir("/") → umount -l
Note over C: PR_SET_NO_NEW_PRIVS
Note over C: capset(全零) + securebits
Note over C: seccomp(SECCOMP_MODE_FILTER, 白名单)
C->>P: write(pipe, ready=1)
Note over C: execve(目标)
P->>P: waitpid
P->>P: 写入审计 JSON
基于证据的裁决引擎。收集有关已执行二进制文件的加权观察,并确定最终裁决(DETERMINISTIC、REJECT 或 UNCERTAIN)。每个观察都带有权重;任何单个观察的权重超过总权重的 50% 即可决定裁决。
通过 clone() 创建五个命名空间:
| 命名空间 | 标志 | 用途 |
|---|---|---|
| Mount | CLONE_NEWNS | 隔离的文件系统树 |
| PID | CLONE_NEWPID | 进程 ID 空间(子进程为 pid 1) |
| Net | CLONE_NEWNET | 无网络接口 |
| IPC | CLONE_NEWIPC | 无共享内存 / 信号量 |
| UTS | CLONE_NEWUTS | 独立的主机名 |
需要在初始命名空间中有 CAP_SYS_ADMIN。
用 --root 目录替换挂载命名空间的根:
MS_BIND|MS_REC)pivot_root(new_root, put_old) — 交换挂载树chdir("/") — 进入新根umount2("/.pivot_old", MNT_DETACH) — 分离旧根rmdir("/.pivot_old") — 清理这严格强于 chroot(2) —— 沙箱进程无法逃脱回主机根,即使通过沙箱内部的 CLONE_NEWNS 也无法做到(但这已被 seccomp 禁止)。
通过以下方式丢弃所有 capabilities:
capset(hdr, data) // data = {0, 0, 0}
prctl(SECBIT_KEEP_CAPS_LOCKED | SECBIT_NO_SETUID_FIXUP | ...)
进程在 capset 之前丢弃 setuid/setgid,以便在仍持有 CAP_SETUID 时使 uid 更改生效。capset 之后,所有 caps 都被移除且 securebits 被锁定——无法重新启用。
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
阻止进程或其子进程通过 setuid 二进制文件、文件 capabilities 或 LSM 转换获得新权限。不可逆。
15 个系统调用的允许列表——不在列表中的任何调用都会触发 SECCOMP_RET_KILL:
| 系统调用 | 编号 | 说明 |
|---|---|---|
read | 0 | 标准输入 |
write | 1 | 标准输出/错误 + 报告管道 |
openat | 257 | 文件访问(不是 open) |
close | 3 | — |
lseek | 8 | — |
brk | 12 | 堆管理 |
mmap | 9 | 参数受限:flags & 4 == 0(无 MAP_SHARED),flags == 0x22(MAP_PRIVATE|MAP_ANONYMOUS) |
munmap | 11 | — |
execve | 59 | 启动时单次执行 |
exit_group | 231 | 干净退出进程 |
rt_sigaction | 13 | 信号处理函数 |
rt_sigprocmask | 14 | 信号掩码 |
getrandom | 318 | 随机数源 |
clock_gettime | 228 | 计时 |
fstat | 5 | 文件元数据 |
BPF 过滤器是动态生成的:对于每个白名单条目,生成一条跳转链,要么允许(如果系统调用匹配),要么落空到 KILL。首先检查架构(AUDIT_ARCH_X86_64)。
该过滤器由一个独立的测试(tests/seccomp_filter_test.c,8/8 通过)验证,该测试 fork+execve 测试用例,针对真实的 prctl(PR_SET_SECCOMP) 运行,无需 root。
每次执行都会生成一个 JSON 审计记录:
{
"schema": "z-jail.audit/v1",
"build_id": "Z-Jail/v1+dev",
"timestamp": 1749000000,
"duration_ns": 8500000,
"executable": "/bin/ls",
"verdict": "DETERMINISTIC",
"exit_code": 0,
"sandbox": {
"seccomp_filter": "whitelist-v1",
"seccomp_whitelist_size": 15,
"seccomp_arg_rules_size": 2,
"namespaces": ["mount","pid","net","ipc","uts"],
"pivot_root": "/var/run/z-jail/roots/default",
"no_new_privs": true,
"capabilities_dropped": true
},
"content_fingerprint": "0e5751c026e543b2e8ab2eb06099daa1..."
}
写入 build/audits/<binary-name>.audit.json。content_fingerprint 是目标二进制文件的 BLAKE2b-256 哈希,由父进程在子进程结束后计算。
z_jail --root=<目录> [--seccomp-enforce] [--self-hash=<十六进制>]
[--quiet] [--verbose] -- <程序> [参数...]
| 标志 | 描述 |
|---|---|
--root=<目录> | 沙箱根目录(必需) |
--seccomp-enforce | 启用 seccomp-BPF 系统调用白名单 |
--self-hash=<十六进制> | 验证二进制文件是否与预期的 BLAKE2b-256 哈希匹配 |
--quiet | 抑制审计输出 |
--verbose | 启用调试日志 |
--version | 显示构建 ID(Z-Jail/v1+dev) |
--help | 显示用法并退出 |
# 使用所有保护运行静态二进制文件
sudo z_jail --root=./roots --seccomp-enforce -- bin/hello_static
# 使用二进制完整性验证运行
sudo z_jail --root=./roots --seccomp-enforce \
--self-hash=$(sha256sum z_jail | cut -c1-64) -- bin/program
# 静默模式(无审计 JSON)
sudo z_jail --root=./roots --quiet -- bin/program
| 代码 | 含义 |
|---|---|
| 0 | 子进程正常退出(裁决:DETERMINISTIC) |
| 1 | 子进程被信号杀死(裁决:REJECT) |
| 2 | Self-hash:十六进制字符串错误或文件不可读 |
| 3 | Self-hash:不匹配(二进制文件已被篡改) |
| 101 | 子进程设置错误(rlimit 等) |
| 102 | 子进程 seccomp 过滤器安装失败 |
| 103 | 子进程 execve 失败(未找到二进制文件,无执行权限) |
| 104 | 子进程 pivot_root 失败 |
| 105 | 子进程 capability 丢弃失败 |
| 125 | 命名空间创建失败(以 root 运行?内核支持?) |