Descrizioni tecniche

 

Il superbug di Windows, come accennato già in precedenza, è stato individuato dalla eEye Digital Security che ha riportato in date differenti due comunicati riguardanti le caratteristiche tecniche di tale bug, le conseguenze e i potenziali rischi.

In entrambi i comunicati, tali vulnerabilità sono state classificate con il massimo grado di criticità in quanto possono consentire ad un attaccante, se opportunamente sfruttate, di eseguire codice da remoto e ottenere così il controllo del sistema attaccato. I sistemi affetti da tali vulnerabilità sono:

-         Microsoft Windows NT 4.0 (tutte le versioni)

-         Microsoft Windows 2000 (SP3 e versioni precedenti)

-         Microsoft Windows XP (tutte le versioni)

-         Microsoft Windows Server 2003

Passiamo ora ad illustrare in maniera più dettagliata le informazioni fornite e pubblicate dalla eEye Digital Security.

 

La scoperta da parte della eEye della prima vulnerabilità risale al 25 luglio 2003. Tale prima vulnerabilità riguarda software come Microsoft Internet Explorer, Microsoft Outlook, Microsoft Outlook Express e altre applicazioni che usano certificati (SSL, e-mail firmate digitalmente, controlli ActiveX firmati, ecc.) e alcuni servizi come Kerberos (UDP/88), Microsoft IIS, NTLMv2 authentication (TCP/135, 139, 445). Essa è presente all’interno della libreria ASN.1 di Microsoft (MSASN1.DLL) che consentirebbe ad un attaccante di sovrascrivere memoria su una macchina vulnerabile e causare l’esecuzione di codice arbitrario.

 

Comunicato del 25 luglio 2003

 

La libreria MSASN1 è piena di integer overflow. Descriveremo in seguito un paio di errori aritmetici in una sezione generica e di basso livello della decodifica BER di ASN.1 che consente di sovrascrivere una gran quantità di memoria.

Questa vulnerabilità colpisce principalmente qualsiasi client di MSASN1.DLL, i più interessanti dei quali sono LSASS.EXE e CRYPT32.DLL (e così qualsiasi applicazione che usa CRYPT32.DLL),

L’idea base dello schema di codifica BER di ASN.1 è che esso è uno schema di codifica per rappresentare in maniera flessibile dati binari, ed è spesso paragonato al “XML binario”.

Ogni blocco di dati è codificato come valore tipato, che è costruito come un tag number che descrive come interpretare i dati che verranno di seguito, poi la lunghezza dei dati, e infine, i dati stessi.

Questo campo lunghezza è l’oggetto della nostra trattazione.

Fornendo in questo campo un valore molto grande, e.g. da 0xFFFFFFFD (4294967293 in decimale) a 0xFFFFFFFF (4294967295 in decimale), è possibile causare un integer overflow in una routine di allocazione di memoria, e, sebbene siano presenti controlli al fine di assicurare la validità della lunghezza fornita, un distinto pointer arithmetic overflow nella routine di verifica dà luogo alla vulnerabilità.

Ecco come:

1.

Quando un valore semplice (ovvero un valore costituito da dati atomici, piuttosto che da più valori) è decodificato da MSASN1, viene invocata la funzione ASN1BERDecLength() per recuperare la lunghezza di tale valore.

La lunghezza così recuperata è poi passata alla funzione ASN1BERDecCheck() per assicurarsi che i dati esistano realmente.

2.

ASN1BERDecCheck() verifica che

(puntatore_a_inizio_dati + lunghezza_dati_riportata)

sia minore o uguale a

(puntatore_a_inizio_blocco_BER + grandezza_totale_blocco_BER).

Se ciò non si verifica, la funzione ritorna con un fallimento e causa l’interruzione della decodifica.

3.

Se la funzione che ha invocato ASN1BERDecLength() tenta di allocare memoria per effettuare una copia dei dati, essa passerà la lunghezza decodificata length (ovvero quella recuperata da ASN1BERDecLength()) alla funzione DecMemAlloc(), che prima arrotonda tale lunghezza a un multiplo di DWORD e poi tenta di allocare il risultato. L’operazione di questa funzione può essere rappresentata come segue:

LocalAlloc (LMEM_ZEROINIT, (length + 3) & ~3).

4.

Se DecMemAlloc() ha successo, la funzione chiamante allora effettua una memcpy() dei dati all’interno del buffer allocato, usando l’originale lunghezza decodificata del valore come conteggio dei byte di dati.

 

Per meglio capire cosa realmente accada in fase di allocazione, chiariamo innanzitutto alcuni dei concetti precedentemente introdotti.

Termine

Significato

length

la lunghezza restituita dalla funzione ASN1BERDecLength

DecMemAlloc

