我目前正在开发一个项目,其中我有一个父进程,它设置一个套接字对、分叉,然后使用该套接字对进行通信。如果子级想要打开文件(或任何其他基于文件描述符的资源),则应始终转到父级,请求资源并获取通过套接字对发送的
fd
。此外,我想防止孩子自己打开任何文件描述符。
我偶然发现了
setrlimit
,它成功地阻止了子进程打开新的文件描述符,但它似乎也使通过初始套接字连接发送的任何文件描述符无效。 Linux上是否有任何方法允许单个进程打开任何文件,将其文件描述符发送给其他进程并让它们使用它们,而不允许这些其他进程自己打开任何文件描述符?
对于我的用例来说,可以是任何内核配置、系统调用等,只要它可以在 fork 之后应用,并且只要它适用于所有文件描述符(不仅是文件,还包括套接字、套接字对等)。
您这里所拥有的正是 seccomp 的用例。
使用 seccomp,您可以以不同的方式过滤系统调用。在这种情况下,您想要做的是,在
fork()
之后,安装一个 seccomp
过滤器,禁止使用 open(2)
、openat(2)
、socket(2)
(等等)。
为此,您可以执行以下操作:
seccomp_init(3)
创建一个 seccomp 上下文,默认行为为 SCMP_ACT_ALLOW
。seccomp_rule_add(3)
为您要拒绝的每个系统调用添加一条规则到上下文中。如果尝试进行系统调用,您可以使用 SCMP_ACT_KILL
终止进程,使用 SCMP_ACT_ERRNO(val)
使系统调用失败,返回指定的 errno
值,或手册页中定义的任何其他 action
值。seccomp_load(3)
加载上下文以使其生效。在继续之前,注意像这样的黑名单方法通常比白名单方法弱。它允许任何未明确禁止的系统调用,并且可能导致绕过过滤器。如果您认为要执行的子进程可能恶意地试图避开过滤器,或者您已经知道子进程需要哪些系统调用,则白名单方法更好,您应该执行与上述相反的操作:使用默认操作
SCMP_ACT_KILL
创建过滤器,并使用 SCMP_ACT_ALLOW
允许所需的系统调用。就代码而言,差异很小(白名单可能更长,但步骤是相同的)。
这是上述示例(为了简单起见,我正在做
exit(-1)
,以防出现错误):
#include <stdlib.h>
#include <seccomp.h>
static void secure(void) {
int err;
scmp_filter_ctx ctx;
int blacklist[] = {
SCMP_SYS(open),
SCMP_SYS(openat),
SCMP_SYS(creat),
SCMP_SYS(socket),
SCMP_SYS(open_by_handle_at),
// ... possibly more ...
};
// Create a new seccomp context, allowing every syscall by default.
ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL)
exit(-1);
/* Now add a filter for each syscall that you want to disallow.
In this case, we'll use SCMP_ACT_KILL to kill the process if it
attempts to execute the specified syscall. */
for (unsigned i = 0; i < sizeof(blacklist) / sizeof(blacklist[0]); i++) {
err = seccomp_rule_add(ctx, SCMP_ACT_KILL, blacklist[i], 0);
if (err)
exit(-1);
}
// Load the context making it effective.
err = seccomp_load(ctx);
if (err)
exit(-1);
seccomp_release(ctx);
}
现在,在您的程序中,您可以调用上述函数在
fork()
之后应用 seccomp 过滤器,如下所示:
child_pid = fork();
if (child_pid == -1)
exit(-1);
if (child_pid == 0) {
secure();
// Child code here...
exit(0);
} else {
// Parent code here...
}
关于 seccomp 的一些重要说明:
fork(2)
或 clone(2)
,则任何子进程都将受到同一过滤器的约束。execve(2)
,则在调用 execve(2)
时将保留现有过滤器。prctl(2)
系统调用,该进程就能够应用进一步的过滤器。