Introduzione
Il bug in questione riguarda il webserver Abyss X1 versione 1.1.2
[1]
e consiste nel mancato controllo della reale
esistenza di una stringa in memoria, cosa che costringe il webserver a
leggere una zona di memoria inaccessibile con il conseguente crash.
La scelta di prendere come esempio pratico il webserver Abyss non è
casuale in quanto questo webserver è perfetto per questo mio articolo:
-
gira su Windows e Linux: quindi chiunque abbia accesso ad uno di
questi due sistemi può vedere con i propri occhi ciò che cercherò
di spiegare.
-
è precompilato: cioè significa che gli indirizzi di memoria che
riporto sono gli stessi su qualsiasi macchina (anche su Linux).
-
è piccolissimo: quindi non si avranno problemi di spazio su disco per
la prova.
-
è ben supportato: ciò non interessa molto l'articolo ma è davvero
bello vedere i programmatori che prendono in seria considerazione e
con molta urgenza le e-mail di segnalazione che vengono loro inviate.
Insomma la gioia di ogni bug researcher.
Gli indirizzi di memoria che riporterò saranno sia quelli di Abyss X1
per Windows che di quelli per Linux, ma per alcuni dettagli mi riferirò
soprattutto alla versione per Windows.
Introduzione al bug
Tale bug naturalmente non riguarda solo i webservers, ma qualsiasi
applicazione che deve leggere un input od una parte di esso dall'utente
e se questo input non c'è o è parziale, non verrà caricata in memoria
nessuna stringa. Se nel programma c'è una funzione che deve leggere
proprio quella stringa allora cominciano i problemi.
Una dimostrazione del bug usando il linguaggio C è la seguente:
char *str;
/* questo e' un puntatore e punta alla zona di
memoria 0x00000000.
Se siete abituati a linguaggi poco legati alla
reale architettura dei computers, sappiate che
qualsiasi variabile o costante in realta' e'
solo un puntatore ad una zona di memoria dove
risiedono realmente i dati:
| MEMORIA |
|---------|
|.........|
str -> |sono una |
|stringa..|
|.........| */
leggistringa(str);
/* supponiamo ora di chiamare un'ipotetica
funzione chiamata leggistringa() che abbia il
compito di leggere una determinata parte
dell'input dell'utente. Ad esempio vogliamo che
l'utente inserisca nome e cognome e str
dovra' puntare al cognome. */
strcmp(str, "ciao");
/* ora chiamiamo la funzione di sistema strcmp()
che ci dice semplicemente se la nostra stringa
str e' uguale o no alla stringa "ciao"
(Chissa' se esiste davvero qualcuno che fa'
Ciao di cognome!) */
Ciò significa che "char *str;" inizializza il nostro puntatore str,
quindi ora str punta alla zona di memoria 0x00000000 (ciò non è sempre
vero ma è buona norma ad esempio usare "char *str = 0" in modo da poter
poi controllare se viene assegnato un valore a tale stringa, salvo poi
le possibili ottimizzazioni che vengono applicate dai compilatori che
vanno considerate un vero e proprio nemico per chi fà debugging).
Poi abbiamo "leggistringa(str);" che preleva il cognome dall'input
dell'utente.
Il nostro utente però, invece di inserire "nome cognome" come richiesto
dal programma, si è divertito ad inserire solo il nome. In questo modo
il puntatore str non punta alla zona di memoria dove è stata inserita
la stringa del cognome ma continua a puntare alla zona di memoria
0x00000000 che è una zona a cui il programma non può accedere.
Ed ora arrivano i problemi in quanto la funzione strcmp() deve per forza
leggere la stringa str byte per byte in modo da poterla confrontare con
la stringa "ciao".
Sfortunatamente la zona di memoria a cui str punta è inaccessibile al
programma ed il sistema operativo ce lo dimostrerà subito segnalandoci
l'errore con il classico MessageBox di Windows o con il Segmentation
Fault dei sistemi Unix.
Un semplice controllo dopo "leggistringa(str);" tipo "if (!str)" avrebbe
quindi evitato che il nostro programma crashasse causando magari non
pochi problemi all'amministratore di sistema (che si sarà preso il
cazziatone dai superiori per non essersi accorto che il server era
fuori uso ih ih ih).
Ma questo piccolo esempio da tipica vita quotidiana da programmatore e
da sistemista non ci può bastare ed allora andiamo a capire meglio la
cosa prendendo in esame un vero esempio reale di questo tipo di
vulnerabilità.
Cosa occorre
Il materiale di cui abbiamo bisogno è naturalmente tutto freeware o
OpenSource:
-
Il webserver AbyssX1 versione 1.1.2: essendo oramai stato sostituito
con il nuovo 1.1.4 è un po' difficile trovare questa versione in
giro, quindi sul mio sito ho messo disponibile tale versione (dentro
c'è sia l'eseguibile per Win che per Linux) [2].
Note:
-
AbyssX1 all'avvio richiederà una chiave di registrazione.
Tale chiave si trova nel file regkey.ser (il programma è
freeware ma chiede ugualmente una chiave fornita direttamente
nel pacchetto d'installazione ... mah).
-
In Linux non esiste la versione 1.1.2 del webserver in quanto
essa contiene solo dei bugfixes per la versione di Windows,
quindi quella che verrà testata è la 1.1.
-
Un debugger: Per Linux non ci sono problemi in quanto GDB [3] è a dir
poco eccellente.
E la cosa più bella è che questo magnifico debugger è stato portato
anche su Windows ed è quasi identico alla versione per Linux.
-
Una utility per collegarsi al webserver: si può utilizzare il semplice
telnet presente nel sistema ma il miglior programma di questo tipo è
senza alcun dubbio Netcat, disponibile sia per Unix [4] che
Windows [5].
-
Un disassembler (opzionale): La mia scelta personale per qualcosa di
veloce ed OpenSource per Windows ricade su Disasm del professore
coreano Sang Cho [6].
In Linux non ci occorre un disassembler, ma se proprio vogliamo, un
buon disassembler (che per la verità è un commentatore che organizza
in modo magnifico l'output di objdump, che è il vero disassembler) è
Examiner [7].
Note
-
Per usare Disasm:
disasm abyssws.exe > file.txt
-
Per usare Examiner:
examiner -x abyssws -o file.txt
Verifica della vulnerabilità
Per verificare la presenza del bug nel nostro webserver dobbiamo solo
avviarlo e prendere la nostra utility di rete per connetterci ad esso.
Prima di usare netcat consiglio di prepararsi un piccolo file di testo
con all'interno le seguenti 3 righe:
GET / HTTP/1.0
Connection:
Ora ci basterà semplicemente lanciare Netcat:
nc host port -v -v < file
dove:
-
host è il nome del computer a cui ci colleghiamo (ad esempio
localhost)
-
port è la porta su cui il webserver ascolta (di default è la 80 per
AbyssX1 per Windows e la 8000 per Linux)
-
file è il file di testo con le 3 righe di testo viste precedentemente
Ops ... qualcosa è crashato!
Debugging
Dopo aver verificato la presenza della vulnerabilità nel server
possiamo iniziare a vedere in profondità la causa del crash.
Come tutti i webserver del mondo anche Abyss legge i campi HTTP come
"Host" e "Referer" ed i loro rispettivi parametri in modo da sapere se
il client vuole recuperare un download oppure per avere informazioni
riguardo al browser usato dal client, quanto tempo il client vuole
restare connesso in modo da non ricollegarsi tutte le volte e tantissimi
altri parametri (se si è interessati ai campi HTTP basta dare una
occhiata alla sezione 14 dell'RFC 2068
[8]).
Il problema in AbyssX1 avviene quando 2 di questi campi HTTP non sono
completi.
Per non completi intendo senza il parametro del campo HTTP, ad esempio
il seguente è un campo HTTP completo:
"User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US) Gecko/20030401"
| |
| parametro
campo HTTP
Il seguente invece è un campo HTTP incompleto o parziale:
difatti nel secondo dopo il campo HTTP non c'è nessun parametro.
I due campi HTTP che causano il crash in Abyss sono:
-
"Connection" usato per specificare se il client vuole restare connesso
al server in modo da fare altre richieste senza doversi riconnettere
altre volte (e quindi perdere tempo)
-
"Range" usato per specificare se si vuole riprendere il download di un
file e da quale posizione partire
Quando Abyss riceve una richiesta HTTP, esso legge ogni campo HTTP ed il
relativo parametro.
Nel caso di "Connection" e "Range" il server non si limiterà ad
immagazzinare in memoria il parametro ma eseguirà anche delle
operazioni su di esso.
Bene, ora dopo questa parentesi ritorniamo al nostro webserver Abyss ed
al punto esatto in cui è avvenuto il crash.
Riavviamo il webserver utilizzando il debugger.
Con GDB ci basterà digitare il classico "gdb ./abyssws" o
"gdb abyssws.exe" se siamo su Windows e successivamente il comando "run"
per avviare il programma e la fase di debugging.
Dopo aver ripetuto l'operazione di invio dati con Netcat il debugger si
fermerà nel punto esatto in cui è avvenuta l'eccezione:
-
0x78013590 in Windows
-
0x42079db7 in Linux.
Ora, a seconda di come siamo abituati a guardare il codice assembly dei
nostri programmi, potremo scegliere se vedere il listato delle
istruzioni del programma in modalità Intel o AT&T.
Chi è abituato con lo Unix tradizionale non avrà problemi perché di
default GDB usa la modalità di AT&T, ma la maggiorparte delle persone
preferisce quella dell'intel (che preferisco anche io):
(gdb) set disassembly-flavor intel
Davanti ai nostri occhi c'è la causa del crash, quindi sempre da GDB
diamo il comando:
(gdb) disassemble 0x78013590 0x780135a0
dove il primo indirizzo è quello dove si è verificato il crash ed il
secondo è solo un indirizzo più grande in modo da poter vedere una
determinata porzione di codice:
Dump of assembler code from 0x78013590 to 0x780135a0:
0x78013590: mov ah,BYTE PTR [esi]
0x78013592: or ah,ah
0x78013594: mov al,BYTE PTR [edi]
0x78013596: je 0x780135b9
0x78013598: or al,al
0x7801359a: je 0x780135b9
0x7801359c: inc esi
0x7801359d: inc edi
0x7801359e: cmp ah,bh
L'istruzione che ci interessa è proprio la prima:
0x78013590: mov ah,BYTE PTR [esi]
| |
| istruzione
indirizzo
Questa piccolissima istruzione non fà nient'altro che copiare il byte
(meglio chiamarlo carattere nel nostro caso visto che abbiamo a che fare
con delle stringhe di testo) "puntato" da ESI nel registro AH, ossia la
parte a 8 bit del registro EAX.
Ci basterà ora digitare:
per conoscere il contenuto del registro ESI:
ESI è uguale a 0x0 e quindi il programma vorrebbe leggere il byte che
è alla posizione 0x00000000 della memoria, ossia una zona inaccessibile
in quanto i programmi possono lavorare solo sulle loro zone di memoria.
Bene abbiamo trovato la causa del crash, ma ci mancano ancora un po' di
dati per risolvere la nostra "indagine" e quindi andiamo a vedere
perché siamo arrivati fino a questo punto.
Backtrace
L'indirizzo 0x78013590 non appartiene ad abyssws.exe ma è l'indirizzo
di qualche funzione esterna di una DLL (ossia una libreria dinamica).
Ciò lo possiamo capire usando il comando:
con il seguente output che ci fà capire che l'indirizzo 0x78013590
non appartiene al nostro eseguibile:
Symbols from "F:\download\abyss112/abyssws.exe".
Win32 child process:
Using the running image of child thread -13673.0xfffe7333.
While running this, GDB does not access memory from...
Local exec file:
`F:\download\abyss112/abyssws.exe', file type pei-i386.
Entry point: 0x410ab0
0x00401000 - 0x00411000 is .text
0x00411000 - 0x00413000 is .rdata
0x00413000 - 0x00417000 is .data
0x00417000 - 0x0041a000 is .rsrc
Ora quello che ci tocca sapere è quale funzione e quale libreria ha
causato il crash.
GDB ci viene in aiuto con il comando "bt" (alias di "backtrace").
In Windows avremo:
(gdb) bt
#0 0x78013590 in ?? ()
#1 0x780134e1 in ?? ()
#2 0x0040e3fa in ?? ()
#3 0x0040a472 in ?? ()
#4 0x0040fb9c in ?? ()
#5 0xbff88f20 in ?? ()
#6 0xbff869ef in ?? ()
mentre in Linux:
(gdb) bt
#0 0x42079db7 in strncasecmp () from /lib/i686/libc.so.6
#1 0x08056f9a in strcpy ()
Il comando backtrace in pratica mostra gli indirizzi di ritorno delle
funzioni che sono state chiamate, quindi gli indirizzi che vediamo a
partire dal numero #1 sono successivi all'indirizzo che ha effettuato
la chiamata alla funzione successiva.
Quindi se siamo in Linux già siamo a conoscenza di quale funzione e
quale libreria ha causato il crash, ossia strncasecmp() della libc,
mentre se siamo in Windows dobbiamo usare un disassembler (infatti il
GDB in ambiente Windows non sembra mostrare il nome delle funzioni a
quanto pare).
L'indirizzo che ci interessa è il primo a partire dall'alto che si
riferisce al programma Abyss, quindi "#2 0x0040e3fa".
Per vedere il codice assembly che ha effettuato la chiamata ci basta
usare un disassembler ed andare a vedere cosa c'è prima dell'indirizzo
0x0040e3fa:
...
:0040E3EA 6A0A push 00A
:0040E3EC 68205E4100 push 00415E20
(StringData)"keep-alive"
:0040E3F1 FF7508 push dword[ebp+08]
:0040E3F4 FF151C114100 call dword[0041111C ->0001205A _strnicmp]
...
Ora anche in Windows sappiamo che la funzione che ha causato il crash è
_strnicmp, ossia la funzione che si occupa di comparare due stringhe in
modo case insensitive (ossia senza far caso al maiuscolo/minuscolo) e
limitando la comparazione ad un determinato numero di caratteri.
strnicmp() e strncasecmp()
A questo punto abbiamo tutto e possiamo vedere con i nostri occhi le
istruzioni che chiamano la funzione di comparazione strnicmp() per il
campo "Connection".
Difatti Abyss effettuerà una comparazione dei parametri dei due campi
HTTP "Connection" e "Range" con alcune stringhe in modo da sapere come
gestire la connessione.
Ad esempio nel caso di "Connection" il server controllerà se il suo
parametro è uguale a "keep-alive", ossia se il client vuole restare
connesso al server.
Mentre nel caso di "Range" farà un confronto con la stringa "bytes".
Questa comparazione fatta dal server avviene appunto in modo case
insensitive e limitandola ad un determinato numero di caratteri usando
la funzione di sistema strnicmp() in Windows o strncasecmp() in Linux.
La funzione strnicmp() necessità di 3 argomenti. Il suo prototipo è:
int strnicmp(const char *, const char *, size_t);
dove:
-
il primo char è la stringa che vogliamo confrontare
-
il secondo char è la stringa usata per il confronto (ad esempio
"keep-alive")
-
size_t è il numero di bytes che vogliamo confrontare
Nel caso di Abyss avremo quindi (tenete a mente i numeri):
-
indirizzo della prima stringa: questo è il parametro di uno dei
campi HTTP. Esso sarà 0x00000000 se la stringa non esiste
(ricordatevi che le stringhe sono solo dei puntatori a zone di
memoria!)
-
indirizzo della stringa usata per il confronto: essa sarà
"keep-alive" se il parametro da confrontare è del campo "Connection"
e "bytes" nel caso di "Range"
-
numero di caratteri da confrontare: ossia la grandezza della seconda
stringa (ad esempio 10 nel caso di "keep-alive")
E nel codice Assembly di Abyss il tutto diventa:
:0040E3EA 6A0A push 00A
:0040E3EC 68205E4100 push 00415E20
(StringData)"keep-alive"
:0040E3F1 FF7508 push dword[ebp+08]
:0040E3F4 FF151C114100 call dword[0041111C ->0001205A _strnicmp]
Spiegazione (con i numeri visti poco fa'):
:0040E3EA viene passato 00A (che in decimale e' 10), ossia la grandezza
della stringa "keep-alive" (3)
:0040E3EC viene passato l'indirizzo della stringa "keep-alive" (2)
:0040E3F1 viene passato l'indirizzo della stringa contenente il
parametro del campo "Connection" (1)
:0040E3F4 finalmente viene chiamata la funzione strnicmp()
(All'offset 0040E473 si può vedere la stessa cosa per il campo HTTP
"Range:")
Ricordo ancora che in Linux accadono esattamente le stesse cose in
quanto la funzione strncasecmp() è la stessa di strnicmp():
8056f8a: 6a 0a push $0xa
# PUSH "keep-alive" on the stack
8056f8c: 68 d9 f2 05 08 push $0x805f2d9
8056f91: ff 74 24 1c pushl 0x1c(%esp,1)
# CALL STRNCASECMP_FUNCT(1c,"keep-alive",a,AX)
8056f95: e8 b6 29 ff ff call 0x8049950
L'output di Examiner è talmente dettagliato che si spiega praticamente
da solo 8-)
Conclusioni
Tutte le volte mi prometto di essere meno prolisso nei miei articoli e
regolarmente tutte le volte vado oltre le 600 linee ... spero almeno di
essere stato chiaro in tutti i punti toccati dall'articolo in quanto
questo bug è davvero molto semplice solo che ho il problema di voler
spiegare sempre troppi dettagli.
Bene, oggi non solo abbiamo visto come nasce questo semplice bug ma
abbiamo anche visto come utilizzare al minimo GDB (e spero che qualcuno
ora sappia che esiste un porting di questo debugger anche su Win),
abbiamo visto qualcosina sull'architettura dei computers e altro ancora.
Le vulnerabilità di accesso a zone di memoria inaccessibili o protette
sono molto comuni e difatti proprio qualche settimana prima di trovare
questo bug in Abyss trovai un altro problema nel client di file sharing
chiamato Emule (
[9]).
Per qualsiasi domanda, consiglio, critica, commento od altro non fatevi
problemi e contattatemi!