ethical.blue Magazine

// Cybersecurity clarified.

Testowanie odporności programu na błędne dane (ang. fuzzing)

...
Fuzzing // // Dawid Farbaniec
Ostatnia modyfikacja:

Spis treści

Common Weakness Enumeration (CWE™)

CWE™ is free to use by any organization or individual for any research, development, and/or commercial purposes, per these CWE Terms of Use. Accordingly, The MITRE Corporation hereby grants you a nonexclusive, royalty-free license to use CWE for research, development, and commercial purposes. Any copy you make for such purposes is authorized on the condition that you reproduce MITRE’s copyright designation and this license in any such copy. CWE is a trademark of The MITRE Corporation. Please contact [email protected] if you require further clarification on this issue.
https://cwe.mitre.org/

Common Vulnerabilities and Exposures (CVE®)

CVE® is a registered trademark of the MITRE Corporation and the authoritative source of CVE details are MITRE's CVE pages. CWE is a registred trademark of the MITRE Corporation and the authoritative source of CWE details are MITRE's CWE pages.
https://www.cve.org/

Podatność (ang. vulnerability)

W sensie bezpieczeństwa systemów informatycznych słowo podatność można wyjaśnić jako słabość czy wrażliwość, które mogą zostać wykorzystane (ang. exploited) i mieć negatywny wpływ (ang. negative impact) na triadę: Poufność (ang. confidentiality), Integralność (ang. integrity) i Dostępność (ang. availability) lub pozwalać na zachowania naruszające jawną lub domyślną politykę bezpieczeństwa.

Poufność, Integralność i Dostępność
Triada: Poufność, Integralność i Dostępność

Odpowiedzialne ujawnienie (ang. coordinated disclosure)

Podejście, które przeważnie polega najpierw na poinformowaniu o podatności przez prywatny kanał komunikacji, ale z zaznaczeniem, że po określonym czasie (ang. deadline) lub po wydaniu poprawki, szczegóły dotyczące odkrytej podatności staną się publicznie dostępne.

Pełne ujawnienie (ang. full disclosure)

Podejście nazywane pełnym ujawnieniem (ang. full disclosure) to ujawnienie informacji o podatności publicznie zaraz po jej odkryciu często z fragmentem kodu udowadniającym możliwość dokonania naruszenia. Pozytywną stroną tego rodzaju poglądu jest presja na producenta, aby nie lekceważył błędu i priorytetowo wydał poprawkę.

Brak publicznego ujawnienia (ang. private disclosure)

W tym podejściu odkrywca podatności informuje prywatnym kanałem komunikacji organizację o znalezionym problemie w zabezpieczeniach. Pozytywną stroną tego rodzaju poglądu jest ograniczenie informacji o podatności do zawężonej grupy podmiotów. Niestety podejście to może spowodować ignorowanie podatności przez organizację, opóźnienia w wydaniu poprawki czy wykorzystywanie podatności przez zawężoną grupę podmiotów zagrażających (ang. threat actor) przez długi czas.

Podatność dnia zerowego (ang. zero day)

Niezawsze szczegóły dotyczące podatności są wysyłane do producenta czy nawet publikowane. Istnieją podatności o których wiedzą tylko niektóre podmioty i to właśnie tego rodzaju ataki są bardzo niebezpieczne.

Czarna, szara i biała skrzynka (ang. black, grey and white box)

Testy czarnej skrzynki są prawdopodobnie najczęstsze wśród hakerów. Bez wiedzy o systemie, który jest celem następują próby znalezienia podatności. Testy szarej skrzynki to sytuacje w których osoba testująca posiada ograniczoną wiedzę o systemie, który jest celem. Testy białej skrzynki to weryfikacja zabezpieczeń posiadając często dokumentację systemu, a nawet dostęp do kodów źródłowych w przypadku aplikacji.

Testy czarnej, szarej i białej skrzynki
Testy czarnej, szarej i białej skrzynki

Próba dymu (ang. smoke test)

