Protostar - Stack Buffer Overflow

Tempo di lettura: 11 minuti
Data pubblicazione: May 1, 2020

Già in altri articoli ci eravamo occupati di buffer overflow, e, per continuare la serie, ho deciso che nelle prossime settimane mi occuperò di diverse tipologie, partendo dallo Stack Buffer Overflow, passando poi all’Heap Overflow, Use After Free ed alcune tecniche di difesa che sono in atto ai giorni nostri.

Per chi abbia bisogno di un ripasso generico, ricordo l’articolo Introduzione al Buffer Overflow che consiglio vivamente (oltre che ai soliti approfondimenti a fondo pagina).

Gli esercizi spiegati di seguito sono presi da ExploitExercise.

Stack0

Una piccola premessa, prima di iniziare i vari esercizi. Per eseguirli ho utilizzato l’ultima versione di Kali Linux x86, con le opportune modifiche. Per prima cosa, ho disabilitato la randomizzazione degli indirizzi con questo comando

Disabilitazione di ASLR
Disabilitazione di ASLR

Mentre per la compilazione del codice, il comando sarà

gcc -mpreferred-stack-boundary=2 -fno-stack-protector -no-pie -z execstack –g programma.c

Dove:

  • -mpreferred-stack-boundary: GCC si assicura che lo stack sia impostato in incrementi di dimensione 2, in modo che la macchina non ottimizzi lo stack (di default è impostato a 4);
  • -fno-stack-protector: disabilita i controlli per la protezione dello stack;
  • -no-pie: disabilita il PIE, una protezione ad-hoc che carica i file binari in posizioni random ad ogni esecuzione
  • -z execstack: permette l’esecuzione di codice nello stack, rendendolo di fatto eseguibile.

Ho inoltre modificato la visione di GDB per rendere più chiare le sezioni di memoria ed eventuali comandi, tramite la configurazione seguente

File di configurazione gdbinit
File di configurazione gdbinit

In modo che ogni volta che eseguirò gdb con questo comando

gdb -tui ./programm

esso sarà eseguito in maniera grafica, con la prima sezione riguardante i registri, la seconda il codice del programma e la terza i comandi di GDB

Visualizzazione grafica
Visualizzazione grafica

Per analizzare i dump del sistema ho invece usato i comandi

kali@kali:~/Desktop/Protostar$ sudo echo "/tmp/core-%e-%s-%u-%g-%p-%t" > /proc/sys/kernel/core_pattern
kali@kali:~/Desktop/Protostar$ ulimit -c unlimited

Ma torniamo all’esercizio

Stack0.c
Stack0.c

La funzione vulnerabile è a riga 11, infatti, dando un occhio alla documentazione di gets, possiamo vedere che è deprecata

Manuale di gets
Manuale di gets

Visto che per superare la sfida è necessario modificare la variabile modified sarà sufficiente inserire una stringa con più di 64 caratteri.

Script in python con i comandi
Script in python con i comandi
Esercizio superato
Esercizio superato

Stack1

Stack1.c
Stack1.c

In questo caso _modified _non solo dev’essere modificata, ma deve essere inserito un valore preciso.

Poiché _modified _e _buffer _saranno entrambi nello stack, uno subito dopo l’altro, andiamo a vedere con GDB cosa succede andando ad inserire una stringa contenent 64 A e 4 B.

Nello screenshot seguente abbiamo:

  • Impostato un breakpoint alla riga 17, in modo che l’esecuzione si fermi poco prima dell’if
  • Stampato la variabile buffer, che giustamente contiene 64 A
  • Stampato il puntatore alla variabile modified, che contiene le 4 B e giustamente non può accedere.
GDB
GDB

Questo conferma la nostra ipotesi che sono vicine e basterà iniettare 0x61626364 dopo le 64 A per superare la sfida

Generiamo lo shellcode
Generiamo lo shellcode
Sfida superata!
Sfida superata!

Stack2

Stack2.c
Stack2.c

