Le nuove librerie "safe CRT" di Visual C++ 2005

Da Raffaele Rialdi, Microsoft MVP – Visual Developer – Security

Per decenni alcuni monumenti del software hanno resistito all'avvento di nuove tecnologie, di nuovi sistemi operativi e delle architetture hardware. Nel software una longevità che superi i 10 anni è già un bel traguardo ma le "C Standard Libraries" o "C Runtime Libraries" (abbreviate con l'acronimo CRT) erano intoccate nella sostanza fin dalla creazione del linguaggio C negli anni '70. La diffusione di internet ha travolto anche questi monumenti ed è stato così necessario ripensare a librerie come le CRT; si, perché la sicurezza si fa a partire dal progetto e non è un optional che si può aggiungere in un secondo tempo. Già nella prima edizione del Kernighan-Ritchie "Il linguaggio C" facevano la loro comparsa funzioni come strcpy e strcat che servono rispettivamente a copiare e a concatenare una stringa. Cos'è andato storto con queste funzioni?

In questa pagina

La validazione dell'input La validazione dell'input
Una piccola parentesi Unicode Una piccola parentesi Unicode
Buffer Overrun Buffer Overrun
Le vecchie CRT Le vecchie CRT
Perché migrare il codice alle nuove CRT Perché migrare il codice alle nuove CRT
Le nuove CRT Le nuove CRT
Minimizzare le modifiche ai sorgenti Minimizzare le modifiche ai sorgenti
Altre novità Altre novità
Conclusioni Conclusioni

La validazione dell'input

Cos'è cambiato dagli anni 70 ad oggi in un PC che renda necessario modificare la funzione, per dirne una, strcpy? Hardware? Sistema operativo? Certamente nessuno di questi. È cambiata radicalmente l'origine dell'input dell'utente.

Anche se l'applicativo gira in monoutenza su un singolo PC, non è certamente folle pensare che la stringa immessa dall'utente provenga da un copia/incolla di una pagina web o da un file scaricato dal browser.

La validazione dell'input non è quindi materia dei solo web-developer ed è bene ricordare che la maggior parte di attacchi oggi non avviene da internet ma dalla intranet.

Le nuove CRT non possono certamente validare la qualità dell'input dell'utente (url non canonici, stringhe contenti script, etc.) visto che la loro idoneità dipende dal contesto in cui vengono utilizzate. Il controllo di cui le nuove CRT si fanno carico è relativo alla dimensione del buffer evitando così problemi noti come lo sfruttamento dei Buffer Overrun.

 

Una piccola parentesi Unicode

Da molti anni tutte le funzioni Ansi hanno il loro equivalente Unicode. La strcpy è Ansi e la sua corrispettiva Unicode è wcscpy. A meno di non sviluppare esclusivamente per piattaforme native Ansi come Windows 98, è assolutamente conveniente usare solo funzioni Unicode che sono native in tutti i Windows della famiglia NT (2000, XP, etc.).

I vantaggi nello sviluppare in solo Unicode sono due. Evitare l'uso della macro TCHAR e relative funzioni che possono rendere inutilmente più complessi i listati; le applicazioni Unicode sono più performanti di quelle Ansi in tutti i Windows della famiglia NT e superiore.

Il "Microsoft Layer for Unicode on Windows 98" (scaricabile dal sito Microsoft) permette agli applicativi Unicode di girare senza ricompilazioni anche per queste piattaforme.

Questo è il motivo per cui nel seguito di questo articolo userò solo la versione Unicode delle CRT che manipolano stringhe. I concetti esposti però valgono anche per le analoghe funzioni Ansi.

 

Buffer Overrun

Il Buffer Overrun è una tecnica di hacking molto diffusa per eseguire codice su un PC remoto. In sostanza l'applicazione vulnerabile riceve dei dati che sono di dimensione maggiore rispetto allo spazio allocato per riceverli; i dati inviati in certe condizioni vengono interpretati come codice che l'applicativo esegue come fossero istruzioni dell'applicativo stesso.

Alla radice del Buffer Overrun c'è il mancato controllo della dimensione dei dati. In passato le funzioni CRT non eseguivano questo controllo al fine di ottenere le massime performance. Oggi invece nella maggior parte dei casi questo tempo è diventato trascurabile mentre le possibilità che ad una applicazione si presentino dati di dimensioni errate, è notevolmente cresciuta.

