阻止进程在 Linux 上打开新的文件描述符,但允许通过套接字接收文件描述符
Prevent process from opening new file descriptor on Linux but allow receiving file descriptors via sockets
我目前正在从事一个项目,其中有一个 parent 进程,该进程设置套接字对、分叉,然后使用该套接字对进行通信。 child,如果它想打开一个文件(或任何其他基于文件描述符的资源),应该总是去 parent,请求资源并获得通过套接字对发送的 fd
。此外,我想防止 child 自己打开任何文件描述符。
我无意中发现 setrlimit
成功地阻止了 child 打开新的文件描述符,但它似乎也使通过初始套接字连接发送的任何文件描述符无效。 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);
}
现在,在您的程序中,您可以调用上述函数在 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 的一些重要说明:
- seccomp 过滤器一旦应用,就不能被进程删除或更改。
- 如果筛选器允许
fork(2)
或 clone(2)
,则任何子进程都将受到同一筛选器的约束。
- 如果允许
execve(2)
,现有过滤器将在对 execve(2)
的调用中保留。
- 如果允许
prctl(2)
系统调用,则该进程能够应用更多过滤器。
我目前正在从事一个项目,其中有一个 parent 进程,该进程设置套接字对、分叉,然后使用该套接字对进行通信。 child,如果它想打开一个文件(或任何其他基于文件描述符的资源),应该总是去 parent,请求资源并获得通过套接字对发送的 fd
。此外,我想防止 child 自己打开任何文件描述符。
我无意中发现 setrlimit
成功地阻止了 child 打开新的文件描述符,但它似乎也使通过初始套接字连接发送的任何文件描述符无效。 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);
}
现在,在您的程序中,您可以调用上述函数在 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 的一些重要说明:
- seccomp 过滤器一旦应用,就不能被进程删除或更改。
- 如果筛选器允许
fork(2)
或clone(2)
,则任何子进程都将受到同一筛选器的约束。 - 如果允许
execve(2)
,现有过滤器将在对execve(2)
的调用中保留。 - 如果允许
prctl(2)
系统调用,则该进程能够应用更多过滤器。