Sandbox de computação com inspeção de ptrace

0

Eu quero lançar executáveis somente de computação não confiáveis no servidor linux. O processo não terá acesso ao sistema e aos arquivos, exceto stdin e stdout

Minha idéia é usar ptrace para capturar e bloquear o syscalls para o kernel do linux. Também eu usá-lo para obter e definir o estado interno do processo (registra + RAM). É uma caixa de areia segura? Que maneiras você sabe para freá-lo?

Também quero limitar o uso de tempo de RAM e CPU para evitar o DOS

    
por Евгений Новиков 12.11.2017 / 09:08

1 resposta

2

É exatamente para isso que seccomp é. O Seccomp é suportado na maioria dos kernels Linux modernos e é projetado para filtrar syscalls. Ele vem em duas formas, chamadas modo 1 e modo 2.

Modo 1 seccomp

O processo tem permissão apenas para 4 chamadas de sistema: read() , write() , rt_sigreturn() e exit() (observe que esse é o exit() syscall, não a função. A função glibc usa a função não whitelisted exit_group() syscall). Se qualquer outra chamada for tentada, ela não retornará e o programa será cancelado. Isso se destina a computar bytecode não confiável em um processo de agente seguro. O código confiável pode criar um processo não confiável que executa bytecodes potencialmente perigosos após ativar o modo 1 seccomp e pode se comunicar com o pai por meio de pipes.

Modo 2 seccomp

Isso também é chamado de seccomp-bpf, já que ele usa o código de bytes do eBPF para criar um filtro dinâmico para restringir o syscalls com base em seu número e seus argumentos. Além disso, pode ser configurado para executar várias ações de violação, desde matar o processo pela força, até negar o syscall e levantar um sinal para ser preso sem matar o processo, retornar um erro personalizado, simplesmente negar o syscall para testar finalidades. A biblioteca libseccomp abstrai grande parte disso, tornando desnecessário escrever o próprio bytecode do eBPF.

Ambos os métodos são significativamente mais rápidos do que uma sandbox baseada em ptrace, o que implicaria uma sobrecarga significativa. Além disso, um ptrace-sandbox não enviaria necessariamente seus filtros a qualquer filho, portanto, as chamadas como execve() , fork() , vfork() e clone() precisariam ser desativadas, para que você não ficasse vulnerável às condições de corrida TOCTOU. Ambos os modos seccomp, por outro lado, preservam o filtro em qualquer execução ou fork.

Exemplo usando o modo 1 seccomp que executa com segurança "return 42" no bytecode:

#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);
    }
}

Exemplo usando o modo 2 seccomp, com vários filtros syscall arbitrários:

#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());
}

Há algumas coisas importantes para lembrar com o seccomp:

  • Chamadas que não são chamadas de sistema "verdadeiras", mas sim vDSOs como gettimeofday() e time() não podem ser filtradas. Para desempenho, eles são executados no espaço do usuário, evitando um switch de contexto caro. No entanto, isso também impede que o seccomp esteja ciente de que eles estão em execução. Isso normalmente não é um problema porque os únicos syscalls que podem ser implementados como um vDSO são normalmente muito simples e expõem virtualmente nenhuma área de superfície de ataque.

  • Antes do Linux 4.8 (?), uma chamada ptrace() na lista de permissões pode ser usada para escapar da sandbox, modificando os registros após a chamada ter sido permitida, mas antes que ela seja realmente executada. A solução em kernels anteriores a 4.8 é simplesmente não incluir essa chamada na lista de permissões.

  • Como o syscalls trabalha passando registradores ao kernel, o seccomp (e qualquer sandbox baseada no ptrace) só pode filtrar com base no conteúdo dos próprios registradores. Isso significa que os argumentos que contêm ponteiros para a memória, como o nome do arquivo fornecido para open() , não podem ser filtrados. O Seccomp verifica apenas o conteúdo dos registradores e não pode examinar a memória.

  • Os filtros não podem ser revogados ou alterados quando estiverem em vigor. Se você quiser usar vários estágios de sandbox, comece com uma política mais solta e garanta que seccomp() (on > = Linux 3.17) e prctl() estejam na lista de permissões até que o próximo estágio seja carregado, pois eles são necessários para adicionar novos filtros. A caixa de proteção do segundo estágio deve incluir na lista branca os mesmos syscalls do primeiro estágio, menos os que você deseja desativar ou uma lista negra seletiva dos syscalls que você deseja desativar.

por 13.11.2017 / 06:42

Tags