Uno degli strumenti degno di nota che il compilatore Visual C++ mette a disposizione per combattere questo problema è l'opzione /GS che inserisce dei security cookie nello stack prima dell'l'indirizzo di ritorno. Se il cookie viene sovrascritto, l'esecuzione verrà terminata prematuramente evitando al probabile hacker di deviare l'esecuzione del codice.

 

Le vecchie CRT

Per combattere efficacemente il Buffer Overrun a livello applicativo è necessario conoscere la dimensione del buffer in modo da validarne i confini ed evitare che sia possibile scrivere oltre alla sua fine. Per raggiungere lo scopo è stato perció necessario cambiare alcune funzioni prototipo che vedremo tra breve.

Naturalmente le vecchie funzioni prototipo esistono sempre e ricompilando con la versione 2005 del compilatore un vecchio listato che usa funzioni delle CRT considerate potenzialmente pericolose, vedremo il warning C4996 che ci avvisa che la funzione utilizzata è stata dichiarata deprecated ossia deprecata.

Le vecchie librerie sono quindi sempre utilizzabili e non necessariamente il sorgente deve essere migrato in modo da utilizzare la nuova versione. La scelta è tutta nostra e dipende ovviamente dalle certezze che abbiamo sull'input che va in pasto alle funzioni.

Se desideriamo eliminare il warning dalla compilazione abbiamo due strade. Il primo è il sistema canonico per i warning e cioè quello di usare la dichiarazione pragma.

#pragma warning(disable : 4996)

Oppure, volendo disabilitare il warning solo per una piccola porzione di codice:

#pragma warning(push)
#pragma warning(disable : 4996)
// codice ...
#pragma warning(pop)

La seconda possibilità è quella definire _CRT_SECURE_NO_DEPRECATE con #define oppure eseguire la definizione nelle opzioni di progetto di Visual Studio o ancora definendo detto simbolo nella command line tramite /D.

#define _CRT_SECURE_NO_DEPRECATE

Attenzione ad usare la normativa #define sempre prima del primo #include che definisce le funzioni soggette alla definizione. Per semplicità si possono aggiungere le definizioni all'inizio del file stdafx.h che è il primo ad essere processato. Può anche essere necessario il rebuild dell'intero progetto.

Analogamente alle CRT esistono anche _AFX_SECURE_NO_DEPRECATE e _ATL_SECURE_NO_DEPRECATE che servono rispettivamente a disabilitare i warning per le librerie MFC ed ATL.

Certamente questa non è la strada preferibile perché così facendo si rinuncia al grosso lavoro di revisione delle CRT fatto dal team di Visual C++.

 

Perché migrare il codice alle nuove CRT

Prima di analizzare le diverse possibilità di migrazione del codice, è anche opportuno cogliere quali siano i vantaggi di cui si beneficia.

Ovviamente la prima motivazione è quella di non essere più vulnerabili (per quello che concerne le CRT) ai Buffer Overrun.

Un'altra motivazione, meno importante in senso assoluto, ma più appetibile dai programmatori riguarda il testing del codice. Mi spiego: il sogno di ogni programmatore dovrebbe essere quello di avere più errori di compilazione possibile diminuendo così gli errori di runtime. Naturalmente questo non è sempre un traguardo raggiungibile, ma in second'ordine possiamo almeno sperare di avere quanti più errori nella compilazione debug invece di quella release, che è quella destinata naturalmente al cliente.

Le nuove CRT riempiono sempre il buffer per tutta la loro capacità con un pattern (il default è 0xFD). Prendiamo in esempio il seguente codice:

void TestRTCs() {
   wchar_t Small[10];
   wcscpy_s(Small, 11, L"Ciao");
}

Nella sola versione debug, nonostante la stringa "Ciao" sia inferiore alla capacità del buffer, il buffer Small verrà riempito per gli 11 caratteri (due byte ciascuno essendo wchar_t) dichiarati con il pattern 0xFD. Grazie allo switch /RTCs del compilatore (attivo di default nella versione debug) all'uscita della funzione riceveremo questo errore di run time:

