PI Guide/ Imprigionare i processi di sistema in GNU/Linux

Caro processo, ti comporti male? E io ti metto in prigione! Ecco come migliorare la sicurezza di un processo in esecuzione confinandolo in un ambiente isolato e creato ad-hoc

L'uso abituale del PC vede l'avvio di programmi (applicazioni) che per svolgere adeguatamente il loro compito necessitano di accedere a un certo numero e tipo di risorse: CPU, RAM, periferiche di I/O e file system. Nella nostra accezione ipotizziamo che un programma sia l'equivalente di un processo. Ora, in relazione ai permessi, GNU/Linux separa i vari processi nello spazio di indirizzamento in memoria: ad esempio, sarà capitato a tutti di ricevere un errore di SegFault (Segmentation Fault) lanciando un programma da shell oppure vedere improvvisamente il programma chiudersi. In questi casi il processo va a "toccare" zone di memoria che non gli competono e il kernel ne sopprime subito l'attività, uccidendolo. La separazione delle proprietà di un processo riguarda non solo la memoria, ma i descrittori dei file, le variabili d'ambiente, la priorità di esecuzione e non ultimo l'identità dell'utente che lancia il programma.

Programma, processo e thread
Prima di procedere è bene fare chiarezza sulle reali differenze fra programma, processo e thread.
Un programma è un file eseguibile contenente un insieme di istruzioni atte a realizzare un lavoro specifico. definito come entità passiva poiché sono righe di codice non residenti nella memoria primaria del computer (RAM) ma sulla memoria secondaria (disco).
Il processo è un'istanza in esecuzione del suddetto programma e soggetto a un algoritmo di schedulazione. noto anche come entità attiva poiché risiede nella memoria primaria. Ogni processo ha un proprio identificatore (PID ossia Process ID) e la system call fork() permette di creare nuovi processi a partire da quello padre: dopo una chiamata a fork() sono due i processi in esecuzione: padre e figlio. Il comando ps ax visualizza i processi in esecuzione sulla macchina.
Infine i thread, la più piccola unità eseguibile di un processo e che ne condivide alcune risorse. Ne discerne che un processo può essere caratterizzato da svariati thread. Per questo motivo un thread è noto anche come processo leggero.

Il concetto dei contenitori
Abbiamo già avuto modo di affrontare il discorso contenitori parlando di Systemd, ma per alcuni potrebbe essere interessante approfondirne la conoscenza facendo un piccolo passo indietro.
In GNU/Linux, e ancor prima nei sistemi Unix, il concetto software dei contenitori nasce dall'idea di poter confinare l'esecuzione di un programma all'interno di una copia virtuale (parziale o totale) del sistema operativo, al fine di limitarne la pericolosità al solo ambiente nel quale è in esecuzione. Ciò permette di limitare le risorse solo a quelle strettamente necessarie, nonché di limitare (o annullare) i danni sul sistema che ospita il contenitore. Ma come crearsi un ambiente del genere? Il modo più semplice, risalente alla fine degli anni '70 quando venne iniziato lo sviluppo del software, è l'utilizzo di un ambiente "chrootato" (chrooted environment) noto anche con il nome di "prigione" o "gabbia" (chroot jail).
La figura seguente illustra un tipico accesso alle risorse: la separazione non è totale per i processi in GNU/Linux e diventa un gradino più alto per ambienti chroot. All'estremo abbiamo le macchine virtuali che vanno a simulare anche un ambiente hardware sottostante.

Risorse: dalla separazione parziale (in alto) a quella totale (in basso)

In merito a questa figura si rammenta che la tabella dei processi in GNU/Linux, come in qualsiasi altro sistema operativo, è una struttura dati presente nella RAM. Mantiene le informazioni sui processi utilizzati dal sistema operativo come il PID, il proprietario, la priorità, le variabili d'ambiente di ogni processo, il processo padre e puntatori al codice eseguibile del processo. Sebbene la soluzione di virtualizzazione risulti essere, per certi versi, l'ideale in termini di sicurezza, non lo è in termini di risorse consumate, a differenza di un ambiente chroot che al più consuma spazio su hard disk però non offre la maggiore sicurezza di una virtualizzazione. Spetta all'amministratore di sistema decidere se una data funzione, test su un programma, debug o altro, sia il caso di eseguirla su una macchina virtuale oppure se l'esecuzione in un ambiente opportunamente e adeguatamente isolato possa essere più che sufficiente.

