Ten wpis jest częścią cyklu “Od zera do bughuntera” – listę wszystkich postów znajdziesz tutaj.
Kolejna część szybkiego wprowadzenia do zagadnień związanych z LibFuzzerem – w tym poście skupię się na nieco “głębszym” wykorzystaniu wbudowanych funkcjonalności, tunigu wydajności fuzzera oraz pokryciu kodu za pomocą SanitizerCoverage.
Bez zbędnego rozwlekania, przechodzimy do konkretów 🙂
W moim rozumieniu: lepiej = większy code coverage || więcej execs/s.. Generalnie w przypadku fuzzerów zawsze należy pamiętać o prawidłowości: więcej execs/s -> większy korpus -> większy code coverage -> więcej potencjalnych crashy.
Autorzy LibFuzzera jako minimum przyjmują 1000 execs/s dla efektywnego fuzzingu.
Słowniki znacząco przyczyniają się do większego pokrycia kodu – fuzzujemy po prostu mądrze, nie tracąc cykli CPU na generowanie bezwartościowego inputu.
Pliki słowników wykorzystywane przez LibFuzzer są kompatybilne z AFL, więc możemy skorzystać z dostarczonych razem z tym fuzzerem. Jeżeli nie znajdziemy tam słownika kompatybilnego z naszym programem, warto poświęcić chwilę i napisać go samemu – ROI jest na prawdę duże 🙂
Dobre słowniki można znaleźć w następujących miejscach:
Leniwi czytelnicy mogą skorzystać z gotowego skryptu pozwalającego na wygenerowanie słownika na podstawie dostarczonej specyfikacji.
W większości przypadków im mniejsze pliki tym lepiej (aczkolwiek nie polecam obsesyjnie się tego trzymać). Nie ma sensu fuzzować biblioteki regex za pomocą przypadków testowych o rozmiarze 100KB i więcej. Polecam przetestować empirycznie własny fuzzer pod kątem wielkości pojedyńczego przypadku testowego – przełącznik “-max_len“.
Osobiście, po kilku iteracjach fuzzingu danego projektu, dzielę korpus testowy względem rozmiaru – pliki mniejsze niż 1, 2, 4… KB i sprawdzam jak wygląda pokrycie kodu dla danego rozmiaru. Następnie idę na rozsądny kompromis (przynajmniej tak mi się wydaje) pomiędzy pokryciem a rozmiarem 😉
Im wyższą wartość niepowodującą degradacji szybkości uda się nam znaleźć, tym otrzymamy lepsze pokrycie kodu.
Tutaj pomoże ortodoksyjne trzymanie się zasady KISS (albo po polsku DUPA) 😉
Parę prostych wskazówek:
Bardzo fajny przykład optymalizacji został przedstawiony na slajdach jednego z modułów warsztatu LibFuzzer Workshop – zachęcam do zapoznania się z całym materiałem 🙂
Przed uruchomieniem fuzzera warto sprawdzić w jakim stopniu nasz korpus testowy pokrywa badany przez nas kod. Niestety do tego będzie potrzebna ponowna kompilacja fuzzera, z innymi przełącznikami (tutaj na przykładzie fuzzera pcre2):
clang++ -std=c++11 pcre2_fuzzer.cc -I pcre/src -Wl,--whole-archive pcre/.libs/*.a -Wl,-no-whole-archive ~/Desktop/libFuzzer.a -O2 -fno-omit-frame-pointer -g -fsanitize=address -fsanitize-coverage=edge,indirect-calls,8bit-counters,trace-cmp,trace-div,trace-gep -o pcre2_fuzzer_coverage
Uwaga! Przełącznik 8bit-counters nie będzie wspierany. W przypadku chęci wykorzystania innych metod badania pokrycia warto wcześniej rzucić okiem na dokumentację SanCova (sporo metod pozostanie bez wsparcia i nie będą rozwijane).
Następnie odpalamy skompilowaną binarkę:
ASAN_OPTIONS=coverage=1 ./pcre2_fuzzer_coverage corpus_dir/ -runs=0
INFO: Seed: 3765786233
INFO: Loaded 0 modules (0 guards):
Loading corpus dir: regex_min/
Loaded 1024/8815 files from regex_min/
Loaded 2048/8815 files from regex_min/
Loaded 4096/8815 files from regex_min/
Loaded 8192/8815 files from regex_min/
INFO: -max_len is not provided, using 2035
#0 READ units: 8814
#2048 pulse exec/s: 409 rss: 132Mb
#4096 pulse exec/s: 178 rss: 143Mb
Slowest unit: 16 s:
artifact_prefix='./'; Test unit written to ./slow-unit-2c9a2da85c6b915818510370f6ff6680c8515d94
Slowest unit: 18 s:
artifact_prefix='./'; Test unit written to ./slow-unit-53e82f19e2337033fcc43d1c9da61da8e5ff90af
#8192 pulse exec/s: 38 rss: 232Mb
#8814 INITED exec/s: 36 rss: 243Mb
ERROR: no interesting inputs were found. Is the code instrumented for coverage? Exiting.
Nie przejmujemy się komunikatem i dodajemy symbole do pliku .sancov :
sancov -symbolize pcre2_fuzzer_coverage pcre2_fuzzer_cov.18822.sancov > pcre2.symcov
Kolejnym krokiem jest odpalenie raportu w HTMLu dotyczący pokrycia kodu dla poszczególnych plików testowanego projektu:
python coverage-report-server.py --symcov pcre2.symcov --srcpath .
Efekt końcowy:
Kiedy mamy już dobre pokrycie i szybki fuzzer warto odpalić go w kilku instancjach aby wycisnąć ostatnie soki i z fuzzera i z CPU 🙂
Libfuzzer ma, przynajmniej dla mnie, niecodzienny model zarządzania kilkoma instancjami. Składa się z dwóch części:
Uruchamiając fuzzer w poniższy sposób:
./myfuzzer -workers=4 -jobs=4 corpus_dir/
Znalezienie crasha spowoduje zabicie jednej instancji fuzzera i fuzzing na trzech lub mniejszej liczbie procesorów (w zależności od liczby znalezionych crashy). Czasami może okazać się, że nasze fuzzery przestały całkowicie pracować – dlatego polecam uruchamianie z dużo większą liczbą jobów:
./myfuzzer -workers=4 -jobs=4000 corpus_dir/
Spowoduje to “ciągły” fuzzing, tak jak w przypadku AFLa, który, moim zdaniem, w tej kwestii dużo lepiej podchodzi do problemu.