Interfejsy API instrumentacji binarnej

https://chacker.pl/

Ogólna instrumentacja binarna, która pozwala na dodawanie nowego kodu w każdym miejscu pliku binarnego, jest znacznie trudniejsza do poprawnej implementacji niż proste techniki modyfikacji binarnej, które przedstawiłeś w rozdziale 7. Pamiętaj, że nie możesz po prostu wstawić nowego kodu do istniejącej sekcji kodu binarnego, ponieważ nowy kod przesunie istniejący kod na inne adresy, tym samym przerywając odwołania do tego kodu. Zlokalizowanie i załatanie wszystkich istniejących odwołań po przeniesieniu kodu jest praktycznie niemożliwe, ponieważ pliki binarne nie zawierają żadnych informacji wskazujących, gdzie znajdują się te odwołania, i nie ma sposobu na wiarygodne odróżnienie adresów, do których się odwołują, od stałych, które wyglądają jak adresy, ale nimi nie są. Na szczęście istnieją ogólne platformy instrumentacji binarnej, których możesz użyć, aby poradzić sobie ze wszystkimi trudnościami implementacji, i oferują one stosunkowo łatwe w użyciu interfejsy API, za pomocą których możesz implementować narzędzia instrumentacji binarnej. Te interfejsy API zazwyczaj umożliwiają instalację wywołań zwrotnych do kodu instrumentacji w wybranych punktach instrumentacji. W dalszej części tego rozdziału zobaczysz dwa praktyczne przykłady instrumentacji binarnej z wykorzystaniem platformy Pin, popularnej platformy instrumentacji binarnej. Za pomocą platformy Pin zaimplementujesz profiler, który rejestruje statystyki dotyczące wykonania pliku binarnego, wspomagając optymalizację. Za pomocą platformy Pin zaimplementujesz również automatyczny depaker, który pomaga w deobfuskacji spakowanych plików binarnych. Można wyróżnić dwie klasy platform instrumentacji binarnej: statyczną i dynamiczną. Najpierw omówimy różnice między tymi dwiema klasami, a następnie przyjrzymy się ich działaniu na niskim poziomie.

Czym jest instrumentacja binarna?

https://chacker.pl/

Wstawianie nowego kodu w dowolnym miejscu istniejącego pliku binarnego w celu obserwacji lub modyfikacji jego działania nazywa się instrumentacją pliku binarnego. Punkt, w którym dodaje się nowy kod, nazywa się punktem instrumentacji, a dodany kod – kodem instrumentacji. Załóżmy na przykład, że chcemy wiedzieć, które funkcje w pliku binarnym są wywoływane najczęściej, aby móc skupić się na ich optymalizacji. Aby to sprawdzić, możemy instrumentować wszystkie instrukcje wywołań w pliku binarnym, dodając kod instrumentacji, który rejestruje cel wywołania, tak aby instrumentowany plik binarny generował listę wywoływanych funkcji podczas wykonywania. Chociaż ten przykład dotyczy jedynie obserwacji działania pliku binarnego, można je również modyfikować. Na przykład, można zwiększyć bezpieczeństwo pliku binarnego przed atakami typu control-hijacking, instrumentując wszystkie pośrednie transfery sterowania (takie jak call rax i ret) kodem, który sprawdza, czy cel przepływu sterowania znajduje się w zestawie oczekiwanych celów. W przeciwnym razie przerywasz wykonywanie i generujesz alert.

INSTRUMENTACJA BINARNA

https://chacker.pl/

Poznałeś kilka technik modyfikowania i rozszerzania programów binarnych. Choć są one stosunkowo proste w użyciu, techniki te są ograniczone pod względem ilości nowego kodu, który można wstawić do pliku binarnego, oraz miejsca, w którym można go wstawić. W tym rozdziale poznasz technikę zwaną instrumentacją binarną, która pozwala na wstawienie praktycznie nieograniczonej ilości kodu w dowolnym miejscu pliku binarnego w celu obserwacji lub modyfikacji jego działania. Po krótkim omówieniu instrumentacji binarnej omówię, jak wdrażać statyczną instrumentację binarną (SBI) i dynamiczną instrumentację binarną (DBI), dwa rodzaje instrumentacji binarnej o różnych kompromisach. Na koniec dowiesz się, jak budować własne narzędzia do instrumentacji binarnej za pomocą Pin, popularnego systemu DBI firmy Intel.

Podsumowanie

https://chacker.pl/

