Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Tecniche: Buffer Overflow

Buffer Overflow: cosa è e come si ottiene. Analisi approfondita del principale sistema di attacco di un sistema operativo. Esempi pratici e consigli per proteggersi
Buffer Overflow: cosa è e come si ottiene. Analisi approfondita del principale sistema di attacco di un sistema operativo. Esempi pratici e consigli per proteggersi
Link copiato negli appunti

L'overflow del buffer è una delle tecniche più avanzate
di hacking del software: se utilizzato a dovere può agevolare
l'accesso a qualsiasi sistema che utilizza un programma vulnerabile.
Il termine (abbreviato in BOF) capita talmente spesso sugli schermi
dei nostri computer che probabilmente ci suona ormai familiare. Buona
parte degli exploit che troviamo sui siti specializzati
infatti sfruttano diverse varianti dell'overflow per raggiungere i loro
scopi; questo articolo si prefigge di far capire i meccanismi che ne
regolano il funzionamento tramite esempi pratici.

Prima di tutto, consideriamo una semplice definizione: parliamo di buffer
overflow
quando una stringa di input è più grande
del buffer (memoria) che la dovrà contenere. Questo comporta
un trabocco (overflow) che finisce per sovrascrivere porzioni
di memoria destinate ad altre istruzioni. Dato che nessun sistema è
immune, questa tecnica può colpire senza alcuna differenza le
applicazioni di Linux e Windows.

Durante l'esecuzione di un programma, le funzioni hanno la necessità
di archiviare i dati che sono oggetto dell'elaborazione. La zona di
memoria fornita dal sistema per salvare i dati è detta stack
(pila). Immaginiamo lo stack come un contenitore.

Quando è necessario, lo stack assume le dimensioni richieste
dalla funzione per contenere i dati. Può capitare che lo spazio
non sia sufficiente: se la funzione non si accorge dell'errore, i dati
vengono memorizzati comunque finendo per sovrascrivere e corrompere
lo stack. Ecco dunque una attività ricorrente negli attacchi
al buffer: colpire lo stack, in inglese smashing the stack.

Come vedremo in seguito, nel linguaggio di basso livello Assembler
esistono dei puntatori che definiscono l'andamento dell'applicazione.
Quando una funzione richiama quella successiva, il puntatore alla prima
funzione viene salvato nello stack sotto forma di indirizzo, in modo
che il programma possa ritornare alla funzione principale.

Fig. 1.1 - Stack overflow
stack

Lo scopo dell'overflow dello stack è proprio di
sovrascrivere questo indirizzo di ritorno (Fig. 1.1). Dato che, come abbiamo
detto, il flusso di dati trabocca oltre lo spazio definito dallo stack,
anche il puntatore viene corrotto e sostituito dal codice preparato ad
hoc dal cracker.

Per poter capire a fondo gli effetti dell'overflow è necessario
introdurre alcune nozioni sull'architettura dei processori Intel. Abbiamo
già parlato dello stack, raffigurato come un contenitore
dove possiamo memorizzare temporaneamente delle informazioni per poi estrarle
quando ne abbiamo bisogno. È molto importante avere un puntatore
che tiene traccia della posizione dei dati immessi; a questo proposito
vediamo quali registri vengono utilizzati dal sistema:

  • EBP
    dello stack, serve per definire dove iniziano i dati memorizzati nello
    stack;
  • ESP
    nello stack, serve a inserire o estrarre dati nella posizione desiderata;
  • EIP
    da eseguire, cioè a quella successiva rispetto alla posizione
    corrente.

