Ten wpis jest częścią cyklu “Od zera do bughuntera” – listę wszystkich postów znajdziesz tutaj.
Niestety, od listopada 2017, legendarny już, fuzzer autorstwa lcamtufa – AFL nie jest rozwijany. Co prawda Google uchroniło projekt przed zapomnieniem, wrzucając go na GitHuba, lecz widoki na wsparcie projektu przez autora / Google, są mizerne. Na szczęście w otwartym kodzie i społeczności tkwi siła, więc dzisiaj opiszę dziesięć różnic pomiędzy “zwykłym” AFL a AFL++, czyli forku projektu, z sukcesem kontynuującego najlepsze tradycje pierwowzoru.
Nazwa forka zobowiązuje: dwa plusy można odczytać jako ficzery dostarczane pierwotnie za pomocą projektów AFLfast i afl-unicorn. W dużym skrócie, pierwszy dostarcza efektywniejszy wybór ścieżek w kodzie oparty o metody statystyczne. Drugi umożliwia fuzzowanie wszystkiego – spektakularnym przykładem jest fuzzing odbiornika fal radiowych.
Pierwowzór przecierał szlaki dla coverage-guided fuzzingu, lecz od tamtego czasu powstały dużo efektywniejsze sposoby instrumentacji kodu specjalizowane pod kątem testów automatycznych. afl++ implementuje InsTrim, czyli opracowaną przez Tajwańczyków lekką instrumentację kodu.
Binarki bez instrumentacji dodawanej przez kompilator, moim zdaniem, przez większość czasu rozwoju AFL był traktowany jako mocno eksperymentalny ficzer – stosunek błędów znalezionych przy jego pomocy był promilem błędów odkrytych w sposób preferowany przez projekt. afl++ korzysta z funkcjonalności nowych wersji QEMU oraz wprowadza odczuwalne poprawki wydajnościowe w postaci włączenia prostego cache’u (około 3x).
Do mieszania w pierwowzorze nie ma się jak przyczepić – jego praca pozwoliła znaleźć masę błędów 🙂 Porównując AFL z honggfuzz wyraźnie widać różnice, jak algorytmy mutatora dochodzą do różnych błędów. Zmiana silnika na MOpt-AFL znacząco wpłynęła na skuteczność bughuntingu.
Niech pierwszy rzuci kamieniem ten, kto nigdy nie wyłączył afl-tmin w trakcie minimalizacji dużego crasha – rzadko się zdarza, aby wszystko przebiegło akceptowalnie szybko. “Pożyczony” kod z projektu TriforceAFL dodaje obsługę forkservera, co znacząco wpływa na czas minimalizacji przypadków testowych.
Mała rzecz, a cieszy 🙂
Domyślna implementacja afl-cmin jest pomysłowa, lecz niestety technicznie lekko przeterminowana. Zawsze używałem i polecam narzędzie afl-pcmin, czyli funkcjonalnie to samo, tylko wielowątkowo. afl++ podchodzi do problemu minimalizacji w inny sposób: mniejszy zbiór przypadków testowych nad mniejsze przypadki testowe.
W afl++ otrzymujemy możliwość wyboru dowolnej lokalizacji pliku .cur_input zawierającego aktualnie testowany plik. Ma to odczuwalny wpływ na wydajność w przypadku lokalizacji pliku na ramdysku.
Na czas pisania posta, aktualną wersją LLVM jest 8.0.1. Wersje te dzieli dokładnie 758 dni – sporo jak na standardy branżowe. Niestety wersje późniejsze generowały błąd kompilacji, który uniemożliwiał wykorzystanie nowszego kodu i w konsekwencji brak “dobroci” trybu LLVM.
Ostatni i jednocześnie bardzo ważna funkcjonalność, bezpośrednio przekładająca się na efektywniejsze odkrywanie kolejnych elementów kodu. Projekt laf-intel wymusza pominięcie optymalizacji podczas kompilacji za pomocą LLVM – powoduje to “uproszczenie” dla fuzzera podczas odkrywania nowych przypadków testowych, gdyż kod nie wykorzystuje “sztuczek” i nietypowych instrukcji.
Brzmi nieźle, na prawdę niewiele zostało ze starego, dobrego afl! Daj znać w komentarzu co sądzisz o afl++ lub jakie masz z nim problemy – chętnie pomogę! 🙂