哈喽,各位好!今天咱们来聊聊一个能让你的C++程序更“乖巧”、更安全的利器:seccomp
。想象一下,你的程序就像一个在房间里玩耍的小朋友,seccomp
就像一道看不见的围栏,规定了小朋友只能玩哪些玩具,不能碰哪些危险的东西。这样,即使小朋友不小心“玩脱了”,也不会造成太大的破坏。
什么是 seccomp
?
seccomp
(Secure Computing Mode) 是 Linux 内核提供的一种安全机制,它允许你限制进程可以执行的系统调用。系统调用,简单来说,就是程序跟操作系统内核“打交道”的方式,比如打开文件、读写数据、创建进程等等。通过 seccomp
,你可以告诉内核:“这个程序只能用这些系统调用,其他的统统不许碰!”
seccomp
的作用
- 增强安全性: 限制恶意代码利用漏洞执行危险的系统调用,比如修改系统文件、执行任意代码等。
- 降低攻击面: 减少程序可能被攻击的入口点。
- 沙箱环境: 创建一个受限的运行环境,用于运行不信任的代码。
seccomp
的几种模式
seccomp
主要有三种模式:
SECCOMP_MODE_DISABLED
(禁用): 这是默认模式,不进行任何限制。SECCOMP_MODE_STRICT
(严格模式): 只允许exit()
,sigreturn()
,read()
,write()
,其他系统调用直接杀死进程。这种模式非常严格,通常只用于非常简单的场景。SECCOMP_MODE_FILTER
(过滤模式): 使用 BPF (Berkeley Packet Filter) 规则来定义允许或禁止的系统调用。这是最灵活、最常用的模式。
SECCOMP_MODE_STRICT
示例
先来个简单的例子,看看 SECCOMP_MODE_STRICT
模式如何“粗暴”地限制系统调用:
#include <iostream>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
// 启用 SECCOMP_MODE_STRICT 模式
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT) == -1) {
perror("prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)");
return 1;
}
// 尝试执行一个不允许的系统调用
std::cout << "尝试打开文件..." << std::endl;
FILE *fp = fopen("test.txt", "w"); // fopen 使用了 open 系统调用
if (fp == NULL) {
perror("fopen");
} else {
fclose(fp);
}
std::cout << "程序结束" << std::endl;
return 0;
}
编译并运行这个程序,你会发现程序在尝试打开文件时直接被 killed
了。因为 fopen
函数底层调用了 open
系统调用,而 SECCOMP_MODE_STRICT
模式不允许 open
调用。
SECCOMP_MODE_FILTER
和 BPF
SECCOMP_MODE_FILTER
模式才是 seccomp
的真正强大之处。它允许你使用 BPF 规则来更精细地控制系统调用。
BPF (Berkeley Packet Filter) 简介
BPF 最初是用于网络数据包过滤的,后来被扩展到其他领域,包括 seccomp
。BPF 规则实际上是一段小型的字节码程序,它会对系统调用进行检查,并根据规则决定是否允许该调用。
BPF 规则结构
BPF 规则通常包含以下几个部分:
- Load: 从内存中加载数据 (比如系统调用号、参数等)。
- Compare: 将加载的数据与预设的值进行比较。
- Jump: 根据比较结果跳转到不同的指令。
- Return: 返回一个值,指示是否允许系统调用。
返回值
BPF 规则的返回值决定了 seccomp
的行为:
返回值 | 含义 |
---|---|
SECCOMP_RET_KILL |
立即杀死进程。 |
SECCOMP_RET_TRAP |
发送 SIGSYS 信号给进程,可以自定义信号处理函数。 |
SECCOMP_RET_ERRNO(errno) |
返回一个指定的 errno 值给系统调用。 |
SECCOMP_RET_TRACE(errno) |
通知 ptrace 追踪器,并传递一个 errno 值。 |
SECCOMP_RET_ALLOW |
允许系统调用。 |
使用 libseccomp
库
为了更方便地编写 BPF 规则,通常会使用 libseccomp
库。这个库提供了一组 API,可以简化 BPF 规则的创建和安装过程。
libseccomp
示例
下面是一个使用 libseccomp
库的例子,它允许 read
、write
和 exit
系统调用,禁止其他所有调用:
#include <iostream>
#include <unistd.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <seccomp.h>
#include <errno.h>
int main() {
// 创建 seccomp 上下文
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // 默认行为是杀死进程
if (!ctx) {
perror("seccomp_init");
return 1;
}
// 允许 read 系统调用
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0) < 0) {
perror("seccomp_rule_add(read)");
seccomp_release(ctx);
return 1;
}
// 允许 write 系统调用
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0) < 0) {
perror("seccomp_rule_add(write)");
seccomp_release(ctx);
return 1;
}
// 允许 exit 系统调用
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0) < 0) {
perror("seccomp_rule_add(exit)");
seccomp_release(ctx);
return 1;
}
// 加载 seccomp 规则
if (seccomp_load(ctx) < 0) {
perror("seccomp_load");
seccomp_release(ctx);
return 1;
}
// 释放 seccomp 上下文
seccomp_release(ctx);
std::cout << "Seccomp 规则已加载,现在只能执行 read, write 和 exit 系统调用。" << std::endl;
// 尝试执行一个允许的系统调用
char buf[100];
ssize_t n = read(0, buf, sizeof(buf));
if (n > 0) {
write(1, buf, n);
}
// 尝试执行一个不允许的系统调用
std::cout << "尝试打开文件..." << std::endl;
FILE *fp = fopen("test.txt", "w"); // fopen 使用了 open 系统调用
if (fp == NULL) {
perror("fopen"); // 这行代码不会被执行,因为进程会被直接杀死
} else {
fclose(fp);
}
std::cout << "程序结束" << std::endl; // 这行代码也不会被执行
return 0;
}
这个程序首先创建了一个 seccomp
上下文,并设置默认行为为 SCMP_ACT_KILL
(杀死进程)。然后,它分别添加了允许 read
、write
和 exit
系统调用的规则。最后,它加载这些规则,并尝试执行一些系统调用。
编译运行这个程序,你会发现,程序可以正常执行 read
和 write
调用,但在尝试打开文件时,会被 seccomp
杀死。
更复杂的规则:参数过滤
seccomp
不仅可以限制系统调用,还可以根据系统调用的参数进行过滤。比如,你可以限制 open
系统调用只能打开特定的文件。
#include <iostream>
#include <unistd.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <seccomp.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
int main() {
// 创建 seccomp 上下文
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
if (!ctx) {
perror("seccomp_init");
return 1;
}
// 允许 exit 系统调用
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0) < 0) {
perror("seccomp_rule_add(exit)");
seccomp_release(ctx);
return 1;
}
// 允许 open 系统调用,但只能打开 "/tmp/allowed.txt" 文件
const char *allowed_file = "/tmp/allowed.txt";
scmp_arg_cmp arg[] = {
SCMP_A_ARG0, SCMP_CMP_EQ, (scmp_datum_t)allowed_file
};
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 1, arg[0]) < 0) {
perror("seccomp_rule_add(open)");
seccomp_release(ctx);
return 1;
}
// 允许 openat 系统调用,但只能打开 "/tmp/allowed.txt" 文件
// openat(dirfd, pathname, flags, mode)
// dirfd: 如果pathname是相对路径,则相对于dirfd指向的目录。如果pathname是绝对路径,则忽略dirfd参数。
// 当 dirfd = AT_FDCWD 时,pathname是相对路径时,等同于 open(pathname, flags, mode)
const char *allowed_file_openat = "/tmp/allowed.txt";
scmp_arg_cmp arg_openat[] = {
SCMP_A_ARG1, SCMP_CMP_EQ, (scmp_datum_t)allowed_file_openat
};
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 1, arg_openat[0]) < 0) {
perror("seccomp_rule_add(openat)");
seccomp_release(ctx);
return 1;
}
// 加载 seccomp 规则
if (seccomp_load(ctx) < 0) {
perror("seccomp_load");
seccomp_release(ctx);
return 1;
}
// 释放 seccomp 上下文
seccomp_release(ctx);
std::cout << "Seccomp 规则已加载,现在只能打开 /tmp/allowed.txt 文件。" << std::endl;
// 尝试打开允许的文件
int fd_allowed = open("/tmp/allowed.txt", O_WRONLY | O_CREAT, 0644);
if (fd_allowed == -1) {
perror("open(/tmp/allowed.txt)");
} else {
std::cout << "成功打开 /tmp/allowed.txt" << std::endl;
close(fd_allowed);
}
// 尝试打开不允许的文件
int fd_forbidden = open("forbidden.txt", O_WRONLY | O_CREAT, 0644);
if (fd_forbidden == -1) {
perror("open(forbidden.txt)"); // 这行代码不会被执行,因为进程会被直接杀死
}
return 0;
}
在这个例子中,我们使用了 SCMP_A_ARG0
来指定要比较的参数,SCMP_CMP_EQ
来指定比较操作符 (等于),allowed_file
来指定允许的文件名。 这样,程序只能打开 /tmp/allowed.txt
文件,尝试打开其他文件会被 seccomp
阻止。
更进一步:使用 SECCOMP_RET_TRACE
进行调试
有时候,你想知道程序到底尝试调用了哪些被 seccomp
阻止的系统调用,以便更好地调整规则。这时,可以使用 SECCOMP_RET_TRACE
。
#include <iostream>
#include <unistd.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <seccomp.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/reg.h>
// 全局变量,用于存储子进程的 PID
pid_t child_pid;
// 信号处理函数,用于处理 SIGSYS 信号
void sigsys_handler(int sig) {
// 信号处理逻辑
std::cout << "接收到 SIGSYS 信号,说明有系统调用被 seccomp 阻止了。" << std::endl;
// 使用 ptrace 获取系统调用号
long syscall_number = ptrace(PTRACE_PEEKUSER, child_pid, sizeof(long) * ORIG_RAX, NULL); // ORIG_RAX 是 x86_64 架构上的系统调用号寄存器
if (syscall_number == -1) {
perror("ptrace(PTRACE_PEEKUSER)");
exit(1);
}
std::cout << "被阻止的系统调用号是: " << syscall_number << std::endl;
// 允许进程继续执行(但仍然会被 seccomp 阻止)
ptrace(PTRACE_CONT, child_pid, NULL, NULL);
}
int main() {
// 创建子进程
child_pid = fork();
if (child_pid == -1) {
perror("fork");
return 1;
}
if (child_pid == 0) {
// 子进程
// 注册 SIGSYS 信号处理函数
signal(SIGSYS, sigsys_handler);
// 启用 ptrace
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
perror("ptrace(PTRACE_TRACEME)");
return 1;
}
// 启用 seccomp
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); // 默认允许所有系统调用
if (!ctx) {
perror("seccomp_init");
return 1;
}
// 禁止 open 系统调用,并使用 SECCOMP_RET_TRACE
if (seccomp_rule_add(ctx, SCMP_ACT_TRACE(1), SCMP_SYS(open), 0) < 0) { // 1 是一个自定义的 errno 值,可以用来区分不同的 TRACE 事件
perror("seccomp_rule_add(open)");
seccomp_release(ctx);
return 1;
}
// 加载 seccomp 规则
if (seccomp_load(ctx) < 0) {
perror("seccomp_load");
seccomp_release(ctx);
return 1;
}
seccomp_release(ctx);
// 尝试打开文件
std::cout << "子进程:尝试打开文件..." << std::endl;
FILE *fp = fopen("test.txt", "w"); // fopen 使用了 open 系统调用
if (fp == NULL) {
perror("fopen"); // 这行代码不会被执行,因为信号处理函数会先被调用
} else {
fclose(fp);
}
std::cout << "子进程:程序结束" << std::endl;
return 0;
} else {
// 父进程
int status;
waitpid(child_pid, &status, 0); // 等待子进程退出
if (WIFSTOPPED(status)) {
// 子进程被信号停止
int sig = WSTOPSIG(status);
std::cout << "子进程接收到信号: " << sig << std::endl;
// 如果是 SIGSYS 信号,表示 seccomp 触发了 TRACE 事件
if (sig == SIGSYS) {
std::cout << "父进程:Seccomp 触发了 TRACE 事件" << std::endl;
}
}
return 0;
}
}
这个例子中,我们首先创建了一个子进程,并在子进程中设置了 seccomp
规则,禁止 open
系统调用,并使用 SECCOMP_RET_TRACE
。 当子进程尝试打开文件时,seccomp
会发送 SIGSYS
信号给子进程。
父进程使用 ptrace
跟踪子进程,并在收到 SIGSYS
信号时,使用 ptrace
获取被阻止的系统调用号。这样,你就可以知道程序到底尝试调用了哪个系统调用,从而更好地调整 seccomp
规则。
一些建议
- 最小权限原则: 只允许程序需要的系统调用,禁止其他所有调用。
- 逐步调整: 先允许所有系统调用,然后逐步添加限制,并使用
SECCOMP_RET_TRACE
进行调试。 - 考虑依赖: 确保允许程序依赖的所有系统调用,包括动态链接库需要的调用。
- 更新规则: 随着程序功能的增加或改变,及时更新
seccomp
规则。 - 不要过度限制: 过度限制可能导致程序无法正常运行,影响用户体验。
总结
seccomp
是一个强大的安全工具,可以有效地限制进程的系统调用,增强程序的安全性。通过合理地配置 seccomp
规则,你可以创建一个更安全、更可靠的运行环境。希望今天的讲解对你有所帮助!记住,安全无小事,多一份安全措施,就少一份风险。下次再见!