使用 ptrace 检查的计算沙箱

使用 ptrace 检查的计算沙箱

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

我的想法是用来ptrace捕获和阻止对 Linux 内核的系统调用。我还用它来获取和设置进程内部状态(寄存器+ RAM)。沙箱安全吗?您知道哪些制动方法?

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

答案1

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

模式 1 秒压缩

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

模式 2 秒计算

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

这两种方法都比基于 ptrace 的沙箱快得多,后者会产生大量开销。此外,ptrace-sandbox 不一定会将其过滤器发送给任何子进程,因此需要禁用 、 、 、 和 等调用,以免您容易受到 TOCTOU 竞争条件execve()fork()影响vfork()clone()另一方面,两种 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);
    }
}

使用模式 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());
}

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

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

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

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

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

相关内容