Bezpieczne kodowanie jest bardzo trudną praktyką. Zazwyczaj, kiedy programiści tworzą oprogramowanie, ich celem jest sprawienie, aby działało, a nie psuło się. W tym procesie, podatności mogą powstać w przypadkach, gdy użyta została starsza funkcja zamiast bardziej bezpiecznej. W konsekwencji, starsze oprogramowanie jest szczególnie podatne na ataki.
C jest jednym z tych języków, który jest z natury bardzo wszechstronny i potężny, ale ma jedną krytyczną wadę – bezpieczeństwo oprogramowania opartego na C zależy od wiedzy programisty. Oznacza to, że jeśli programista jest świadomy bezpiecznego kodowania, jego oprogramowanie również będzie bezpieczne. Z drugiej strony, i to stanowi główny problem, jeśli programista nie jest wystarczająco wyrafinowany, w jego oprogramowaniu znajdą się luki, które ostatecznie doprowadzą do exploita.
Dla ludzi takich jak ja, którzy znają się na programowaniu, ale są nowi w branży bezpieczeństwa, bardzo ważne jest studiowanie podatnego na ataki kodu i zrozumienie możliwych konsekwencji. Pomaga to doskonalić umiejętności kodowania i rozwijać postawę atakującego W TRAKCIE fazy kodowania, a nie PO zakodowaniu całego oprogramowania.
Przyznam szczerze, że studiowanie pełnego kodu źródłowego aplikacji w poszukiwaniu luk, takich jak przepełnienie bufora, jest dość kłopotliwe. Chociaż metoda ta ma swoje zalety, nie jest to najłatwiejsza metoda do znalezienia prostych luk, które mogą być krytyczne. Takie luki muszą być natychmiast usuwane, a najprostszym sposobem na ich znalezienie jest technika zwana Fuzzingiem.
Fuzzing jest techniką służącą do znajdowania „łatwych” luk w kodzie poprzez wysyłanie „losowo” wygenerowanych danych do programu wykonywalnego. Ogólnie rzecz biorąc, istnieją trzy rodzaje fuzzerów:
- Mutacja: Rodzaj „głupiego” fuzzingu, w którym generowane są źle sformowane próbki danych wejściowych i dostarczane do programu wykonywalnego. Te dane wejściowe mogą, ale nie muszą być zgodne z typem danych oczekiwanych przez aplikację, więc prawdopodobieństwo znalezienia prawdziwych błędów nie jest wysokie.
- Generacja: Rodzaj „inteligentnego” fuzzingu, który wymaga pewnych początkowych danych testowych, na podstawie których algorytm fuzzera może wygenerować zniekształcone dane wejściowe od podstaw. Ten typ fuzzingu jest w wielu przypadkach lepszy niż dumb fuzzing, ponieważ program otrzymuje dane wejściowe, których oczekuje.
- Evolutionary: Tego typu fuzzery używają informacji zwrotnej od każdego „fuzza”, aby nauczyć się z czasem formatu danych wejściowych.
W tym poście przyjrzymy się fuzzingowi z American Fuzzy Lop (AFL). Jest to typ ewolucyjnego fuzzera, który nadaje się do fuzzowania programów, które pobierają dane wejściowe z STDIN lub pliku.
Istnieje wiele fuzzerów na wolności, w tym Peach i syzkaller. Więc, dlaczego AFL?
- Przypadek użycia. To jest najważniejszy punkt do rozważenia. Mój przypadek użycia to aplikacja, która pobiera dane wejściowe z pliku. Ważne jest, aby zauważyć, że AFL nie ma możliwości fuzzingu przez sieci.
- Jest prosty do zainstalowania.
- Interfejs UI AFL zawiera mnóstwo informacji, w tym statystyki procesu fuzzingu w czasie rzeczywistym.
Skonfigurowanie AFL
Skonfigurowanie AFL jest proste i ułatwiłem ci to pisząc prosty (ale surowy) skrypt powłoki, który zainstaluje go za ciebie! Uruchom ten skrypt z uprawnieniami użytkownika, a zainstaluje on wszystkie zależności, AFL i powiązane narzędzia. Skrypt powłoki można znaleźć tutaj: https://github.com/nikhilh-20/enpm691_project/blob/master/install_afl.sh
Wybierz aplikację do fuzzowania
W tym poście zajmiemy się tylko fuzzowaniem tych aplikacji, do których mamy kod źródłowy. Jest to spowodowane tym, że AFL instrumentuje kod źródłowy w celu monitorowania wykonania, błędów i innych rzeczy związanych z wydajnością. Możliwe jest również bezpośrednie badanie kodu wykonywalnego, ale jest to eksperymentalne i wykracza poza zakres tego wpisu (wskazówka: wymaga QEMU).
Wybierz dowolny system open source z GitHuba do fuzzingu. Im bardziej znany jest twój wybór, tym mniejszą liczbę luk prawdopodobnie będzie posiadał. Inni też szukają błędów! Jedną z prostych metod, której używam, aby znaleźć podatny kod, jest użycie GitHub Search. Oto co robię:
- Szukaj podatnej funkcji, powiedzmy strcpy.
- Wyniki będą liczone w milionach. Przejdź do kategorii commits w wynikach. To właśnie tam znajdziesz repozytoria, w których strcpy zostało użyte (lub może usunięte). Te repozytoria są dobrym punktem wyjścia do rozpoczęcia fuzzingu.
Instrumentowanie aplikacji
Ze względu na prywatność, nie mogę ujawnić repozytorium, którego używam.
Klonujemy repozytorium git.
nikhilh@ubuntu:~$ git clone https://github.com/vuln; cd vuln
Ustawiamy zmienną środowiskową, AFL_HARDEN=1. To aktywuje pewne opcje utwardzania kodu w AFL podczas kompilacji, co ułatwia wykrywanie błędów związanych z uszkodzeniem pamięci.
nikhilh@ubuntu:~/vuln$ export AFL_HARDEN=1
Ustaw pewne flagi kompilatora, tak aby aplikacja była kompilowana w sposób, który ułatwi nam znalezienie (i wykorzystanie) luk. Idealnie byłoby użyć zmiennych środowiskowych, aby ustawić to, czego potrzebujemy, ale jest tu dość dużo dostosowywania, więc będziemy bezpośrednio edytować plik Makefile.
Upewniamy się, że kompilator używany to afl-gcc lub afl-clang zamiast odpowiednio gcc i clang. Pozwoli to AFL na instrumentację kodu źródłowego.
Dodaj flagi kompilatora:
-fno-stack-protector wyłącza ochronę stosu, co pozwoli nam na wykorzystanie przepełnień bufora.
-m32 jest potrzebne tylko jeśli używasz 32-bitowej maszyny, w przeciwnym razie nie.
Jak już skończysz z tymi zmianami, nadszedł czas na skompilowanie aplikacji. Uruchom make. Kiedy to zrobisz, MUSISZ zobaczyć w logu takie stwierdzenia jak to:
Instrumented 123 locations (32-bit, hardened-mode, ratio 100%).
Jeśli nie widzisz takich stwierdzeń, oznacza to, że AFL nie aktywował kodu aplikacji do fuzzingu. Innymi słowy, nie udało mu się poprawnie zinstrumentalizować kodu źródłowego.
Próbki testowe
AFL jest ewolucyjnym typem fuzzera. Oznacza to, że podobnie jak fuzzery generacyjne, wymaga on również wstępnych danych testowych, aby zrozumieć, jakiego rodzaju danych oczekuje docelowa aplikacja. W przypadku systemów open source jest to łatwe do znalezienia. Wystarczy zajrzeć do ich katalogu testowego, a znajdziemy tam wszystkie potrzebne dane testowe.
nikhilh@ubuntu:~/vuln$ mkdir afl_in afl_out
nikhilh@ubuntu:~/vuln$ cp test/* afl_in/
Fuzzing Begins
Gdy mamy już nasze próbki testowe, jesteśmy gotowi do fuzzingu!
Och, czekaj… musimy też zmienić, gdzie trafiają powiadomienia o awarii aplikacji. 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. To jest miejsce gdzie przechowujemy początkowe dane testowe.
- -o- To jest katalog gdzie AFL zapisuje użyteczne informacje o awariach, zawieszeniach, itp.
- -S – To jest tryb Slave. Zasadniczo AFL będzie losowo zmieniał dane wejściowe powodując niedeterministyczny fuzzing.
- Opcja -M to tryb Master, który jest deterministycznym fuzzingiem, co zasadniczo oznacza, że każdy bit danych wejściowych jest w jakiś sposób modyfikowany. (To jest powolne! … Oczywiście.)
- @@ – To jest pozycja, gdzie będzie znajdował się wejściowy plik testowy. AFL podstawia to za ciebie automatycznie. Jeśli twój program wykonawczy pobiera dane z STDIN, to nie jest to potrzebne.
Wyniki fuzzingu
Pokazywanie tego zajmie trochę czasu. Często zdarza się, że ludzie fuzzują przez więcej niż 24 godziny (i mogą skończyć z niczym). W moim przypadku, myślę, że aplikacja była trochę zbyt podatna na ataki, więc miałem 516 unikalnych crashy w ciągu godziny. Nie oznacza to jednak, że istnieje 516 podatności!
Możesz zakończyć sesję fuzzingu, naciskając 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 pokaże, jakie dane wejściowe spowodowały awarię aplikacji. W tym przypadku, plik: id:000046,sig:11,src:000004,op:havoc,rep:4 spowodował StackBufferOverflow w aplikacji. Takie pliki można znaleźć pod ../afl_out-slaveX/crashes/
Done!
To tyle, jeśli chodzi o szybki start w fuzzing! Proces jest naprawdę prosty i bardzo wygodny, ponieważ wszystko jest zautomatyzowane. Następnym krokiem będzie przeanalizowanie, dlaczego dane wejściowe spowodowały przepełnienie bufora i poszukanie sposobu na ich wykorzystanie. Pamiętaj, że nie wszystkie luki w zabezpieczeniach mogą prowadzić do ich wykorzystania.