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.
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 (
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
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 (
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.