Skip to content

第一百六十章:沙箱之术

渡劫期

涉及内核源码:

林小源离开身份殿,来到一片荒芜的平原。平原中央矗立着一座透明的牢笼,牢笼中有一个进程在缓慢运行。

"这是什么?"林小源问。

"seccomp,"seccomp.c 说,他是一位严厉的狱卒,手持一本厚厚的规则手册。"Secure Computing——我负责把进程关进沙箱,限制它的行为。"

"为什么要把进程关起来?"

"因为有些进程不可信,"狱卒说,"浏览器渲染进程、容器中的应用——它们可能被攻击。如果不限制它们的系统调用,攻击者就能利用漏洞为所欲为。"

林小源走近牢笼,看到进程试图调用一个系统调用。狱卒翻开手册,看了一眼,摇头说:"不允许。"

进程被拒绝了。

"不过记住,"狱卒把规则手册合上,声音忽然严厉,"我不是完整的沙箱。"

"不是?"

"我只过滤系统调用,缩小进程能触碰的内核入口。文件权限、对象标签、命名空间、资源配额,那是 DAC、LSM、namespaces、cgroups 的疆域。把我单独当成铜墙铁壁,迟早会出事。"

林小源看着透明牢笼,终于看清它的边界:它挡住的是门,不是整个世界。

破关试炼

牢门之试

官方文档强调,seccomp filter 是完整沙箱,还是用于缩小系统调用攻击面的工具?

答对后才能继续滑动和进入下一章。

"seccomp 有两种模式,"狱卒说,"STRICT 模式只允许 read、write、exit、sigreturn 四个系统调用。FILTER 模式更灵活,使用 BPF 程序过滤。"

"BPF?"

"Berkeley Packet Filter,"狱卒说,"它是一种小型的虚拟机,可以编写简单的程序来判断系统调用是否允许。seccomp_data 结构包含系统调用号和参数,BPF 程序返回 ALLOW、ERRNO 或 KILL。"

"KILL 会杀死进程?"

"是的,"狱卒说,"如果进程试图调用被禁止的系统调用,seccomp 可以直接杀死它。这是最严厉的惩罚。"

"FILTER 模式还有几条铁律。"狱卒伸出手指,逐条点在规则书上。"第一,安装过滤器前,要么拥有 CAP_SYS_ADMIN,要么先立下 PR_SET_NO_NEW_PRIVS 誓约,保证之后不能借 exec 或 setuid 得到新特权。"

"第二,BPF 程序能看系统调用号、架构和六个参数,但不能解引用用户指针。这样虽然少了很多花样,却避开了系统调用拦截中常见的 TOCTOU 陷阱。"

"第三,一定要检查 。同一个系统调用号,在不同架构上可能不是同一件事。只看 nr,就像只看令牌编号不看宗门印记。"

"第四,多个过滤器叠加时,返回动作按优先级裁决:KILL_PROCESSKILL_THREADTRAPERRNOUSER_NOTIFTRACELOGALLOW。越危险的惩罚,优先级越高。USER_NOTIF 还能把某些请求交给用户态监督者处理,容器管理器常借它代办少数 syscall,但监督者也必须小心竞态。"

林小源望着牢笼中的进程,心中感慨。在这片内核的深处,自由是有边界的。

破关试炼

滤符之试

安装 seccomp filter 前,普通非特权进程通常要先设置哪个不再获得新特权的标志?

答对后才能继续滑动和进入下一章。

"seccomp 在哪里使用?"林小源问。

狱卒合上手册,说:"容器运行时、浏览器沙箱、systemd 服务——任何需要限制系统调用的地方。一个 Web 服务容器不需要 mount、reboot 等系统调用,seccomp 让它更加安全。"

"但配置 seccomp 很复杂吧?"

"确实,"狱卒说,"你需要知道进程需要哪些系统调用,然后编写相应的 BPF 程序。配置错误会导致进程无法运行。但这是值得的——安全需要代价。"

"更完整的沙箱,往往不是我一个人完成。"狱卒指向远处的诸峰,"namespaces 隔离进程看到的世界,cgroups 限制资源,LSM 和 Landlock 判断对象访问,capabilities 剥掉多余神通。我负责堵住不该出现的 syscall。"

"Landlock 和你有什么区别?"

"它管对象访问,尤其是把规则绑到文件层级等内核对象上;我管 syscall 入口。一个进程或许可以调用 ,但 Landlock 还能判断它能不能打开那条路径。"

林小源望着那座透明的牢笼,心中暗下决心。在这片内核的深处,沙箱不是最后的防线,而是一组防线的合阵。

破关试炼

合阵之试

seccomp 主要过滤 syscall 入口;Landlock 更偏向限制哪类访问?

答对后才能继续滑动和进入下一章。