In questo esercizio come prima cosa viene controllato se esiste la variabile d’ambiente GREENIE, poi viene copiata in buffer ed infine controllato se la variabile modified è impostata al valore 0x0d0a0d0a. Essendo copiata la variabile dentro buffer, basterà impostare il payload all’interno della stessa.

kali@kali:~/Desktop/Protostar$ GREENIE=`perl -e 'print "A"x64 . "\x0a\x0d\x0a\x0d"'`





kali@kali:~/Desktop/Protostar$ export GREENIE
Sfida superata!
Sfida superata!

Stack3

Stack3.c
Stack3.c

In questo esercizio viene introdotta una piccola differenza, ossia quella di una seconda funzione oltre al main. Guardando il codice, si può notare che la funzione win in realtà non viene mai chiamata, infatti eseguendo il codice

Esecuzione di stack3
Esecuzione di stack3

oltre a prendere buffer esegue fp, che però non punta a nulla, essendo una B ripetuta quattro volte.

EIP punta a 0x42424242, indirizzo non esistente
EIP punta a 0x42424242, indirizzo non esistente

L’obiettivo in questo caso è quindi quello di riuscire a chiamare l’altra funzione, che ci permetterà di superare la sfida. Per farlo, ci serve sapere l’indirizzo di memoria in cui risiede win, tramite objdump

Analisi dell'eseguibile con objdump
Analisi dell'eseguibile con objdump

Ovviamente è possibile solo grazie al fatto che la randomizzazione degli indirizzi è disattivata, altrimenti questo cambierebbe ad ogni esecuzione.

Generiamo lo shellcode

Shellcode in python
Shellcode in python
Sfida superata!
Sfida superata!

Stack4

Stack4.c
Stack4.c

Aumentando la difficoltà, ora non c’è più nessuna chiamata ad un puntatore e si deve trovare un’altra strada. Quella più comoda in questo caso è andare a sovrascrivere direttamente il registro EIP, in modo che una volta inserito il buffer, invece che chiudere il programma, vada a chiamare la funzione win.

Struttura dello stack simile al nostro caso
Struttura dello stack simile al nostro caso

Per trovare l’offset tra il buffer e EIP ci può venire in aiuto il tool pattern_create (ma è possibile anche riempire di lettere casuali e indagare con GDB)

Per generare il pattern, il comando è il seguente

kali@kali:~/Desktop/Protostar$ /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 100<br>Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A

Lo vado poi ad inserire nel programma generando un segmentation fault, che andrò ad indagare tramite GDB

Segmentation fault casuato da un indirizzo di memoria non esistente
Segmentation fault casuato da un indirizzo di memoria non esistente

GDB avvisa che l’errore è che non può accedere all’indirizzo di memoria 0x41346341

Segmentation fault in GDB
Segmentation fault in GDB

Per trovare l’offset, ci basterà eseguire il tool _pattern_offset _con l’indirizzo trovato.

kali@kali:~/Desktop/Protostar$ /usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x41346341<br>[*] Exact match at offset 72

Ed ecco che, una volta conosciuta la lunghezza dell’offset, ci basterà usare nuovamente objdump per trovare l’indirizzo della funzione win e sovrascrivere EIP con quello.

Shellcode generato con python
Shellcode generato con python
Sfida superata!
Sfida superata!

Stack5

Stack5.c
Stack5.c

Bene, ora non abbiamo proprio nulla e dobbiamo trovare un’altra strada. Ce ne potrebbero essere diverse, ad esempio quella in cui, modificando il flusso del codice e sovrascrivendo EIP, iniettiamo una nuova funzione che stampa “You win”.

