Nesta página, você vai aprender a criar seu próprio ambiente em sandbox com o Sandbox2. Você vai aprender a definir uma política de sandbox e alguns ajustes avançados, mas comuns. Use as informações aqui como um guia, junto com os exemplos e a documentação de código nos arquivos principais.
1. Escolher um método de executor de sandbox
O sandbox começa com um executor (consulte Executor de sandbox), que é responsável por executar o Sandboxee. O arquivo principal executor.h contém a API necessária para essa finalidade. A API é muito flexível e permite que você escolha o que funciona melhor para seu caso de uso. As seções a seguir descrevem os três métodos diferentes que você pode escolher.
Método 1: independente – executar um binário com o sandbox já ativado
Essa é a maneira mais simples de usar o sandbox e é o método recomendado quando você quer colocar em sandbox um binário inteiro para o qual não tem código-fonte. Essa também é a maneira mais segura de usar o sandbox, já que não há inicialização sem sandbox que possa ter efeitos adversos.
No snippet de código a seguir, definimos o caminho do binário a ser colocado em sandbox e os argumentos que precisamos transmitir para uma chamada de sistema execve. Como você pode ver no
executor.h
arquivo principal, não especificamos um valor para envp e, portanto, copiamos o
ambiente do processo pai. Lembre-se de que o primeiro argumento é sempre o nome do programa a ser executado, e nosso snippet não define nenhum outro argumento.
Exemplos desse método de executor são: static e tool.
#include "sandboxed_api/sandbox2/executor.h"
std::string path = "path/to/binary";
std::vector<std::string> args = {path}; // args[0] will become the sandboxed
// process' argv[0], typically the
// path to the binary.
auto executor = absl::make_unique<sandbox2::Executor>(path, args);
Método 2: Forkserver do Sandbox2 – informar ao executor quando colocar em sandbox
Esse método oferece a flexibilidade de não ser colocado em sandbox durante a inicialização e, em seguida, escolher quando entrar no sandbox chamando ::sandbox2::Client::SandboxMeHere(). Ele exige que você possa definir no
código quando quer iniciar o sandbox e precisa ser de linha única (leia
o motivo nas Perguntas frequentes).
No snippet de código a seguir, usamos o mesmo código descrito no Método 1 acima. No entanto, para permitir que o programa seja executado de maneira não colocada em sandbox durante a inicialização, chamamos set_enable_sandbox_before_exec(false).
#include "sandboxed_api/sandbox2/executor.h"
std::string path = "path/to/binary";
std::vector<std::string> args = {path};
auto executor = absl::make_unique<sandbox2::Executor>(path, args);
executor->set_enable_sandbox_before_exec(false);
Como o executor agora tem um sandbox desativado até ser notificado pelo Sandboxee, precisamos criar uma instância ::sandbox2::Client, configurar a comunicação entre o executor e o Sandboxee e, em seguida, notificar o executor de que nossa inicialização foi concluída e queremos iniciar o sandbox agora chamando sandbox2_client.SandboxMeHere().
// main() of sandboxee
int main(int argc, char** argv) {
gflags::ParseCommandLineFlags(&argc, &argv, false);
// Set-up the sandbox2::Client object, using a file descriptor (1023).
sandbox2::Comms comms(sandbox2::Comms::kSandbox2ClientCommsFD);
sandbox2::Client sandbox2_client(&comms);
// Enable sandboxing from here.
sandbox2_client.SandboxMeHere();
…
Um exemplo desse método de executor é
crc4, em que crc4bin.cc é o
Sandboxee e notifica o executor (crc4sandbox.cc) quando ele deve entrar no
sandbox.
Método 3: Forkserver personalizado – preparar um binário, aguardar solicitações de fork e colocar em sandbox por conta própria
Esse modo permite iniciar um binário, prepará-lo para o sandbox e, em um momento específico do ciclo de vida do binário, disponibilizá-lo para o executor.
O executor vai enviar uma solicitação de fork para o binário, que vai fork() (via ::sandbox2::ForkingClient::EnterForkLook()). O processo recém-criado estará pronto para ser colocado em sandbox com ::sandbox2::Client::SandboxMeHere().
#include "sandboxed_api/sandbox2/executor.h"
// Start the custom ForkServer
std::string path = "path/to/binary";
std::vector<std::string> args = {path};
auto fork_executor = absl::make_unique<sandbox2::Executor>(path, args);
fork_executor->StartForkServer();
// Initialize Executor with Comms channel to the ForkServer
auto executor = absl::make_unique<sandbox2::Executor>(
fork_executor->ipc()->GetComms());
Lembre-se de que esse modo é bastante complicado e aplicável apenas em alguns casos específicos, por exemplo, quando você tem requisitos de memória restritos. Você vai se beneficiar do COW, mas terá a desvantagem de não haver ASLR real. Outro exemplo de uso típico seria quando o Sandboxee tem uma inicialização longa e com uso intenso da CPU que pode ser executada antes que os dados não confiáveis sejam processados.
Para conferir um exemplo desse método de executor, consulte custom_fork.
2. Criar uma política de sandbox
Depois de ter um executor, é provável que você queira definir uma Sandbox Policy para o Sandboxee. Caso contrário, o Sandboxee só será protegido pela política de chamada de sistema padrão.
Com a política de sandbox, o objetivo é restringir as chamadas de sistema e os argumentos que o Sandboxee pode fazer, bem como os arquivos que ele pode acessar. Você precisa ter um entendimento detalhado das chamadas de sistema exigidas pelo código que planeja colocar em sandbox. Uma maneira de observar as chamadas de sistema é executar o código com a ferramenta de linha de comando strace do Linux.
Depois de ter a lista de chamadas de sistema, você pode usar o PolicyBuilder para definir a política.
PolicyBuilder vem com muitas funções de conveniência e funções auxiliares que permitem muitas
operações comuns (muitas das quais já estão incluídas em
DefaultPolicyBuilder()). A lista a seguir é apenas um pequeno trecho das
funções disponíveis:
- Permitir qualquer chamada de sistema para inicialização do processo:
AllowStaticStartup();AllowDynamicStartup();
- Permitir qualquer chamada de sistema open/read/write*:
AllowOpen();AllowRead();AllowWrite();
- Permitir qualquer chamada de sistema relacionada a saída/acesso/estado:
AllowExit();AllowStat();AllowAccess();
- Permitir qualquer chamada de sistema relacionada a suspensão/tempo:
AllowTime();AllowSleep();
Essas funções de conveniência permitem qualquer chamada de sistema relevante. Isso tem a vantagem de que a mesma política pode ser usada em diferentes arquiteturas em que determinadas chamadas de sistema não estão disponíveis (por exemplo, o ARM64 não tem chamada de sistema OPEN), mas com o pequeno risco de segurança de ativar mais chamadas de sistema do que o necessário. Por exemplo, AllowOpen() permite que o Sandboxee chame qualquer chamada de sistema relacionada a aberto. Se você quiser adicionar à lista de permissões apenas uma chamada de sistema específica, use AllowSyscall(); para adicionar várias chamadas de sistema à lista de permissões de uma só vez, use AllowSyscalls().
Até agora, a política só verifica o identificador de chamada de sistema. Se você precisar fortalecer ainda mais a política e quiser definir uma política em que só permita uma chamada de sistema com argumentos específicos, use AddPolicyOnSyscall() ou AddPolicyOnSyscalls(). Essas funções não apenas usam o ID da chamada de sistema como um 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 mais informações sobre o BPF. Se você estiver escrevendo um código BPF repetitivo que acha que deveria ter um wrapper de usabilidade, envie uma solicitação de recurso.
Além das funções relacionadas a chamadas de sistema, o PolicyBuilder também oferece várias funções relacionadas ao sistema de arquivos, como AddFile() ou AddDirectory(), para montar um arquivo/diretório no sandbox. O auxiliar AddTmpfs() pode ser usado para adicionar um armazenamento de arquivos temporário no sandbox.
Uma função particularmente útil é AddLibrariesForBinary(), que adiciona as bibliotecas e o vinculador exigidos por um binário.
Infelizmente, ainda é um pouco de trabalho manual para criar as chamadas de sistema para a lista de permissões. Crie uma política com as chamadas de sistema que você sabe que seu binário precisa e execute-a com uma carga de trabalho comum. Se uma violação for acionada, adicione a chamada de sistema à lista de permissões e repita o processo. Se você encontrar uma violação que acha que pode ser arriscada para adicionar à lista de permissões e o programa processa erros normalmente, tente fazer com que ele retorne um erro com BlockSyscallWithErrno().
O exemplo a seguir constrói sandbox2::PolicyBuilder diretamente para demonstrar a restrição de argumentos write e mprotect e o bloqueio de openat (que DefaultPolicyBuilder() permitiria de outra forma):
#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();
}
3. Ajustar limites
A política de sandbox impede que o Sandboxee chame chamadas de sistema específicas e, assim, reduz a superfície de ataque. No entanto, um invasor ainda pode causar efeitos indesejados executando um processo indefinidamente ou esgotando a RAM e outros recursos.
Para resolver essa ameaça, o Sandboxee é executado com limites de execução restritos por padrão. Se esses limites padrão causarem problemas para a execução legítima do programa, você poderá ajustá-los usando a classe sandbox2::Limits chamando limits() no objeto executor.
O snippet de código abaixo mostra alguns exemplos de ajustes de limite. Todas as opções disponíveis estão documentadas no arquivo de cabeçalho limits.h.
// Restrict the address space size of the sandboxee to 4 GiB.
executor->limits()->set_rlimit_as(4ULL << 30);
// Kill sandboxee with SIGXFSZ if it writes more than 1 GiB to the filesystem.
executor->limits()->set_rlimit_fsize(1ULL << 30);
// Number of file descriptors which can be used by the sandboxee.
executor->limits()->set_rlimit_nofile(1ULL << 10);
// The sandboxee is not allowed to create core files.
executor->limits()->set_rlimit_core(0);
// Maximum 300s of real CPU time.
executor->limits()->set_rlimit_cpu(300);
// Maximum 120s of wall time.
executor->limits()->set_walltime_limit(absl::Seconds(120));
Para conferir um exemplo do uso da classe sandbox2::Limits, consulte a ferramenta de exemplo
tool.
4. Executar o sandbox
Nas seções anteriores, você preparou o ambiente em sandbox, a política e o executor e o Sandboxee. A próxima etapa é criar o objeto Sandbox2 e executá-lo.
Executar de maneira síncrona
O sandbox pode ser executado de maneira síncrona, bloqueando até que haja um resultado. O snippet de código abaixo demonstra a instanciação do objeto Sandbox2 e a execução síncrona dele. Para conferir um exemplo mais detalhado, consulte
static.
#include "sandboxed_api/sandbox2/sandbox2.h"
sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
sandbox2::Result result = s2.Run(); // Synchronous
LOG(INFO) << "Result of sandbox execution: " << result.ToString();
Executar de maneira assíncrona
Também é possível executar o sandbox de maneira assíncrona, sem bloquear até que haja um resultado. Isso é útil, por exemplo, ao se comunicar com o Sandboxee. O snippet de código abaixo demonstra esse caso de uso. Para conferir exemplos mais detalhados, consulte crc4 e tool.
#include "sandboxed_api/sandbox2/sandbox2.h"
sandbox2::Sandbox2 s2(std::move(executor), std::move(policy));
if (s2.RunAsync()) {
// Communicate with sandboxee, use s2.Kill() to kill it if needed
// ...
}
Sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
5. Comunicar com o Sandboxee
Por padrão, o executor pode se comunicar com o Sandboxee por descritores de arquivo. Isso pode ser tudo o que você precisa, por exemplo, se quiser apenas compartilhar um arquivo com o Sandboxee ou ler a saída padrão do Sandboxee.
No entanto, é muito provável que você precise de uma lógica de comunicação mais complexa entre o executor e o Sandboxee. A API comms (consulte o comms.h arquivo principal) pode ser usada para enviar números inteiros, strings, buffers de bytes, protobufs ou descritores do arquivo.
Compartilhar descritores de arquivo
Usando a API de comunicação entre processos (consulte
ipc.h),
você pode usar MapFd() ou ReceiveFd():
Use
MapFd()para mapear descritores do arquivo do executor para o Sandboxee. Isso pode ser usado para compartilhar um arquivo aberto do executor para uso no Sandboxee. Um exemplo de uso pode ser encontrado em static.// The executor opened /proc/version and passes it to the sandboxee as stdin executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);Use
ReceiveFd()para criar um endpoint de socketpair. Isso pode ser usado para ler a saída padrão ou erros padrão do Sandboxee. Um exemplo de uso pode ser encontrado na ferramenta.// The executor receives a file descriptor of the sandboxee stdout int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
Usar a API comms
O Sandbox2 oferece uma API comms API conveniente. Essa é uma maneira simples e fácil de compartilhar números inteiros, strings ou buffers de bytes entre o executor e o Sandboxee. Confira abaixo alguns snippets de código que você pode encontrar no exemplo crc4.
Para começar a usar a API comms, primeiro você precisa receber o objeto comms do objeto Sandbox2:
sandbox2::Comms* comms = s2.comms();
Depois que o objeto comms estiver disponível, os dados poderão ser enviados ao Sandboxee usando uma das funções da família Send*. Você pode encontrar um exemplo de uso da API comms
no crc4. O snippet de código abaixo mostra um trecho desse exemplo. O executor envia um unsigned char
buf[size] com SendBytes(buf, size):
if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
/* handle error */
}
Para receber dados do Sandboxee, use uma das funções Recv*. O snippet de código abaixo é um trecho do
crc4. O executor recebe o checksum em um número inteiro sem assinatura de 32 bits: uint32_t crc4;
if (!(comms->RecvUint32(&crc4))) {
/* handle error */
}
Compartilhar dados com buffers
Outra funcionalidade de compartilhamento de dados pessoais é usar a API de buffer para compartilhar grandes quantidades de dados e evitar cópias caras que são enviadas de um lado para o outro entre o executor e o Sandboxee.
O executor cria um buffer, por tamanho e dados a serem transmitidos ou diretamente
de um descritor de arquivo, e o transmite para o Sandboxee usando comms->SendFD()
no executor e comms->RecvFD() no Sandboxee.
No snippet de código abaixo, você pode conferir o lado do executor. O sandbox é executado de maneira assíncrona e compartilha dados por um buffer com o Sandboxee:
// start the sandbox asynchronously
s2.RunAsync();
// instantiate the comms object
sandbox2::Comms* comms = s2.comms();
// random buffer data we want to send
constexpr unsigned char buffer_data[] = /* random data */;
constexpr unsigned int buffer_dataLen = 34;
// create sandbox2 buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
sandbox2::Buffer::CreateWithSize(1ULL << 20 /* 1Mib */);
std::unique_ptr<sandbox2::Buffer> buffer_ptr = std::move(buffer).value();
// point to the sandbox2 buffer and fill with data
uint8_t* buf = buffer_ptr‑>data();
memcpy(buf, buffer_data, buffer_data_len);
// send the data to the sandboxee
comms‑>SendFd(buffer_ptr‑>fd());
No lado do Sandboxee, você também precisa criar um objeto de buffer e ler os dados do descritor de arquivo enviado pelo executor:
// establish the communication with the executor
int fd;
comms.RecvFD(&fd);
// create the buffer
absl::StatusOr<std::unique_ptr<sandbox2::Buffer>> buffer =
sandbox2::Buffer::createFromFd(fd);
// get the data
auto buffer_ptr = std::move(buffer).value();
uint8_t* buf = buffer_ptr‑>data();
/* work with the buf object */
6. Sair do sandbox
Dependendo de como você executa o sandbox (consulte esta etapa), você precisa ajustar a maneira como você encerra o sandbox e, portanto, também o Sandboxee.
Sair de um sandbox em execução síncrona
Se o sandbox estiver em execução síncrona, a execução só será retornada quando o Sandboxee terminar. Portanto, nenhuma etapa adicional para encerramento é necessária. O snippet de código abaixo mostra esse cenário:
Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();
Sair de um sandbox em execução assíncrona
Se o sandbox estiver em execução assíncrona, duas opções estarão disponíveis para encerramento. Primeiro, você pode apenas aguardar a conclusão do Sandboxee e receber o status de execução final:
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
Como alternativa, você pode encerrar o Sandboxee a qualquer momento, mas ainda é recomendável chamar AwaitResult(), porque o Sandboxee pode ser encerrado por outro motivo nesse período:
s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
7. Teste
Como qualquer outro código, a implementação do sandbox precisa ter testes. Os testes de sandbox não são destinados a testar a correção do programa, mas sim a verificar se o programa em sandbox pode ser executado sem problemas, como violações de sandbox. Isso também garante que a política de sandbox esteja correta.
Um programa em sandbox é testado da mesma maneira que você o executaria na produção, com os argumentos e arquivos de entrada que ele normalmente processaria.
Esses testes podem ser tão simples quanto um teste de shell ou testes de C++ usando subprocessos. Confira os exemplos para se inspirar.
Conclusão
Agradecemos por ler até aqui. Esperamos que você tenha gostado do nosso guia e agora se sinta capacitado a criar seus próprios sandboxes para ajudar a proteger seus usuários.
A criação de sandboxes e políticas é uma tarefa difícil e propensa a erros sutis. Para ficar do lado seguro, recomendamos que você peça a um especialista em segurança para revisar sua política e seu código.