
guide
Tutti i linguaggi per diventare uno sviluppatore di app per Android.
Buffer Overflow: cosa è e come si ottiene. Analisi approfondita del principale sistema di attacco di un sistema operativo. Esempi pratici e consigli per proteggersi
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.
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:
La conoscenza delle istruzioni di base dell’Assembler è essenziale per capire gli esempi che seguono. Ecco quelle più utili alla nostra “causa”:
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.
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 di tipo char alla terza riga, alla quale vengono riservati 20 bytes, e la chiamata alla funzione gets incaricata di raccogliere 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 (per chi
preferisce i linguaggi di alto livello, la riga equivale a “esp =
esp – 20”).
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 dalla stringa che abbiamo preparato, cioè “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). Se tutto è andato 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.
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!
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).
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:
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.
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
Se vuoi aggiornamenti su Tecniche: Buffer Overflow inserisci la tua email nel box qui sotto:
Compilando il presente form acconsento a ricevere le informazioni relative ai servizi di cui alla presente pagina ai sensi dell'informativa sulla privacy.
Abbiamo ricevuto la tua richiesta di iscrizione. Se è la prima volta che ti registri ai nostri servizi, conferma la tua iscrizione facendo clic sul link ricevuto via posta elettronica.
Se vuoi ricevere informazioni personalizzate compila anche i seguenti campi opzionali.
Compilando il presente form acconsento a ricevere le informazioni relative ai servizi di cui alla presente pagina ai sensi dell'informativa sulla privacy.
Tutti i linguaggi per diventare uno sviluppatore di app per Android.
Come creare applicazioni per il Web con PHP e MySQL per il DBMS.
Tutte le principali tecnologie per diventare uno sviluppatore mobile per iOS.
I fondamentali per lo sviluppo di applicazioni multi piattaforma con Java.
Diventare degli esperti in tema di sicurezza delle applicazioni Java.
Usare Raspberry Pi e Arduino per avvicinarsi al mondo dei Maker e dell’IoT.
Le principali guide di HTML.it per diventare un esperto dei database NoSQL.
Ecco come i professionisti creano applicazioni per il Cloud con PHP.
Lo sviluppo professionale di applicazioni in PHP alla portata di tutti.
Come sviluppare applicazioni Web dinamiche con PHP e JavaScript.
Fare gli e-commerce developer con Magento, Prestashop e WooCommerce.
Realizzare applicazioni per il Web utilizzando i framework PHP.
Creare applicazioni PHP e gestire l’ambiente di sviluppo come un pro.
Percorso base per avvicinarsi al web design con un occhio al mobile.
Realizzare siti Web e Web application con WordPress a livello professionale.
Configurazione avanzata di Pi-hole per utilizzare RaspberryPi come ad-blocker.
Come individuare errori durante l’esecuzione di funzioni e procedure. Tenere sott’occhio le stored procedure con il gestore di errori di MySQL
Come utilizzare Dreamweaver MX per creare nel proprio sito delle pagine protette con password
Jelastic Cloud è un servizio PaaS (Platform as a Service) offerto da Aruba, e destinato a tutte le aziende e gli sviluppatori interessati al deploy di applicazioni complesse direttamente su un’infrastruttura potente e rodata in Cloud. In questa guida scopriremo le tecnologie supportate (WordPress, Magento, Docker, Kubernetes, PHP, Java, Node.js) e come sfruttarle per un deploy di applicazioni e CMS.