Cos'è chroot?
Chroot è la contrazione di CHange ROOT. Se in un terminale provassimo a impartire il comando man chroot ci verranno prospettate due scelte, una porta al comando chroot e l'altra alla funzione omonima - chiamata di sistema - chroot(). Sul comando non c'è molto da dire se non che si tratti di un usuale comando come "find" o "grep" caratterizzato dalla sua sintassi nella forma chroot NuovaRoot Comando Argomento. L'operazione di cambio della directory è affidata alla chiamata della funzione chroot() la quale riceve come argomento il nuovo percorso che verrà utilizzato come nuova radice del file system, ovvero /. Solo un processo privilegiato può chiamare la funzione chroot() attraverso l'omonimo comando. La chiamata di sistema chroot() è spesso fraintesa e alcune volte sopravvalutata. Non commettiamo questo errore! Vi sono utilizzi associati alla sicurezza, ma solo quando questa viene intesa come uno strumento per isolare processi e test vari.

Costruiamo la prigione
Premessa: tutti i comandi che seguono verranno impartiti con le credenziali dell'amministratore. La procedura necessita almeno di una buona infarinatura dell'ambiente GNU/Linux (librerie, eseguibili e file di configurazione) che daremo per scontata anche se non mancheremo di riportare quanti più riferimenti possibili. I percorsi riportati nel seguito fanno riferimento ad una OpenSUSE 13.2 a 64 bit. Iniziamo a creare la directory per l'ambiente chroot, che chiameremo "gabbia", ma qualsiasi altro nome va bene, evitando di utilizzare (primo suggerimento) una partizione di sistema: ad esempio, possiamo provare su un'altra partizione o su altro disco. Creiamo la cartella che ospiterà il nuovo ambiente con mkdir /mnt/Dati/gabbia. Poiché questa cartella ancora non è stata "convertita" in un ambiente chroot, entrando in essa (cd /mnt/Dati/gabbia/) avremo a disposizione gli usuali comandi: ad esempio, ls -l (man ls) ci restituirà totale 0 (cartella vuota). Se ora provassimo a impartire, secondo la sintassi riportata nel precedente paragrafo, il comando chroot /mnt/Dati/gabbia, dopo aver premuto Invio riceveremo l'errore chroot: failed to run command '/bin/bash': No such file or directory. Il comando chroot, invocato senza alcun parametro, esegue subito una shell. Ma la shell Bash è presente nel sistema (la stiamo utilizzando!), allora cos'è accaduto? Poiché il comando chroot ha determinato la chiamata di sistema omonima (man 2 chroot) allora il percorso /mnt/Dati/gabbia diventa la nuova radice la quale, essendo completamente priva di file, non può lanciare qualcosa che non esiste. In essa non è possibile impartire alcun comando, tant'è che se provassimo con:

chroot /mnt/Dati/gabbia /bin/ls

riceveremo il medesimo errore. Allora un ambiente "isolato", prima di poterlo utilizzare, occorre costruirlo a dovere.

Partiamo dal comando ls il quale, nelle distribuzioni in uso, è presente (comando which ls) in /usr/bin/ così come in /bin. Ricordiamo, infatti, che i comandi essenziali del sistema (usati da utenti, amministratori e non, e tra questi rientra ls) devono essere disponibili anche quando non ci sono altri file system montati oltre la radice, ad esempio all'avvio o quando si è nella modalità single user mode: per questo motivo la directory /bin non può stare su un file system diverso da quello della radice. Come procedere per implementarlo? Due scelte: compilarlo da sorgenti (il comando è presente nel pacchetto coreutils) disabilitando l'uso di librerie dinamiche oppure "prelevarlo" direttamente dal sistema (distribuzione) in uso. Seguiremo questa seconda strada, andando così oltre una usuale compilazione.