la funzione che si occupa realmente di allocare la memoria necessaria a contenere i dati del blocco BER

DWORD [6]

abbreviazione di double word, indica una dimensione pari al doppio di una word; il suo valore varia in base all'architettura utilizzata. Nel nostro caso DWORD vale 32 bit (ovvero 4 byte).

LocalAlloc [7]

la funzione che viene utilizzata per allocare memoria dallo heap

LMEM_ZEROINIT

flag della funzione LocalAlloc che, se specificato, fa sì che l'intero blocco di memoria allocata sia inizializzato a zero

Fatta questa breve precisazione, passiamo ora a mostrare altrettanto brevemente alcuni esempi che chiariranno il funzionamento della funzione DecMemAlloc. Come introdotto in precedenza, l'operazione di questa funzione può essere rappresentata con la seguente istruzione:

LocalAlloc (LMEM_ZEROINIT, (length + 3) & ~3).

In base a quanto detto in precedenza, tale operazione allocherà un blocco di memoria inizializzato a zero di dimensione "(length + 3) & ~3". Perché si usa tale espressione per stabilire la dimensione del blocco di memoria da allocare? Il motivo risiede nel fatto che la funzione DecMemAlloc non alloca semplicemente la memoria in base a quanto riportato da ASN1BERDecLength, ma alloca sempre una porzione di memoria di dimensione pari ad un multiplo di DWORD sufficientemente grande. Tale espressione serve proprio ad adempiere a questo compito.

Esempio 1

Supponiamo che la funzione ASN1BERDecLength restituisca un valore di length = 5. Analizziamo da vicino le operazioni che vengono effettuate:

Termini

Valori in decimale

Valori in binario (32 bit)

Length

5

00000000000000000000000000000101

3

3

00000000000000000000000000000011

length + 3

8

00000000000000000000000000001000

~3

-

11111111111111111111111111111100

(length + 3) & ~3

8

00000000000000000000000000001000

Verrà quindi allocato un blocco di memoria di dimensione 8 byte, ovvero 2 * DWORD

 

 

Esempio 2

Supponiamo che la funzione ASN1BERDecLength restituisca un valore di length = 9. Analizziamo da vicino le operazioni che vengono effettuate:

Termini

Valori in decimale

Valori in binario (32 bit)

Length

9

00000000000000000000000000001001

3

3

00000000000000000000000000000011

length + 3

12

00000000000000000000000000001100

~3

-

11111111111111111111111111111100

(length + 3) & ~3

12

00000000000000000000000000001100

Verrà quindi allocato un blocco di memoria di dimensione 12 byte, ovvero 3 * DWORD

 

 

Esempio 3

Supponiamo che la funzione ASN1BERDecLength restituisca un valore di length = 17. Analizziamo da vicino le operazioni che vengono effettuate:

Termini

Valori in decimale

Valori in binario (32 bit)

Length

17

00000000000000000000000000010001

3

3

00000000000000000000000000000011

length + 3

20

00000000000000000000000000010100

~3

-

11111111111111111111111111111100

(length + 3) & ~3

12

00000000000000000000000000010100

Verrà quindi allocato un blocco di memoria di dimensione 20 byte, ovvero 5 * DWORD

 

 

Per essere più specifici, se viene data una lunghezza nell’intervallo da 0xFFFFFFFD a 0xFFFFFFFF (ovvero 4294967293, 4294967294 o 4294967295), essa passerà attraverso ASN1BERDecCheck() senza alcun problema. Tuttavia, a causa dell’arrotondamento in DecMemAlloc(), i 3 valori in questo intervallo saranno tutti arrotondati a zero.

Chiariamo brevemente cosa succede: se ai tre valori considerati sommiamo 3, otteniamo 4294967296, 4294967297 e 4294967298 che sono più grandi di 232 - 1. Ciò implica che non possono essere rappresentati con 32 bit (ovvero i 4 byte di una word): per questo i valori vengono troncati prendendo i 32 bit meno significativi che sono tutti zeri. Tali valori troncati in AND con ~3 danno ancora zero.

In seguito, LocalAlloc() con successo alloca un blocco di memoria di dimensione zero il cui indirizzo è restituito al chiamante, mentre alla funzione memcpy() viene passato il valore originale della lunghezza, ovvero quello molto grande non arrotondato. Il risultato è una classica, completa sovrascrittura dell’heap, dove tutta la memoria contigua al blocco di lunghezza zero è cancellata da dati arbitrari.

 

 

Esempio che genera overflow

Supponiamo che la funzione ASN1BERDecLength restituisca un valore di length = 4294967294. Analizziamo da vicino le operazioni che vengono effettuate:

Termini

Valori in decimale

Valori in binario (32 bit)

Length

4294967294

