带 ptrace 检查的计算沙箱

Евг*_*ков 1 linux sandbox

我想在 linux 服务器中启动不受信任的仅计算可执行文件。进程将无法访问系统和文件,除了stdinstdout

我的想法是用来ptrace捕获和阻止对 linux 内核的系统调用。我也用它来获取和设置进程内部状态(寄存器 + RAM)。它是安全的沙箱吗?刹车的方法你知道吗?

我还想限制 RAM 和 CPU 时间使用以避免 DOS

小智 5

这正是 seccomp 的用途。大多数现代 Linux 内核都支持 Seccomp,旨在过滤系统调用。它有两种形式,称为模式 1 和模式 2。

模式 1 seccomp

该进程只允许 4 个系统调用:read()write()rt_sigreturn()、 和exit()(注意这是exit()系统调用,而不是函数。glibc 函数使用非白名单exit_group()系统调用)。如果尝试任何其他调用,它们将不会返回,并且程序将被终止。这用于在安全代理进程中计算不受信任的字节码。受信任的代码可以创建一个不受信任的进程,该进程在启用模式 1 seccomp 后执行具有潜在危险的字节码,并且可以通过管道与父进程通信。

模式 2 秒压缩

这也称为 seccomp-bpf,因为它使用 eBPF 字节码创建动态过滤器,用于根据系统调用的数量和参数来限制系统调用。它还可以设置为对违规采取各种行动,从强制终止进程,到拒绝系统调用并在不终止进程的情况下发出被捕获的信号,到返回自定义错误号,到简单地拒绝系统调用以进行测试目的。libseccomp 库抽象了其中的大部分内容,因此无需自己编写 eBPF 字节码。

这两种方法都比基于 ptrace 的沙箱快得多,后者会产生大量开销。此外,ptrace的沙盒不一定会派其过滤器的任何孩子,所以调用喜欢execve()fork()vfork(),并且clone()将需要禁用,以免你变得脆弱TOCTOU竞争条件。另一方面,两种 seccomp 模式都在任何执行或分叉中保留过滤器。

使用模式 1 seccomp 在字节码中安全运行“return 42”的示例:

#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <linux/seccomp.h>

void main(void)
{
    /* "mov al,42; ret" aka "return 42" */
    static const unsigned char code[] = "\xb0\x2a\xc3";
    int fd[2], ret;

    /* spawn child process, connected by a pipe */
    pipe(fd);
    if (fork() == 0) {
        /* we're the child, so let's close this end of the pipe */
        close(fd[0]);

        /* enter mode 1 seccomp and execute untrusted bytecode */
        prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
        ret = (*(uint8_t(*)())code)();

        /* send result over pipe, and exit */
        write(fd[1], &ret, sizeof(ret));
        syscall(SYS_exit, 0);
    } else {
        /* we're the parent, so let's close this end of the pipe */
        close(fd[1]);

        /* read the result from the pipe, and print it */
        read(fd[0], &ret, sizeof(ret));
        printf("untrusted bytecode returned %d\n", ret);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用模式 2 seccomp 的示例,带有几个任意的系统调用过滤器:

#include <seccomp.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

void main(void)
{
    /* initialize the libseccomp context */
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);

    /* allow exiting */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);

    /* allow getting the current pid */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getpid), 0);

    /* allow changing data segment size, as required by glibc */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);

    /* allow writing up to 512 bytes to fd 1 */
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 2,
        SCMP_A0(SCMP_CMP_EQ, 1),
        SCMP_A2(SCMP_CMP_LE, 512));

    /* if writing to any other fd, return -EBADF */
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EBADF), SCMP_SYS(write), 1,
        SCMP_A0(SCMP_CMP_NE, 1));

    /* load and enforce the filters */
    seccomp_load(ctx);
    seccomp_release(ctx);

    printf("this process is %d\n", getpid());
}
Run Code Online (Sandbox Code Playgroud)

使用 seccomp 需要记住一些重要的事情:

  • 不是“真实”系统调用而是 vDSO 的调用,例如gettimeofday()time()无法过滤。为了性能,它们在用户空间执行,避免了昂贵的上下文切换。但是,这也阻止了 seccomp 意识到它们正在运行。这通常不是问题,因为唯一可以作为 vDSO 实施的系统调用通常非常简单,并且几乎不会暴露任何攻击面。

  • 在 Linux 4.8 (?) 之前,ptrace()可以使用列入白名单的调用通过在允许调用之后但实际执行之前修改寄存器来逃避沙箱。在 4.8 之前的内核上的解决方案就是不将该调用列入白名单。

  • 由于系统调用通过将寄存器传递给内核来工作,seccomp(以及任何基于 ptrace 的沙箱)只能根据寄存器本身的内容进行过滤。这意味着open()无法过滤包含内存指针的参数,例如提供给 的文件名。Seccomp 只检查寄存器的内容,不能检查内存。

  • 过滤器一旦就位就不能撤销或更改。如果您想使用沙盒的多个阶段,请从更宽松的策略开始,并确保seccomp()(在 >= Linux 3.17 上)并prctl()在下一阶段加载之前列入白名单,因为它们是添加新过滤器所必需的。第二阶段沙箱应该将与第一阶段相同的系统调用列入白名单,减去您想要禁用的系统调用,或者是您想要禁用的系统调用的选择性黑名单。