Powinieneś teraz czuć się swobodnie korzystając z Capstone, aby rozpocząć tworzenie własnych deasemblerów. Wszystkie przykłady w tym rozdziale są dostępne na maszynie wirtualnej dołączonej do tej książki. Eksperymentowanie z nimi to dobry punkt wyjścia do opanowania API Capstone. Skorzystaj z poniższych ćwiczeń i wyzwań, aby sprawdzić swoje umiejętności w zakresie deasemblacji!

Ćwiczenia

  1. Uogólnianie deasemblera

Wszystkie narzędzia do deasemblacji, które widziałeś w tym rozdziale, skonfigurowały Capstone do deasemblacji tylko kodu x64. Dokonałeś tego, przekazując CS_ARCH_X86 i CS_MODE_64 jako argumenty architektury i trybu do cs_open. Uogólnijmy te narzędzia, aby automatycznie wybierały odpowiednie parametry Capstone do obsługi innych architektur, sprawdzając typ załadowanego pliku binarnego za pomocą pól arch i bits w klasie Binary udostępnianej przez program ładujący. Aby dowiedzieć się, które argumenty architektury i trybu przekazać do Capstone, pamiętaj, że plik /usr/include/capstone/capstone.h zawiera listy wszystkich możliwych wartości cs_arch i cs_mode.

  1. Jawne wykrywanie nakładających się bloków

Chociaż przykładowy rekurencyjny deasembler radzi sobie z nakładającymi się blokami podstawowymi, nie wyświetla on żadnego wyraźnego ostrzeżenia o takim nałożeniu kodu. Rozszerz deasembler, aby poinformować użytkownika, które bloki się nakładają.

  1. Wyszukiwarka gadżetów międzywariantowych

Podczas kompilacji programu ze źródeł, wynikowy plik binarny może się znacznie różnić w zależności od takich czynników, jak wersja kompilatora, opcje kompilacji czy architektura docelowa. Ponadto strategie randomizacji, które zabezpieczają pliki binarne przed wykorzystaniem poprzez zmianę alokacji rejestrów lub przetasowanie kodu, komplikują proces wykorzystania. Oznacza to, że podczas tworzenia exploita (takiego jak exploit ROP), nie zawsze będziesz wiedzieć, który „wariant” binarny programu jest uruchomiony na serwerze docelowym. Na przykład, czy serwer docelowy został skompilowany za pomocą gcc czy llvm? Czy działa na systemie 32-bitowym czy 64-bitowym? Jeśli zgadniesz źle, Twój exploit prawdopodobnie się nie powiedzie. W tym ćwiczeniu Twoim celem jest rozszerzenie wyszukiwarki gadżetów ROP tak, aby przyjmowała jako dane wejściowe dwa lub więcej plików binarnych, reprezentujących różne warianty tego samego programu. Powinna ona wygenerować listę VMA zawierających użyteczne gadżety we wszystkich wariantach. Twój nowy program do wyszukiwania gadżetów powinien być w stanie przeskanować każdy z wejściowych plików binarnych w poszukiwaniu gadżetów, ale wyprowadzać tylko te adresy, w których wszystkie pliki binarne zawierają gadżet, a nie tylko niektóre z nich. Dla każdego zgłoszonego VMA, gadżety powinny również implementować podobne operacje. Na przykład, będą zawierać instrukcję add lub mov. Implementacja użytecznego pojęcia podobieństwa będzie częścią wyzwania. Efektem końcowym powinna być wielowariantowa wyszukiwarka gadżetów, która może być używana do tworzenia exploitów działających jednocześnie na wielu wariantach tego samego programu! Aby przetestować swój program do wyszukiwania gadżetów, możesz utworzyć warianty wybranego programu, kompilując go wielokrotnie z różnymi opcjami kompilacji lub różnymi kompilatorami.

Uruchamianie wyszukiwarki gadżetów

https://chacker.pl/

Interfejs wiersza poleceń wyszukiwarki gadżetów jest taki sam, jak w przypadku narzędzi do deasemblacji. Listing  pokazuje, jak powinien wyglądać wynik.

Przykładowy wynik skanera ROP

Każdy wiersz danych wyjściowych zawiera ciąg gadżetu, a następnie adresy, pod którymi znajduje się ten gadżet. Na przykład, pod adresem 0x406697 znajduje się gadżet add al, ch; ret, którego można użyć w ładunku ROP, aby dodać rejestry al i ch. Przegląd dostępnych gadżetów, taki jak ten, bardzo pomaga w wyborze odpowiednich gadżetów ROP do użycia podczas tworzenia ładunku ROP w celu wykorzystania w exploicie.

