Cosa è il buffer overflow?

Il problema del Buffer Overflow è uno dei più “antichi” ma attuali legati al mondo dell’informatica.

Considerando i sistemi operativi più diffusi sia per Personal Computer e Server che per dispositivi mobile i 3 linguaggi di programmazione più diffusi sono:

  1. Java
  2. C
  3. C++

Ecco alcuni Sistemi “critici” che utilizzano software realizzato in C/ C++:

  • X Windows Server, shell
  • Microsoft IIS, Apache httpd, nginx (99% dei siti web funzionano grazie a questi Web Server)
  • Sistemi embedded utilizzati nelle automobili , nei sistemi di controllo industriale e anche su Mars Rover

Il primo attacco basato su buffer overflow è stato realizzato nel 1988 (Morris Worm), l’ultimo forse oggi. Nel 2014 è stata scoperta una vulnerabilità basata su buffer overflow presente nel software X11 Server da 23 anni!

Questo il grafico fornito da https://web.nvd.nist.gov relativo alle vulnerabilità basate su buffer overflow:

Vulnerabilità Buffer Overflow

Come riportato da wikipedia:

https://it.wikipedia.org/wiki/Buffer_overflow

“buffer overflow è una condizione di errore che si verifica a runtime quando in un buffer di una data dimensione vengono scritti dati di dimensioni maggiori. Quando questo accade viene sovrascritta parte della zona di memoria immediatamente adiacente al buffer in questione, con diversi effetti possibili a seconda di dove è situato il buffer e di come è organizzata la memoria in quella particolare piattaforma software; in alcuni programmi software questo provoca delle vulnerabilità di sicurezza”

In generale il buffer overflow è un problema che affligge i sistemi realizzati con linguaggi di programmazione “a basso livello” come C e C++. La maggior parte dei software scritti in altri linguaggi di programmazione in una situazione simile al buffer overflow terminano in modo anomalo senza generare problemi di sicurezza. In alcune situazione l’attaccante può:

  • Rubare informazioni presenti in memoria
  • Corrompere informazioni presenti in memoria
  • Eseguire codice indesiderato

Configurazione della memoria di un computer utilizzabile da un processo

Ad ogni processo viene fornita uno spazio di memoria dal sistema operativo. Nei sistemi a 32bit la dimensione massima fornita ad un processo è di 4 GB (da 0x00000000 a 0xffffffff):

Memory-Layout

  • La parte bassa (a partire dall’indice 0x00000000) è dedicata alle istruzioni che quel processo può eseguire
  • Successivamente c’è uno spazio per le costanti e le variabili globali/statiche inizializzate e non inizializzate
  • In seguito c’è l’area dedicata all’Heap che è utilizzata per la gestione delle variabili dinamiche dichiarate dal programmatore nel codice sorgente. In C/C++ l’utilizzo dell’heap è a gestione del programmatore che tramite delle funzioni di malloc(), calloc() e realloc() richiede dello spazio di memoria per le sue variabili oppure utilizza la free() per liberarla.
  • Poi c’è lo stack utilizzato dalle funzioni e dalle variabili locali alle funzioni. Utilizza il comportamento LIFO e d è gestito dal sistema operativo. Questo spazio viene allocato dinamicamente a runtime e dipende da come viene utilizzato il sistema. Quindi ad ogni esecuzione i dati potrebbero trovarsi in posizioni diverse. Ogni locazione dello stack è di 4 Bytes. Le operazioni principali sono Push e Pop che aggiungono o eliminano un elemento nella “pila”. L’accesso agli elementi dello stack (in lettura e scrittura) avvengono grazie agli indirizzi virtuali riportati in alcuni registri.

Lo stack e heap crescono in direzioni opposte:

  • lo stack dall’alto (0xffffffff) verso il basso
  • l’heap dal basso verso l’alto

