I driver scritti con il CLR, il CLR nel kernel: come aumentare la security

Prendo lo spunto da un commento che ho fatto a un post di Alessandro Perilli relativo alle vulnerabilità che potrebbero esistere nel kernel di Windows (descritte in questo whitepaper) per spiegare più in dettaglio come potrebbe migliorare la sicurezza del sistema operativo grazie alla scrittura di driver in codice managed.


Gli attuali sistemi operativi (come Windows e Linux) sfruttano in genere un’architettura dei microprocessori che consente privilegi di esecuzione differenti al codice macchina: i servizi del sistema operativo richiedono il massimo dei privilegi, mentre i normali processi applicativi (Word, Excel, ma anche Internet Explorer) utilizzano dei privilegi minori che consentono di proteggere i dati del sistema operativo e di isolare la memoria degli altri processi. In altre parole, il mio Word non può andare a “curiosare” nella memoria del mio programma di contabilità, indipendentemente dalla sua volontà. Una tecnica che viene usata per aggirare questo limite è quella di utilizzare privilegi di particolari utenti (come quelli di un amministratore) per richiedere (in modo apparentemente legittimo) al sistema operativo di effettuare operazioni altrimenti vietate (una situazione in cui ciò è legale è il debug di un altro processo in esecuzione, ma non è l’unica). Per questo motivo l’uso di utenti con privilegi minimi è preferibile all’uso continuo di utenti con diritti amministrativi, proprio per limitare la superficie di attacco.


Le macchine virtuali (come Java e .NET) introducono un concetto più evoluto di questa forma di protezione: non esistono solo delle modalità di esecuzione delle istruzioni che, in pratica, limitano l’accesso a determinate zone di memoria; viene definita, a livello di “macchina virtuale”, una sorta di policy che definisce quali funzioni possono essere richieste da un determinato codice, consentendo l’accesso non solo in base all’utente che sta eseguendo l’applicazione ma anche in base alla provenienza del codice. Quest’ultimo concetto è sviluppato molto bene in .NET con la Code Access Security: un programma può, per esempio, leggere la clipboard solo se l’autore, il produttore, il sito di provenienza del codice o addirittura il singolo eseguibile è stato autorizzato a farlo, indipendentemente dall’utente che esegue il programma (sia esso amministratore o guest). Si tratta di un grande passo in avanti, perché si possono definire dei perimetri di sicurezza molto precisi e personalizzati: è un po’ come prestare l’automobile a un amico e, invece di dargli le chiavi con cui può andare dappertutto, gli si fornisce una limitazione alle strade che potrà percorrere, alla velocità massima che potrà mantenere e alle radio che potrà ascoltare, pur avendo una chiave del mezzo che gli consentirebbe in teoria di avere libero arbitrio. [Notare che, a parte la limitazione delle radio selezionabili, per il resto ci sono già sistemi di antifurto satellitare che fanno esattamente quello che ho appena descritto]


Tutto questo è molto bello e in realtà funziona. Prova ne è il fatto che, fino a oggi, non sono ancora state scovate serie vulnerabilità nel Framework .NET o in programmi scritti in .NET. Il vero problema è che .NET vive in un ambiente (Windows) che è meno sicuro se si esegue del codice unmanaged, quindi è molto più facile attaccare il lato unmanaged di un sistema che non quello managed. Forse anche per questo fino a oggi abbiamo visto pochi tentativi di attacco (e nessuno con successo) attraverso codice 100% managed.


Proteggere il sistema operativo esterno a .NET è possibile e si può arrivare a eliminare Win32 mantenendo, per il livello applicativo, solo codice managed. Siamo già su questa strada, che sarà ancora molto molto lunga e richiederà ancora molti anni (almeno 10, credo). Si tratta però di una linea di tendenza già chiara nei suoi principi, ora diventa “solo” una questione implementativa e di distribuzione/diffusione. Se però andiamo alla parte più “interna” del sistema operativo, scopriamo che gran parte del codice che viene eseguito con il massimo dei privilegi è composto da driver per la gestione di periferiche e dispositivi vari. In Windows questo è necessario perché solo un driver può accedere direttamente all’hardware esterno, rispondere a degli interrupt del microprocessore e definire dei device di I/O logici o fisici.