Znajdowanie wszystkich gadżetów w danym korzeniu

https://chacker.pl/

Jak wspomniano, funkcja find_gadgets_at_root znajduje wszystkie gadżety, które kończą się w danej instrukcji korzenia. Rozpoczyna od przydzielenia bufora instrukcji, który jest potrzebny podczas korzystania z cs_disasm_iter. Następnie wchodzi w pętlę, która przeszukuje wstecz od instrukcji korzenia, zaczynając od jednego bajtu przed adresem korzenia i dekrementując adres wyszukiwania w każdej iteracji pętli, aż do 15 x 5 bajtów od korzenia (4). Dlaczego 15 x 5? Dzieje się tak, ponieważ gadżety składają się maksymalnie z pięciu instrukcji, a ponieważ instrukcje x86 nigdy nie składają się z więcej niż 15 bajtów każda, najdalszy dystans, jaki kiedykolwiek będzie potrzebny do przeszukania wstecz od dowolnego korzenia, to 15 x 5 bajtów. Dla każdego przesunięcia wyszukiwania, wyszukiwarka gadżetów wykonuje liniowe przeszukiwanie dezasemblujące (5). W przeciwieństwie do wcześniejszego przykładu liniowego demontażu, ten przykład wykorzystuje funkcję cs_disasm_iter firmy Capstone dla każdego cyklu demontażu. Powodem jest to, że zamiast dezasemblować cały bufor na raz, wyszukiwarka gadżetów musi sprawdzić serię warunków zatrzymania po każdej instrukcji. Najpierw przerywa cykl liniowego demontażu, jeśli napotka nieprawidłową instrukcję, odrzucając gadżet i przechodząc do następnego adresu wyszukiwania, rozpoczynając od tego miejsca nowy cykl liniowy. Sprawdzanie nieprawidłowych instrukcji jest ważne, ponieważ gadżety o niesymetrycznych przesunięciach często są niesymetryczne. Wyszukiwarka gadżetów przerywa również cykl demontażu, jeśli natrafi na instrukcję o adresie poza korzeniem (6). Możesz się zastanawiać, jak to możliwe, że dezasemblacja dociera do instrukcji poza korzeniem bez wcześniejszego trafienia na sam korzeń. Aby zobaczyć przykład, pamiętaj, że niektóre adresy, które demontujesz, są niesymetryczne względem normalnego strumienia instrukcji. Oznacza to, że jeśli zdeasemblujesz wielobajtową, niezsynchronizowaną instrukcję, deasemblacja może wykorzystać instrukcję główną jako część kodu operacyjnego lub operandów niezsynchronizowanej instrukcji, tak aby sama instrukcja główna nigdy nie pojawiła się w strumieniu niezsynchronizowanych instrukcji.Na koniec, wyszukiwarka gadżetów zatrzymuje deasemblację danego gadżetu, jeśli znajdzie instrukcję przepływu sterowania inną niż return (7). W końcu gadżety są łatwiejsze w użyciu, jeśli nie zawierają żadnego przepływu sterowania poza końcową instrukcją return. Wyszukiwarka gadżetów odrzuca również gadżety, które są dłuższe niż maksymalny rozmiar gadżetu (8). Jeśli żaden z warunków zatrzymania nie jest spełniony, wyszukiwarka gadżetów dołącza nowo zdeasemblowaną instrukcję (cs_ins) do ciągu zawierającego dotychczas utworzony gadżet (9). Gdy analiza dotrze do instrukcji głównej, gadżet jest ukończony i dodawany do mapy gadżetów (10). Po rozważeniu wszystkich możliwych punktów początkowych w pobliżu korzenia funkcja find_gadgets_at_root kończy działanie i przekazuje sterowanie do głównej funkcji find_gadgets, która następnie kontynuuje działanie od następnej instrukcji korzenia, jeśli jakieś pozostały.

Skanowanie w poszukiwaniu korzeni i mapowanie gadżetów

https://chacker.pl/

