2 - Vulnerabilità del Buffer Overflow e Attacchi

 

Lo scopo complessivo di un attacco di buffer overflow è di sovvertire la funzione di un programma privilegiato in modo che l'attaccante possa prendere il controllo di quel programma, e, se esso è sufficientemente privilegiato, controllare l'host. Tipicamente l'attaccante sceglie come "vittima" programmi di root e immediatamente esegue codice simile a exec(sh) per ottenere una shell con i privilegi di root; ma non sempre!

Per ottenere questo risultato l'attaccante deve:

  1. Predisporre il codice adatto, da eseguire nello spazio d'indirizzamento del programma.
  2. Permettere al programma di saltare a quel codice, con parametri esatti, caricati nei registri e nella memoria.

Gli attacchi di questo tipo sono organizzati in termini dei punti 1. e 2.

Possiamo formalizzare quanto detto, con la seguente figura:

 

Figura 1 - Stack violato

 


 

2.1 - Come utilizzare Codice di Attacco

 

Ci sono due modi per realizzare tutto ciò ai danni di un programma vittima: iniettare codice eseguibile, oppure usare quello già esistente.

 

Iniettare il codice:

L'attaccante fornisce una stringa in input al programma che lo stesso carica in un buffer. La stringa contiene bytes che sono istruzioni della CPU che è stata appena attaccata. Qui l'attaccante sta usando i buffers del programma vittima per memorizzarvi il codice d'attacco.

Alcune sfumature di questo metodo:

·        L'attaccante non ha bisogno di far traboccare nessun buffer per fare questo; quantità sufficienti di dati possono essere iniettati all'interno di un buffer senza eccedere il limite fisico della sua dimensione;

·        Il buffer può essere localizzato ovunque:

·        Sullo stack (variabili automatiche);

·        Sull'heap (malloc di variabili);

·        Nell'area dei dati statici (inizializzati e non);

 

Il codice è già lì:

Spesso, il codice adatto a fare ciò che l'attaccante vuole è già presente nello spazio di indirizzamento del programma. L'attaccante ha bisogno solo di parametrizzare il codice e quindi fare in modo che il programma salti ad esso. Per esempio, se il codice d'attacco deve eseguire exec(/bin/sh), ed esiste il codice in libc che esegue exec(arg) dove arg è una stringa puntatore all'argomento, allora l'attaccante ha bisogno solo di cambiare un puntatore verso /bin/sh e saltare all'appropriata istruzione nella libreria libc.


 

2.2 - Modifica del Flusso

 

Tutti questi metodi tendono ad alterare il controllo del flusso del programma in modo da permettergli di saltare al codice d'attacco. Il metodo base è di far traboccare un buffer che ha un debole o inesistente controllo sul limite dei suoi input, con il risultato di corrompere una parte adiacente allo stato del programma: puntatori adiacenti, etc.

Con la rottura del buffer, l'attaccante può sovrascrivere la parte adiacente allo stato del programma con un'arbitraria sequenza di byte. 

La classificazione è sul tipo di stato del programma che l'attaccante tenta di corrompere. In principio, lo stato corrotto può essere di qualsiasi tipo. Per esempio, il worm originale di Morris[2] usò un buffer overflow sul programma fingerd per corrompere il nome di un file che fingerd voleva eseguire. In pratica, molti buffer overflow tentano di corrompere  i puntatori, in particolare, l'indirizzo di ritorno della funzione attiva. I fattori che distinguono gli attacchi di buffer overflow sono il tipo di stato corrotto e dove nello stato della memoria esso è localizzato.

 

Record d'Attivazione:

Ogni volta che una funzione è chiamata, essa si colloca in un record di attivazione sullo stack che include, tra le altre cose, l'indirizzo di ritorno al quale il programma dovrà saltare quando la funzione termina. Corrompendo l'indirizzo di ritorno nel record di attivazione, l'attaccante causa il salto del programma al codice d'attacco quando la funzione vittima restituisce e dereferenzia l'indirizzo di ritorno. Questo tipo di buffer overflow è chiamato stack smashing attack (attacco che fracassa lo stack) e costituisce la maggioranza dei correnti attacchi di buffer overflow.

 

Puntatori a Funzione:

void (* foo)() - dichiara una variabile foo che è di tipo puntatore a funzione che ritorna void.

I puntatori a funzione possono essere allocati ovunque (stack, heap, area di dati statici) e quindi, l'attaccante, necessita solo di trovare un buffer adiacente al puntatore a funzione in una qualunque di queste aree e mandarle in overflow per cambiare il puntatore. Successivamente, quando il programma effettua una chiamata attraverso questo puntatore a funzione salterà alla locazione di memoria desiderata dall'attaccante.

 

Longjmp Buffer:

Il linguaggio C include un semplice sistema  di controllo del ritorno chiamato setjmp/longjmp. L'idioma è dire setjmp(buffer) al checkpoint, e longjmp(buffer) per ritornare al checkpoint. Comunque, se l'attaccante può corrompere lo stato del buffer, allora longjmp(buffer) salterà al codice di attacco. Come i puntatori a funzione, longjmp(buffer) può essere allocato ovunque, quindi, l'attaccante ha bisogno solo di trovare un buffer vulnerabile adiacente.


 

2.3 - Tecnica Tipica di Attacco

 

La più semplice e più comune forma d'attacco tramite buffer overflow combina la tecnica d'iniezione con la corruzione del record d'attivazione in un'unica stringa. L'attaccante localizza una variabile automatica soggetta ad overflow, fornisce al programma un'ampia stringa che contiene il codice di attacco da iniettare per far traboccare il buffer allo scopo di cambiare il record d'attivazione.

L'iniezione e la corruzione non avvengono in un'unica azione. L'attaccante può iniettare il codice in un buffer senza farlo traboccare, e far traboccare un buffer differente per corrompere il puntatore al codice. Questo è fatto tipicamente se il buffer che s'intende attaccare non ha controlli sulla taglia, ma se non è così, il buffer potrà essere fatto traboccare solo con un certo numero di byte. L'attaccante non ha posto per piazzare il codice nel buffer vulnerabile, allora il codice è semplicemente inserito in un buffer differente di taglia sufficiente.

Se l'attaccante sta cercando di usare codice già residente invece di iniettarlo, egli ha bisogno di parametrizzare il codice. Per esempio, ci sono frammenti di codice in libc che effettuano una chiamata a exec(qualcosa) dove qualcosa è un parametro. L'attaccante, quindi, usa il buffer overflow per corrompere l'argomento e un altro buffer overflow per corrompere il puntatore che, in libc, punta all'appropriato frammento di codice.

 


 

 

 

Previous Page

Next Page