2. Criar uma política de sandbox

Depois de criar um executor, é provável que você queira definir uma política de sandbox para o sandbox. Caso contrário, o sandboxee será protegido somente pela política padrão de sistema de chamada.

O objetivo da política do sandbox é restringir as chamadas do sistema e os argumentos que o sandboxee pode gerar, bem como os arquivos que ele pode acessar. Você precisará ter uma compreensão detalhada das chamadas de sistema exigidas pelo código que planeja colocar no sandbox. Uma maneira de observar chamadas do sistema é executar o código com o strace da ferramenta de linha de comando do Linux.

Com a lista de chamadas do sistema, você pode usar o PolicyBuilder para definir a política. O PolicyBuilder vem com muitas funções auxiliares e conveniência que permitem muitas operações comuns. A lista a seguir é apenas um pequeno trecho das funções disponíveis:

  • Autorize qualquer chamada do sistema à inicialização do processo:
    • AllowStaticStartup();
    • AllowDynamicStartup();
  • Autorize qualquer chamada do sistema aberta /read/write*:
    • AllowOpen();
    • AllowRead();
    • AllowWrite();
  • Adicione à lista de permissões todas as chamadas do sistema relacionadas a saída/acesso/estado:
    • AllowExit();
    • AllowStat();
    • AllowAccess();
  • Autorize qualquer chamada do sistema relacionada ao sono/tempo:
    • AllowTime();
    • AllowSleep();

Essas funções de conveniência colocam na lista de permissões qualquer chamada do sistema relevante. A vantagem disso é que a mesma política pode ser usada em diferentes arquiteturas em que certas chamadas do sistema não estão disponíveis (por exemplo, ARM64 não tem chamada de sistema ABERTA), mas com o menor risco de segurança de ativar mais scripts do que o necessário. Por exemplo, AllowOpen() permite que o Sandboxee chame qualquer chamada do sistema aberta relacionada. Se você quiser colocar apenas uma chamada do sistema específica na lista de permissões, use AllowSyscall(); para permitir várias chamadas de uma vez. Além disso, use AllowSyscalls().

Até o momento, a política verifica apenas o identificador syscall. Se você precisar fortalecer ainda mais a política e quiser definir uma política em que você permita apenas uma chamada de sistema com argumentos específicos, use AddPolicyOnSyscall() ou AddPolicyOnSyscalls(). Essas funções não só pegam o ID syscall como argumento, mas também um filtro seccomp-bpf bruto usando as macros auxiliares bpf do kernel do Linux. Consulte a documentação do kernel para saber mais sobre o BPF. Se você escrever um código BPF repetitivo e achar que deveria ter um wrapper de usabilidade, fique à vontade para enviar uma solicitação de recurso.

Além das funções relacionadas à chamada do sistema, o PolicyBuilder também fornece diversas funções relacionadas ao sistema de arquivos, como AddFile() ou AddDirectory(), para ativar a vinculação de um arquivo/diretório no sandbox. O auxiliar AddTmpfs() pode ser usado para adicionar um armazenamento temporário de arquivos ao sandbox.

Uma função particularmente útil é AddLibrariesForBinary(), que adiciona as bibliotecas e o vinculador exigidos por um binário.

Criar as chamadas do sistema para a lista de permissões ainda é um pouco trabalho manual, infelizmente. Crie uma política com as syscalls que você conhece das suas necessidades binárias e execute-a com uma carga de trabalho comum. Se uma violação for acionada, coloque a chamada do sistema na lista de permissões e repita o processo. Se você encontrar uma violação que acredita ser de risco para a lista de permissões, e o programa tratar os erros corretamente, tente fazer com que ela retorne um erro em vez de BlockSyscallWithErrno().

#include "sandboxed_api/sandbox2/policy.h"
#include "sandboxed_api/sandbox2/policybuilder.h"
#include "sandboxed_api/sandbox2/util/bpf_helper.h"

std::unique_ptr<sandbox2::Policy> CreatePolicy() {
  return sandbox2::PolicyBuilder()
    .AllowSyscall(__NR_read)  // See also AllowRead()
    .AllowTime()              // Allow time, gettimeofday and clock_gettime
    .AddPolicyOnSyscall(__NR_write, {
        ARG(0),        // fd is the first argument of write (argument #0)
        JEQ(1, ALLOW), // allow write only on fd 1
        KILL,          // kill if not fd 1
    })
    .AddPolicyOnSyscall(__NR_mprotect, {
        ARG_32(2), // prot is a 32-bit wide argument, so it's OK to use *_32
                   // macro here
        JNE32(PROT_READ | PROT_WRITE, KILL), // prot must be the RW, otherwise
                                             // kill the process
        ARG(1), // len is a 64-bit argument
        JNE(0x1000, KILL),  // Allow single page syscalls only, otherwise kill
                            // the process
        ALLOW,              // Allow for the syscall to proceed, if prot and
                            // size match
    })
    // Allow the openat() syscall but always return "not found".
    .BlockSyscallWithErrno(__NR_openat, ENOENT)
    .BuildOrDie();
}