c
/*
 * seccomp 的模式:
 *
 * 1. SECCOMP_MODE_STRICT
 *    只允许 read, write, exit, sigreturn
 *    最严格的模式
 *
 * 2. SECCOMP_MODE_FILTER
 *    使用 BPF 程序过滤
 *    灵活的模式
 *
 * BPF 过滤器:
 *   seccomp_data 结构:
 *     nr — 系统调用号
 *     arch — 架构
 *     instruction_pointer — 指令地址
 *     args[6] — 系统调用参数
 *
 *   返回值:
 *     SECCOMP_RET_KILL — 杀死进程
 *     SECCOMP_RET_ERRNO — 返回错误
 *     SECCOMP_RET_TRACE — 通知 tracer
 *     SECCOMP_RET_ALLOW — 允许
 *
 * 使用场景:
 *   容器运行时
 *   浏览器沙箱
 *   systemd 服务
 */

/* 模拟 seccomp 过滤器 */
struct seccomp_data {
    int nr;           /* 系统调用号 */
    int arch;         /* 架构 */
    unsigned long args[6]; /* 参数 */
};

/* BPF 指令 */
struct sock_filter {
    unsigned short code;
    unsigned char jt;
    unsigned char jf;
    unsigned int k;
};

/* 返回值 */
#define SECCOMP_RET_KILL   0x00000000
#define SECCOMP_RET_ERRNO  0x00050000
#define SECCOMP_RET_ALLOW  0x7fff0000

/* 常见系统调用号 */
#define __NR_read   0
#define __NR_write  1
#define __NR_open   2
#define __NR_close  3
#define __NR_execve 59

unsigned int seccomp_filter(struct seccomp_data *data) {
    /* 允许 read */
    if (data->nr == __NR_read)
        return SECCOMP_RET_ALLOW;

    /* 允许 write */
    if (data->nr == __NR_write)
        return SECCOMP_RET_ALLOW;

    /* 允许 close */
    if (data->nr == __NR_close)
        return SECCOMP_RET_ALLOW;

    /* 禁止 open */
    if (data->nr == __NR_open) {
        printf("  [seccomp] 拦截 open: 返回 EPERM\n");
        return SECCOMP_RET_ERRNO;
    }

    /* 禁止 execve */
    if (data->nr == __NR_execve) {
        printf("  [seccomp] 拦截 execve: 杀死进程\n");
        return SECCOMP_RET_KILL;
    }

    /* 其他系统调用: 禁止 */
    printf("  [seccomp] 拦截未知 syscall %d\n", data->nr);
    return SECCOMP_RET_ERRNO;
}

const char *ret_name(unsigned int ret) {
    if (ret == SECCOMP_RET_KILL) return "KILL";
    if (ret == SECCOMP_RET_ERRNO) return "ERRNO";
    if (ret == SECCOMP_RET_ALLOW) return "ALLOW";
    return "UNKNOWN";
}

printf("=== 沙箱之术 — 系统调用过滤 ===\n\n");

printf("seccomp: 限制进程可用的系统调用\n\n");

/* 测试不同的系统调用 */
struct seccomp_data tests[] = {
    { .nr = __NR_read, .args = {0} },
    { .nr = __NR_write, .args = {0} },
    { .nr = __NR_open, .args = {0} },
    { .nr = __NR_close, .args = {0} },
    { .nr = __NR_execve, .args = {0} },
};

int n = sizeof(tests) / sizeof(tests[0]);

printf("--- 过滤结果 ---\n");
for (int i = 0; i < n; i++) {
    unsigned int ret = seccomp_filter(&tests[i]);
    printf("syscall %d: %s\n", tests[i].nr, ret_name(ret));
}

printf("\n--- seccomp 模式 ---\n");
printf("1. STRICT 模式:\n");
printf("   只允许 read, write, exit, sigreturn\n");
printf("   最严格\n\n");
printf("2. FILTER 模式:\n");
printf("   BPF 程序过滤\n");
printf("   灵活\n\n");

printf("--- 使用场景 ---\n");
printf("容器:\n");
printf("  docker --security-opt seccomp=...\n\n");
printf("systemd:\n");
printf("  SystemCallFilter=read write open\n\n");
printf("浏览器:\n");
printf("  Chrome 沙箱\n");

道藏笔记

内核启示

seccomp 的思路很简单:进程能调哪些系统调用,由过滤器决定。但它不是完整沙箱,而是缩小内核攻击面的工具。STRICT 模式只放行 read、write、exit、sigreturn;FILTER 模式用 BPF 检查 seccomp_data 中的 syscall nr、arch 和参数。

安装过滤器前要处理 no_new_privsCAP_SYS_ADMIN,过滤器不能解引用用户指针以减少 TOCTOU,规则还必须检查 。容器运行时、Chrome 沙箱、systemd 的 SystemCallFilter 都会把 seccomp 与 namespaces、cgroups、LSM、Landlock 等机制叠起来用。

seccomp 不是整座牢城——它是守住系统调用入口的牢门。


破关试炼

沙箱之试

沙箱之术中,负责限制进程可用系统调用、可用 BPF 过滤的机制是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道