Test nazywany próbą dymu to przeważnie uruchomienie wczesnej wersji programu w celu sprawdzenia czy krytyczna funkcjonalność jest dostępna. Inna sytuacja to uruchomienie aplikacji po wprowadzeniu dość znaczących zmian, aby zweryfikować czy aktualizacja nie uszkodziła podstawowej funkcjonalności.

Próba dymu
Próba dymu (ang. smoke test)

Przesyłanie zniekształconych danych (ang. fuzzing)

Technika określana terminem fuzzing polega na zautomatyzowanym sprawdzaniu odporności oprogramowania lub innego systemu na błędne dane i skrajne przypadki, które mogą wystąpić.

Kłaczek
Kłaczek (ang. fuzz)

Podstawowe wektory ataku na aplikacje

Wszelkie wejścia do programu mogą stać się wektorem ataku i być narażone na wprowadzenie błędnych danych. W przypadku aplikacji internetowych należy odpowiednio zabezpieczyć wszelkie parametry możliwe do zmodyfikowania przez użytkownika końcowego. Jeśli dla celów eksperymentalnych pod adresem hxxps://ethical.blue/fuzz/ działała by aplikacja internetowa przyjmująca parametr np. hxxps://ethical.blue/fuzz/3, wtedy jest to potencjalny wektor ataku. Zależnie od funkcjonalności metody /fuzz można użyć skanera lub stworzyć własne narzędzie, aby sprawdzić reakcję programu na nietypowe dane np.
hxxps://ethical.blue/fuzz/0
hxxps://ethical.blue/fuzz/-1
hxxps://ethical.blue/fuzz/ffffffff
hxxps://ethical.blue/fuzz/text
hxxps://ethical.blue/fuzz/..
hxxps://ethical.blue/fuzz/a.svg
Zdeformowane i skrajne wartości podawane do programu nazywane są ładunkami (ang. payload). Inne wektory ataku to np. pola tekstowe (wpisanie tekstu zamiast liczby itp.), użycie kontrolek interfejsu użytkownika w nietypowej kolejności lub w nieprzewidziany sposób, parametry podawane przez Wiersz polecenia, wysyłane żądania przez sieć (przy weryfikacji protokołu lub interfejsu nazywanego API), importowany plik niezgodny z formatem, który później trafia do parsera, niepoprawne dane umieszczone na stosie programu i inne zależne od projektu.

Testowanie określane anglojęzycznym terminem fuzzing pozwala oszczędzić czas dzięki automatyzacji. Jednak nie zastępuje indywidualnego podejścia na które składają się m.in. analiza kodu źródłowego (lub inżynieria odwrotna) oraz samodzielnie napisane generatory ładunków (ang. payload) ukierunkowane na określoną aplikację.

Zasięg testów (ang. coverage)

W przypadku testów istotna jest możliwość zmierzenia jaka część kodu została przetestowana. Może to być np. sprawdzenie liczby wykonanych linii kodu i później zamiana ułamka na wartość procentową.

Zasięg testów
Zasięg testów (ang. coverage)

Zabieg tego rodzaju daje ogólny pogląd na ilość wykonanych linii kodu, wywołanych metod czy też które ścieżki w przypadku instrukcji warunkowych się wykonały (zostały przetestowane).

Wykonanie symboliczne (ang. symbolic execution)

Podejście tego rodzaju polega w skrócie na użyciu symboli zamiast konkretnych wartości podczas testów programu. Pozwala to utworzyć drzewo wykonania, które uwzględnia różne ścieżki kodu.

Wykonanie symboliczne
Wykonanie symboliczne (ang. symbolic execution)