Per prima cosa, bisogna creare lo shellcode della funzione “You win” (utilizzerò lo stesso della soluzione #3 di questo articolo )

Per convertire il binario in esadecimale, si può usare uno script in bash, ad esempio

for i in $(objdump -d $1 -M intel |grep "^ " |cut -f2); do
<code>echo -n '\x'$i</code>
done;
echo
Conversione da eseguibile ad esadecimale
Conversione da eseguibile ad esadecimale

Dall’esercizio di prima sappiamo che l’offset è 72, il payload è lungo 40, quindi ci servono 32 caratteri NOP per sovrascrivere l’EIP.

Creazione dello shellcode
Creazione dello shellcode

In questo caso non ha funzionato perché alla fine del payload c’è un carattere \x0a, che interrompe l’esecuzione. Proviamo a cancellarli, aggiungere due NOP ed eseguire nuovamente

Sfida superata!
Sfida superata!

PS: l’indirizzo bffff2c4 è la posizione di partenza della variabile buffer nello stack. La si può trovare sia con GDB (inserendo un breakpoint e stampandola, ma attenzione che GDB setta delle variabili d’ambiente che spostano di poco i valori della memoria rispetto alla mera esecuzione sul terminale, oppure andando a modificare direttamente il codice e stampando l’indirizzo di buffer)

Trovare l'indirizzo di EBP
Trovare l'indirizzo di EBP

Stack6

Stack6.c
Stack6.c

In questo caso, sembra che tutte le tecniche di prima non possano essere eseguite. La condizione if a riga 17 controlla se l’indirizzo ret non inizi con 0xbf, e noi l’abbiamo sempre sovrascritto con indirizzi di quel tipo, proprio perché erano presenti nello stack. Infatti, se andiamo ad utilizzare la tecnica precedente

La tecnica precedente non funziona
La tecnica precedente non funziona

Finiamo quindi direttamente dentro la condizione e il programma termina.

Per bypassare il controllo useremo un’altra tecnica, ossia ret2libc. Questa tipologia ci può essere utile perché non andremo ad iniettare sull’EIP un indirizzo di memoria preso da una variabile come nell’esercizio precedente, ma andremo ad utilizzare chiamate di sistema che sono già in memoria al momento dell’esecuzione, come _system _ed exit. L’idea alla base è quindi quella di inserire i soliti NOP, aggiungere la chiamata a system, la chiamata ad exit ed infine la chiamata ad un comando già presente in memoria, come /bin/bash (per chi vuole approfondire consiglio questo paper).

Idea visiva di quello che andremo a fare
Idea visiva di quello che andremo a fare

Per ottenere questi tre indirizzi è sufficiente mandare in crash il programma ed analizzare il dump con GDB

Crash dell'eseguibile e creazione del dump
Crash dell'eseguibile e creazione del dump
Indirizzi di memoria che servono
Indirizzi di memoria che servono

Il comando _x/s *((char **)environ) _stampa l’indirizzo in memoria in cui risiedono le varie variabili d’ambiente.

Ora che li abbiamo tutti, costruiamo il nostro shellcode:

Shellcode in python
Shellcode in python
Sfida superata!
Sfida superata!

L’unione in pipe tra (cat shellcode6; cat) e stack6 è stata fatta poiché se eseguito come gli altri esercizi la shell si chiude immediatamente senza aspettare nessun comando.

Stack7

Stack7.c
Stack7.c

In quest’ultimo esercizio la differenza con il precedente è la condizione, in quanto ora verifica che ret non inizi con 0xb, quindi l’attacco ret2libc non funzionerà più.

Dobbiamo usare una tecnica diversa, ossia quella del Return Oriented Programming. È simile a ret2libc, ma oltre che iniettare le precedenti istruzioni, dovrà esserne iniettata un’altra, una chiamata ad un’istruzione RET, che ci permetterà di bypassare la condizione. Per trovare RET è sufficiente lanciare objdump e greppare l’istruzione.

RIcerca di istruzioni RET nell'eseguibile
RIcerca di istruzioni RET nell'eseguibile
Generazione dello shellcode
Generazione dello shellcode
Sfida superata!
Sfida superata!

Conclusioni

Siamo passati da una semplice modifica di una variabile a tecniche più avanate come ret2libc e ROP. Quest’ultime due ricordo che funzionano anche se lo stack non permette l’esecuzione, quindi anche senza l’opzione -z execstack.

Nei prossimi articoli andremo ad approfondire altre tecniche di Buffer Overflow, seguendo sempre gli esercizi di Protostar. Alcuni articoli che consiglio per approfondire la materia sono: