In questa pagina scoprirai come creare il tuo ambiente in sandbox con Sandbox2. Imparerai a definire una policy della sandbox, e alcune modifiche avanzate, ma comuni. Utilizza le informazioni qui riportate come guida, insieme agli esempi e alla documentazione del codice nei file di intestazione.
1. Scegli un metodo di esecuzione della sandbox
La sandbox inizia con un executor (vedi Executor della sandbox), che è 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 la soluzione più adatta al tuo caso d'uso. Le sezioni seguenti descrivono i tre metodi diversi tra cui puoi scegliere.
Metodo 1: autonomo: esegui un file binario con la sandbox già abilitata
Questo è il modo più semplice per utilizzare la sandbox ed è il metodo consigliato quando vuoi eseguire la sandbox di un intero file binario di cui non hai il codice sorgente. È anche il modo più sicuro per utilizzare la sandbox, poiché non esiste un'inizializzazione non in sandbox che potrebbe avere effetti negativi.
Nello snippet di codice riportato di seguito, definiamo il percorso del file binario di cui eseguire la sandbox e gli argomenti da passare a una syscall execve. Come puoi vedere nel
executor.h
file di intestazione, 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 altri argomenti.
Esempi di questo metodo di esecuzione sono: 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);
Metodo 2: Sandbox2 Forkserver: indica all'executor quando eseguire la sandbox
Questo metodo offre la flessibilità di non essere in sandbox durante l'inizializzazione e di scegliere quando entrare nella sandbox chiamando ::sandbox2::Client::SandboxMeHere(). Richiede di poter definire nel
codice quando vuoi avviare la sandbox e deve essere a thread singolo (scopri
perché nelle Domande frequenti).
Nello snippet di codice riportato di seguito, utilizziamo lo stesso codice descritto nel Metodo 1 sopra.
Tuttavia, per consentire al programma di essere eseguito in modo non in 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'executor ora ha una sandbox disabilitata finché non viene avvisato da Sandboxee, dobbiamo creare un'istanza ::sandbox2::Client, configurare la comunicazione tra l'executor e Sandboxee e poi avvisare l'executor che l'inizializzazione è terminata e che vogliamo avviare la sandbox ora 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 di esecuzione è
crc4, dove crc4bin.cc è il
Sandboxee e avvisa l'executor (crc4sandbox.cc) quando deve entrare nella
sandbox.
Metodo 3: Forkserver personalizzato: prepara un file binario, attendi le richieste di fork ed esegui la sandbox in autonomia
Questa modalità ti consente di avviare un file binario, prepararlo per la sandbox e, in un momento specifico del ciclo di vita del file binario, renderlo disponibile per l'executor.
L'executor invierà una richiesta di fork al tuo file binario, che eseguirà fork() (tramite ::sandbox2::ForkingClient::EnterForkLoop()). Il processo appena creato sarà pronto per la 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());
Tieni presente che questa modalità è piuttosto complicata e applicabile solo in alcuni casi specifici, ad esempio quando hai requisiti di memoria rigorosi. Potrai usufruire di COW, ma non avrai un vero ASLR. Un altro esempio di utilizzo tipico è quando Sandboxee ha un'inizializzazione lunga e intensiva della CPU che può essere eseguita prima dell'elaborazione dei dati non attendibili.
Per un esempio di questo metodo di esecuzione, vedi custom_fork.
2. Crea una policy della sandbox
Una volta che hai un executor, probabilmente vorrai definire una Sandbox Policy per Sandboxee. In caso contrario, Sandboxee è protetto solo dalla policy delle syscall predefinita.
Con la policy della sandbox, l'obiettivo è limitare le syscall e gli argomenti che Sandboxee può eseguire, nonché i file a cui può accedere. Dovrai avere una conoscenza dettagliata delle syscall richieste dal codice di cui prevedi di eseguire la sandbox. Un modo per osservare le syscall è eseguire il codice con lo strumento a riga di comando strace di Linux.
Una volta che hai l'elenco delle syscall, puoi utilizzare il PolicyBuilder per definire la policy.
PolicyBuilder include molte funzioni di utilità e funzioni helper che consentono di eseguire molte
operazioni comuni (molte delle quali sono già incluse in
DefaultPolicyBuilder()). Il seguente elenco è solo un piccolo estratto delle
funzioni disponibili:
- Consenti qualsiasi syscall per l'avvio del processo:
AllowStaticStartup();AllowDynamicStartup();
- Consenti qualsiasi syscall open/read/write*:
AllowOpen();AllowRead();AllowWrite();
- Consenti qualsiasi syscall relativa a uscita/accesso/stato:
AllowExit();AllowStat();AllowAccess();
- Consenti qualsiasi syscall relativa a sleep/time:
AllowTime();AllowSleep();
Queste funzioni di utilità consentono qualsiasi syscall pertinente. Questo ha il vantaggio che la stessa policy può essere utilizzata su architetture diverse in cui alcune syscall non sono disponibili (ad es. ARM64 non ha syscall OPEN), ma con il piccolo rischio per la sicurezza di abilitare più syscall di quelle necessarie. Ad esempio, AllowOpen() consente a Sandboxee di chiamare qualsiasi syscall correlata a open. Se vuoi consentire solo una syscall specifica, puoi utilizzare AllowSyscall(); per consentire più syscall contemporaneamente puoi utilizzare AllowSyscalls().
Finora la policy controlla solo l'identificatore syscall. Se devi rafforzare ulteriormente la policy e vuoi definire una policy in cui consenti solo una syscall con argomenti specifici, devi utilizzare AddPolicyOnSyscall() o AddPolicyOnSyscalls(). Queste funzioni non solo accettano l'ID syscall come argomento, ma anche un filtro seccomp-bpf non elaborato utilizzando le macro helper bpf del kernel Linux. Per ulteriori informazioni su BPF, consulta la documentazione
del
kernel. Se ti ritrovi a scrivere codice BPF ripetitivo che ritieni debba avere un wrapper di usabilità, non esitare a presentare una richiesta di funzionalità.
Oltre alle funzioni relative alle syscall, PolicyBuilder fornisce anche una serie di funzioni relative al file system, come AddFile() o AddDirectory(), per eseguire il bind-mount di un file/una directory nella sandbox. L'helper AddTmpfs() può essere utilizzato per aggiungere uno spazio di archiviazione file temporaneo all'interno della sandbox.
Una funzione particolarmente utile è AddLibrariesForBinary() che aggiunge le librerie e il linker richiesti da un file binario.
Purtroppo, l'individuazione delle syscall da consentire è ancora un po' di lavoro manuale. Crea una policy con le syscall di cui sai che il tuo file binario ha bisogno ed eseguila con un carico di lavoro comune. Se viene attivata una violazione, consenti la syscall e ripeti la procedura. Se riscontri una violazione che ritieni possa essere rischiosa da consentire e il programma gestisce gli errori in modo normale, puoi provare a far sì che restituisca un errore con BlockSyscallWithErrno().
L'esempio seguente crea sandbox2::PolicyBuilder direttamente per dimostrare la limitazione degli argomenti write e mprotect e il blocco di openat (che DefaultPolicyBuilder() altrimenti consentirebbe):
#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
La policy della sandbox impedisce a Sandboxee di chiamare syscall specifiche e quindi riduce la superficie di attacco. Tuttavia, un utente malintenzionato potrebbe comunque causare effetti indesiderati eseguendo un processo a tempo indeterminato o esaurendo la RAM e altre risorse.
Per contrastare questa minaccia, Sandboxee viene eseguito con limiti di esecuzione rigorosi per impostazione predefinita. Se questi limiti predefiniti causano problemi per l'esecuzione legittima del programma, puoi modificarli utilizzando la classe sandbox2::Limits chiamando limits() sull'oggetto executor.
Lo snippet di codice riportato di seguito mostra alcuni esempi di modifiche dei limiti. Tutte le opzioni disponibili sono documentate nel 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
tool.
4. Esegui la sandbox
Nelle sezioni precedenti hai preparato l'ambiente in sandbox, la policy, l'executor e Sandboxee. Il passaggio successivo consiste nel creare l'oggetto Sandbox2 ed eseguirlo.
Esegui in modo sincrono
La sandbox può essere eseguita in modo sincrono, bloccandosi fino a quando non viene restituito un risultato. Lo snippet di codice riportato di seguito mostra l'istanza dell'oggetto Sandbox2 e la sua esecuzione sincrona. Per un esempio più dettagliato, vedi
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();
Esegui in modo asincrono
Puoi anche eseguire la sandbox in modo asincrono, senza bloccare fino a quando non viene restituito un risultato. Questo è utile, ad esempio, quando comunichi con Sandboxee. Lo snippet di codice riportato di seguito mostra 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. Comunicazione con Sandboxee
Per impostazione predefinita, l'executor può comunicare con Sandboxee tramite i descrittori di file. Questo potrebbe essere tutto ciò di cui hai bisogno, ad esempio se vuoi solo condividere un file con Sandboxee o leggere l'output standard di Sandboxee.
Tuttavia, molto probabilmente avrai bisogno di una logica di comunicazione più complessa tra l'executor e Sandboxee. L'API comms (vedi il comms.h ) può essere utilizzata per inviare numeri interi, stringhe, buffer di byte, protobuf o descrittori di file.
Condivisione dei descrittori di file
Utilizzando l'API di comunicazione tra processi (vedi
ipc.h),
puoi utilizzare MapFd() o ReceiveFd():
Utilizza
MapFd()per mappare i descrittori del file dall'executor a Sandboxee. Può essere utilizzato per condividere un file aperto dall'executor per l'utilizzo in Sandboxee. Un esempio di utilizzo è disponibile in static.// The executor opened /proc/version and passes it to the sandboxee as stdin executor->ipc()->MapFd(proc_version_fd, STDIN_FILENO);Utilizza
ReceiveFd()per creare un endpoint socketpair. Può essere utilizzato per leggere l'output standard o gli errori standard di Sandboxee. Un esempio di utilizzo è disponibile nello strumento.// The executor receives a file descriptor of the sandboxee stdout int recv_fd1 = executor->ipc())->ReceiveFd(STDOUT_FILENO);
Utilizzo dell'API comms
Sandbox2 fornisce una comoda comms API. È un modo semplice e facile per condividere numeri interi, stringhe o buffer di byte tra l'executor e Sandboxee. Di seguito sono riportati alcuni snippet di codice che puoi trovare nell'esempio crc4.
Per iniziare a utilizzare l'API comms, devi prima ottenere 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 funzioni della famiglia Send*. Puoi trovare un esempio di utilizzo dell'API comms
nell'esempio crc4. Lo snippet di codice riportato di seguito mostra un estratto di questo esempio. L'executor 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 Recv*. Lo snippet di codice
riportato di seguito è un estratto dell'
crc4. L'executor riceve il checksum in un numero intero senza segno a 32 bit: uint32_t crc4;
if (!(comms->RecvUint32(&crc4))) {
/* handle error */
}
Condivisione dei dati con i buffer
Un'altra funzionalità di condivisione dei dati è l'utilizzo dell'API buffer per condividere grandi quantità di dati ed evitare copie costose che vengono inviate avanti e indietro tra l'executor e Sandboxee.
L'executor crea un buffer, in base alle dimensioni e ai dati da passare o direttamente da un descrittore del file, e lo passa a Sandboxee utilizzando comms->SendFD() nell'executor e comms->RecvFD() in Sandboxee.
Nello snippet di codice riportato di seguito, puoi vedere il lato dell'executor. La sandbox viene eseguita in modo asincrono e condivide i dati tramite un buffer con 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());
Anche sul lato di Sandboxee devi creare un oggetto buffer e leggere i dati dal descrittore del file inviato dall'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. Uscita dalla sandbox
A seconda di come esegui la sandbox (vedi questo passaggio), devi modificare il modo in cui la termini e, di conseguenza, anche Sandboxee.
Uscita da una sandbox in esecuzione sincrona
Se la sandbox è stata eseguita in modo sincrono, Run restituirà un valore solo al termine di Sandboxee. Pertanto, non è necessario alcun passaggio aggiuntivo per la terminazione. Lo snippet di codice riportato di seguito mostra questo scenario:
Sandbox2::Result result = s2.Run();
LOG(INFO) << "Final execution status: " << result.ToString();
Uscita da una sandbox in esecuzione asincrona
Se la sandbox è stata eseguita in modo asincrono, sono disponibili due opzioni per la terminazione. Innanzitutto, puoi semplicemente attendere il completamento di Sandboxee e ricevere lo stato di esecuzione finale:
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
In alternativa, puoi terminare Sandboxee in qualsiasi momento, ma ti consigliamo comunque di chiamare AwaitResult() perché Sandboxee potrebbe terminare per un altro motivo nel frattempo:
s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
7. Test
Come qualsiasi altro codice, l'implementazione della sandbox deve avere dei test. I test della sandbox non hanno lo scopo di verificare la correttezza del programma, ma di controllare se il programma in sandbox può essere eseguito senza problemi come violazioni della sandbox. In questo modo, la policy della sandbox è corretta.
Un programma in sandbox viene testato nello stesso modo in cui lo eseguiresti in produzione, con gli argomenti e i file di input che elaborerebbe normalmente.
Questi test possono essere semplici come un test della shell o test C++ che utilizzano sottoprocessi. Dai un'occhiata a gli esempi per trarre ispirazione.
Conclusione
Grazie per aver letto fin qui. Ci auguriamo che la nostra guida ti sia piaciuta e che ora tu possa creare le tue sandbox per proteggere i tuoi utenti.
La creazione di sandbox e policy è un'attività difficile e soggetta a errori sottili. Per non correre rischi, ti consigliamo di far esaminare la policy e il codice da un esperto di sicurezza.