Niech za przykład posłuży metoda Simple(int v, int x), która w przypadku poprawnych wartości zwraca prawdę (ang. true), a w przypadku błędu zwraca fałsz (ang. false). Metoda ta przyjmuje dwa parametry od wartości których zależna jest zwracana wartość typu logicznego bool. Niech zmienna v będzie oznaczona symbolem a, natomiast zmienna x niech będzie oznaczona symbolem b. Te nazwy symboli mogą być dowolne. Zmienna z przyjmuje wartość v razy trzy. Pamiętamy, że zmienna v to symbol a. Instrukcja warunkowa if weryfikuje czy zmienna z jest różna od zero, a z to v razy trzy. Dlatego pierwszy blok to będzie sprawdzenie czy a razy trzy jest różne od zero. Kolejny warunek to weryfikacja czy wartość zmiennej v jest równa wartości zmiennej x, czyli używając nadanych wcześniej symboli oznacza to sprawdzenie czy a jest równe b. Kolejna instrukcja warunkowa to sprawdzenie czy wartość zmiennej v jest większa od wartości zmiennej x. Używając nadanych symboli oznacza to weryfikację czy a jest większe od b.

Wykonanie metody używając konkretnych wartości

Przykładowy kod można też wykonać używając konkretnych wartości zamiast symboli. Np. v = 0, x = 3 czy też v = 3, x = 1. Te dwa podejścia współpracują ze sobą podczas testów, a ich połączenie określa się po angielsku concolic (słowa concrete i symbolic).

Console.WriteLine($"{Simple(0, 3)}"); //False
Console.WriteLine($"{Simple(3, 1)}"); //True

Instrumentacja kodu programu (ang. code instrumentation)

Technika ta polega na wstrzyknięciu lub wstawieniu specjalnych procedur do kodu aplikacji, których zadaniem jest nadzorowanie wykonania programu. Dodatkowe procedury mogą być umieszczone ręcznie np. w formie instrukcji wypisujących wartości parametrów na konsolę debugowania. Mogą też np. mierzyć czas wykonania kodu w celu optymalizacji.

W przypadku testowania odporności programu na błędne dane (ang. fuzzing) wstrzykiwane procedury analizują wykonanie programu i mogą wspomagać decydowanie o tym w jaki sposób dalej testować poszczególne ścieżki wykonania kodu.

W celu zwiększenia zasięgu testów (ang. coverage) wykonanie techniką concolic pozwala zebrać informacje o ograniczeniach (ang. constraint), aby weryfikować nieprzetestowane dotąd ścieżki wykonania.

Przypadki brzegowe i skrajne

Przypadki brzegowe (ang. edge case) to sytuacje lub problemy występujące przy minimalnej lub maksymalnej wartości parametru weryfikowanego systemu. Na przykład: Doświadczenie postaci w grze RPG reprezentuje 16-bitowa liczba całkowita bez znaku i przypadkiem brzegowym może być uzyskanie doświadczenia równego 65535 dziesiętnie (0xFFFF szesnastkowo).

Natomiast przypadki skrajne (ang. corner case) nazywane też patologicznymi mogą wystąpić, gdy wartości wielu parametrów powodują nieprawidłowe działanie systemu.

Mutacje danych (ang. mutation)

Całkowicie losowe dane wysyłane do programu czy innego testowanego systemu mogą być łatwo rozpoznane i odrzucone. Testowanie odporności programu na błędne dane oparte na mutacjach pozwala modyfikować zbiór poprawnych wartości za pomocą kodu np. zamieniając jeden znak, wstawiając dodatkowy znak na koniec etc.

Mutacje danych w fuzzingu
Mutacje danych w fuzzingu

Generacje danych i gramatyka

Testowanie odporności programu na błędy za pomocą mutacji danych jest często niewystarczające. W przypadku przetwarzania np. pliku o określonym formacie należy wysyłać do programu lub testowanego systemu ładunki (ang. payload) zgodne z formatem lub bardzo mu bliskie. W anglojęzycznej literaturze narzędzia oparte wyłącznie na mutacjach są określane jako głupie (ang. dumb), natomiast te oparte o generacje z obsługą gramatyki jako inteligentne (ang. intelligent).

Wprowadzenie do narzędzia AFL++ (American Fuzzy Lop)

Narzędzie AFL++ jest oparte na znanym programie American Fuzzy Lop.

Podstawy obsługi terminala systemu Ubuntu

Terminal systemu Ubuntu z rodziny Linux
Terminal systemu Ubuntu z rodziny Linux

