Articoli Manifesto Tools Links Canali Libri Contatti ?
Sicurezza / Exploits

Stringhe inesistenti e zone di memoria inaccessibili = crash!

Abstract
Quest'articolo è nato dalla necessità di dover tradurre in italiano un advisory che ho rilasciato all'inizio di Aprile 2003 e soprattutto dal fatto che questo tipo di bug è abbastanza interessante e molto diffuso.
Data di stesura: 07/05/2003
Data di pubblicazione: 04/06/2003
Ultima modifica: 04/04/2006
di Luigi Auriemma Discuti sul forum   Stampa

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:
  1. GET / HTTP/1.0 
  2. Connection: 
  3.  
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:
 "User-Agent:"
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:

(gdb) info reg esi
per conoscere il contenuto del registro ESI:
esi            0x0      0
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:
(gdb) info file
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):
  1. 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!)
  2. 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"
  3. 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!

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

È possibile consultare l'elenco degli articoli scritti da Luigi Auriemma.

Altri articoli sul tema Sicurezza / Exploits.

Risorse

  1. Sito del webserver Abyss X1.
    http://www.aprelium.com
  2. Abyss X1 1.1.2 per Windows e Linux.
    http://www.pivx.com/luigi/misc/abyssx1-112.zip
  3. La versione per Windows di GDB, The GNU Debugger.
    http://sourceforge.net/project/showfiles.php?group_id=2435
  4. Netcat per Unix.
    http://www.atstake.com/research/tools/network_utilities/
  5. Netcat per Windows.
    http://www.atstake.com/research/tools/network_utilities/
  6. Disasm, disassemblatore per Windows.
    http://www.geocities.com/SiliconValley/Foothills/4078/disasm.html
  7. Examiner, commentatore per l'output di objdump.
    http://www.academicunderground.org/examiner/
  8. Capitolo 14 dell'RFC 2068 (Hypertext Transfer Protocol -- HTTP/1.1).
    http://zvon.org/tmRFC/RFC2068/Output/chapter14.html
  9. Crash remoto in Emule.
    http://www.pivx.com/luigi/adv/emule-adv-ita.txt
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.