第一百六十章:沙箱之术
渡劫期涉及内核源码:
一
林小源离开身份殿,来到一片荒芜的平原。平原中央矗立着一座透明的牢笼,牢笼中有一个进程在缓慢运行。
"这是什么?"林小源问。
"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_PROCESS、KILL_THREAD、TRAP、ERRNO、USER_NOTIF、TRACE、LOG、ALLOW。越危险的惩罚,优先级越高。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 更偏向限制哪类访问?
/*
* 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");#include <stdio.h>
#include <string.h>
/*
* 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";
}
int main() {
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");
return 0;
}道藏笔记
内核启示
seccomp 的思路很简单:进程能调哪些系统调用,由过滤器决定。但它不是完整沙箱,而是缩小内核攻击面的工具。STRICT 模式只放行 read、write、exit、sigreturn;FILTER 模式用 BPF 检查 seccomp_data 中的 syscall nr、arch 和参数。
安装过滤器前要处理 no_new_privs 或 CAP_SYS_ADMIN,过滤器不能解引用用户指针以减少 TOCTOU,规则还必须检查 。容器运行时、Chrome 沙箱、systemd 的 SystemCallFilter 都会把 seccomp 与 namespaces、cgroups、LSM、Landlock 等机制叠起来用。
seccomp 不是整座牢城——它是守住系统调用入口的牢门。
沙箱之试
沙箱之术中,负责限制进程可用系统调用、可用 BPF 过滤的机制是什么?