Testowanie odporności programu na błędne dane (ang. fuzzing)
Spis treści
- Podatność (ang. vulnerability)
- Odpowiedzialne ujawnienie (ang. coordinated disclosure)
- Pełne ujawnienie (ang. full disclosure)
- Brak publicznego ujawnienia (ang. private disclosure)
- Podatność dnia zerowego (ang. zero day)
- Czarna, szara i biała skrzynka (ang. black, grey and white box)
- Próba dymu (ang. smoke test)
- Przesyłanie zniekształconych danych (ang. fuzzing)
- Podstawowe wektory ataku na aplikacje
- Zasięg testów (ang. coverage)
- Wykonanie symboliczne (ang. symbolic execution)
- Wykonanie metody używając konkretnych wartości
- Instrumentacja kodu programu (ang. code instrumentation)
- Przypadki brzegowe i skrajne
- Mutacje danych (ang. mutation)
- Generacje danych i gramatyka
- Wprowadzenie do narzędzia AFL++ (American Fuzzy Lop)
- 30 porad zwiększających świadomość zagrożeń
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.
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.
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.
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ć.
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ą.
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.
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.
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
Na początek poleceniem pwd
można sprawdzić
bieżący katalog roboczy, a za pomocą ls
wyświetlić
zawartość tego katalogu.
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.
Po wcześniejszej synchronizacji można zainstalować
narzędzie AFL++ za pomocą polecenia sudo apt install afl++
.
W celu kontynuowania instalacji należy wpisać literę T
i zatwierdzić Enter.
W celu weryfikacji instalacji narzędzia AFL++ można
uruchomić je bez parametrów poleceniem 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
.
#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.
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.
Należy jeszcze wyłączyć wysyłanie informacji o
zrzucie w przypadku błędów poleceniami:
sudo su
Polecenie
echo core >/proc/sys/kernel/core_pattern
su ethicalblue
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.
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).
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;
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] https://www.cve.org/ResourcesSupport/Glossary [dostęp: 2024-11-13]
- [2] https://www.cve.org/ResourcesSupport/FAQs [dostęp: 2024-11-13]
- [3] https://certcc.github.io/CERT-Guide-to-CVD/ [dostęp: 2024-11-13]
- [4] https://www.cvedetails.com/cwe-definitions/ [dostęp: 2024-11-13]
- [5] Dawid Farbaniec, ethical.blue Magazine Volume 2. Podatności (ang. vulnerability), ISBN: 978-83-962697-3-7