Funkcja find_gadgets jest wywoływana z pliku main i uruchamia się w znany sposób. Najpierw ładuje sekcję .text i inicjuje Capstone w trybie szczegółowego demontażu. Po inicjalizacji find_gadgets pętlą po każdym bajcie w .text i sprawdza, czy jest on równy wartości 0xc3, czyli kodowi operacji dla instrukcji ret x86 (1). Koncepcyjnie każda taka instrukcja jest potencjalnym „korzeniem” dla jednego lub więcej gadżetów, które można znaleźć, przeszukując wstecz, zaczynając od korzenia. Wszystkie gadżety kończące się daną instrukcją ret można wyobrazić sobie jako drzewo zakorzenione w tej instrukcji. Aby znaleźć wszystkie gadżety połączone z danym korzeniem, istnieje osobna funkcja o nazwie find_gadgets_at_root (o nazwie at (2)), którą omówię wkrótce. Wszystkie gadżety są dodawane do struktury danych mapy C++, która mapuje każdy unikalny gadżet (w formie ciągu znaków) na zestaw adresów, pod którymi można go znaleźć. Faktyczne dodawanie gadżetów do mapy odbywa się w funkcji find_gadgets_at_root. Po zakończeniu wyszukiwania gadżetów, find_gadgets drukuje całe mapowanie gadżetów (3), a następnie czyści i zwraca wynik.

Znajdowanie gadżetów ROP

https://chacker.pl/

Poniższy listing przedstawia kod wyszukiwarki gadżetów ROP. Wyświetla on listę gadżetów ROP, które można znaleźć w danym pliku binarnym. Możesz użyć tej listy, aby wybrać odpowiednie gadżety i połączyć je w exploit dla pliku binarnego. Jak wspomniano, chcesz znaleźć gadżety, które kończą się instrukcją return. Ponadto, chcesz wyszukać zarówno gadżety wyrównane, jak i niewyrównane, w odniesieniu do normalnego strumienia instrukcji pliku binarnego. Użyteczne gadżety powinny mieć dobrze zdefiniowaną i prostą semantykę, więc ich długość powinna być dość ograniczona. W tym przypadku ograniczmy (dowolnie) długość gadżetu do pięciu instrukcji. Aby znaleźć zarówno gadżety wyrównane, jak i niewyrównane, jednym z możliwych podejść jest deasemblacja pliku binarnego z każdego możliwego bajtu początkowego i sprawdzenie, dla których bajtów otrzymasz użyteczny gadżet. Można jednak usprawnić proces, najpierw skanując plik binarny w poszukiwaniu lokalizacji instrukcji powrotu (zgodnych lub niezgodnych), a następnie cofając się od tego miejsca, budując coraz dłuższe gadżety. W ten sposób nie trzeba rozpoczynać deasemblacji pod każdym możliwym adresem, a jedynie pod adresami w pobliżu instrukcji powrotu. Wyjaśnijmy, co to dokładnie oznacza, przyglądając się bliżej kodowi wyszukiwarki gadżetów pokazanemu na Listingu .

Listing: capstone_gadget_finder.cc

Wyszukiwarka gadżetów na Listingu 8-11 nie wprowadza żadnych nowych koncepcji Capstone. Funkcja główna jest taka sama, jak ta, którą widzieliście w liniowym i rekurencyjnym dezasemblerze, a funkcje pomocnicze (is_cs_cflow_group, is_cs_cflow_ins i is_cs_ret_ins) są podobne do tych, które widzieliście wcześniej. Funkcja dezasemblacji Capstone, cs_disasm_iter, również była już wcześniej znana. Ciekawostką w wyszukiwarce gadżetów jest to, że wykorzystuje ona Capstone do analizy pliku binarnego w sposób niemożliwy do przeprowadzenia za pomocą standardowego liniowego lub rekurencyjnego dezasemblera. Wszystkie funkcje wyszukiwania gadżetów są zaimplementowane w funkcjach find_gadgets i find_gadgets_at_root, więc skupmy się na nich.

Wprowadzenie do programowania zwrotnego

https://chacker.pl/

