Capire il Buffer Overflow: Un’Indagine Sulla Sicurezza Informatica
Il buffer overflow rappresenta uno dei bachi di programmazione più antichi e problematici, rimanendo ancora oggi una delle vulnerabilità più comuni e questo articolo mira a illustrarne le basi, fornendo un esempio e discutendo come gli sviluppatori possono proteggersi da questa tipologia di attacchi.
Cos’è un Buffer Overflow?
Il buffer overflow è un tipo di vulnerabilità che si verifica quando scriviamo più dati in un buffer di quanto ne possa contenere. Questo può causare la sovrascrittura di dati in zone di memoria adiacenti, comportando comportamenti imprevisti, inclusa l’esecuzione di codice arbitrario.
Ecco come funziona in dettaglio:
1. Un programma, durante la sua esecuzione, riserva delle aree di memoria chiamate buffer per memorizzare temporaneamente dei dati. Questi buffer hanno una dimensione fissa, determinata al momento della creazione.
2. Se un attaccante riesce a inviare più dati di quanti il buffer possa contenere, i dati in eccesso possono sovrascrivere aree di memoria adiacenti al buffer.
3. In alcuni casi, un attaccante potrebbe essere in grado di sfruttare un buffer overflow per eseguire codice arbitrario. Questo è particolarmente pericoloso, poiché potrebbe permettere all’attaccante di prendere il controllo completo del sistema.
Un Esempio di Buffer Overflow
Consideriamo il seguente programma C:
#include <string.h> void funzione_vulnerabile(char *arg) { char buffer[100]; strcpy(buffer, arg); } int main(int argc, char **argv) { funzione_vulnerabile(argv[1]); return 0; }
Questo programma è vulnerabile agli attacchi di buffer overflow perché utilizza la funzione `strcpy()` per copiare l’argomento `arg` nel buffer senza controllare la lunghezza dell’argomento. Se l’argomento è più lungo di 100 caratteri, i caratteri in eccesso sovrascriveranno la memoria adiacente al buffer.
L’Analisi del Buffer Overflow
Gli strumenti come GDB (GNU Debugger) e Valgrind possono essere utilizzati per analizzare il comportamento di un programma durante un overflow di buffer. Ad esempio, puoi utilizzare GDB per impostare un breakpoint all’interno della funzione vulnerabile e osservare come cambia la memoria quando viene fornito un input troppo grande.
Supponiamo che tu abbia compilato il codice C vulnerabile con il comando gcc con l’opzione -g, che include le informazioni di debug:
gcc -g -o programma_vulnerabile programma_vulnerabile.c
Per eseguire il programma all’interno di GDB, potresti utilizzare il comando seguente:
gdb ./programma_vulnerabile
Una volta all’interno di GDB, potresti impostare un breakpoint all’interno della funzione vulnerabile, ad esempio con il comando
break funzione_vulnerabile
Quindi, potresti eseguire il programma con un argomento di tua scelta, ad esempio con
run AAAA
A questo punto, il programma dovrebbe fermarsi al breakpoint che hai impostato.
Per generare un input di lunghezza arbitraria può essere utilizzato Python . Ad esempio, il seguente script Python genera una stringa di 200 caratteri ‘A’:
python print('A' * 200)
Questo input può poi essere passato al programma per osservare cosa succede quando si verifica un overflow di buffer.
Potresti quindi utilizzare comandi come next per eseguire il programma un passo alla volta, e print o x per esaminare il contenuto della memoria.
Impostazione di un breakpoint
Un breakpoint è un punto nel tuo codice dove desideri che l’esecuzione si fermi. Puoi impostare un breakpoint utilizzando il comando “break”. Ad esempio:
break funzione_vulnerabile
Eseguire il codice fino al breakpoint
Una volta impostato il breakpoint, puoi eseguire il codice fino a quel punto utilizzando il comando “run” seguito dagli argomenti del tuo programma:
run ARGOMENTI
Analisi della memoria: dopo che il tuo programma si è fermato ad un breakpoint, puoi esaminare il contenuto della memoria con il comando “x”. Ad esempio, il seguente comando visualizza 20 indirizzi di memoria a partire dall’indirizzo specificato:
x/20x $esp
Modificare il contenuto della memoria: Puoi utilizzare il comando “set” per modificare il contenuto della memoria. Ad esempio, il seguente comando imposta il valore dell’indirizzo di memoria specificato a 0:
set {int}0x804a000 = 0
GDB è uno strumento potente con molte funzionalità avanzate, ed il suo uso va oltre queste funzioni di base. La documentazione ufficiale di GDB è una grande risorsa per apprendere di più sulle sue capacità.
Nell’ambito di un overflow di buffer, gli attaccanti spesso sovrascrivono l’indirizzo di ritorno di una funzione per far puntare al loro codice dannoso (o shellcode). Questo indirizzo di ritorno è memorizzato sulla stack frame della funzione, vicino al buffer che si sta cercando di far andare in overflow.
Per trovare l’indirizzo di ritorno, si può eseguire il programma con GDB e impostare un breakpoint in un punto del programma dove si può ispezionare lo stack. Una volta raggiunto il breakpoint, è possibile utilizzare il comando info frame per visualizzare le informazioni sulla stack frame corrente, compreso l’indirizzo di ritorno.
Ad esempio, si può fare qualcosa del genere:
(gdb) break nome_funzione (gdb) run (gdb) info frame
Questo ti mostrerà un’output simile al seguente:
Stack level 0, frame at 0x7fffffffdf60: rip = 0x5555555547ed in nome_funzione (source.c:16); saved rip = 0x555555554816 called by frame at 0x7fffffffdf90 source language c. Arglist at 0x7fffffffdf50, args: Locals at 0x7fffffffdf50, Previous frame's sp is 0x7fffffffdf60 Saved registers: rip at 0x7fffffffdf58
Nell’output sopra, saved rip è l’indirizzo di ritorno che verrà utilizzato quando la funzione corrente termina. Nel caso di un overflow di buffer, questo è l’indirizzo che vorrai sovrascrivere con l’indirizzo del tuo shellcode.
Tuttavia, ci sono molte tecniche di protezione che possono rendere difficile o impossibile utilizzare questo tipo di attacco su un sistema moderno, come il DEP (Data Execution Prevention), l’ASLR (Address Space Layout Randomization) e le stack canaries.
Ricorda, è importante utilizzare queste informazioni in modo responsabile e solo in un contesto legale e sicuro.
A questo punto abbiamo bisogno di creare lo shellcode da lanciare e da inserire in memoria al posto giusto.
Lo shellcode è un pezzo di codice utilizzato durante l’exploit di una vulnerabilità per fornire un payload che viene eseguito nel contesto del processo compromesso. Normalmente, lo shellcode è scritto in assembly e deve essere molto compatto; a scopo didattico, di seguito è possibile vedere lo shellcode che esegue una chiamata di sistema `execve` per lanciare `/bin/sh`, che apre una shell. L’esempio è scritto in il linguaggio assembly. L’implementazione corretta dipende dall’architettura del sistema operativo, ma ecco un esempio molto basilare per un sistema Linux x86-64:
section .text global _start _start: xor eax, eax push eax push 0x68732f2f6e69622f mov ebx, esp push eax push ebx mov ecx, esp mov al, 11 int 0x80
Lo shellcode è una sequenza di istruzioni di codice macchina che l’exploit inietta in un programma in esecuzione. Nel contesto di un buffer overflow, lo shellcode viene spesso utilizzato per sovrascrivere l’indirizzo di ritorno di una funzione e reindirizzare il flusso di controllo del programma.
Questo shellcode può essere convertito in una sequenza di byte utilizzando un assembler, come `nasm`, e quindi esaminato con `objdump`:
nasm -f elf32 -o shellcode.o shellcode.asm ld -m elf_i386 -o shellcode shellcode.o objdump -d shellcode
Il risultato sarà una sequenza di byte che rappresenta lo shellcode, che può essere iniettata in un programma vulnerabile. Tuttavia, lo shellcode dovrà evitare determinati caratteri (ad esempio, null bytes) che potrebbero interrompere la stringa iniettata. Inoltre, lo shellcode dovrà essere posizionato in un luogo prevedibile in memoria, che potrebbe richiedere ulteriori tecniche di exploit.
L’operazione di sovrascrittura dell’indirizzo di ritorno con l’indirizzo del tuo shellcode è la fase critica dell’exploit.
Per farlo, devi innanzitutto conoscere l’esatto offset tra l’inizio del buffer e l’indirizzo di ritorno e come visto prima può essere fatto sperimentalmente, ad esempio inserendo una lunga sequenza di caratteri nel buffer e vedendo dove si verifica il crash del programma.
Una volta conosciuto l’offset, devi organizzare l’input al programma in modo tale da riempire il buffer fino all’indirizzo di ritorno, e poi inserire l’indirizzo del tuo shellcode. L ‘indirizzo deve essere inserito nel formato corretto, cioè in ordine inverso di byte (little endian) a causa del modo in cui i processori x86 leggono i numeri.
Supponiamo che l’offset sia di 100 byte e che il tuo shellcode sia localizzato all’indirizzo `0x7fffffffdf40`; , supponendo che stai utilizzando la shell bash per passare l’input al tuo programma, il comando per eseguire l’exploit potrebbe apparire così:
./vulnerable_program $(python -c ‘print “A”*100 + “\x40\xdf\xff\xff\xff\x7f”‘)
Questo comando utilizza Python per generare una stringa di 100 caratteri ‘A’, seguita dall’indirizzo del tuo shellcode in formato little endian. Il tutto viene passato al tuo programma come input.
Proteggersi dai Buffer Overflow
Per prevenire i buffer overflow, gli sviluppatori devono adottare buone pratiche di programmazione e considerare l’utilizzo di strumenti di protezione. Ecco alcune tecniche che possono aiutare a prevenire i buffer overflow:
Controllo dell’input: Gli sviluppatori dovrebbero sempre controllare la lunghezza e il formato degli input prima di utilizzarli, per evitare di scrivere oltre la fine di un buffer.
Utilizzo di funzioni sicure: Alcune funzioni di libreria, come `strcpy()`, sono note per essere vulnerabili ai buffer overflow. Esistono versioni più sicure di queste funzioni, come `strncpy()`, che consentono agli sviluppatori di specificare la lunghezza massima della stringa di destinazione.
Canary Stack: Questa tecnica implica l’aggiunta di un “canary” (un valore noto e non prevedibile) tra il buffer e il controllo del flusso di dati sullo stack. Se un buffer overflow avviene, il canary verrà sovrascritto, il che può essere rilevato e il programma può essere interrotto prima che il controllo del flusso venga alterato.
Address Space Layout Randomization (ASLR): Questa tecnica rende più difficile per un attaccante trovare indirizzi di memoria utilizzabili nel processo di exploit rendendo casuale la disposizione dello spazio degli indirizzi di un processo.
Non-Executable Stack: Molti attacchi di buffer overflow tentano di eseguire codice iniettato sullo stack. Rendere lo stack non eseguibile può prevenire questi attacchi.
Data Execution Prevention (DEP): Questa tecnica, nota anche come NX (No Execute), può prevenire l’esecuzione di codice in regioni di memoria non autorizzate.
Ricordate, il modo più efficace per prevenire i buffer overflow è scrivere codice sicuro. Anche se gli strumenti di protezione possono aiutare, non possono prevenire tutte le forme di attacco e non sono un sostituto per buone pratiche di programmazione.
Sono amante della tecnologia e delle tante sfumature del mondo IT, ho partecipato, sin dai primi anni di università ad importanti progetti in ambito Internet proseguendo, negli anni, allo startup, sviluppo e direzione di diverse aziende; Nei primi anni di carriera ho lavorato come consulente nel mondo dell’IT italiano, partecipando attivamente a progetti nazionali ed internazionali per realtà quali Ericsson, Telecom, Tin.it, Accenture, Tiscali, CNR. Dal 2010 mi occupo di startup mediante una delle mie società techintouch S.r.l che grazie alla collaborazione con la Digital Magics SpA, di cui sono Partner la Campania, mi occupo di supportare ed accelerare aziende del territorio .
Attualmente ricopro le cariche di :
– CTO MareGroup
– CTO Innoida
– Co-CEO in Techintouch s.r.l.
– Board member in StepFund GP SA
Manager ed imprenditore dal 2000 sono stato,
CEO e founder di Eclettica S.r.l. , Società specializzata in sviluppo software e System Integration
Partner per la Campania di Digital Magics S.p.A.
CTO e co-founder di Nexsoft S.p.A, società specializzata nella Consulenza di Servizi in ambito Informatico e sviluppo di soluzioni di System Integration, CTO della ITsys S.r.l. Società specializzata nella gestione di sistemi IT per la quale ho partecipato attivamente alla fase di startup.
Sognatore da sempre, curioso di novità ed alla ricerca di “nuovi mondi da esplorare“.
Comments