Użyjmy struktury Pwntools, aby uprościć zadanie pisania naszego exploita. Upewnijmy się, że Pwntools jest zainstalowany. Uruchommy meet_exploit.py, który znajduje się w folderze ~/GHHv6/ch10:
Zadziałało!
Użyjmy struktury Pwntools, aby uprościć zadanie pisania naszego exploita. Upewnijmy się, że Pwntools jest zainstalowany. Uruchommy meet_exploit.py, który znajduje się w folderze ~/GHHv6/ch10:
Zadziałało!
Pamiętaj, że w laboratorium 10-1 rozmiar potrzebny do nadpisania EIP w meet.c wynosi 412. Dlatego użyjemy Pythona do stworzenia naszego exploita. Najpierw wyłączmy ASLR dla tego laboratorium, wykonując następujące polecenie:
Teraz użyjmy printf i wc do obliczenia rozmiaru naszego kodu powłoki:
Następnie użyjemy gdb, aby znaleźć miejsce, w którym należy wskazać EIP, aby wykonać nasz kod powłoki. Wiemy już, że możemy nadpisać EIP 412 bajtami, więc naszym pierwszym krokiem jest załadowanie i zawieszenie pliku binarnego z gdb. Aby to zrobić, wydamy następujące polecenie:
Następnie użyjemy gdb, aby znaleźć miejsce, w którym należy wskazać EIP, aby wykonać nasz kod powłoki. Wiemy już, że możemy nadpisać EIP 412 bajtami, więc naszym pierwszym krokiem jest załadowanie i zawieszenie pliku binarnego z gdb. Aby to zrobić, wydamy następujące polecenie:
Następnie użyjemy gdb, aby znaleźć miejsce, w którym należy wskazać EIP, aby wykonać nasz kod powłoki. Wiemy już, że możemy nadpisać EIP 412 bajtami, więc naszym pierwszym krokiem jest załadowanie i zawieszenie pliku binarnego z gdb. Aby to zrobić, wydamy następujące polecenie:
Shellcode to termin zarezerwowany dla kodu maszynowego, który wykona polecenia hakera. Pierwotnie termin ten został wymyślony, ponieważ celem złośliwego kodu było dostarczenie atakującemu prostej powłoki. Od tego czasu termin ten ewoluował, obejmując kod, który jest używany do zrobienia czegoś więcej niż dostarczenia powłoki, na przykład do podniesienia uprawnień lub wykonania pojedynczego polecenia w zdalnym systemie. Ważne jest, aby zdać sobie sprawę, że kod powłoki jest w rzeczywistości ciągiem binarnych kodów operacji dla eksploatowanej architektury (w tym przypadku Intel x86 32 bit), często reprezentowanych w formie szesnastkowej. W Internecie można znaleźć mnóstwo bibliotek kodu powłoki, gotowych do użycia na wszystkich platformach. Użyjemy kodu powłoki Aleph1 (pokazanego w programie testowym) w następujący sposób:
Skompilujmy i uruchommy program testowy shellcode.c
Zadziałało — otrzymaliśmy powłokę główną.
W kodzie asemblera polecenie NOP (no operation) oznacza po prostu, aby nic nie robić, tylko przejść do następnego polecenia. Hakerzy nauczyli się używać NOP do wypełnienia. Umieszczone na początku bufora exploita wypełnienie to nazywa się NOP sled. Jeśli EIP jest skierowane do NOP sled, procesor przesunie się na sled bezpośrednio do następnego komponentu. W systemach x86 kod operacji 0x90 oznacza NOP. W rzeczywistości jest ich znacznie więcej, ale 0x90 jest najczęściej używany. Każda sekwencja operacji, która nie koliduje z wynikiem exploita, byłaby uważana za równoważną NOP.
Aby zbudować skuteczny exploit w sytuacji przepełnienia bufora, musisz utworzyć większy bufor niż program oczekuje, używając następujących składników: NOP sled, shellcode i adresu powrotu.
Jednym z głównych celów wykorzystywania lokalnego przepełnienia bufora jest kontrolowanie EIP w celu uzyskania wykonania dowolnego kodu w celu osiągnięcia eskalacji uprawnień. W tej sekcji omówimy niektóre z najczęstszych luk w zabezpieczeniach i sposoby ich wykorzystania
Program meet.c wgląda następująco:
Użyjemy Pythona, aby przepełnić bufor 400-bajtowy w meet.c. Python jest językiem interpretowanym, co oznacza, że nie trzeba go prekompilować, co czyni go bardzo wygodnym w użyciu w wierszu poleceń. Na razie musisz zrozumieć tylko jedno polecenie Pythona:
To polecenie po prostu wydrukuje 600 A na standardowym wyjściu (stdout) — spróbuj!
UWAGA Znaki odwrotnego apostrofu (`) są używane do opakowania polecenia i wykonania polecenia przez interpreter powłoki oraz zwrócenia wartości. Skompilujmy i wykonajmy meet.c:
Teraz przekażmy 600 A do programu meet.c jako drugi parametr w następujący sposób:
Zgodnie z oczekiwaniami, Twój 400-bajtowy bufor przepełnił się; miejmy nadzieję, że EIP również. Aby to sprawdzić, uruchom ponownie gdb:
UWAGA: Twoje wartości mogą być inne. Pamiętaj, że próbujemy tu przekazać koncepcję, a nie wartości pamięci.
Nie tylko nie kontrolowaliśmy EIP, ale przenieśliśmy się daleko do innej części pamięci. Jeśli spojrzysz na meet.c, zauważysz, że po funkcji strcpy() w funkcji powitania następuje wywołanie printf(), które z kolei wywołuje vfprintf() w bibliotece libc. Funkcja vfprintf() wywołuje następnie strlen. Ale co mogło pójść nie tak? Masz kilka zagnieżdżonych funkcji, a zatem kilka ramek stosu, z których każda została umieszczona na stosie. Kiedy spowodowałeś przepełnienie, musiałeś uszkodzić argumenty przekazane do funkcji printf(). Przypomnij sobie z poprzedniej sekcji, że wywołanie i prolog funkcji sprawiają, że stos wygląda jak na poniższej ilustracji:
Jeśli zapiszesz poza EIP, nadpiszesz argumenty funkcji, zaczynając od temp1. Ponieważ funkcja printf() używa temp1, będziesz miał problemy. Aby sprawdzić tę teorię, sprawdźmy ponownie w gdb. Kiedy uruchomimy gdb ponownie, możemy spróbować uzyskać listę źródłową:
W poprzednim pogrubionym wierszu widać, że argumenty funkcji temp1 i temp2 zostały uszkodzone. Wskaźniki wskazują teraz na 0x41414141, a wartości to „” (lub null). Problem polega na tym, że printf() nie przyjmuje wartości null jako jedynego wejścia i dlatego się dławi. Zacznijmy więc od mniejszej liczby A, takiej jak 405, a następnie powoli ją zwiększajmy, aż uzyskamy pożądany efekt:
Jak widać, gdy w gdb występuje błąd segmentacji, wyświetlana jest bieżąca wartość EIP. Ważne jest, aby zdać sobie sprawę, że liczby (400–412) nie są tak ważne jak koncepcja zaczynania od niskiego poziomu i powolnego zwiększania, aż do przepełnienia zapisanego EIP i niczego więcej. Dzieje się tak z powodu wywołania printf bezpośrednio po przepełnieniu. Czasami będziesz mieć więcej miejsca na oddech i nie będziesz musiał się tym zbytnio martwić. Na przykład, gdyby nic nie następowało po podatnym poleceniu strcpy, nie byłoby problemu z przepełnieniem powyżej 412 bajtów w tym przypadku.
UWAGA: Pamiętaj, że używamy tutaj bardzo prostego fragmentu wadliwego kodu; w prawdziwym życiu napotkasz wiele takich problemów. Ponownie, chcemy, abyś zrozumiał koncepcje, a nie liczby wymagane do przepełnienia konkretnego podatnego fragmentu kodu.
eraz, gdy znasz już podstawy, możemy przejść do konkretów. Jak opisano w rozdziale 2, bufory służą do przechowywania danych w pamięci. Nas interesują głównie bufory przechowujące ciągi znaków. Same bufory nie mają mechanizmów ograniczających, które uniemożliwiałyby dodawanie większej ilości danych niż oczekiwano. W rzeczywistości, jeśli jako programista będziesz niedbały, możesz szybko
przekroczyć przydzieloną przestrzeń. Na przykład poniższy kod deklaruje ciąg znaków w pamięci o rozmiarze 10 bajtów:
Co się stanie jeśli wykonasz poniższe polecenie?
Teraz musimy skompilować i wykonać program 32-bitowy. Ponieważ mamy 64-bitowy Kali Linux, najpierw musimy zainstalować gcc-multilib, aby dokonać cross-kompilacji 32-bitowych plików binarnych:
Po zainstalowaniu gcc-multilib następnym krokiem jest kompilacja naszego programu przy użyciu opcji -m32 i -fno-stack-protector w celu wyłączenia ochrony Stack Canary:
UWAGA: W systemach operacyjnych w stylu Linuxa warto zwrócić uwagę na konwencję dotyczącą monitów, która pomaga odróżnić powłokę użytkownika od powłoki roota. Zazwyczaj powłoka roota będzie miała znak # jako część monitu, podczas gdy powłoki użytkownika zazwyczaj mają znak $ w monicie. Jest to wizualna wskazówka, która pokazuje, gdy udało Ci się eskalować swoje uprawnienia, ale nadal będziesz chciał to zweryfikować za pomocą polecenia, takiego jak whoami lub id. Dlaczego otrzymałeś błąd segmentacji? Zobaczmy, uruchamiając gdb (GNU Debugger):
UWAGA Randomizacja układu przestrzeni adresowej (ASLR) działa poprzez losowe ustalanie lokalizacji różnych sekcji programu w pamięci, w tym bazy wykonywalnej, stosu, sterty i bibliotek, co utrudnia atakującemu niezawodne przejście do określonego adresu pamięci. Aby wyłączyć ASLR, uruchom następujące polecenie w wierszu poleceń:$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space Teraz przyjrzyjmy się atakowi na meet.c.
Koncepcję stosu w informatyce można najlepiej wyjaśnić, porównując ją do stosu tacek z lunchem w szkolnej stołówce. Gdy kładziesz tacę na stosie, taca, która wcześniej była na górze, jest teraz przykryta. Gdy bierzesz tacę ze stosu, bierzesz tacę z góry stosu, który jest ostatnią tam położoną. Bardziej formalnie, w terminologii informatycznej, stos jest strukturą danych, która ma jakość kolejki FILO (first in, last out). Proces umieszczania elementów na stosie nazywa się push i jest wykonywany w kodzie języka asemblera za pomocą polecenia push. Podobnie proces pobierania elementu ze stosu nazywa się pop i jest wykonywany za pomocą polecenia pop w kodzie języka asemblera.
Każdy uruchomiony program ma swój własny stos w pamięci. Stos rośnie wstecz od najwyższego adresu pamięci do najniższego. Oznacza to, że używając naszego przykładu tacy kawiarnianej, dolna taca byłaby najwyższym adresem pamięci, a górna taca najniższa. Dwa ważne rejestry zajmują się stosem: Extended Base Pointer (EBP) i Extended Stack Pointer (ESP). Jak pokazano na rysunku, rejestr EBP jest podstawą bieżącej ramki stosu procesu (wyższy adres). Rejestr ESP zawsze wskazuje na szczyt stosu (niższy adres). Jak wyjaśniono w rozdziale 2, funkcja jest samodzielnym modułem kodu, który może być wywoływany przez inne funkcje, w tym funkcję main(). Gdy funkcja jest wywoływana, powoduje to skok w przepływie programu. Gdy funkcja jest wywoływana w kodzie asemblera, dzieją się trzy rzeczy:
W kodzie języka asemblera wywołanie wygląda następująco:
Obowiązki wywoływanej funkcji to najpierw zapisanie rejestru EBP wywołującego programu na stosie, następnie zapisanie bieżącego rejestru ESP do rejestru EBP (ustawienie bieżącej ramki stosu), a następnie zmniejszenie rejestru ESP, aby zrobić miejsce dla zmiennych lokalnych funkcji. Na koniec funkcja otrzymuje możliwość wykonania swoich instrukcji. Ten proces nazywa się prologiem funkcji. W kodzie asemblera prolog wygląda tak:
Ostatnią rzeczą, jaką robi wywoływana funkcja przed powrotem do wywołującego programu, jest wyczyszczenie stosu poprzez zwiększenie ESP do EBP, co skutecznie czyści stos jako część instrukcji leave. Następnie zapisany EIP jest usuwany ze stosu jako część procesu powrotu. Jest to określane jako epilog funkcji. Jeśli wszystko pójdzie dobrze, EIP nadal przechowuje następną instrukcję do pobrania, a proces jest kontynuowany za pomocą instrukcji po wywołaniu funkcji. W kodzie asemblera epilog wygląda następująco:
Te małe fragmenty kodu języka asemblera będziesz wielokrotnie spotykać podczas poszukiwania przepełnień bufora.
Dlaczego badać exploity? Etyczni hakerzy powinni badać exploity, aby zrozumieć, czy luki w zabezpieczeniach są podatne na wykorzystanie. Czasami specjaliści ds. bezpieczeństwa błędnie wierzą i publicznie stwierdzają, że dana luka w zabezpieczeniach nie jest możliwa do wykorzystania, ale hakerzy black hat wiedzą co innego. Niezdolność jednej osoby do znalezienia exploita dla luki w zabezpieczeniach nie oznacza, że ktoś inny nie może tego zrobić. To kwestia czasu i poziomu umiejętności. Dlatego etyczni hakerzy muszą zrozumieć, jak wykorzystywać luki w zabezpieczeniach i sprawdzić to sami. W tym procesie mogą musieć stworzyć kod proof-of-concept, aby wykazać dostawcy, że luka w zabezpieczeniach jest możliwa do wykorzystania i należy ją naprawić. W tym rozdziale skupimy się na wykorzystywaniu 32-bitowych przepełnień stosu Linuksa, wyłączaniu technik łagodzenia exploitów w czasie kompilacji i losowym rozmieszczeniu przestrzeni adresowej (ASLR). Postanowiliśmy zacząć od tych tematów, ponieważ są łatwiejsze do zrozumienia.Dlaczego badać exploity? Etyczni hakerzy powinni badać exploity, aby zrozumieć, czy luki w zabezpieczeniach są podatne na wykorzystanie. Czasami specjaliści ds. bezpieczeństwa błędnie wierzą i publicznie stwierdzają, że dana luka w zabezpieczeniach nie jest możliwa do wykorzystania, ale hakerzy black hat wiedzą co innego. Niezdolność jednej osoby do znalezienia exploita dla luki w zabezpieczeniach nie oznacza, że ktoś inny nie może tego zrobić. To kwestia czasu i poziomu umiejętności. Dlatego etyczni hakerzy muszą zrozumieć, jak wykorzystywać luki w zabezpieczeniach i sprawdzić to sami. W tym procesie mogą musieć stworzyć kod proof-of-concept, aby wykazać dostawcy, że luka w zabezpieczeniach jest możliwa do wykorzystania i należy ją naprawić. W tym rozdziale skupimy się na wykorzystywaniu 32-bitowych przepełnień stosu Linuksa, wyłączaniu technik łagodzenia exploitów w czasie kompilacji i losowym rozmieszczeniu przestrzeni adresowej (ASLR). Postanowiliśmy zacząć od tych tematów, ponieważ są łatwiejsze do zrozumienia.