In questa pagina imparerai a creare il tuo ambiente con sandbox con Sandbox2. Scoprirai come definire un criterio sandbox e alcune modifiche avanzate, ma comuni. Utilizza queste informazioni come guida, insieme agli esempi e alla documentazione sul codice nei file di intestazione.
1. Scegli un metodo di esecuzione della sandbox
La sandbox inizia con un esecutore (vedi Sandbox Esecutor), responsabile dell'esecuzione di Sandboxee. Il file di intestazione executor.h contiene l'API necessaria a questo scopo. L'API è molto flessibile e ti consente di scegliere quella più adatta al tuo caso d'uso. Le seguenti sezioni descrivono i tre metodi diversi tra cui puoi scegliere.
Metodo 1: autonomo – Esegui un programma binario con la sandbox già abilitata
Questo è il modo più semplice per utilizzare la sandbox ed è il metodo consigliato quando vuoi sandbox un intero programma binario per il quale non hai codice di origine. È anche il modo più sicuro per utilizzare la sandbox, poiché non esiste un'inizializzazione senza sandbox che potrebbe avere effetti negativi.
Nello snippet di codice seguente, definiamo il percorso del programma binario per la limitazione tramite sandbox e gli argomenti da passare a una chiamata execve syscall. Come puoi vedere nel file di intestazione executor.h, non specifichiamo un valore per envp
e quindi copiamo l'ambiente dal processo padre. Ricorda che il primo argomento è sempre il nome del programma da eseguire e il nostro snippet non definisce nessun altro argomento.
Esempi di questo metodo esecutore sono: statico e strumento.
#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);
Metodo 2: Sandbox2 Forkserver: comunica all'esecutore quando attivare la sandbox
Questo metodo offre la flessibilità di disattivare la sandbox durante l'inizializzazione e di scegliere quando attivare la sandbox chiamando ::sandbox2::Client::SandboxMeHere()
. È necessario che tu possa definire il codice quando vuoi avviare la limitazione tramite sandbox e deve essere a thread singolo (leggi le Domande frequenti).
Nello snippet di codice seguente, utilizziamo lo stesso codice descritto nel Metodo 1 descritto sopra. Tuttavia, al fine di consentire l'esecuzione del programma senza limitazione tramite sandbox durante l'inizializzazione, chiamiamo 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);
Poiché l'esecutore ora dispone di una sandbox disabilitata fino a quando non riceve una notifica da Sandboxee, dobbiamo creare un'istanza ::sandbox2::Client
, configurare la comunicazione tra l'esecutore e Sandboxee, quindi notificare all'esecutore che l'inizializzazione è terminata e che vogliamo iniziare la limitazione tramite sandbox chiamando 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();
…
Un esempio di questo metodo esecutore è crc4, dove crc4bin.cc
è la sandbox e invia una notifica all'esecutore (crc4sandbox.cc
) quando deve accedere alla sandbox.
Metodo 3: forkserver personalizzato: prepara un programma binario, attendi le richieste di fork e sandbox autonomamente
Questa modalità ti consente di avviare un programma binario, prepararlo per il sandbox e, in un momento specifico del ciclo di vita del tuo programma binario, renderlo disponibile per l'esecutore.
L'esecutore invierà una richiesta fork al tuo programma binario, che sarà fork()
(tramite ::sandbox2::ForkingClient::WaitAndFork()
). Il processo appena creato sarà pronto per essere limitato tramite sandbox con ::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());
Ricorda che questa modalità è piuttosto complicata e applicabile solo in alcuni casi specifici, ad esempio quando hai requisiti di memoria rigidi. Trarrai vantaggio dalla COW, ma svantaggi del fatto che non esiste un vero e proprio ASLR. Un altro esempio di utilizzo tipico è quando Sandboxee ha un'inizializzazione lunga e ad alta intensità di CPU, che può essere eseguita prima dell'elaborazione dei dati non attendibili.
Per un esempio di questo metodo dell'esecutore, consulta custom_fork.
2. Crea un criterio Sandbox
Quando avrai un esecutore, probabilmente vorrai definire un criterio sandbox per la sandbox. In caso contrario, Sandboxee è protetto solo dal Criterio di chiamata Sys predefinito.
L'obiettivo del criterio Sandbox è limitare le chiamate di sistema e gli argomenti che Sandboxee può eseguire, nonché i file a cui può accedere. Dovrai avere una comprensione dettagliata delle chiamate di sistema richieste dal codice che intendi impostare come sandbox. Un modo per osservare le chiamate di sistema è eseguire il codice con lo strumento a riga di comando di Linux.
Una volta creato l'elenco delle chiamate di sistema, puoi utilizzare PolicyBuilder per definire il criterio. PolicyBuilder include molte funzioni helper e helper che consentono molte operazioni comuni. Il seguente elenco è solo un piccolo estratto delle funzioni disponibili:
- Aggiungi alla lista consentita qualsiasi syscall per l'avvio del processo:
AllowStaticStartup();
AllowDynamicStartup();
- Aggiungi a lista consentita tutte le chiamate di sistema aperte/lettura/scrittura*:
AllowOpen();
AllowRead();
AllowWrite();
- Aggiungi a lista consentita tutte le chiamate di sistema correlate all'uscita, all'accesso e allo stato:
AllowExit();
AllowStat();
AllowAccess();
- Aggiungi a lista consentita le chiamate di sistema relative al sonno e all'ora:
AllowTime();
AllowSleep();
Queste funzioni consentono di inserire eventuali syscall pertinenti nella lista consentita. Ciò presenta il vantaggio che lo stesso criterio può essere utilizzato su diverse architetture in cui non sono disponibili determinate chiamate di sistema (ad es. ARM64 non ha una chiamata di sistema aperta) ma con il rischio di sicurezza minimo di abilitare più programmi di quanto potrebbe essere necessario. Ad esempio, AllowOpen() consente a Sandboxee di chiamare qualsiasi syscall correlata. Se vuoi inserire un solo sistema syscall nella lista consentita, puoi utilizzare AllowSyscall();
per consentire più chiamate di sistema alla volta, con AllowSyscalls()
.
Finora il criterio controlla solo l'identificatore syscall. Se hai bisogno di rafforzare ulteriormente il criterio e vuoi definire un criterio in cui consenti solo una chiamata di sistema con argomenti particolari, devi utilizzare AddPolicyOnSyscall()
o AddPolicyOnSyscalls()
. Queste funzioni non prendono solo l'ID syscall come argomento, ma anche un filtro seccomp-bpf non elaborato che utilizza le macro bpf helper del kernel Linux. Per saperne di più su BPF, consulta la documentazione del kernel. Se ti capita di dover scrivere codice BPF ripetitivo, che ritieni debba avere una funzione di wrapping sull'usabilità, non esitare a inviare una richiesta di funzionalità.
Oltre alle funzioni relative a syscall, PolicyBuilder fornisce anche alcune funzioni relative al filesystem come AddFile()
o AddDirectory()
per associare un file/una directory alla sandbox. Puoi utilizzare l'helper AddTmpfs()
per aggiungere un'archiviazione temporanea per i file nella sandbox.
Una funzione particolarmente utile è AddLibrariesForBinary()
, che aggiunge le librerie e il linker richiesti da un programma binario.
Trovare le syscalls nella lista consentita è ancora un po' di lavoro manuale. Crea un criterio con le chiamate di sistema che conosci e di cui hai bisogno per il programma binario ed eseguilo con un carico di lavoro comune. Se viene attivata una violazione, autorizza la chiamata di sistema e ripeti la procedura. Se riscontri una violazione che ritieni possa essere rischiosa nella lista consentita e il programma gestisce gli errori senza alcun problema, puoi provare a restituirla invece con 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();
}
3. Regola i limiti
Il criterio Sandbox impedisce a Sandboxee di chiamare specifiche chiamate di sistema e quindi riduce la superficie di attacco. Tuttavia, un utente malintenzionato potrebbe comunque causare effetti indesiderati eseguendo un processo a tempo indeterminato o esaurindo la RAM e altre risorse.
Per risolvere questa minaccia, per impostazione predefinita Sandboxee viene eseguito in limiti di esecuzione rigorosi. Se questi limiti predefiniti causano problemi per l'esecuzione legittima del programma, puoi modificarli utilizzando la classe sandbox2::Limits
chiamando limits()
nell'oggetto esecutore.
Lo snippet di codice riportato di seguito mostra alcuni esempi di aggiustamenti dei limiti. Tutte le opzioni disponibili sono documentate nel file di intestazione 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));
Per un esempio dell'utilizzo della classe sandbox2::Limits
, vedi lo strumento di esempio.
4. Esegui la sandbox
Nelle sezioni precedenti, hai preparato l'ambiente, il criterio, l'esecutore e Sandboxee con sandbox. Il passaggio successivo consiste nel creare l'oggetto Sandbox2
ed eseguirlo.
Esegui in modo sincrono
La sandbox può essere eseguita in modalità sincrona bloccando così i risultati. Lo snippet di codice seguente mostra l'istanza dell'oggetto Sandbox2
e la sua esecuzione sincrona. Per un esempio più dettagliato, consulta la sezione statica.
#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();
Esegui in modo asincrono
Puoi anche eseguire la sandbox in modo asincrono, quindi non bloccarla fino a quando non ottieni un risultato. Questo è utile, ad esempio, quando comunichi con Sandboxee. Lo snippet di codice riportato di seguito illustra questo caso d'uso. Per esempi più dettagliati, vedi 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. Comunicare con la sandbox
Per impostazione predefinita, l'esecutore può comunicare con il sandbox tramite i descrittori dei file. Questo potrebbe essere tutto ciò che ti serve, ad esempio se vuoi semplicemente condividere un file con Sandboxee o leggere l'output standard di Sandboxee.
Tuttavia, molto probabilmente hai bisogno di una logica di comunicazione più complessa tra l'esecutore e Sandboxee. L'API comms (vedi il file dell'intestazione comms.h) può essere utilizzata per inviare numeri interi, stringhe, buffer di byte, protobuf o descrittori di file.
Condivisione dei descrittori dei file
Utilizzando l'API Inter-Process Communication API (vedi ipc.h), puoi utilizzare MapFd()
o ReceiveFd()
:
Utilizza
MapFd()
per mappare i descrittori dei file dall'esecutore alla sandbox. Può essere utilizzato per condividere un file aperto dall'esecutore per l'utilizzo in Sandboxee. Esempio di utilizzo: statico.// The executor opened /proc/version and passes it to the sandboxee as stdin executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);
Usa
ReceiveFd()
per creare un endpoint socketpair Può essere utilizzato per leggere gli output standard o gli errori standard di Sandboxee. Puoi vedere un esempio di utilizzo nello strumento.// The executor receives a file descriptor of the sandboxee stdout int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
Utilizzo dell'API per le comunicazioni
Sandbox2 fornisce una comoda API per le comunicazioni. Questo è un modo semplice e semplice per condividere numeri interi, stringhe o buffer di byte tra l'esecutore e Sandboxee. Di seguito sono riportati alcuni snippet di codice disponibili nell'esempio crc4.
Per iniziare a utilizzare l'API comms, devi prima recuperare l'oggetto comms dall'oggetto Sandbox2:
sandbox2::Comms* comms = s2.comms();
Una volta che l'oggetto comms è disponibile, i dati possono essere inviati a Sandboxee utilizzando una delle famiglie di funzioni Send*. Puoi trovare un esempio di utilizzo dell'API comms nell'esempio crc4. Lo snippet di codice riportato di seguito mostra un estratto dell'esempio. L'esecutore invia un unsigned char buf[size]
con SendBytes(buf, size)
:
if (!(comms->SendBytes(static_cast<const uint8_t*>(buf), sz))) {
/* handle error */
}
Per ricevere dati da Sandboxee, utilizza una delle funzioni di Recv*
. Lo snippet di codice di seguito è un estratto dell'esempio crc4. L'esecutore riceve il checksum in un numero intero senza segno a 32 bit:
uint32_t crc4;
if (!(comms->RecvUint32(&crc4))) {
/* handle error */
}
Condivisione di dati con i buffer
Un'altra funzionalità di condivisione dei dati consiste nell'utilizzare l'API buffer per condividere grandi quantità di dati ed evitare costose copie da inviare tra l'esecutore e Sandboxee.
L'esecutore crea un buffer, per dimensioni e dati, da passare o direttamente da un descrittore di file, e lo passa al sandbox utilizzando comms->SendFD()
nell'esecutore e comms->RecvFD()
nel sandbox.
Nel seguente snippet puoi vedere il lato dell'esecutore. La sandbox viene eseguita in modo asincrono e condivide i dati tramite un buffer con la sandbox:
// 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());
Per quanto riguarda Sandboxee, devi anche creare un oggetto buffer e leggere i dati dal descrittore del file inviato dall'esecutore:
// 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. Uscire dalla sandbox
A seconda di come esegui la sandbox (vedi questo passaggio), devi modificare il modo in cui termina la sandbox e, di conseguenza, anche la sandbox.
Uscire da una sandbox in esecuzione in modo sincrono
Se la sandbox è stata eseguita in modo sincrono, il comando Run verrà restituito solo al termine dell'esecuzione. Pertanto, non sono richiesti ulteriori passaggi per la risoluzione. Lo snippet di codice seguente mostra questo scenario:
Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();
Uscire da una sandbox in esecuzione in modo asincrono
Se la sandbox è stata eseguita in modo asincrono, sono disponibili due opzioni per la chiusura. Per prima cosa, puoi semplicemente attendere il completamento della sandbox e ricevere lo stato finale dell'esecuzione:
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
In alternativa, puoi uccidere la Sandbox in qualsiasi momento, ma è comunque consigliabile chiamare AwaitResult()
, in quanto nel frattempo potrebbe terminare per un altro motivo:
s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
7. Test
Come con qualsiasi altro codice, l'implementazione della sandbox deve avere test. I test sandbox non sono concepiti per verificare la correttezza del programma, ma per verificare se possono essere eseguiti senza problemi come violazioni della sandbox. Ciò garantisce anche che il criterio sandbox sia corretto.
Un programma con sandbox viene testato nello stesso modo in cui viene eseguito in produzione, con gli argomenti e i file di input che verrebbero elaborati normalmente.
Puoi eseguire test semplici come test shell o test C++ utilizzando processi secondari. Dai un'occhiata agli esempi per trarre ispirazione.
Conclusione
Grazie per aver letto tutto questo. Ci auguriamo che la nostra guida sia stata di tuo gradimento e ora ti invitiamo a creare le tue sandbox per garantire la sicurezza degli utenti.
Creare sandbox e criteri è un'attività difficile e soggetta a errori sottili. Per sicurezza, ti consigliamo di far controllare un criterio e un codice da un esperto di sicurezza.