Run-Time Check Failure #2 - Stack around the variable 'Small' was corrupted.

Non potevamo augurarci nulla di meglio che questo splendido errore. L'opzione /RTCs ha riservato una DWORD prima ed una dopo ciascun buffer dichiarato nello stack ed inizializzato i suoi valori a 0xCCCCCCCC. La funzione wcscpy_s, dopo aver copiato la stringa "Ciao" ha sovrascritto i restanti byte con il pattern di default 0xFD. Al termine della funzione TestRTCs, sempre grazie a /RTCs, viene invocata la funzione _RTC_CheckStackVars che si accorge che la DWORD che segue il buffer Small è stata sovrascritta e provoca l'errore di runtime indicando nel messaggio il nome della allocazione corrotta.

Le nuove CRT hanno quindi il pregio addizionale di poter effettuare un controllo sistematico se la dimensione dichiarata del buffer è effettivamente corretta, anche se nella fase di test viene utilizzata con dati che non saturino la sua lunghezza. Se questo controllo può risultare banale in casi simili a quello mostrato nell'esempio, non lo è altrettanto quando la dimensione del buffer è calcolata dinamicamente.

Del tutto analogo è il controllo che viene eseguito in caso di allocazione non nello stack ma con new, grazie alla versione debug di questo operatore, anche se il messaggio non è altrettanto lampante:

Heap block at 0034A2A8 modified at 0034A2E8 past requested size

Per un programmatore questa diagnostica è oro.

 

Le nuove CRT

Per tutte le funzioni CRT che devono scrivere su un buffer, sono disponibili due nuove funzioni con un'implementazione che evita di uscire dalle dimensioni del buffer stesso.

Una prima implementazione è quella che usa i template:

wchar_t Buffer[1024];
wcscpy_s<1024>(Buffer, L"Hello, world");

L'unica limitazione della versione template è che la dimensione del buffer deve essere una costante e non può contenere perciò una variabile con la dimensione calcolata dinamicamente.

La seconda versione della funzione safe ha un parametro aggiuntivo che specifica la dimensione del buffer:

wchar_t *Buffer = new wchar_t[1024];
wcscpy_s(Buffer, 1024, L"Hello, world");
// ...
delete[] Buffer;

Questa versione è la più versatile e può essere utilizzata in qualsiasi contesto. A mio avviso tutti i nuovi sorgenti dovrebbero utilizzare questa versione che rende il listato più leggibile e manutenibile.

Ci sono ancora due funzioni di cui sono state rese disponibili le versioni safe che però non sono attivate di default. Si tratta della memcpy (e wmemcpy) e della memmove (e wmemmove). Su queste funzioni non vedremo alcun warning dal compilatore ma è consigliabile definire:

#define _CRT_SECURE_DEPRECATE_MEMORY

Questa definizione attiva il warning che suggerisce l'uso delle corrispondenti funzioni memcpy_s e memmove_s (e analoghe versioni w). Ecco un esempio d'uso:

wchar_t BufSource[100];
wchar_t BufTarget[10];
memset(BufSource, 0xC6, 100);  // BufSource tutto a 0xC6
memcpy_s(BufTarget, 10, BufSource, 10); // copio 10 byte

 

Minimizzare le modifiche ai sorgenti

È proprio necessario cambiare tutte le chiamate alle funzioni CRT? In certi casi non è un problema ma in altri può essere una vera scocciatura.

Può in qualche modo il compilatore capire qual'è la dimensione del buffer utilizzato da una funzione delle CRT? Qualche volta si e altre no. La dimensione è deducibile solo per buffer allocati nello stack come per esempio con un classico:

wchar_t Buffer[100];

In questo specifico caso possiamo minimizzare le modifiche al sorgente beneficiando comunque delle nuove funzioni.

La prima possibilità è quella che si ottiene definendo:

#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1

Così facendo vengono creati in compilazione degli overload delle funzioni CRT che usano la forma template in cui la dimensione del buffer è automaticamente dedotta. Questo significa che queste due chiamate sono equivalenti:

wchar_t Buffer[1024];
wcscpy(Buffer, L"Hello, world");  // ci pensa il compilatore
wcscpy_s<1024>(Buffer, L"Hello, world");  // equivalente

