La codifica sicura è una pratica molto difficile. Di solito, quando i programmatori sviluppano software, il loro obiettivo è quello di far funzionare il software piuttosto che romperlo. In questo processo, le vulnerabilità possono svilupparsi nei casi in cui è stata usata una funzione legacy invece di una più sicura. Di conseguenza, il software legacy è particolarmente vulnerabile.
C è uno di quei linguaggi che è intrinsecamente molto versatile e potente, ma ha uno svantaggio critico – la sicurezza del software basato su C dipende dalla conoscenza del programmatore. Questo significa che se il programmatore è ben consapevole della codifica sicura, anche il suo software sarà sicuro. D’altra parte, e questa è la parte più importante, se il programmatore non è abbastanza sofisticato, ci saranno delle scappatoie nel suo software che porteranno ad un exploit.
Per persone come me, che conoscono la programmazione ma sono nuove nel settore della sicurezza, è molto importante studiare il codice vulnerabile e capire le possibili conseguenze. Questo aiuta ad affinare le abilità di codifica e a sviluppare un atteggiamento da attaccante DURANTE la fase di codifica piuttosto che DOPO aver codificato l’intero software.
In tutta onestà, è abbastanza ingombrante studiare il codice sorgente completo di un’applicazione quando si cercano vulnerabilità come i buffer overflow. Anche se questo metodo ha i suoi meriti, non è il metodo più semplice per trovare semplici vulnerabilità che possono essere critiche. Tali vulnerabilità devono essere risolte immediatamente e il modo più semplice per trovarle è attraverso una tecnica chiamata Fuzzing.
Fuzzing è una tecnica per trovare vulnerabilità “facili” nel codice inviando dati generati “a caso” ad un eseguibile. In generale, ci sono tre tipi di fuzzer:
- Mutazione: Un tipo di fuzzing “stupido” in cui campioni di input malformati sono generati e forniti all’eseguibile. Questo input può o non può essere conforme al tipo di input previsto dall’applicazione, quindi la probabilità di trovare bug reali non è alta.
- Generazione: Un tipo di fuzzing “intelligente” che richiede alcuni dati di test iniziali da cui l’algoritmo del fuzzer può generare input malformati da zero. Questo tipo di fuzzing è migliore del fuzzing stupido in molti casi perché il programma riceve l’input che si aspetta.
- Evolutivo: Questo tipo di fuzzer usa il feedback di ogni “fuzz” per imparare nel tempo il formato dell’input.
In questo post, vedremo il fuzzing con American Fuzzy Lop (AFL). Si tratta di un tipo di fuzzer evolutivo che è adatto ai programmi fuzz che prendono input da STDIN o da un file.
Ci sono una serie di fuzzer in libertà tra cui Peach e syzkaller. Quindi, perché AFL?
- Il caso d’uso. Questo è il punto più importante da considerare. Il mio caso d’uso era di fare il fuzz su un’applicazione che prende input da un file. È importante notare che AFL non ha la capacità di fare fuzz sulle reti.
- È semplice da installare.
- L’interfaccia UI di AFL contiene una tonnellata di informazioni, comprese le statistiche in tempo reale del processo di fuzzing.
Impostare AFL
Impostare AFL è facile e l’ho reso più facile per voi scrivendo un semplice (ma rozzo) script di shell che lo installerà per voi! Esegui lo script con i tuoi privilegi utente e installerà tutte le dipendenze, AFL e gli strumenti correlati. Lo script di shell può essere trovato qui: https://github.com/nikhilh-20/enpm691_project/blob/master/install_afl.sh
Scegliere l’applicazione per il fuzz
In questo post, ci occuperemo solo del fuzzing delle applicazioni di cui abbiamo il codice sorgente. Questo perché AFL strumenta il codice sorgente per monitorare l’esecuzione, gli errori e altre cose relative alle prestazioni. E’ anche possibile fare il fuzzing direttamente su un eseguibile, ma questo è sperimentale e fuori dallo scopo di questo post (suggerimento: richiede QEMU).
Scegliete qualsiasi sistema open source da GitHub per il fuzzing. Più nota è la vostra scelta, meno vulnerabilità avrà probabilmente. Anche altri sono alla ricerca di bug! Un metodo semplice che uso per trovare codice vulnerabile è quello di usare GitHub Search. Questo è quello che faccio:
- Cerca una funzione vulnerabile, diciamo strcpy.
- I risultati saranno milioni. Vai alla categoria commits dei risultati. Qui è dove troverete quei repository dove strcpy è stato usato (o forse rimosso). Questi repository sono un buon punto di partenza per iniziare il fuzzing.
Instrumentare l’applicazione
Per ragioni di privacy, non posso rivelare il repository che sto usando.
Clona il repository git.
nikhilh@ubuntu:~$ git clone https://github.com/vuln; cd vuln
Imposta una variabile di ambiente, AFL_HARDEN=1. Questo attiva certe opzioni di indurimento del codice in AFL durante la compilazione, il che rende più facile individuare i bug di corruzione della memoria.
nikhilh@ubuntu:~/vuln$ export AFL_HARDEN=1
Imposta certi flag del compilatore, in modo che l’applicazione sia compilata in un modo che ci renda facile trovare (e sfruttare) le vulnerabilità. Idealmente useremmo le variabili d’ambiente per impostare ciò di cui abbiamo bisogno, ma c’è un bel po’ di personalizzazione. quindi modificheremo direttamente il Makefile.
Assicurati che il compilatore usato sia afl-gcc o afl-clang invece di gcc e clang rispettivamente. Questo è ciò che permette ad AFL di strumentare il codice sorgente.
Aggiungi i flag del compilatore:
-fno-stack-protector disattiva lo stack protector che ci permetterà di sfruttare i buffer overflow.
-m32 è necessario solo se stai usando una macchina a 32 bit, altrimenti no.
Una volta che hai finito con queste modifiche, è il momento di compilare l’applicazione. Eseguite make. Quando lo fate, DOVETE vedere dichiarazioni come queste nel log:
Instrumented 123 locations (32-bit, hardened-mode, ratio 100%).
Se non vedete tali dichiarazioni, significa che AFL non ha attivato il codice dell’applicazione per il fuzzing. In altre parole, non ha strumentato il codice sorgente con successo.
Campioni di test
AFL è un fuzzer di tipo evolutivo. Ciò significa che, come i fuzzer basati sulla generazione, richiede anche dati di test iniziali per capire che tipo di dati l’applicazione target si aspetta. Quando si punta a sistemi open source, questo è facile da trovare. Basta guardare nella loro directory di test e troverete tutti i dati di test di cui avete bisogno.
nikhilh@ubuntu:~/vuln$ mkdir afl_in afl_out
nikhilh@ubuntu:~/vuln$ cp test/* afl_in/
Fuzzing Begins
Ora che abbiamo i nostri campioni di test, siamo pronti per il fuzz!
Oh, aspetta… dobbiamo anche cambiare la destinazione delle notifiche di crash dell’applicazione. By default, when an application crashes, the core dump (basically, the contents of RAM are stored in a file to help in debugging) notification is sent to the system’s core handler. We don’t want this. Why? By the time this notification reaches AFL, it’ll be classified as a timeout rather than a crash.
nikhilh@ubuntu:~/vuln$ sudo su
password for nikhilh:
root@ubuntu:/home/nikhilh/vuln# echo core > /proc/sys/kernel/core_pattern
root@ubuntu:/home/nikhilh/vuln# exit
NOW, we are ready to fuzz!
nikhilh@ubuntu:~/vuln$ afl-fuzz -i afl_in -o afl_out -S slaveX — ./vuln @@
Command line flags used:
- -i — This marks the test input directory. Qui è dove abbiamo memorizzato i dati iniziali del test.
- -o- Questa è la directory dove AFL scrive le informazioni utili riguardanti crash, blocchi, ecc.
- -S – Questa è la modalità Slave. Fondamentalmente, AFL modificherà casualmente l’input causando fuzzing non deterministico.
- L’opzione -M è la modalità Master che è fuzzing deterministico, il che significa fondamentalmente che ogni bit dell’input viene modificato in qualche modo. (Questo è lento! … Ovviamente.)
- @@ – Questa è la posizione in cui sarà il file di test di input. AFL lo sostituisce automaticamente. Se il tuo eseguibile prende l’input da STDIN, allora questo non è necessario.
Risultati del fuzzing
Questo richiederà del tempo per essere mostrato. Molte volte, le persone fanno fuzzing per più di 24 ore (e possono finire con niente). Nel mio caso, penso che l’applicazione fosse un po’ troppo vulnerabile, quindi ho avuto 516 crash unici in un’ora. Tuttavia, questo non significa che ci siano 516 vulnerabilità!
Puoi uscire dalla sessione di fuzzing con un Ctrl-C.
Analysis Phase
Now that we have results, we need to analyze them to see which ones are exploitable. To this end, we will use one of AFL’s utilities called afl-collect. This will have been installed through the installation script as well.
nikhilh@ubuntu:~/afl-utils$ afl-collect -d crashes.db -e gdb_script -r -rr ~/vuln/afl_out/slaveX ./output_dir — ~/vuln/vuln
To understand what each command line flag does, refer to its help section.
nikhilh@ubuntu:~/afl-utils$ afl-collect — help
If you see lines such as these in the output, celebrate! You’ve found something interesting to try and exploit.
*** GDB+EXPLOITABLE SCRIPT OUTPUT ***
…
…
slaveX:id:000000,sig:11,src:000000,op:havoc,rep:2……………:EXPLOITABLE
…
slaveX:id:000046,sig:11,src:000004,op:havoc,rep:4……………:EXPLOITABLE
…
AFL vi mostrerà quale input ha causato il crash dell’applicazione. In questo caso, il file: id:000046,sig:11,src:000004,op:havoc,rep:4 ha causato uno StackBufferOverflow nell’applicazione. Tali file possono essere trovati sotto ../afl_out-slaveX/crashes/
Fatto!
Questo è tutto per un rapido inizio nel fuzzing! Il processo è davvero semplice e molto conveniente poiché è tutto automatizzato. Il prossimo passo sarebbe quello di analizzare perché l’input ha causato un Buffer Overflow e cercare un modo per sfruttarlo. Ricordate che non tutte le vulnerabilità possono portare ad un exploit.