Il primo eseguibile
Apriamo un terminale e spostiamoci nella futura "prigione" (cd /mnt/Dati/gabbia), quindi creiamo la directory bin nel nuovo ambiente (mkdir bin). Si faccia attenzione al corretto uso degli slash: poiché siamo nella directory gabbia allora creiamo la directory con bin e non /bin! A questo punto copiamo l'eseguibile ls del sistema operativo nel percorso appena creato. Siamo sempre nella cartella gabbia quindi il comando sarà cp /bin/ls bin. Ora lanciamo di nuovo il comando chroot /mnt/Dati/gabbia /bin/ls: premendo Invio riceveremo il medesimo errore! Cosa e/o dove stiamo sbagliando? La pressoché totalità dei binari degli odierni sistemi operativi viene compilata facendo uso delle librerie condivise riconoscibili per il suffisso .so ("shared object"). Il comando ls per funzionare a quale librerie fa rifermento? Per saperlo usiamo il comando ldd /bin/ls.


Librerie condivise necessarie al comando ls

Creiamo le directory necessarie con i comandi mkdir lib64 e mkdir -p usr/lib64 e copiamo in esse le librerie:

cp /lib64/{libselinux.so.1,libcap.so.2,...} lib64/

dove al posto dei punti aggiungeremo le rimanenti librerie per lo stesso percorso. Analogamente per l'unica libreria in usr/lib64 con cp /usr/lib64/libpcre.so.1 usr/lib64/. Provando nuovamente il comando otterremo un (parziale) corretto funzionamento (notare il punto nel comando chroot ad indicare la directory corrente, poiché siamo nella cartella gabbia):

chroot. /bin/ls -l
drwxr-xr-x 2 0 0 4096 Jun 30 15:56 bin
drwxr-xr-x 2 0 0 4096 Jun 30 15:59 lib64
drwxr-xr-x 3 0 0 4096 Jun 30 15:57 usr

Parziale perché non vengono elencati parametri come proprietario e gruppo e l'orario locale è sbagliato: il motivo è che non vi è alcun file di configurazione a cui può fare riferimento ls. Creiamo la cartella etc (mkdir etc) quindi estraiamo la password di root presente nel file /etc/passwd del sistema importandola nel nuovo ambiente con grep root /etc/passwd > etc/passwd e stesso comando per il file group. Copiamo i file cp /etc/{localtime,nsswitch.conf} etc e infine due librerie cp /lib64/{libnss_compat.so.2, libnsl.so.1) lib64/, entrambe fanno parte del pacchetto vitale glibc. A questo punto impartendo il comando chroot /mnt/Dati/gabbia /bin/ls potremo vedere il corretto funzionamento per ls, ma solo per ls! Qualunque altro comando e/o programma si voglia implementare necessiterà della stessa procedura.

Delucidazioni su file e librerie
Prima di tutto osserviamo come non sia stata importata la libreria linux-vdso.so.1. Acronimo di Virtual Dynamic Shared Object, trattasi di una libreria condivisa che il kernel mappa automaticamente nello spazio degli indirizzi di tutte le applicazioni user-space: nel nuovo ambiente la shell così come il comando ls rientra tra queste.
Il file localtime riporta il timezone in uso, ovvero una copia del file che per la nostra Nazione è /usr/share/zoneinfo/Europe/Rome.
Un po' meno intuitivo è il file nsswitch.conf. Il problema di ottenere delle corrispondenze tra un identificativo numerico e un valore simbolico, come avviene per il nome utente e gli associati ID (UID e GID), si presenta anche con la corrispondenza dei nomi associati alla macchina e gli indirizzi di rete. L'introduzione di queste associazioni lasciava scoperto il problema di come indicare alle varie funzioni dove prendere le informazioni. Venne così adottato il Name Service Switch, introdotto per la prima volta nel sistema Solaris e in seguito ripreso e incorporato dalle librerie GNU. Ogni servizio è implementato da una libreria condivisa di nome libnss_SERVIZIO.so.x (man nsswitch.conf).