Prawie każde wprowadzenie do eksploatacji luk w zabezpieczeniach obejmuje klasyczny artykuł Alepha One’a „Smashing the Stack for Fun and Profit”, który wyjaśnia podstawy eksploatacji przepełnień bufora stosu. Kiedy artykuł ten został opublikowany w 1996 roku, eksploatacja była stosunkowo prosta: należało znaleźć lukę, załadować złośliwy kod powłoki do bufora (zazwyczaj bufora stosu) w aplikacji docelowej i wykorzystać lukę do przekierowania przepływu sterowania do kodu powłoki. Od tego czasu w świecie bezpieczeństwa wiele się zmieniło, a eksploatacja luk stała się znacznie bardziej skomplikowana. Jedną z najpowszechniejszych metod obrony przed klasycznymi eksploitami tego typu jest zapobieganie wykonywaniu danych (DEP), znane również jako WꚛX lub NX. Zostało ono wprowadzone w systemie Windows XP w 2004 roku i w niezwykle prosty sposób zapobiega wstrzykiwaniu kodu powłoki. DEP wymusza, aby żaden obszar pamięci nie był jednocześnie zapisywalny i wykonywalny. Zatem jeśli atakujący wstrzyknie kod powłoki do bufora, nie będzie mógł go wykonać. Niestety, hakerzy wkrótce znaleźli sposób na obejście mechanizmu DEP. Nowe mechanizmy obronne uniemożliwiły wstrzyknięcie kodu powłoki, ale nie były w stanie powstrzymać atakującego przed wykorzystaniem luki w zabezpieczeniach do przekierowania przepływu sterowania do istniejącego kodu w eksploitowanym pliku binarnym lub bibliotek, z których korzysta. Ta słabość została po raz pierwszy wykorzystana w klasie ataków znanych jako return-to-libc (ret2libc), w których przepływ sterowania jest przekierowywany do wrażliwych funkcji w powszechnie używanej bibliotece libc, takich jak funkcja execve, która może zostać użyta do uruchomienia nowego procesu wybranego przez atakującego. W 2007 roku pojawiła się uogólniona odmiana ret2libc, znana jako programowanie zorientowane na powrót (ROP). Zamiast ograniczać ataki do istniejących funkcji, ROP pozwala atakującemu na zaimplementowanie dowolnej złośliwej funkcjonalności poprzez łączenie krótkich, istniejących sekwencji kodu w przestrzeni pamięci programu docelowego. Te krótkie sekwencje kodu nazywane są w terminologii ROP gadżetami. Każdy gadżet kończy się instrukcją powrotu i wykonuje podstawową operację, taką jak dodawanie lub porównanie logiczne. Poprzez staranny dobór gadżetów o dobrze zdefiniowanej semantyce, atakujący może stworzyć coś, co jest w zasadzie spersonalizowanym zestawem instrukcji, w którym każdy gadżet tworzy instrukcję, a następnie wykorzystać ten zestaw instrukcji do stworzenia dowolnej funkcjonalności, zwanej programem ROP, bez wstrzykiwania nowego kodu. Gadżety mogą być częścią normalnych instrukcji programu hosta, ale mogą być również niespójnymi sekwencjami instrukcji, takimi jak te, które widziałeś w przykładzie zaciemnionego kodu . Program ROP składa się z serii adresów gadżetów starannie ułożonych na stosie, tak aby instrukcja powrotu kończąca każdy gadżet przekazywała sterowanie do następnego gadżetu w łańcuchu. Aby uruchomić program ROP, wykonujesz początkową instrukcję powrotu (na przykład wyzwalając ją za pomocą exploita), która przeskakuje do pierwszego adresu gadżetu. Rysunek  ilustruje przykładowy łańcuch ROP.

Jak widać, wskaźnik stosu (rejestr esp) początkowo wskazuje na adres pierwszego gadżetu g1 w łańcuchu. Po wykonaniu początkowej instrukcji return, zdejmuje ona ten pierwszy adres gadżetu ze stosu i przekazuje mu sterowanie, powodując uruchomienie g1. Gadżet g1 wykonuje instrukcję pop, która ładuje stałą ze stosu do rejestru eax i inkrementuje esp, aby wskazywała na adres gadżetu g2. Następnie instrukcja ret z g1 przekazuje sterowanie do g2, który z kolei dodaje stałą z eax do rejestru esi. Gadżet g2 wraca następnie do gadżetu g3 i tak dalej, aż wszystkie gadżety g1; : : : ;gn zostaną wykonane. Jak zapewne zauważyłeś, stworzenie exploita ROP wymaga od atakującego wybrania odpowiedniego zestawu gadżetów ROP do użycia. W poniższej sekcji zaimplementujemy narzędzie, które skanuje plik binarny w poszukiwaniu użytecznych gadżetów ROP i tworzy ich przegląd, aby ułatwić tworzenie exploitów ROP.

Implementacja skanera gadżetów ROP

https://chacker.pl/

Wszystkie dotychczasowe przykłady to niestandardowe implementacje znanych technik dezasemblacji. Jednak dzięki Capstone możesz zdziałać o wiele więcej! W tej sekcji poznasz bardziej wyspecjalizowane narzędzie, którego potrzeby dezasemblacji nie są zaspokajane przez standardowy dezasembler liniowy ani rekurencyjny. Dokładniej, poznasz narzędzie niezbędne do pisania nowoczesnych exploitów: narzędzie skanujące, które może znaleźć gadżety do wykorzystania w exploitach ROP. Najpierw wyjaśnimy, co to oznacza.