Qui arriviamo a un punto debole storico di Windows. Un driver che fa un errore manda in crash l’intero sistema operativo. I driver non sono isolati tra loro come i processi “utente” cui siamo abituati, e come se non bastasse i driver hanno accesso all’intera memoria di sistema, senza alcune limitazione di sorta (un driver è più potente di un amministratore, insomma). Storicamente, dicevo, questo tallone d’achille di Windows è noto come blue screen (anche detto BSOD, Blue Screen Of Death). Molto è stato fatto da Microsoft per migliorare questo problema (causato il più delle volte da driver di terze parti) e oggi il BSOD è infinitamente meno frequente di quanto avveniva ai tempi di NT4. Poco o nulla, però, è stato fatto dal punto di vista della security. Come evidenziato dal whitepaper che ho già citato, una vulnerabilità di un driver potrebbe essere sfruttata da un attaccante per attacchi assolutamente devastanti rispetto alle risorse di una macchina: il codice dell’attaccante si viene a trovare in uno stato di “grazia” in cui ha accesso a tutto quello che c’è su un computer e, teoricamente, è il tipo di vulnerabilità peggiore che si possa immaginare (dico teoricamente perché sfruttare queste vulnerabilità richiede particolari acrobazie… ma il punto è che se una cosa è possibile, prima o poi si avvera).


Sono abbastanza certo che in Microsoft abbiano già pensato a questi scenari. Già a PDC 2003 avevo colto i segnali dei primi studi su come mettere il CLR nel kernel. All’epoca, devo dire la verità, mi ero concentrato più sul problema della stabilità del sistema operativo rispetto a dei driver difettosi piuttosto che sulle problematiche di sicurezza. Il ragionamento però è, per la sicurezza, ancora più importante. Con il CLR il codice è gestito e verificato prima della sua esecuzione: eliminando alla radice la possibilità di un buffer overrun, gran parte delle tecniche di attacco attuali vengono sterilizzate; con la Code Access Security, un driver per la scheda video si vedrebbe anche impossibilitato a scrivere su un socket TCP/IP o a installare un filtro per intercettare tutti gli eventi della tastiera (tutte cose che oggi un qualsiasi driver può fare senza che il sistema operativo possa opporsi). Il problema, ovviamente, è che tutto ha un prezzo e la difficoltà tecnica sta nel coniugare il costo di avere del codice managed con le esigenze prestazionali richieste a un driver, in particolare per quanto riguarda i tempi di latenza (ritardo nella risposta rispetto alle richieste in arrivo) più che per la semplice “forza bruta” di calcolo (anche se resta importante avere dei driver che non siano affamati di CPU).


Anche qui si tratta, in fin dei conti, di una questione implementativa: il fatto che sia teoricamente possibile creare un sistema operativo 100% managed si può dare per scontato. Come ci si possa arrivare oggi lo è un po’ meno, ci sono tante questioni sul tappeto e non è facile capire quale sarà la strategia vincente. Io ipotizzo che l’hardware potrà dare una mano: processori sempre più specializzati (si pensi ai processori delle schede video) toglieranno sempre più compiti attualmente svolti da driver del sistema operativo (pensate solo ai vari gestori/codec audio/video esistenti). Il pericolo è che poi il problema si sposti su questi ultimi (volete che una scheda video dell’ultima generazione non abbia bug più o meno nascosti?), ma essendo processori dedicati il loro “campo d’azione” sarebbe necessariamente limitato: difficile che la scheda video riesca a fare un bonifico via internet dopo aver intercettato le mie password, almeno nelle architetture hardware attuali.


Ovviamente un sistema operativo 100% managed non sarebbe la panacea di tutti i problemi. Se però riuscissimo ad alzare il livello minimo di protezione di un sistema già ci scosteremmo dall’attuale situazione per cui oggi chi naviga in Internet ha un PC accessibile quanto lo sarebbe il proprio appartamento con un segnalatore vicino al citofono che comunica ai passanti se c’è qualcuno in casa e se la porta è rimasta aperta (o è facilmente forzabile). Ecco, riuscire ad avere più o meno tutti una chiave sulla porta d’ingresso, un citofono che non dica se siamo in casa prima ancora di aver suonato e la possibilità di vedere chi bussa senza dover necessariamente aprire la porta… sarebbe già un primo traguardo; non saremo ancora “al sicuro” ma saremo meno “sprovveduti” di oggi.