Scrivere un exploit dimostrativo di esecuzione di codice su Windows
Abstract
Come funzionano gli exploit sotto windows? E perché sono così pericolosi?
In questo articolo vediamo un esempio passo per passo.
È propedeutica la conoscenza dell'assembly 80386.
Data di stesura: 11/02/2003
Data di pubblicazione:
23/02/2003
Ultima modifica: 04/04/2006
Quello che vorrei descrivere in questo articolo, altro non è che un
modo (il più semplice possibile) per poter scrivere dei buoni proof
of concept per quelle vulnerabilità che permettono di eseguire codice
su una macchina vittima.
La vulnerabilità a cui mi riferisco dovrà avere come requisiti:
possibilità di utilizzare qualsiasi byte (quindi anche NULL bytes)
deve essere causata dalla lettura non controllata di bytes da un file
L'ultimo requisito non è indispensabile, ma è utile per sapere bene a
cosa ci si riferisce in questo articolo, perché l'esempio che proporrò
tratta proprio tale problema.
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.
Cosa bisogna conoscere
Trattandosi di un exploit di esecuzione di codice un requisito
fondamentale dovrebbe essere la conoscenza dell'Assembly, ma non c'è
bisogno di conoscerlo a fondo. Dopotutto noi vogliamo un exploit
dimostrativo di buon effetto, in poco tempo e con poco lavoro mentale.
I registri dei processori x86 che ci interessano al momento sono 2:
EIP (puntatore all'istruzione successiva): esso punta alla posizione
del codice in memoria che dovremo eseguire.
ESP (puntatore allo stack): esso punta alla zona dati in memoria.
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.
Cosa ci serve
Un disassemblatore: per sapere "cosa" fà il programma. Esso è utile
soprattutto nel momento in cui vogliamo copiare una funzione già
usata dal programma vulnerabile e riprodurla nel nostro codice.
Editor esadecimale: essenziale... serve per modificare il file che
verrà letto dal programma vulnerabile
Calcolatrice esadecimale: calc di Windows è perfetta
Bladeenc 0.94.2 i586 per Win: (guardare l'Introduzione)
Opzionale:
Un debugger: per poter "seguire" il codice e soprattutto conoscere il
valore dei registri x86 e soprattutto per poter scorrere la memoria
Sia come disassemblatore che come debugger uso W32Dasm
(http://members.home.net/w32dasm/[2])
con cui mi trovo molto bene ed è semplicissimo da usare.
Come editor esadecimale, uno vale l'altro (XVI32 è molto comodo e lo
si può trovare qui: http://www.chmaas.handshake.de[3])
Primo passo (cosa non và in Bladeenc?)
Il problema di Bladeenc è nell'utilizzo di un numero intero con segno
anzichè di uno unsigned per poter leggere i dati in un file.
Ciò comporta che il programma dovrà leggere una porzione di dati ma
questa "porzione" potrebbe essere negativa, ossia invece di leggere 80
bytes il programma dovrà leggerne -80.
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.
Secondo passo (preparare il file WAVE)
Siamo quasi pronti per iniziare, dobbiamo solo realizzare un file WAVE
minimale che possa essere letto da Bladeenc.
Questo è quello che ho usato io:
La struttura di riferimento dei file WAVE è la seguente:
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)
Come possiamo vedere l'unico parametro da cambiare è il Chunk size del
chunk "fmt " che è proprio la variabile int offset usata dalla
funzione vulnerabile myFseek().
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)
Personalmente quando ho dovuto creare il primo exploit per questo bug
ho usato un normale file wave grande 5 Kb (difatti il Groupsize 0x12cc
appartiene ad un file wave di 4820 Kb) che ho trovato casualmente nel
mio Hard-disk.
Terzo passo (debugging: EIP ed ESP in memoria e sul file)
Finalmente si inizia.
Abbiamo una minima conoscenza di cosa va storto in Bladeenc, abbiamo
un file wave che fa comparire il problema, ora ciò che ci manca sono
gli indirizzi EIP ed ESP ed i loro "corrispettivi" nel file wave.
Questo esempio si basa SOLO su Windows98 in quanto l'ESP cambia da un
sistema operativo all'altro (non ci sono differenze tra Win98 prima
edizione e special edition).
Per gli altri sistemi operativi Microsoft la procedura è la stessa,
l'unica differenza riguarda appunto gli indirizzi di memoria.
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
Se usiamo Win98 comparirà la classica schermata di errore critico con
tanto di dump dei valori dei registri del processore.
Eccoli tutti quanti:
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
Ottimo stavolta siamo stati abbastanza fortunati in quanto il nostro
codice si trova proprio dove punta ESP, ma altre volte ci toccherà
spendere 2 minuti in più col debugger.
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.
Se il debugger è d'obbligo (opzionale)
Nel caso specifico di Bladeenc non è necessario usare un debugger in
quanto la semplice schermata di errore critico di Win98 ha già tutto
ciò che ci serve.
Se invece nell'immagine dello stack non riconosciamo nessuno dei bytes
che abbiamo nel file o più semplicemente vogliamo fare un lavoro fatto
bene e controllare che tutto sia a posto, dobbiamo avviare il nostro
debugger preferito o comunque poter vedere e scorrere la memoria che è
intorno allo stack pointer (0x0069e888 appunto)
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.
Quarto passo (gli ultimi preparativi)
Ci servono le ultime 2 cose per poter scrivere il nostro codice:
una funzione che visualizzi un messaggio
una funzione per terminare il programma
La prima funzione si può trovare con un debugger oppure guardando il
listato Assembly del programma.
Difatti in tutti (o quasi) i programmi c'è una funzione che mostra a
video una stringa se si tratta di un programma per console o di un
MessageBox o simile se usa le API di Windows.
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:
il disassemblatore se è "serio" ci mostrerà tutte le stringhe che
vengono richiamate da ogni funzione di visualizzazione
con l'editor esadecimale troviamo una stringa che sappiamo verrà
visualizzata e prendiamo l'offset del primo carattere (che è sempre
preceduto da un byte NULL).
Dopodiché con un semplice convertitore real->virtual address
ricaveremo l'indirizzo che tale stringa assumerà in memoria quando
il programma verrà eseguito.
Non è compito di questo articolo descrivere l'utilizzo di un
programma simile, comunque RVA
(http://linux20368.dn.net/protools/files/utilities/rva.zip[4]) vi sarà
di prezioso aiuto.
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
Dopodiché dobbiamo trovare la funzione per terminare il programma e
qui ci viene in aiuto KERNEL32.ExitProcess che si trova all'indirizzo
0x00414be0 ed è uguale ai bytes: ff1524d04100.
Quinto passo (impastiamo gli ingredienti...)
Finalmente abbiamo tutti gli "ingredienti", quindi dobbiamo solo creare
l'impasto che nel nostro caso è il file wave con il codice da eseguire
sulla macchina vittima.
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:
all'offset 0x00000130 del nostro file (dove viene sovrascritto l'EIP)
inseriremo l'indirizzo di ESP o comunque l'indirizzo dove inizia il
nostro codice in memoria (ossia 0x0069e888).
IMPORTANTE: i processori x86 utilizzano il metodo little-endian
quindi qualsiasi indirizzo andrà scritto invertendo i 4 bytes:
0x0069e888 --> 0x88e86900
copiamo tutti i bytes che vanno da 0x0040c9e0 a 0x0040c9f5 nel nostro
file partendo dall'offset 0x00000134
copiamo i bytes per terminare l'applicazione: ff1524d04100
scriviamo un messaggio di qualsiasi lunghezza che termini con un byte
NULL finale
puliamo tutto il resto del file usando il byte 0x90 che corrisponde
al NOP (no operation, serve per occupare spazio senza eseguire nulla)
Il nostro file wave ora dovrebbe essere simile a questo:
Gli indirizzi da ricalcolare facendo riferimento alla posizione del
nostro codice in memoria sono:
l'indirizzo della nostra stringa
l'indirizzo della funzione di visualizzazione
Per calcolare l'indirizzo dell'istruzione successiva o di qualsiasi
indirizzo nel nostro file non dobbiamo far altro che eseguire:
ESP + indirizzo_nel_file - ESP_nel_file
Esempio:
la nostra stringa si trova all'offset 0x0000014f, ESP è 0x0069e888 e
l'ESP nel file si trova a 0x00000134 (in pratica da dove partono i
bytes che vanno in memoria).
0x0069e888 + 0x0000014f - 0x00000134 = 0x0069e8a3
Ciò significa che quando il nostro codice andrà a finire in memoria
la nostra stringa si troverà esattamente all'indirizzo 0x0069e8a3.
Invece l'indirizzo della funzione di visualizzazione è già noto ed è
0x00414c3f.
Se volessimo disassemblare il codice nel nostro file wave, avremmo:
Ora sostituiamo il puntatore alla vecchia stringa nella prima funzione
con quello alla nostra stringa:
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ì:
ind_destinazione - ind_istruzione_successiva
Per calcolare l'indirizzo in memoria dell'istruzione successiva ci
affidiamo all'operazione che abbiamo eseguito prima per trovare
l'indirizzo della stringa:
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>
Eh eh, ciao mio caro amico "codice dimostrativo" 8-)
Conclusioni
Quello che abbiamo visto oggi è uno dei modi più semplici per creare
un exploit dimostrativo che faccia eseguire codice ad un programma
vulnerabile.
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)
Informazioni sull'autore
Luigi Auriemma, classe 80, vive in provincia di Milano. Appassionato di computers e "cacciatore di bugs" in qualsiasi applicazione soprattutto quando è annoiato. Sostenitore di tutto ciò che sia libero, dal software Open Source alla "Full Disclosure". Il suo sito web è http://aluigi.altervista.org/.