Ora proviamo con la shell
Volendo implementare nel nuovo ambiente anche la shell dovremo seguire la medesima dinamica. Prima copiamo l'eseguibile dalla distribuzione in uso cp /bin/bash bin per poi passare a verificare le librerie utilizzate ldd /bin/bash. Non meravigliamoci se alcune sono le medesime del comando ls, non si chiamerebbero librerie condivise altrimenti. Copiamo le librerie mancanti nel nuovo ambiente utilizzando la stessa procedura con il comando cp. Al termine con chroot. /bin/bash ne verifichiamo il funzionamento: dovremmo vedere un nuovo prompt, qualcosa del tipo bash-4.2# a indicare che siamo nella Bash del nuovo ambiente. Questa situazione ci permette di utilizzare i comandi shell, quelli built-in come pwd o cd (sezione Comandi incorporati della shell nel manuale online man 1 bash) e l'unico comando esterno ls. Ora, pwd è un comando interno della shell e indica l'acronimo di Print Work Directory ovvero visualizza il percorso completo della directory di lavoro corrente (per approfondimenti: man pwd nel sistema e non nel nuovo ambiente poiché in esso non c'è alcun manuale installato): impartito nella shell Bash installata nel nuovo ambiente osserviamo come restituisca l'output / ovvero la shell vede il percorso /mnt/Dati/gabbia come se si trattasse della radice del file system.


Siamo nella gabbia! Il comando built-in exit della shell ci farà uscire

Ciò che abbiamo creato è visibile anche nella figura seguente, dalla quale si evince come sia possibile arrivare a lanciare anche multiple distribuzioni senza fare uso alcuno della virtualizzazione.


La nuova situazione creata con la procedura riportata

Considerazioni e conclusioni
Il nostro scopo consisteva nel mostrare la creazione di un ambiente che oscurasse il file system del sistema operativo dalle azioni di un determinato processo il quale, anche se compromesso, entro certi limiti da un pirata, l'attaccante non possa andare oltre la nuova root, confinando così le azioni. Da questa pagina Web possiamo scaricare il file Applicazione.pdf che mostra tutta la procedura da seguire per poter "imprigionare" una tipica applicazione. Ma può il processo "scappare" dalla prigione? Certo che si! Se il processo "cede" sotto gli attacchi di un cracker e quest'ultimo riesce ad acquisire i diritti del super user a quel punto non esisterà alcuna differenza tra un ambiente chroot e un ambiente libero. Ecco perché nell'utilizzare ambienti chroot vanno in genere seguite un minimo di regole:

- Inserire nell'ambiente chroot solo ed esclusivamente lo stretto necessario. Ad esempio, se il comando ls così come la shell non ci occorrono, allora è inutile implementarli poiché si andrebbe a fornire qualche chance in più a un attaccante per trovare punti deboli per elevare i diritti a quelli di amministratore;

- Fare attenzione alla cooperazione tra due processi, ad esempio uno in esecuzione dentro la prigione e l'altro in esecuzione fuori di essa, perché in questi casi anche una accorta gabbia non potrà fornire il massimo della protezione;

- Evitare l'uso delle librerie condivise: ciò è possibile previa ricompilazione dei binari che si devono inserire nella prigione in maniera statica, ovvero disabilitando le librerie condivise: verrà creato un eseguibile che occupa un po' più di spazio sull'hard disk;

- Limitare al massimo l'utilizzo dei permessi di amministratore. Altrimenti detto: evitare che i demoni nell'ambiente chroot girino con i permessi di root e se sono strettamente necessari in una prima fase, dopo aver terminato il lavoro ritornino ai privilegi degli utenti "normali";

- Configurare al meglio alcune capability come CAP_SYS_ADMIN e CAP_MKNOD (man 7 capabilities) poiché permettono di creare dei device file. In questi casi un attaccante attraverso l'uso di mknod(man 1 mknod) può creare un file di accesso "simil /dev/kmem" manipolando direttamente la memoria del kernel per cercare di raggirare le limitazioni dell'ambiente chroot.
Notizie collegate