Introduzione al buffer overflow

Quando si accetta un input esterno, un’applicazione deve allocare memoria per archiviare tale input. Molti linguaggi di programmazione di alto livello lo faranno dietro le quinte, ma alcuni linguaggi (come C / C ++) consentono al programmatore di allocare memoria direttamente tramite funzioni come malloc.

Si verifica una vulnerabilità di buffer overflow quando l’applicazione tenta di archiviare più dati nella memoria allocata di quanto non vi sia spazio disponibile. Ciò può verificarsi per vari motivi, tra cui:

  • Impossibilità a controllare la lunghezza dell’input durante la lettura
  • Essersi dimenticati di allocare spazio per il terminatore null
  • Lunghezze di input che causano un overflow di numeri interi

Indipendentemente dal motivo, se un’applicazione tenta di scrivere nella memoria oltre l’intervallo del buffer allocato significa che sta scrivendo nella memoria allocata per altri scopi all’interno dell’applicazione. A causa della struttura di come la memoria viene allocata all’interno di un computer, questo può essere estremamente utile per un utente malintenzionato poiché consente di controllare l’esecuzione del programma.

Sfruttamento del buffer overflow

Lo sfruttamento di una vulnerabilità di overflow del buffer è abbastanza semplice. Se un programma alloca in modo errato la memoria per l’input dell’utente o legge in modo non sicuro i dati in quello spazio di memoria, esiste una vulnerabilità. Questa vulnerabilità può essere sfruttata da un hacker semplicemente fornendo un input all’applicazione più grande di quanto il buffer allocato sia in grado di contenere.

Il travaso di un buffer con input insignificanti o casuali può causare semplicemente un errore di segmentazione o un errore nel programma. Tuttavia, la struttura dello stack porta a che un exploit buffer ben progettato può fare molto di più, consentendo a un utente malintenzionato di controllare il flusso di esecuzione ed eseguire codice dannoso sul sistema.

Lo stack

Un’applicazione può allocare memoria nello stack o nell’heap. Lo stack viene comunemente utilizzato per argomenti di funzione e variabili locali e l’heap memorizza la memoria dinamica (allocata utilizzando il nuovo comando in C ++). Sia lo stack che l’heap possono essere sfruttati da un attacco di buffer overflow, ma la struttura dello stack lo rende estremamente delicato.

Come suggerisce il nome, lo stack è organizzato come uno stack di memoria. Lo stack cresce da indirizzi di memoria elevati a indirizzi inferiori. La posizione corrente nello stack è indicata da una variabile (il puntatore dello stack) che punta alla parte superiore dello stack corrente. Man mano che i dati vengono aggiunti o rimossi dallo stack, anche il puntatore dello stack viene aggiornato.

Quando una funzione viene chiamata da un’altra funzione, le informazioni vengono inviate nello stack per fornire a quella funzione i dati che deve eseguire. Questi dati vengono inseriti nello stack nel seguente ordine:

  1. Argomenti della funzione chiamata (in ordine inverso)
  2. L’indirizzo dell’istruzione successiva dopo la funzione chiamata ritorna
  3. Variabili locali della funzione chiamata

In genere, l’input dell’utente a una funzione verrà archiviato in una variabile locale, il che significa che verrà inserito nello spazio di memoria direttamente sopra l’indirizzo di ritorno nello stack. Ciò è utile per un utente malintenzionato che esegue un buffer overflow, poiché la memoria che verrà sovrascritta da un buffer overflow è il puntatore all’istruzione successiva da eseguire.

Programmazione orientata al ritorno (ROP)

Il fatto che un utente malintenzionato possa sovrascrivere l’indirizzo di ritorno di una funzione nello stack è la base per la programmazione orientata al ritorno (ROP). In ROP, un utente malintenzionato tenta di sfruttare un overflow del buffer che causa il ritorno della funzione vulnerabile in un’area del programma sotto il controllo dell’attaccante.

Quest’area può essere lo stesso buffer riempito durante l’attacco o un’altra area controllata dall’utente. Se ha esito positivo, l’utente malintenzionato potrebbe essere in grado di guidare l’applicazione a interpretare l’input fornito come istruzioni del programma, consentendo all’utente malintenzionato di eseguire codice dannoso.

Una delle principali sfide di ROP è lo sviluppo di codice che fa ciò che l’attaccante vuole in uno spazio limitato. Per questo motivo, il codice generalmente cerca di chiamare funzioni di libreria che sono già all’interno dello spazio di memoria del processo per abbreviare il codice necessario. Alcune mitigazioni contro ROP si concentrano sul rendere questa funzionalità più difficile da localizzare ed eseguire per lo shellcode.

Mitigazioni del buffer overflow