La conoscenza delle istruzioni di base dell'Assembler
essenziale per capire gli esempi che seguono. Ecco quelle più utili
alla nostra "causa":

  • PUSH
  • POP
    LIFO, gli ultimi dati aggiunti sono i primi ad essere rimossi ->
    Last In First Out
  • CALL
    e inserisce l'indirizzo dell'istruzione successiva (EIP) nello stack;
  • RET
    per ritornare alla funzione principale
  • Ogni routine ha inizio con un CALL e termina con un RET. Se l'aggressore
    è riuscito a sovrascrivere l'indirizzo di ritorno, nella fase di
    RET può ottenere il totale controllo del processore.

    Dentro il codice

    Esaminiamo una semplice applicazione in C che utilizza una funzione vulnerabile
    ad attacchi di overflow del buffer (Tab. 2.1).

    01
    void func(void)
    02
    {
    03
    char bof[20];
    04
    gets(bof);
    05
    }

    Tab 2.1 - Funzione vulnerabile

    Nel listato troviamo la definizione di una variabile bof
    tipo char
    e la chiamata alla funzione gets
    l'input dell'utente e salvarlo nella variabile. Quando viene invocata
    la funzione (CALL), il processore salva in memoria il valore contenuto
    in EIP; in questo modo, dopo il RET può riprendere l'esecuzione
    dall'istruzione immediatamente successiva alla chiamata.

    Una porzione del codice Assembler corrispondente (Tab. 2.2) mostra come
    avviene invece il salvataggio del puntatore EBP (che, ricordiamo, si riferisce
    alla base dello stack utilizzato finora) e l'allocazione dello spazio
    destinato alle variabili.

    01
    push ebp
    02
    mov ebp,
    esp
    03
    sub esp,
    20

    Tab 2.2 - Codice Assembler

    01. Le istruzioni della prima riga salvano il valore del registro EBP
    nello stack;
    02. La posizione attuale nello stack (ESP) viene memorizzata in EBP, così
    da diventare la nuova base (dove iniziano i dati della funzione);
    03. Riserva la memoria necessaria alla variabile bof
    preferisce i linguaggi di alto livello, la riga equivale a "esp =
    esp - 20").

    Fig. 2.1 - Dati memorizzati
    Dati memorizzati

    Dopo l'esecuzione, il "contenitore" ha l'aspetto che vediamo
    in Fig. 2.1. Il problema ovviamente si presenta quando la stringa di input
    è maggiore dei 20 bytes allocati e l'overflow corrompe lo stack
    (cfr. Fig. 1.1). Ora che sappiamo riconoscere le applicazioni a rischio
    possiamo tuffarci in un'esercitazione pratica, molto più stimolante
    della pura teoria.

    Prima di iniziare con l'esercitazione pratica assicuriamoci di avere
    tutti gli strumenti necessari a portata di mano. Per questo esempio bastano
    un compilatore e un debugger; le immagini nel tutorial si riferiscono
    al compilatore Dev-C++ e al debugger GoVest, entrambi scaricabili gratuitamente:

    1. Dev-C++ 4.0 - http://ftp1.sourceforge.net/dev-cpp/devcpp4.zip
    2. GoVest 0.9 - http://www.geocities.com/govest/govest.zip

    Dopo aver installato il compilatore, eseguiamolo e apriamo un nuovo progetto
    (File > New Project > Console application) selezionando
    "C project". Diamo l'ok e inseriamo un nome per il progetto
    (es: test), quindi salviamo il file come ci viene richiesto. Nella finestra
    principale scriviamo il codice della nostra applicazione (Tab. 3.1).

    #include
    <stdio.h>
    #include
    <string.h>
     
    void
    func(char *p)
    {
    char
    stack_temp[20];
    strcpy(stack_temp,
    p);
    printf(stack_temp);
    }
     
    int
    main(int argc, char* argv[])
    {
    func("QUESTO
    TESTO PROVOCA UN OVERFLOWxxxxyyyy");
    return
    0;
    }

    Tab 3.1 - Listato di test.c

    La funzione vulnerabile in questo caso è strcpy
    perchè non controlla se la stringa inserita dall'utente è
    più grande del buffer riservato (20). L'input dell'utente viene
    simulato
    "QUESTO TESTO PROVOCA UN OVERFLOWxxxxyyyy". È evidente a tutti
    che la stringa supera i 20 caratteri (si conteggiano anche gli spazi),
    però come facciamo a sapere dove vanno a finire i caratteri in
    più e come creare una stringa adeguata allo scopo?

    Prima di tutto compiliamo il listato ed eseguiamo il programma che abbiamo
    creato (da Execute > Compile and Run
    per il verso giusto, Windows ci avviserà che si è verificato
    un errore in test.exe e quindi l'applicazione dovrà essere terminata.
    Notiamo i dettagli della segnalazione (Fig. 3.1): il programma è
    terminato bruscamente quando ha cercato di leggere l'offset (indirizzo)
    79797979.

    Fig. 3.1 - Segnalazione errore
    Segnalazione errore

    Un indirizzo formato dallo stesso numero che si ripete è abbastanza
    curioso...infatti qualsiasi editor esadecimale potrà confermarvi
    che "79" equivale a "y", e ovviamente "yyyy"
    è la parte terminale della nostra stringa. L'applicazione ha eseguito
    il codice che noi abbiamo passato come input, ha interpretato i caratteri
    in più come offset e - cosa che non dovrebbe mai succedere - ha cercato di leggere quell'indirizzo. Complimenti:
    il vostro primo buffer overflow!

    Una istantantea

    Per comprendere meglio cosa è successo all'interno dei registri,
    aiutiamoci con il debugger. Questa applicazione ci serve per eseguire
    il nostro programma passo passo, così da visualizzare le operazioni
    compiute in Assembler e i valori contenuti nei registri in qualsiasi momento.
    Entriamo in GoVest e apriamo test.exe selezionandolo
    da File > Load process, quindi eseguiamolo con F5 (oppure
    Debug > Run). Non lasciamoci intimorire dalla quantità di informazioni
    visualizzate. L'area di lavoro è organizzata molto semplicemente:
    il codice Assembler a sinistra, il contenuto dei registri e l'editor esadecimale
    a destra.

    Continuiamo l'esecuzione tramite Debug > Step over, quindi premiamo
    F10 per avanzare di un passo per volta. Saremo interrotti
    da un avviso di errore simile al precedente, sempre riguardo all'offset
    79797979. A differenza di prima, però, abbiamo a disposizione una
    istantanea dei registri al momento del "crash" (Fig. 3.2).

    Fig. 3.2 - Contenuto dei registri
    Contenuto dei registri

    Come abbiamo visto nella parte teorica lo stack riserva
    lo spazio alle variabili, quindi al puntatore della base e infine all'indirizzo
    di ritorno. Ora abbiamo la conferma tangibile: la stringa "xxxx"
    è stata memorizzata in EBP (78 equivale a x in esadecimale),
    mentre i caratteri successivi "yyyy" in EIP (indirizzo di
    ritorno). Questi offset non esistono e il programma si blocca...ma immaginiamo
    di passare degli indirizzi validi come stringa: a quel punto saremmo
    in grado di far eseguire del codice arbitrario al programma e ottenere
    il controllo del sistema!

    Prima di passare dall'altra parte della barricata, ossia la protezione
    dagli attacchi, chiariamo un ultimo dubbio: visto che la memoria riservata
    alla variabile è di 20 caratteri, come mai i
    registri vengono sovrascritti dai valori passati solo a partire dalla
    fine del carattere n°32? Dove finiscono i rimanenti 12 caratteri?
    Per capire di cosa stiamo parlando, provate a contare il numero di caratteri
    della stringa utilizzata, spazi compresi. Questo è molto importante
    per imparare a creare codici di lunghezza adeguata.

    La risposta è semplice: possiamo vedere dal listato che sono
    presenti altre variabili, oltre a quella da 20 bytes. Per convenzione
    vengono riservati 4 byte per ciascuna, se non indicato diversamente.
    Nella creazione dell'input per l'overflow dovremo quindi tenere conto
    di questi valori:

    • 20 bytes riservati a stack_temp
    • 4 bytes per la variabile p
    • 4 bytes per argc
    • 4 bytes per argv
    • 4 bytes per il registro EBP
    • 4 bytes per l'indirizzo di ritorno in EIP

    Facendo la somma dei primi 4 valori considerati (20+4+4+4=32), appare
    evidente che i registri inizieranno a essere sovrascritti proprio a partire
    dal carattere successivo al 32! Sapendo questo, abbiamo impostato la stringa
    di modo che la prima "x" fosse alla posizione n°33.

    Programmazione sicura

    Per poterci proteggere dagli attacchi di buffer overflow siamo costretti
    ad affidarci alla professionalità dei programmatori. Selezioniamo
    bene i programmi da utilizzare, dunque, e cerchiamo di restare al passo
    con gli updates che risolvono i problemi delle applicazioni. Le normali protezioni
    contro gli attacchi, come i firewall, in questo caso servono a poco. Anzi,
    il firewall stesso potrebbe essere soggetto a un buffer overflow.

    Se siamo programmatori, invece, ricordiamoci di effettuare maggiori controlli
    sull'input degli utenti e non utilizziamo le funzioni a rischio (Tab.
    4.1).

    strcpy
    lstrcpy
    lstrcpyA
    lstrcpyW
    lstrcpyn
    lstrcpynA
    lstrcpynW
    wstrcpy
    strncpy
    wstrncpy
    sptrintf
    swptrinf
    gets
    getws
    strcat
    lstracat
    lstrcatW
    wcscat
    strncat
    wstrncat
    memcpy
    memmove
    scanf
    wscanf
    fgets

    Tab. 4.1 - Funzioni a rischio

Ti consigliamo anche