Il processo per poter utilizzare la memoria necessita di alcuni registri, che contengono delle locazioni di memoria (da 0x00000000 a 0xffffffff)

  • %eip = Instruction Pointer (che punta all’istruzione corrente)

Funzionamento dello stack

Supponiamo di avere una funzione che utilizza 2 argomenti:

void funzione(int arg1, int arg2)

{

int val1;

}

lo stack sarà configurato così:

… – val1 – %ebp – %eip – arg1 – arg2 – … 0xffffffff

questo rappresenta lo “Stack Frame” di funzione.
Per poter accedere alle informazioni presenti nel frame il processo usa i registri:

  • %ebp = Frame Pointer (Base Pointer) che è la locazione dove inizia lo Stack Frame di una funzione
  • %esp = Stack Pointer. La parte fine dello stack, dove possono essere inseriti altri dati.

Ad esempio nel momento in cui il processo deve accede a va1 utilizza = Frame Pointer – 8 Byte

Quando viene chiamata una funzione:

  1. Vengono inseriti gli argomenti nello stack (in ordine inverso)
  2. Inserisce nello stack l’indirizzo di ritorno cioè l’indirizzo dell’istruzione che deve essere eseguita nel momento in cui l’esecuzione della funzione termina
  3. Viene effettuato un “jump” all’indirizzo di memoria dove si trova la funzione nello stack

La Funzione chiamata:

  1. Viene inserito nello stack il vecchio %edp
  2. Viene impostato l’attuale %edp alla fine dello stack (%esp)
  3. Vengono inserite nello stack le variabili locali alla funzione

Per ritornare alla funzione che ha chiamato quella attuale:

  1. Viene reimpostato lo stack frame: %esp = %ebp, %ebp= (%ebp)
  2. Viene effettuato un jump all’indirizzo di ritorno: %eip = 4($esp)

Come avviene un buffer overflow

Per effettuare un buffer overflow basta una delle molte funzioni che riesco a scrivere direttamente nelle variabili presenti nello stack, ad esempio strcpy(). In questo esempio una mancata validazione dell’input di una funzione, genera un buffer overflow

#include <stdio.h>
#include <string.h>

void func(char *arg1){
   int autenticato=0;
   char buf[4];
   strcpy(buf,arg1);
   …
      if (autenticato){
      …
     }
}

int main(){
   char *str=”Autenticami!”;
   func(str);
}

La funzione strcpy scrive il contenuto ricevuto in input nella locazione della variabile buf[4] che è troppo piccola e per questo motivo viene sovrascritto il contenuto della variabile autenticato e del registro %ebp

buffer 4 byte – autenticato 4 byte – %ebp – %eip – &arg1 -> aute – ntic – ami! – %eip – &arg1

Un attacco di solito è suddiviso nelle seguenti fasi:

  1. Scrivere il codice malevolo da eseguire in memoria: Il codice malevolo deve essere già in formato assembler per quell’architettura e non deve contenere sequenze di 0 altrimenti funzioni come sprintf, gets e scanf non lo copieranno. Di solito il codice che si fa eseguire è una shell, in particolare viene chiamato shellcode.
  2. Capire dove è stato scritto il codice malevolo: un approccio può essere quello di cercare in differenti locazioni limitando le locazioni a quelle in prossimità di una conosciuta oppure riempendo parte della memoria con dei “nop” che faranno scorrere l’esecuzione fino all’inizio del codice malevolo
  3. Fare puntare %eip nella locazione dove è scritto il codice malevolo da eseguire: Per farlo è necessario sovrascrivere il contenuto di %eip con l’indirizzo dove è stato memorizzato il codice da eseguire.

Questo tipo di attacco viene chiamato stack smashing. Il primo ad utilizzarlo fu Aleph One nel 1996.

Altri tipi di attacco basati su buffer overflow sono Heap Overflow, Integer Overflow, Read Overflow

Uno dei più famosi attacchi basati su buffer overflow è stato HeartBleed (CVE-2014-0160)

Author