Lo sfruttamento del buffer overflow può rappresentare una grave minaccia per la sicurezza poiché il codice ROP inserito ed eseguito dall’attaccante viene eseguito con gli stessi privilegi dell’applicazione sfruttata. Tuttavia, esistono diversi mezzi per prevenire o mitigare gli attacchi di buffer overflow.

Protezioni dello stack

L’obiettivo primario di un exploit buffer overflow è consentire all’attaccante di eseguire codice arbitrario tramite una programmazione orientata al ritorno. Diverse soluzioni sono state implementate per aiutare a proteggere contro il POR.

Stack canaries

Affinché ROP sia possibile nello stack, l’utente malintenzionato deve essere in grado di riscrivere un indirizzo di ritorno della funzione per puntare a una regione di memoria sotto il loro controllo. Il concetto è stato inventato per aiutare a rilevare e prevenire questo. 

Uno stack canary è un valore noto al programma che viene inserito prima dell’indirizzo di ritorno nello stack. Prima che ritorni una funzione, viene verificato il valore del canary e viene generato un errore se non è corretto (indicando che si è verificato un attacco di overflow del buffer).

Prevenzione dell’esecuzione dei dati

La programmazione orientata al ritorno si basa sull’input dell’utente interpretato dal programma per essere interpretato come dati da interpretare come codice. Ciò è possibile perché i dati e le informazioni di controllo sono spesso intrecciati nello stack senza confini chiari.

Data Execution Prevention (DEP) contrassegna alcune aree della memoria come non eseguibili. Questo aiuta a proteggere dal buffer overflow poiché, anche se l’attaccante può modificare un indirizzo di ritorno per puntare al proprio shellcode, non verrà eseguito dal programma. Tuttavia, DEP può essere aggirato da un attacco return-to-libc, rendendo necessaria la randomizzazione del layout dello spazio degli indirizzi (ALSR).

Randomizzazione del layout dello spazio degli indirizzi

La maggior parte delle applicazioni sono progettate per essere orientate agli oggetti, con applicazioni che fanno un uso pesante di librerie condivise che importano nel loro spazio di memoria. Mentre le funzioni di queste librerie condivise sono utili per legittimare il codice, sono utili anche per ROP.

La randomizzazione del layout dello spazio degli indirizzi (ASLR) è progettata per rendere più difficile per un utente malintenzionato trovare le funzioni di libreria di cui ha bisogno. Invece di importare le funzioni di libreria per impostare gli indirizzi in ogni applicazione, ASLR randomizza dove verrà importata una particolare libreria. Ciò rende il POR più difficile, poiché l’aggressore deve trovare una libreria in memoria prima di poter utilizzare le sue funzioni.

Convalida dell’input

Gli attacchi di buffer overflow sono causati quando un utente malintenzionato scrive più dati su un blocco di memoria rispetto allo spazio allocato dall’applicazione per tali dati. Ciò è possibile per una serie di motivi, ma il più comune è l’uso di letture illimitate che leggono fino a quando non viene trovato un terminatore nullo sull’input. Utilizzando letture a lunghezza fissa progettate per adattarsi allo spazio buffer allocato, un’applicazione può essere resa immune dagli attacchi di buffer overflow.

Controllo di overflow dell’intero

I buffer overflow possono anche essere abilitati da vulnerabilità di overflow dei numeri interi. Ciò si verifica quando la lunghezza del valore memorizzato in una variabile è maggiore di quella che la variabile può contenere, causando la caduta dei bit più significativi che non si adattano. Di conseguenza, un input molto grande può essere interpretato come avente una lunghezza più breve a causa di un overflow, causando il dimensionamento del buffer allocato. 

Il controllo degli overflow dei numeri interi nelle lunghezze di input è importante per la protezione dagli attacchi del buffer overflow.

Librerie standard

Mentre C ++ consente a uno sviluppatore di allocare manualmente la memoria per l’input dell’utente, ciò non significa che sia sempre una buona idea farlo. La libreria di modelli standard C ++ (STL) ha funzioni (come le stringhe) che gestiscono correttamente la gestione della memoria dietro le quinte. Il passaggio è un modo semplice per mitigare la minaccia di vulnerabilità di buffer overflow.

Buffer overflow per l’hacking etico

I buffer overflow sono una semplice vulnerabilità che può essere facilmente sfruttata ma anche risolta facilmente. Tuttavia, anche oggi, il software contiene vulnerabilità sfruttabili di buffer overflow. Nell’ottobre 2018, in Whatsapp è stata scoperta una vulnerabilità di buffer overflow che ha consentito l’uso della vulnerabilità se un utente aveva appena risposto a una chiamata o una videochiamata. 

Queste vulnerabilità meritano sicuramente una verifica quando si esegue un hack etico e devono essere corrette nel codice il più rapidamente possibile.