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 le informazioni riportate qui come guida, insieme agli esempi e alla documentazione relativa al codice nei file di intestazione.
1. Scegli un metodo di esecuzione della sandbox
Il sandboxing inizia con un esecutore (vedi Sandbox Executor), responsabile del comando di Sandboxee. Il file dell'intestazione executor.h contiene l'API necessaria a questo scopo. L'API è molto flessibile e ti permette di scegliere quella più adatta al tuo caso d'uso. Le seguenti sezioni descrivono i tre diversi metodi tra cui scegliere.
Metodo 1: autonomo: esegui un programma binario con sandbox già abilitata
Questo è il modo più semplice per utilizzare il sandbox ed è il metodo consigliato quando vuoi sandbox un intero programma binario per il quale non hai codice sorgente. È anche il modo più sicuro per utilizzare il sandbox, in quanto non esiste un'inizializzazione senza sandbox che possa 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 syscall execve. 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: server sandbox Sandbox2: comunica all'esecutore quando deve essere limitato il sandbox
Questo metodo offre la flessibilità di disattivare la limitazione tramite sandbox durante l'inizializzazione e poi di scegliere quando attivare la limitazione tramite sandbox chiamando ::sandbox2::Client::SandboxMeHere()
. Devi avere la possibilità di definire il codice quando vuoi iniziare la limitazione tramite sandbox e devi inserire un solo thread (scopri perché nelle Domande frequenti).
Nel seguente snippet di codice, utilizziamo lo stesso codice descritto sopra nel Metodo 1. Tuttavia, per consentire l'esecuzione del programma in modo non limitato 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 ha una sandbox disabilitata fino a quando non riceve la notifica da Sandboxee, dobbiamo creare un'istanza ::sandbox2::Client
, configurare la comunicazione tra l'esecutore e Sandboxee, quindi notificare all'esecutore che la nostra inizializzazione è terminata e vogliamo iniziare subito a 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 è crc4, dove crc4bin.cc
è il sandbox e invia una notifica all'esecutore (crc4sandbox.cc
) quando deve entrare nella 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 la limitazione tramite sandbox e, in un momento specifico del ciclo di vita del programma binario, renderlo disponibile per l'esecutore.
L'esecutore invierà una richiesta a 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());
Tieni presente che questa modalità è piuttosto complicata e applicabile solo in alcuni casi specifici, ad esempio in caso di requisiti di memoria limitati. Beneficerai della mucca, ma hai lo svantaggio che non esiste una vera ASLR. Un altro esempio di utilizzo tipico potrebbe verificarsi quando Sandboxee ha un'inizializzazione lunga e ad alta intensità di CPU che può essere eseguita prima che i dati non attendibili vengano elaborati.
Per un esempio di questo metodo esecutore, vedi custom_fork.
2. Crea un criterio sandbox
Dopo aver trovato un esecutore, probabilmente vorrai definire un criterio sandbox per la sandbox. In caso contrario, la sandbox non è protetta dai Criteri predefiniti di Syscall.
Con il criterio della sandbox, l'obiettivo è quello di limitare le chiamate di sistema e gli argomenti che la sandbox può fare, nonché i file a cui può accedere. Dovrai avere una comprensione dettagliata delle chiamate di sistema richieste dal codice che prevedi di 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 di convenienza e supporto che consentono molte operazioni comuni. Il seguente elenco è solo un piccolo estratto delle funzioni disponibili:
- Autorizza qualsiasi syscall all'avvio del processo:
AllowStaticStartup();
AllowDynamicStartup();
- Aggiungi a lista consentita tutte le chiamate di sistema aperte/read/write*:
AllowOpen();
AllowRead();
AllowWrite();
- Aggiungi a lista consentita eventuali chiamate di sistema relative a uscite, accessi e stati:
AllowExit();
AllowStat();
AllowAccess();
- Aggiungi a lista consentita eventuali chiamate di sistema relative a sonno/tempo:
AllowTime();
AllowSleep();
Queste funzioni di convenienza autorizzano qualsiasi syscall pertinente. Questo ha il vantaggio di poter utilizzare lo stesso criterio su architetture diverse in cui non sono disponibili determinati syscall (ad esempio, ARM64 non ha syscall aperto), ma con il minor rischio per la sicurezza che potrebbe essere necessario abilitare più sycalls. Ad esempio, AllowOpen() consente al Sandboxee di chiamare qualsiasi syscall aperto correlato. Se vuoi inserire un solo sistema syscall nella lista consentita, puoi utilizzare AllowSyscall();
per consentire più chiamate di sistema contemporaneamente tramite AllowSyscalls()
.
Finora il criterio ha controllato solo l'identificatore syscall. Se hai la necessità di rafforzare ulteriormente il criterio e vuoi definire un criterio in cui consenti solo una chiamata di sistema con argomenti specifici, devi utilizzare AddPolicyOnSyscall()
o AddPolicyOnSyscalls()
. Queste funzioni non solo prendono l'ID syscall come argomento, ma anche un filtro seccomp-bpf non elaborato che usa le macro helper bpf del kernel Linux. Per ulteriori informazioni sul BPF, consulta la documentazione del kernel. Se ti ritrovi a scrivere codice BPF ripetitivo e ritieni che debba avere un wrapping di usabilità, non esitare a presentare una richiesta di funzionalità.
Oltre alle funzioni relative a syscall, PolicyBuilder fornisce anche una serie di funzioni relative al file system, come AddFile()
o AddDirectory()
, per associare un file/directory alla sandbox. L'helper AddTmpfs()
può essere utilizzato per aggiungere un archivio temporaneo di file all'interno della sandbox.
Una funzione particolarmente utile è AddLibrariesForBinary()
, che aggiunge le librerie e il linker richiesti da un programma binario.
Trovare l'aiuto di syscall per la lista consentita è ancora un po' impegnativo manuale. Crea un criterio con i syscall che conosci per le tue esigenze binarie ed eseguilo con un carico di lavoro comune. Se viene attivata una violazione, autorizza la chiamata di sistema e ripeti la procedura. Se ti imbatti in una violazione che ritieni possa essere rischiosa inserire nella lista consentita e il programma gestisce gli errori in modo controllato, puoi provare a restituirne l'errore 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 della sandbox impedisce alla sandbox di chiamare chiamate syscall specifiche e riduce così la superficie di attacco. Tuttavia, un utente malintenzionato potrebbe comunque essere in grado di causare effetti indesiderati eseguendo un processo a tempo indeterminato o consumando RAM e altre risorse.
Per affrontare questa minaccia, Sandboxee è sottoposto a limiti di esecuzione stretti per impostazione predefinita. Se questi limiti predefiniti causano problemi per l'esecuzione legittima del tuo programma, puoi modificarli utilizzando la classe sandbox2::Limits
chiamando limits()
sull'oggetto executor.
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 con sandbox, i criteri, gli esecutori e i sandbox. Il passaggio successivo consiste nel creare l'oggetto Sandbox2
ed eseguirlo.
Esegui in modo sincrono
La sandbox può essere eseguita in modo sincrono, bloccando così tutto il risultato. Lo snippet di codice riportato di seguito illustra l'istanza dell'oggetto Sandbox2
e la sua esecuzione sincrona. Per un esempio più dettagliato, vedi statico.
#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 bloccarla finché non si verifica un risultato. È utile, ad esempio, quando comunichi con la sandbox. 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 Sandboxee tramite i descrittori dei file. Questo potrebbe essere tutto ciò che ti serve, ad esempio se vuoi semplicemente condividere un file con il sandbox o leggere l'output standard del sandbox.
Tuttavia, è più probabile che tu abbia bisogno di una logica di comunicazione più complessa tra l'esecutore e la sandbox. L'API comms (vedi il file di intestazione comms.h) può essere utilizzata per inviare numeri interi, stringhe, buffer di byte, protobuf o descrittori di file.
Condivisione di descrittori di file
Con l'API Inter-Process Communication (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 nella sandbox. 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);
Utilizza
ReceiveFd()
per creare un endpoint di socketpair Consente di leggere l'output standard o gli errori standard di Sandboxee. Un esempio di utilizzo è visibile 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 un'API di comunicazione pratica. Si tratta di un metodo semplice e semplice per condividere numeri interi, stringhe o buffer di byte tra l'esecutore e Sandbox. Di seguito sono riportati alcuni snippet di codice che puoi trovare nell'esempio di 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 al Sandboxee utilizzando una delle famiglie di funzioni Invia*. Puoi trovare un esempio di utilizzo dell'API comms in 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 dal sandbox, utilizza una delle funzioni di Recv*
. Lo snippet di codice riportato 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 copie costose scambiate tra l'esecutore e Sandboxee.
L'esecutore crea un buffer, per dimensioni e dati da trasmettere, oppure direttamente da un descrittore di file e lo passa al Sandboxee utilizzando comms->SendFD()
nell'esecutore e comms->RecvFD()
nel Sandbox.
Nello snippet di codice riportato di seguito, 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 la sandbox, inoltre, devi 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. Uscita dalla sandbox
A seconda di come esegui la sandbox (vedi questo passaggio), devi modificare il modo in cui la chiudi e quindi anche il sandbox.
Uscire da una sandbox in esecuzione sincrona
Se la sandbox è stata eseguita in modo sincrono, l'esecuzione verrà eseguita solo al termine della sandbox. Pertanto, non sono richiesti ulteriori passaggi per la chiusura. Lo snippet di codice riportato di seguito 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 l'interruzione. In primo luogo, puoi semplicemente attendere il completamento della sandbox e ricevere lo stato di esecuzione finale:
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
In alternativa, puoi terminare il Sandbox in qualsiasi momento, ma è comunque consigliabile chiamare AwaitResult()
, perché nel frattempo il comando Sandbox in questione potrebbe terminare a causa di un altro motivo:
s2.Kill();
sandbox2::Result result = s2.AwaitResult();
LOG(INFO) << "Final execution status: " << result.ToString();
7. Test
Come per qualsiasi altro codice, l'implementazione della sandbox deve includere test. I test sandbox non sono concepiti per verificare la correttezza del programma, ma per verificare se il programma sandbox può essere eseguito senza problemi quali violazioni della sandbox. Ciò garantisce anche che il criterio della 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.
Questi test possono essere semplici come un test di shell o test C++ utilizzando processi secondari. Dai un'occhiata agli esempi per trarre ispirazione.
Conclusione
Grazie per aver letto fino a questo momento. Ci auguriamo che la nostra guida ti sia piaciuta e che tu abbia ora la possibilità di creare le tue sandbox per mantenere i tuoi utenti al sicuro.
Creare sandbox e criteri è un'attività complessa e soggetta a sottili errori. Per stare al sicuro, ti consigliamo di far controllare il tuo codice e le tue norme da un esperto di sicurezza.