Na początek poleceniem pwd można sprawdzić bieżący katalog roboczy, a za pomocą ls wyświetlić zawartość tego katalogu.

Polecenie pwd w systemie Ubuntu
Polecenie pwd w systemie Ubuntu

Przed instalacją oprogramowania zawsze warto wykonać synchronizację za pomocą polecenia sudo apt-get update, które informuje apt-get, aby pobrało najnowsze informacje o dostępnych wersjach programów.

Polecenie sudo apt-get update w systemie Ubuntu
Polecenie sudo apt-get update w systemie Ubuntu

Po wcześniejszej synchronizacji można zainstalować narzędzie AFL++ za pomocą polecenia sudo apt install afl++.

Polecenie sudo apt install w systemie Ubuntu
Polecenie sudo apt install w systemie Ubuntu

W celu kontynuowania instalacji należy wpisać literę T i zatwierdzić Enter.

Kontynuacja instalacji AFL++
Kontynuacja instalacji AFL++

W celu weryfikacji instalacji narzędzia AFL++ można uruchomić je bez parametrów poleceniem afl-fuzz.

Uruchomienie narzędzia afl-fuzz
Uruchomienie narzędzia afl-fuzz

Przykładowy program z podatnościami (vuln.cpp)

Za pomocą polecenia pwd można sprawdzić bieżący katalog roboczy. Dalej jest utworzenie pliku o nazwie vuln.cpp poleceniem touch. Po wpisaniu ls można zobaczyć, że plik został utworzony. Zawartość pliku można edytować np. za pomocą polecenia editor ./vuln.cpp.

Uruchomienie edytora GNU nano
Uruchomienie edytora GNU nano
#include <iostream>
#include <fstream>
#include <sstream>

int main(int argc, char* argv[])
{
    /* This is vulnerable C++ program for fuzzing exercise */

    if (argc != 2)
        return EXIT_SUCCESS;

    std::string path = argv[1];

    std::fstream fileStream(path.c_str(), std::fstream::in);
    fileStream.open(path.c_str());
    std::stringstream textStream;
    textStream << fileStream.rdbuf();

    std::cout << "Payload is ";
    std::cout << textStream.str() << std::endl;

    if(textStream.str().c_str()[0] != 'F')
        return EXIT_SUCCESS;

    std::cout << "Crash?!" << std::endl;
    reinterpret_cast<void(*)()>(0xDEAD)();

    return EXIT_SUCCESS;
}

Po wprowadzeniu przykładowego kodu źródłowego można zapisać zmiany wciskając CTRL+O i zatwierdzając Enter. Natomiast, aby wyjść z edytora należy wcisnąć CTRL+X.

Obsługa edytora GNU nano
Obsługa edytora GNU nano

Dalsze przygotowania środowiska do eksperymentu to utworzenie katalogów test_cases oraz findings. Pierwszy katalog będzie zawierał pliki, które posłużą za przypadki testowe, a drugi katalog jest na artefakty znalezione przez narzędzie typu fuzzer. Do celów prostej prezentacji wystarczy jeden przypadek testowy, który spowoduje błąd wcześniej napisanego programu.

Poleceniem cat > ./test_cases/data.txt można utworzyć plik o nazwie data.txt w katalogu test_cases o zawartości Payload_example. W konsoli po wpisaniu zawartości należy wcisnąć CTRL+D, aby wyjść z trybu edycji.

Utworzenie katalogów i przypadku testowego
Utworzenie katalogów i przypadku testowego

Należy jeszcze wyłączyć wysyłanie informacji o zrzucie w przypadku błędów poleceniami:
sudo su
echo core >/proc/sys/kernel/core_pattern
su ethicalblue
Polecenie sudo su to zalogowanie na bardziej uprzywilejowane konto root. Dalej jest polecenie sugerowane przez narzędzie AFL++ i powrót na konto zwykłego użytkownika za pomocą polecenia su <nazwa-użytkownika>.

Kolejny krok to utworzenie pustego pliku wykonywalnego.

touch ./vuln.elf