11111111111111111111111111111110

3

3

00000000000000000000000000000011

length + 3

4294967297

00000000000000000000000000000001 (troncato a 32 bit)

~3

-

11111111111111111111111111111100

(length + 3) & ~3

0

00000000000000000000000000000000

Verrà quindi allocato un blocco di memoria di dimensione 0 byte, quando in realtà i dati effettivi occupano 4294967294 byte.

 

 

Il seguente è un campione di funzioni di decodifica vulnerabile:

 

ASN1BerDecCharString
ASN1BERDecChar16String
ASN1BERDecChar32String
ASN1BERDecEoid
ASN1BERDecGeneralizedTime
ASN1BERDecMultibyteString
ASN1BERDecOctetString
ASN1BERDecOpenType
ASN1BERDecSXVal
ASN1BERDecUTCTime
ASN1BERDecUTF8String
ASN1BERDecZeroCharString
ASN1BERDecZeroChar16String
ASN1BERDecZeroChar32String
ASN1BERDecZeroMultibyteString

 

Nel secondo comunicato della eEye Digital Security, risalente al 25 settembre 2003, viene riportata la scoperta di una seconda vulnerabilità critica nella libreria ASN.1 di Microsoft che come la prima consentirebbe ancora ad un attaccante di guadagnare da remoto il controllo di una macchina vulnerabile.

 

 

Comunicato del 25 settembre 2003

 

Grazie ad un altro paio di overflow di interi, il software che usa MSASN1 direttamente o indirettamente è ancora vulnerabile ad una completa sovrascrittura di una porzione della sua memoria. Questa volta l’attacco è specifico a valori di stringhe di bit (tag 03h e 23h), ma il risultato è lo stesso che si ha con la corruzione dell’heap che coinvolge dati di grande lunghezza descritto nel comunicato precedente.

Nello schema di codifica usato da MSASN1, nel caso di una stringa di bit, il primo byte di dati è il numero di bit (da 0 a 7) da escludere dalla fine della stringa, poiché i dati sono naturalmente forniti in byte. I byte rimanenti, allora, contengono gli (8 * (lunghezza_del_valore – 1) – numero_bit_inutilizzati) bit che compongono la stringa di bit.

C’è un interessante overflow di interi qui quando viene data una stringa di bit di lunghezza di un byte (ovvero costituita dal solo campo “numero_bit_inutilizzati”, senza bit di dati successivi), e un numero diverso da zero di bit inutilizzati.

ASN1BERDecBitString() e ASN1BERDecBitString2() riporteranno entrambe che la lunghezza in bit di tale stringa di bit è (0 – numero_bit_inutilizzati), un numero che può cadere nell’intervallo da 0xFFFFFFFFFFFFFFF9 (-7 in decimale) a 0xFFFFFFFFFFFFFFFF (-1 in decimale), sebbene nessuna delle due tenterà di copiare una quantità di dati basata su questo conto. La prima funzione tenterà di copiare la lunghezza dei dati originali meno un byte – in questo caso zero – senza provocare alcun danno. La seconda restituisce solo un puntatore all’interno del blocco originale codificato in BER e la lunghezza in bit dei dati, e anche questa è inoffensiva.

Sebbene sia possibile che qualche applicazione client da qualche parte possa usar male questo numero di bit e creare una condizione sfruttabile da un attaccante, ciò non è molto importante in quanto esiste un ulteriore overflow di interi in MSASN1 che creerà tale condizione definitivamente. ASN1BERDecBitString() ha un modo speciale di gestire stringhe di bit composte (tag 23h): essa concatena ciascuna delle stringhe di bit semplici contenute in quella composta. Fornendo una valida  stringa di bit composta che contiene una singola, semplice stringa di bit di lunghezza 1 e 7 bit inutilizzati, un secondo overflow di interi occorre mentre si aggiunge il numero di bit nella stringa di bit al totale cumulativo.

Per rendere più chiaro quanto finora espresso facciamo un esempio.

Supponiamo che la prima stringa di bit semplice incontrata abbia una lunghezza riportata da ASN1BERDecBitString() di 0xFFFFFFF9 (-7) bit. Al momento della creazione della stringa composta, al numero totale di bit finora accumulati (0) verranno aggiunti la lunghezza della stringa di bit concatenata (-7), e un 7 addizionale al fine di arrotondare, per arrivare ad una lunghezza totale di 0.

Questa somma è passata alla funzione DecMemReAlloc() per allocare un blocco sull’heap di lunghezza zero, ma poi le lunghezze originali delle stringhe di bit sono passate ad una funzione chiamata ASN1bitcpy() (non mostrata qui), che, anche in questo caso, effettua una tipica memcpy() e sovrascrive un intero blocco di memoria heap come risultato.