Moltissime persone sanno cosa è un exploit, ma chi si affaccia per la prima volta nel mondo della sicurezza informatica potrebbe non sapere nemmeno di cosa si tratti e per questo iniziamo a porre le basi spiegando prima di tutto cos'è un exploit.
Un exploit non è nient'altro che un programma scritto tipicamente in C (ma anche in altri linguaggi) che permette di sfruttare delle falle (le cosiddette vulnerabilità) di altri programmi per poter eseguire sul sistema del codice dannoso che in condizioni "normali" non dovrebbe essere possibile eseguire.
Gli attacchi di tipo buffer overflow rappresentano la stragrande maggioranza di tutti gli attacchi ai sistemi informatici in quanto le vulnerabilità del buffer overflow sono comuni e quindi relativamente facili da sfruttare.
Le vulnerabilità del buffer overflow sono la principale causa di intrusioni in sistemi informatici anche perché esse offrono all'attaccante la possibilità di eseguire codice dannoso. Il codice introdotto viene eseguito dal sistema con i privilegi del programma bucato e permette all'attaccante di poter controllare altri servizi dell'host vittima che gli interessa controllare.
Buffer e Stack
Il buffer è un blocco contiguo di memoria che risiede nella memoria del PC e che immagazzina dati relativi all'applicazione. In C un buffer viene chiamato array, questi ultimi possono essere dichiarati statici o dinamici: le variabili statiche sono allocate al momento del caricamento sul segmento dati, quelle dinamiche sono allocate al momento del caricamento sullo stack. L'overflow consiste nel riempire un buffer oltre il limite. Per la realizzazione di un exploit di buffer overflow vengono sfruttati gli array dinamici.
Organizzazione della memoria di un processo (Stack)
Per comprendere il funzionamento dei buffer sullo stack bisogna capire il sistema con cui il computer gestisce la memoria di un processo, questi processi sono divisi in tre parti, testo, dati e stack.
La regione testo è fissa e contiene il codice del programma in modalità di sola lettura, e per questa ragione un eventuale tentativo di scrittura provoca una violazione di segmento.
La regione dati contiene i dati inizializzati e non inizializzati, in questa parte vengono immagazzinate le variabili statiche. La dimensione di questa area può essere cambiata con la chiamata di sistema brk().
Cos'è lo Stack
Lo stack è un tipo di dato astratto. Uno stack di oggetti ha la proprietà Last In First Out (LIFO), cioè l'ultimo oggetto inserito è il primo ad essere rimosso. Le due operazioni principali sono push e pop: push aggiunge un elemento in cima allo stack e pop lo rimuove.
I linguaggi di programmazione moderni hanno il costrutto di procedura o funzione. Una chiamata a procedura altera il flusso di controllo come un salto (jump), ma, diversamente da un salto, una volta finito il proprio compito, una funzione ritorna il controllo all'istruzione successiva alla chiamata. Quest'astrazione può essere implementata con il supporto di uno stack.
Lo stack è usato anche per allocare dinamicamente le variabili locali usate nelle funzioni, per passare parametri alle funzioni e per restituire valori dalle stesse.
Uno stack è un blocco di memoria contiguo contenente dei dati. Un registro noto come stack pointer (SP) punta alla cima dello stack. La base dello stack è un indirizzo fisso. La dimensione è variata dinamicamente dal kernel. La CPU implementa le operazioni push e pop.
Lo stack consiste di un insieme di segmenti logici (stack frame) che vengono impilati sullo stack quando viene chiamata una funzione e spilati quando la funzione ritorna. Uno stack frame contiene i parametri della funzione, le sue variabili locali, i dati necessari per ripristinare il precedente stack frame, incluso l'indirizzo dell'istruzione successiva alla chiamata (contenuto nell'instruction pointer o IP). A seconda dell'implementazione, lo stack, cresce verso l'alto o il basso.
Oltre allo stack pointer si ha anche un frame pointer (FP) che punta ad una locazione fissa nel frame, noto anche come base pointer (BP). Le variabili locali possono essere referenziate specificando l'offset dallo stack pointer. Tuttavia, poiché nuovi dati sono impilati e spilati, tale offset cambia. Quindi si usa referenziare le variabili locali e i parametri con un offset da FP.
La prima cosa che fa una procedura quando viene chiamata, è salvare il FP precedente (per poterlo ripristinare al ritorno). Poi copia SP su FP per creare il nuovo FP ed incrementa SP per puntare allo spazio riservato per la successiva variabile locale. Questo codice è il prologo della procedura. Al momento dell'uscita dalla procedura, lo stack deve essere ripulito. Le funzioni delle CPU Intel per fare ciò sono ENTER e LEAVE.
Vulnerabilità di buffer overflow e attacchi
Il motivo di fondo di un attacco di buffer overflow è di prendere il controllo del programma eseguito e nel caso che il software abbia sufficienti privilegi di prendere il controllo dell'host stesso.
E' consuetudine per l'attaccante di scegliere un applicativo che "gira" come root per poter avere direttamente una shell di root sul sistema attaccato, questo è anche uno dei motivi per cui software come sendmail (che gira come root) sono stati tra i più utilizzati per attacchi di buffer overflow.
Fortunamente questo non è sempre possibile in quanto per poter ottenere una shell di root chi attacca il sistema deve preparare il codice da utilizzare, da far eseguire nello spazio d'indirizzamento del programma, permettere all'applicativo di saltare a quella porzione di codice con parametri esatti, caricati nei registri e nella memoria.
Modalità di utilizzo di Codice di Attacco
Per poter far eseguire codice di attacco al sistema ci sono due strade perseguibili:
- Sfruttare il codice esistente
- Inserire codice eseguibile
L'attaccante fornisce in input al programma una stringa che verrà caricata in un buffer, ad esempio in un'applicazione web tramite POST da form di questa stringa o tramite il metodo di GET
La stringa inviata ovviamente non sarà casuale ma conterrà delle istruzioni per la CPU del sistema vittima usando i buffer del software attaccato.
Vantaggi di questo sistema:
- Il buffer può essere localizzato ovunque (Stack, Heap area dati statici)
- Non serve nessun buffer overflow in quanto quantità sufficienti di dati possono essere passati al programma senza oltrepassare il limite del buffer.
Il secondo sistema prevede che il codice sia già adatto ad ottenere ciò che si vuole ed è già presente nello spazio di indirizzamento del software.
A questo punto chi attacca deve solo parametrizzare il codice affinchè il programma esegua la parte di codice desiderata ed atta ad offendere l'host e il software vittima.
Questi sistemi per essere attuati si basano sulla modifica del flusso del programma in modo da consentire il salto al codice d'attacco.
Il metodo per ottenere questo è di sfruttare falle nelle applicazioni dovute al mancato controllo sulla lunghezza nei parametri di input con il risultato di "rompere" il buffer dove l'attaccante potrà sovrascrivere la parte adiacente allo stato del programma con una a propria scelta.
Tecnica tipica di attacco
La tecnica più utilizzata per eseguire un exploit di buffer overflow consiste nell'individuare una variabile automatica soggetta a possibile overflow e invia al programma una stringa molto grande in grado di far tracimare il buffer per poter cambiare il record di attivazione.
Con questo sistema l'iniezione del codice maligno e la corruzione del buffer non avvengono contemporaneamente.
L'attaccante può iniettare il codice in un buffer senza farlo andare in overflow e far tracimare un altro buffer corrompendo il puntatore di quest'ultimo e farlo puntare al codice maligno.
Il buffer che andrà in overflow non deve avere controlli sulla sua dimensione, altrimenti sarà possibile farlo traboccare solo di pochi byte. In questo caso il codice risiederà in un buffer sufficientemente grande e che sarà richiamato dal buffer in overflow.
Se l'attaccante sta cercando di utilizzare codice già presente deve parametrizzare il codice per utilizzarlo ai suoi scopi.