Niniejszym poleceniem możliwe jest ustawienie atrybutów rwx (ang. read, write and execute) dla bieżącego użytkownika (u) dla pliku vuln.elf w bieżącym katalogu (./).

chmod u+rwx ./vuln.elf

W celu zbudowania programu wraz z instrumentacją należy użyć polecenia:

afl-clang-fast++ ./vuln.cpp -o
./vuln.elf

Jeśli polecenia się powiodły, to na konsoli powinna pojawić się informacja o dodaniu instrumentacji.

Kompilacja przykładowego programu z dodaniem instrumentacj
Kompilacja przykładowego programu z dodaniem instrumentacji

Poniższe polecenie uruchamia fuzzer AFL++ wskazując katalog ./test_cases jako wejście oraz katalog ./findings na znalezione artefakty. Znaki -- oddzielają wywołanie narzędzia fuzzer od pliku testowanego programu, którym to plikiem jest tutaj vuln.elf. Znaki @@ oznaczają miejsce, gdzie ma być wstawiony ładunek (ang. payload), czyli plik wczytywany przez program, którego odporność jest testowana.

afl-fuzz -i ./test_cases -o ./findings -- ./vuln.elf @@

Narzędzie AFL++ (American Fuzzy Lop) wyświetla 4362 awarie (w tym 3 warte zapisania).

Narzędzie AFL++ podczas działania
Narzędzie AFL++ podczas działania

W celu wyświetlenia znalezionych podatności (ang. vulnerability) z folderu ./findings/crashes można zastosować polecenie:

for f in ./findings/default/crashes/*:*; do xxd "$f"; echo ""; done;

Wyświetlenie ładunków (ang. payload) z narzędzia AFL++
Wyświetlenie ładunków (ang. payload) z narzędzia AFL++

30 porad zwiększających świadomość zagrożeń

1. Nie przesyłaj danych przez sieć bez szyfrowania i uwierzytelnienia obu stron (najlepiej certyfikatem).
2. Unikaj szczegółowych komunikatów o błędzie, które mogą zdradzić szczegóły związane z bezpieczeństwem systemu.
3. Nie pozwalaj na dostęp do krytycznych parametrów aplikacji z zewnątrz.
4. Jeśli aplikacja przyjmuje pliki, to zwróć uwagę jak reaguje na pliki skrótu (*.LNK) i dowiązania symboliczne (*.SYMLINK).
5. Nie pozwalaj na bezpośrednią możliwość wprowadzenia nazwy pliku do odczytania np. w parametrze widocznym na zewnątrz.
6. Jeśli nie potrafisz odpowiednio zneutralizować niebezpiecznych sekwencji, to utwórz ścisłą listę jakie dane wejściowe są dozwolone, a unikniesz wstrzyknięcia złośliwego kodu.
7. Staraj się unikać tworzenia własnych filtrów na niebezpieczne dane. Stosuj sprawdzone rozwiązania. Pamiętaj, że takie same bajty mogą być zapisane na różne sposoby. W prostych słowach: liczba to bajty, tekst to bajty i grafika czy emotikona to też bajty.
8. Jeśli w raportach pojawiają się dane, które spowodowały awarię, to pamiętaj, aby odpowiednio zneutralizować elementy specjalne, a unikniesz wstrzyknięcia złośliwego kodu.
9. Wartości liczbowe np. o rozmiarze 16 bit mogą przyjąć wartości od 0x0000 do 0xFFFFF (heksadecymalnie). Pamiętaj, aby sprawdzić jak logika aplikacji zareaguje na minimalną oraz maksymalną wartość oraz jak je zinterpretuje (0xFFFF to dziesiętnie -1 ze znakiem, albo 65535 bez znaku).
10. Wartości liczbowe o określonym rozmiarze w bajtach mają dolną i górną granicę. Pamiętaj, aby sprawdzić jak logika aplikacji reaguje na wyjście poza te wartości. Np. czy dane się nasycą i przyjmą minimalną lub maksymalną wartość czy też może nastąpi przekręcenie jak w liczniku w starym samochodzie.
11. W przypadku obliczania rozmiaru ciągu znaków w bajtach zwróć uwagę jaki rozmiar ma jeden znak.
12. Wyrażenia regularne potrafią być bardzo kosztowne obliczeniowo. Ma to szczególne znaczenie w przypadku chmury.
Nieprawidłowe wyrażenia mogą przyjąć niebezpieczne dane i przekazać je dalej, albo wywołać awarię typu odmowa usługi (ang. denial of service).
13. Nie polegaj na tym, że elementy w pamięci operacyjnej są ułożone w dany sposób, gdyż mogą zostać przemieszczone w najmniej spodziewanym momencie.
14. Indeksy elementów są przeważnie numerowane od zera, czyli int a[10]; a[10] = 0xDEAD; to błąd, gdyż ostatni element ma tutaj indeks dziewięć.
15. Dane wrażliwe nie powinny być przechowywane w katalogu głównym aplikacji internetowej, nawet pod ścieżką w stylu /0b596f4a3cebea1ba38-801e59586fefaf2-592c5c3f0b06dd680-0aead3f5fc466-4827a81d3e4-6f2655cc8526a50- a7e5a5f82f3434d3-f06affdc576804a5-68bbfe/secret.txt.
16. Niesprawdzanie wartości zwracanych z funkcji czy metod oraz niezłapane wyjątki mogą narazić aplikację na awarię.
17. Nigdy nie przechowuj haseł w postaci czystego tekstu lub w możliwym do odzyskania formacie. Stosuj sprawdzone algorytmy kryptograficzne, nie wymyślaj swoich.
18. Nie polegaj na adresie protokołu internetowego (adresie IP) w kwestii uwierzytelniania. Lokalizacja sieciowa może być przedstawiona na wiele sposobów, a maszyna obsługująca DNS (Domain Name System) może zwrócić inny adres IP dla tej samej nazwy hosta. Zależy przez kogo jest kontrolowana.
19. Przy dostępie do bardzo krytycznych funkcji powinno być ponowne uwierzytelnienie.
20. Uwierzytelnianie nie powinno być oparte na jednym składniku np. tylko hasło.
21. Wartości losowe powinny mieć odpowiedni zakres, aby nie były łatwe do przewidzenia czy zgadnięcia.
22. Nie wykonuj deserializacji z niezaufanego źródła bez weryfikacji czy rezultat operacji zwróci poprawne dane.
23. Nie umieszczaj kodu uwierzytelniającego client-side i nie polegaj na zaciemnieniu (ang. obfuscation) jako podstawowej metodze zapewniającej bezpieczeństwo.
24. Nie umieszczaj danych uwierzytelniających na stałe w kodzie (ang. hardcoded).
25. Zarezerwowane bity czy parametry aplikacji na przyszłość powinny być wyłączone i nie mieć wpływu na logikę systemu.
26. Zapewnij odpowiednie bezpieczeństwo fizyczne dla danych.
27. Rejestry przechowujące ustawienia związane z bezpieczeństwem powinny być odpowiednio zerowane przy resecie i nie pozostawać niezainicjalizowane.
28. Pamiętaj, że istnieje tak zwane race condition. Przykład. Plik jest usuwany z katalogu po odczytaniu. Jednak funkcja odczytująca zablokowała plik trochę dłużej i procedura czyszcząca go nie usunęła.
29. To nie wszystko. Te porady nie wystarczą.
30. Musisz się wciąż rozwijać i dużo czytać.

Wykaz literatury

  1. [1] https://www.cve.org/ResourcesSupport/Glossary [dostęp: 2024-11-13]
  2. [2] https://www.cve.org/ResourcesSupport/FAQs [dostęp: 2024-11-13]
  3. [3] https://certcc.github.io/CERT-Guide-to-CVD/ [dostęp: 2024-11-13]
  4. [4] https://www.cvedetails.com/cwe-definitions/ [dostęp: 2024-11-13]
  5. [5] Dawid Farbaniec, ethical.blue Magazine Volume 2. Podatności (ang. vulnerability), ISBN: 978-83-962697-3-7