Questa definizione non è sufficiente per ridefinire gli overload delle funzioni che hanno un count nel loro prototipo, come la wcsncpy. Per queste funzioni è necessaria una definizione addizionale:

#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1
#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES_COUNT 1

Il che rende equivalenti queste due chiamate a funzione:

wcsncpy(Buffer, L"Hello, world", 5);
wcsncpy_s<1024>(Buffer, L"Hello, world", 5);  // equivalente

Il motivo di questa differenziazione è dovuto al fatto che la nuova funzione wcsncpy_s inserisce automaticamente il null finale al contrario di quanto accadeva con la wcsncpy. Inoltre il comportamente di troncamento è controllabile tramite _TRUNCATE, come si può vedere dalla documentazione su MSDN. Questi due fatti meritano un particolare occhio di riguardo durante la migrazione.

Personalmente trovo molto pericoloso e ingannevole utilizzare questa prima soluzione che, se di fatto può risolvere la situazione quando le dimensioni dei sorgenti sono ragguardevoli, rende il listato meno leggibile.

Esiste quindi una seconda possibilità attivata di default ottenuta grazie a questa definizione (e disabilitabile definendola con zero):

#define _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES 1

In questo caso si usufruisce di un overload della versione "_s" delle funzioni, rendendo così più esplicito che si sta utilizzando la versione safe. Queste due chiamate a funzione sono equivalenti:

wchar_t Buffer[1024];
wcscpy_s(Buffer, L"Hello, world");  // ci pensa il compilatore
wcscpy_s<1024>(Buffer, L"Hello, world");  // equivalente

Ovviamente in questo caso è necessario eseguire una piccola revisione del codice che comunque è indispensabile per tutte le allocazioni dinamiche eseguite con new o altri allocatori.

 

Altre novità

Vi sono ancora un paio di novità che vale la pena menzionare.

Gli iteratori delle Standard Template Library erano vulnerabili ai Buffer Overrun ed ora è possibile attivare la nuova implementazione sicura definendo:

#define _SECURE_SCL 1

Vi sono infine alcune variabili globali usate in time.h e stdlib.h, come ad esempio daylight, che sono state rese deprecate e sostituite da funzioni (come ad esempio _get_daylight). Se è assolutamente necessario mantenerle nel sorgente ma allo stesso tempo non si vuole disabilitare il warning C4996 con il generico _CRT_SECURE_NO_DEPRECATE che avrebbe effetto su tutte le CRT, è preferibile invece definire:

#define _CRT_SECURE_NO_DEPRECATE_GLOBALS

che evita il warning esclusivamente per l'uso di queste varaibili globali deprecate.

 

Conclusioni

Il Buffer Overrun è uno dei nemici più temibili ma non è certamente l'unico. Questo significa che la revisione di un applicativo in chiave di sicurezza non può certamente fermarsi alle CRT. Le revisione sull'uso delle CRT sono però un passo importante e, a mio avviso, irrinunciabile.

Fortunatamente l'uso di queste librerie non solo contribuisce a rendere più sicure le applicazioni, ma provvede al tempo stesso un valido strumento diagnostico per identificare anzitempo alcuni bug tra i più complessi. Questa può essere un'ottima scusa per cominciare a pensare alla sicurezza e non solamente adeguarsi a danni già fatti.

Microsoft stessa ha fatto della revisione dei sorgenti uno dei punti di forza negli utlimi anni (XP SP2 è forse la revisione più nota al pubblico) conseguendo importanti risultati tangibili. Le statistiche (per esempio di SQL Server) evidenziano una netta diminuzione delle patch di sicurezza e questi sono sono dati oggettivi che parlano chiaro. Lo stesso Visual Studio e molti altri prodotti sono stati comunque oggetti di revisioni in chiave di sicurezza, segno che non sono solo i prodotti server a dover temere vulnerabilità.

Le vecchie CRT sono sempre state un punto di riferimento per i programmatori che sviluppano cross platform. Per risolvere questo nodo importante Microsoft ha proposto un draft che è al vaglio della commissione (http://std.dkuug.dk/jtc1/sc22/wg14/www/docs/n1031.pdf) per ottenere la standarizzazione ISO delle nuove CRT.