Sicheres Coding ist eine sehr schwierige Praxis. Wenn Programmierer Software entwickeln, ist ihr Ziel in der Regel, dass die Software funktioniert und nicht kaputt geht. In diesem Prozess können Schwachstellen entstehen, wenn eine alte Funktion anstelle einer sichereren verwendet wird. Folglich ist Legacy-Software besonders anfällig.
C ist eine der Sprachen, die von Natur aus sehr vielseitig und leistungsfähig sind, aber sie hat einen entscheidenden Nachteil – die Sicherheit von C-basierter Software hängt von den Kenntnissen des Programmierers ab. Das bedeutet, dass, wenn der Programmierer sich mit sicherer Kodierung auskennt, seine Software auch sicher sein wird. Auf der anderen Seite, und das ist der größte Nachteil, wenn der Programmierer nicht gut genug ist, wird es Schlupflöcher in seiner Software geben, die letztendlich zu einem Missbrauch führen.
Für Leute wie mich, die sich mit Programmierung auskennen, aber neu in der Sicherheitsbranche sind, ist es sehr wichtig, anfälligen Code zu studieren und die möglichen Konsequenzen zu verstehen. Das hilft dabei, die Programmierfähigkeiten zu verfeinern und eine Haltung gegenüber Angreifern zu entwickeln, und zwar WÄHREND der Programmierphase und nicht erst NACH dem Programmieren der gesamten Software.
Ganz ehrlich, es ist ziemlich mühsam, den gesamten Quellcode einer Anwendung zu studieren, wenn man nach Schwachstellen wie Pufferüberläufen sucht. Obwohl diese Methode ihre Vorzüge hat, ist sie nicht die einfachste Methode, um einfache Schwachstellen zu finden, die kritisch sein können. Solche Schwachstellen müssen sofort behoben werden, und der einfachste Weg, sie zu finden, ist eine Technik namens Fuzzing.
Fuzzing ist eine Technik zum Auffinden „einfacher“ Schwachstellen im Code, indem „zufällig“ generierte Daten an eine ausführbare Datei gesendet werden. Im Allgemeinen gibt es drei Arten von Fuzzern:
- Mutation: Eine Art „dummes“ Fuzzing, bei dem missgestaltete Eingabemuster generiert und an die ausführbare Datei gesendet werden. Diese Eingabe kann mit der von der Anwendung erwarteten Art von Eingabe übereinstimmen oder auch nicht, so dass die Wahrscheinlichkeit, echte Fehler zu finden, nicht hoch ist.
- Generierung: Eine Art von „intelligentem“ Fuzzing, das einige anfängliche Testdaten erfordert, aus denen der Fuzzer-Algorithmus von Grund auf eine fehlerhafte Eingabe generieren kann. Diese Art von Fuzzing ist in vielen Fällen besser als Dumb Fuzzing, da das Programm die Eingaben erhält, die es erwartet.
- Evolutionär: Diese Art von Fuzzern verwendet Feedback von jedem „Fuzz“, um mit der Zeit das Format der Eingabe zu lernen.
In diesem Beitrag werden wir uns das Fuzzing mit American Fuzzy Lop (AFL) ansehen. Dabei handelt es sich um eine Art evolutionären Fuzzer, der sich zum Fuzzing von Programmen eignet, die Eingaben von STDIN oder einer Datei erhalten.
Es gibt eine Vielzahl von Fuzzern in freier Wildbahn, darunter Peach und syzkaller. Warum also AFL?
- Der Anwendungsfall. Dies ist der wichtigste Punkt, den es zu berücksichtigen gilt. Mein Anwendungsfall war es, eine Anwendung zu fuzzen, die Eingaben aus einer Datei entgegennimmt. Es ist wichtig zu wissen, dass AFL nicht in der Lage ist, über Netzwerke zu fuzzen.
- Es ist einfach zu installieren.
- Die Benutzeroberfläche von AFL enthält eine Menge Informationen, einschließlich Echtzeit-Statistiken des Fuzzing-Prozesses.
AFL einrichten
Das Einrichten von AFL ist einfach und ich habe es für Sie erleichtert, indem ich ein einfaches (aber grobes) Shell-Skript geschrieben habe, das es für Sie installiert! Führen Sie das Skript mit Ihren Benutzerrechten aus und es wird alle Abhängigkeiten, AFL und die dazugehörigen Tools installieren. Das Shell-Skript ist hier zu finden: https://github.com/nikhilh-20/enpm691_project/blob/master/install_afl.sh
Wählen Sie die zu fuzzende Anwendung
In diesem Beitrag werden wir uns nur mit dem Fuzzen von Anwendungen beschäftigen, für die wir den Quellcode haben. Das liegt daran, dass AFL den Quellcode instrumentiert, um die Ausführung, Fehler und andere leistungsbezogene Dinge zu überwachen. Es ist auch möglich, eine ausführbare Datei direkt zu fuzzen, aber das ist experimentell und liegt außerhalb des Rahmens dieses Beitrags (Tipp: es erfordert QEMU).
Wählen Sie ein beliebiges Open-Source-System von GitHub für das Fuzzing. Je bekannter Ihre Wahl ist, desto weniger Schwachstellen wird es wahrscheinlich haben. Auch andere suchen nach Bugs! Eine einfache Methode, die ich verwende, um verwundbaren Code zu finden, ist die Verwendung der GitHub-Suche. So gehe ich vor:
- Suchen Sie nach einer verwundbaren Funktion, z. B. strcpy.
- Die Ergebnisse werden in die Millionen gehen. Gehen Sie zu der Kategorie „Commits“ der Ergebnisse. Hier finden Sie die Repositories, in denen strcpy verwendet (oder vielleicht entfernt) wurde. Diese Repositories sind ein guter Ausgangspunkt, um mit dem Fuzzing zu beginnen.
Instrumentierung der Anwendung
Aus Gründen des Datenschutzes kann ich das Repository, das ich verwende, nicht offenlegen.
Klonen Sie das Git-Repository.
nikhilh@ubuntu:~$ git clone https://github.com/vuln; cd vuln
Setzen Sie eine Umgebungsvariable, AFL_HARDEN=1. Dies aktiviert bestimmte Code-Hardening-Optionen in AFL während des Kompilierens, was es einfacher macht, Speicherfehler zu entdecken.
nikhilh@ubuntu:~/vuln$ export AFL_HARDEN=1
Setzen Sie bestimmte Compiler-Flags, so dass die Anwendung so kompiliert wird, dass es für uns einfach ist, Schwachstellen zu finden (und auszunutzen). Idealerweise würden wir Umgebungsvariablen verwenden, um das zu setzen, was wir brauchen, aber es gibt eine Menge Anpassungen, also werden wir direkt das Makefile bearbeiten.
Stellen Sie sicher, dass der verwendete Compiler afl-gcc oder afl-clang ist, anstatt gcc bzw. clang. Dies ermöglicht es AFL, den Quellcode zu instrumentieren.
Fügen Sie Compiler-Flags hinzu:
-fno-stack-protector schaltet den Stack-Protector aus, was es uns ermöglicht, Pufferüberläufe auszunutzen.
-m32 wird nur benötigt, wenn Sie eine 32-Bit-Maschine verwenden, ansonsten nicht.
Nachdem Sie diese Änderungen vorgenommen haben, ist es Zeit, die Anwendung zu kompilieren. Starten Sie make. Wenn Sie das tun, MÜSSEN Sie Aussagen wie diese im Protokoll sehen:
Instrumented 123 locations (32-bit, hardened-mode, ratio 100%).
Wenn Sie solche Aussagen nicht sehen, bedeutet das, dass AFL den Code der Anwendung nicht für Fuzzing aktiviert hat. Mit anderen Worten, es hat den Quellcode nicht erfolgreich instrumentiert.
Testbeispiele
AFL ist ein evolutionärer Typ von Fuzzer. Das bedeutet, dass er, wie generationenbasierte Fuzzer, auch erste Testdaten benötigt, um zu verstehen, welche Art von Daten die Zielanwendung erwartet. Bei Open-Source-Systemen ist dies leicht zu finden. Schauen Sie einfach in deren Testverzeichnis und Sie werden alle benötigten Testdaten finden.
nikhilh@ubuntu:~/vuln$ mkdir afl_in afl_out
nikhilh@ubuntu:~/vuln$ cp test/* afl_in/
Fuzzing beginnt
Nun, da wir unsere Testmuster haben, sind wir bereit zu fuzzern!
Oh, Moment… wir müssen auch ändern, wohin die Absturzbenachrichtigungen der Anwendung gehen. 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. Hier haben wir die anfänglichen Testdaten gespeichert.
- -o- Dies ist das Verzeichnis, in das AFL nützliche Informationen über Abstürze, Hänger, etc. schreibt.
- -S – Dies ist der Slave-Modus. Im Grunde wird AFL die Eingabe zufällig verändern, was zu nicht-deterministischem Fuzzing führt.
- Die Option -M ist der Master-Modus, der deterministisches Fuzzing ist, was im Grunde bedeutet, dass jedes Bit der Eingabe auf irgendeine Weise verändert wird. (Das ist langsam! … Offensichtlich.)
- @@ – Dies ist die Position, an der sich die Eingabetestdatei befinden wird. AFL ersetzt dies automatisch für Sie. Wenn Ihre ausführbare Datei Eingaben von STDIN entgegennimmt, wird dies nicht benötigt.
Fuzzing-Ergebnisse
Es wird einige Zeit dauern, dies zu zeigen. Oftmals fuzzen die Leute mehr als 24 Stunden (und finden am Ende vielleicht nichts). In meinem Fall war die Anwendung wohl etwas zu anfällig, so dass ich innerhalb einer Stunde 516 einzelne Abstürze hatte. Das heißt aber nicht, dass es 516 Sicherheitslücken gibt!
Sie können die Fuzzing-Sitzung mit Strg-C beenden.
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 zeigt Ihnen, welche Eingabe den Absturz der Anwendung verursacht hat. In diesem Fall hat die Datei: id:000046,sig:11,src:000004,op:havoc,rep:4 einen StackBufferOverflow in der Anwendung verursacht. Solche Dateien finden Sie unter ../afl_out-slaveX/crashes/
Fertig!
Das war’s für einen Schnellstart ins Fuzzing! Der Prozess ist wirklich einfach und sehr bequem, da er vollständig automatisiert ist. Der nächste Schritt wäre zu analysieren, warum die Eingabe einen Buffer Overflow verursacht hat, und nach einem Weg zu suchen, diesen auszunutzen. Denken Sie daran, dass nicht alle Sicherheitslücken zu einem Exploit führen können.