Deasemblacja bufora kodu

https://chacker.pl/

Mając teraz uchwyt Capstone i załadowaną sekcję kodu, możesz rozpocząć deasemblację! Wymaga to tylko jednego wywołania funkcji cs_disasm (4). Pierwszym parametrem tego wywołania jest dis, czyli uchwyt Capstone. Następnie cs_disasm oczekuje bufora (a konkretnie const uint8_t*) zawierającego kod do deasemblacji, liczby całkowitej size_t wskazującej liczbę bajtów kodu w buforze oraz uint64_t wskazującej adres pamięci wirtualnej (VMA) pierwszego bajtu w buforze. Bufor kodu i powiązane wartości są wygodnie wstępnie ładowane w obiekcie Section reprezentującym sekcję .text ładowanego pliku binarnego. Dwa ostatnie parametry funkcji cs_disasm to size_t, który wskazuje liczbę instrukcji do zdeasemblowania (tutaj 0 oznacza deasemblację jak największej liczby instrukcji) oraz wskaźnik do bufora instrukcji Capstone (cs_insn**). Ten ostatni parametr zasługuje na szczególną uwagę, ponieważ typ cs_insn odgrywa kluczową rolę w aplikacjach opartych na Capstone.

Inicjalizacja Capstone

https://chacker.pl/

Zacznijmy od funkcji main, która oczekuje pojedynczego argumentu wiersza poleceń: nazwy pliku binarnego do zdeasemblowania. Funkcja main przekazuje nazwę tego pliku binarnego do funkcji load_binary (zaimplementowanej w rozdziale 4), która ładuje plik binarny do obiektu binarnego o nazwie bin (1). Następnie funkcja main przekazuje bin do funkcji disasm (2), czeka na jej zakończenie i na koniec usuwa plik binarny z pamięci. Jak można się domyślić, cała faktyczna praca deasemblacyjna jest wykonywana w funkcji disasm. Aby zdeasemblować sekcję .text danego pliku binarnego, disasm rozpoczyna od wywołania bin->get_text_section(), aby uzyskać wskaźnik do obiektu Section reprezentującego sekcję .text. Do tej pory powinno to być znane z rozdziału 4. Teraz przejdźmy do faktycznego kodu Capstone! Pierwsza funkcja Capstone wywoływana przez disasm jest typowa dla każdego programu korzystającego z Capstone. Funkcja ta nazywa się cs_open i jej celem jest otwarcie poprawnie skonfigurowanej instancji Capstone (3). W tym przypadku poprawnie skonfigurowana instancja to taka, która jest skonfigurowana do deasemblacji kodu x86-64. Pierwszym parametrem przekazywanym do cs_open jest stała o nazwie CS_ARCH_X86, informująca Capstone, że chcesz deasemblować kod dla architektury x86. Dokładniej, przekazujesz Capstone wartość CS_MODE_64 jako drugi parametr, informując go, że kod będzie 64-bitowy. Trzeci parametr to wskaźnik do obiektu typu csh (skrót od „uchwytu Capstone”). Wskaźnik ten nazywa się dis. Po pomyślnym zakończeniu działania funkcji cs_open, ten uchwyt reprezentuje w pełni skonfigurowaną instancję Capstone, która będzie potrzebna do wywołania dowolnej z pozostałych funkcji API Capstone. Jeśli inicjalizacja zakończy się pomyślnie, cs_open zwraca wartość CS_ERR_OK.

Liniowy deasembler z Capstone

https://chacker.pl/

Z perspektywy wysokiego poziomu, Capstone przyjmuje bufor pamięci zawierający blok bajtów kodu jako dane wejściowe i generuje instrukcje zdeasemblowane z tych bajtów. Najprostszym sposobem użycia Capstone jest przekazanie bufora zawierającego wszystkie bajty kodu z sekcji .text danego pliku binarnego, a następnie liniowa deasemblacja tych instrukcji do postaci czytelnej dla człowieka, czyli mnemoników instrukcji. Oprócz kodu inicjalizującego i analizującego dane wyjściowe, Capstone umożliwia implementację tego trybu użycia za pomocą jednego wywołania API do funkcji cs_disasm. Przykład na Listingu 8-4 implementuje proste narzędzie podobne do objdump. Aby załadować plik binarny do bloku bajtów, z którego może korzystać Capstone, ponownie wykorzystamy ładowarkę plików binarnych opartą na bibliotece libbfd (loader.h).

Listing: basic_capstone_linear.cc

#include <stdio.h>

#include <string>

#include <capstone/capstone.h>

#include „../inc/loader.h”

int disasm(Binary *bin);

int

main(int argc, char *argv[])

{

Binary bin;

std::string fname;

if(argc < 2) {

printf(„Usage: %s <binary>\n”, argv[0]);

return 1;

}

fname.assign(argv[1]);

(1) if(load_binary(fname, &bin, Binary::BIN_TYPE_AUTO) < 0) {

return 1;

}

(2) if(disasm(&bin) < 0) {

return 1;

}

unload_binary(&bin);

return 0;

}

int

disasm(Binary *bin)

{

csh dis;

cs_insn *insns;

Section *text;

size_t n;

text = bin->get_text_section();

if(!text) {

fprintf(stderr, „Nothing to disassemble\n”);

return 0;

}

(3) if(cs_open(CS_ARCH_X86, CS_MODE_64, &dis) != CS_ERR_OK) {

fprintf(stderr, „Failed to open Capstone\n”);

return -1;

}

(4) n = cs_disasm(dis, text->bytes, text->size, text->vma, 0, &insns);

if(n <= 0) {

fprintf(stderr, „Disassembly error: %s\n”,

cs_strerror(cs_errno(dis)));

return -1;

}

(5) for(size_t i = 0; i < n; i++) {

printf(„0x%016jx: „, insns[i].address);

for(size_t j = 0; j < 16; j++) {

if(j < insns[i].size) printf(„%02x „, insns[i].bytes[j]);

else printf(” „);

}

printf(„%-12s %s\n”, insns[i].mnemonic, insns[i].op_str);

}

(6) cs_free(insns, n);

cs_close(&dis);

return 0;

}

To wszystko, czego potrzebujesz do zaimplementowania prostego, liniowego deasemblera! Zwróć uwagę na wiersz na górze kodu źródłowego z poleceniem #include <capstone/capstone.h>. Aby użyć Capstone w programie w C, wystarczy dołączyć ten plik nagłówkowy i połączyć program z biblioteką Capstone za pomocą flagi linkera -lcapstone. Wszystkie pozostałe pliki nagłówkowe Capstone są #includowane z pliku capstone.h, więc nie trzeba ich #includować ręcznie. Mając to za sobą, przejrzyjmy resztę kodu źródłowego z Listingu .

Instalacja Capstone

https://chacker.pl/

Capstone w wersji 3.0.5 jest preinstalowany na maszynie wirtualnej dołączonej do tej książki. Jeśli chcesz wypróbować Capstone na innym komputerze, jego instalacja jest dość prosta. Strona internetowa Capstone3 udostępnia gotowe pakiety dla systemów Windows i Ubuntu, a także archiwum źródłowe do instalacji Capstone na innych platformach. Jak zwykle, nasze narzędzia oparte na Capstone napiszemy w języku C/C++, ale do szybkich eksperymentów możesz również zapoznać się z Capstone za pomocą Pythona. W tym celu będziesz potrzebować powiązań Capstone z Pythonem. Są one również preinstalowane na maszynie wirtualnej, ale ich instalacja na własnym komputerze jest łatwa, jeśli masz menedżera pakietów Pythona pip. Upewnij się, że masz już pakiet Capstone Core, a następnie wpisz w wierszu poleceń następujące polecenie, aby zainstalować powiązania Capstone z Pythonem:

pip install capstone

Po zainstalowaniu powiązań z Pythonem możesz uruchomić interpreter Pythona i rozpocząć własne eksperymenty deasemblacji w Pythonie, jak pokazano na Listingu .

Listing : Eksploracja powiązań Capstone w Pythonie

Ten przykład importuje pakiet Capstone i używa wbudowanego polecenia help Pythona do eksploracji Capstone (1). Klasą zapewniającą główną funkcjonalność jest capstone.Cs (2). Co najważniejsze, zapewnia ona dostęp do funkcji disasm Capstone, która dezasembluje bufor kodu i zwraca wynik dezasemblacji. Aby poznać pozostałą funkcjonalność oferowaną przez powiązania Capstone z Pythonem, użyj wbudowanych poleceń help i dir Pythona! W dalszej części rozdziału skupię się na budowaniu narzędzi Capstone w C/C++, ale API jest bardzo podobne do API Capstone w Pythonie.

Wprowadzenie do Capstone

https://chacker.pl/

Capstone to framework do deasemblacji, zaprojektowany w celu zapewnienia prostego i lekkiego API, które transparentnie obsługuje najpopularniejsze architektury instrukcji, w tym x86/x86-64, ARM i MIPS. Posiada powiązania z C/C++ i Pythonem (oraz innymi językami, ale my będziemy używać C/C++ jak zwykle) i działa na wszystkich popularnych platformach, w tym Windows, Linux i macOS. Jest również całkowicie darmowy i ma otwarty kod źródłowy. Tworzenie narzędzi do deasemblacji za pomocą Capstone to prosty proces o niezwykle wszechstronnych możliwościach. Chociaż API koncentruje się na zaledwie kilku funkcjach i strukturach danych, nie poświęca użyteczności na rzecz prostoty. Dzięki Capstone można łatwo odzyskać praktycznie wszystkie istotne szczegóły zdeasemblowanych instrukcji, w tym kody operacji instrukcji, mnemoniki, klasy, rejestry odczytywane i zapisywane przez instrukcję i wiele innych. Najlepszym sposobem nauki Capstone jest nauka na przykładach, więc przejdźmy do konkretów.

Inne powody, dla których warto napisać własny deasembler

https://chacker.pl/

Zaciemniony kod to nie jedyny powód, dla którego warto zbudować własny przebieg deasemblacji. Ogólnie rzecz biorąc, dostosowywanie jest przydatne w każdej sytuacji, w której potrzebna jest pełna kontrola nad procesem deasemblacji. Jak wspomniałem wcześniej, takie sytuacje występują podczas analizy zaciemnionych lub specjalnych plików binarnych, lub gdy trzeba przeprowadzić specjalistyczne analizy, do których uniwersalne deasemblery nie są przeznaczone. W dalszej części zobaczysz przykład wykorzystania własnego deasemblera do zbudowania skanera gadżetów ROP, który wymaga deasemblacji pliku binarnego z wielu przesunięć początkowych – operacji, która nie jest łatwo obsługiwana przez większość deasemblerów. Skanowanie gadżetów ROP polega na znalezieniu każdej możliwej sekwencji kodu w pliku binarnym, w tym tych niespójnych, które mogłyby zostać wykorzystane w exploicie ROP. I odwrotnie, czasami warto pominąć niektóre ścieżki kodu z deasemblacji zamiast znaleźć każdą możliwą sekwencję kodu. Na przykład, jest to przydatne, gdy chcesz zignorować fałszywe ścieżki utworzone przez obfuscator lub zbudować hybrydową analizę statyczno-dynamiczną i skupić dezasemblację na określonych ścieżkach, które już dynamicznie eksplorowałeś. Istnieją również przypadki, w których zbudowanie własnego narzędzia do dezasemblacji może nie być konieczne ze względów technicznych, ale możesz zdecydować się na to w celu zwiększenia wydajności lub obniżenia kosztów. Na przykład, zautomatyzowane narzędzia do analizy binarnej często wymagają jedynie bardzo podstawowej funkcjonalności dezasemblacji. Najtrudniejszą częścią ich pracy jest niestandardowa analiza zdezasemblowanych instrukcji, a ten krok nie wymaga rozbudowanych interfejsów użytkownika ani udogodnień, które oferują zautomatyzowane dezasemblery. W takich przypadkach możesz zdecydować się na zbudowanie własnych narzędzi, korzystając wyłącznie z darmowych bibliotek dezasemblacji open source, zamiast polegać na dużych, komercyjnych dezasemblerach, które mogą kosztować nawet tysiące dolarów. Innym powodem tworzenia własnego dezasemblera jest wydajność. Skrypty w standardowych dezasemblerach zazwyczaj wymagają co najmniej dwóch przebiegów kodu: jednego dla wstępnej dezasemblacji i drugiego dla postprocessingu wykonywanego przez skrypt. Co więcej, te skrypty są zazwyczaj napisane w językach wysokiego poziomu (takich jak Python), co skutkuje stosunkowo niską wydajnością w czasie wykonywania. Oznacza to, że podczas przeprowadzania złożonej analizy wielu dużych plików binarnych, często można znacznie poprawić wydajność, tworząc narzędzie, które może działać natywnie i wykonywać wszystkie niezbędne analizy w jednym przebiegu. Teraz, gdy wiesz, dlaczego dezasemblacja niestandardowa jest przydatna, przyjrzyjmy się, jak to zrobić! Zacznę od krótkiego wprowadzenia do Capstone, jednej z najpopularniejszych bibliotek do tworzenia niestandardowych narzędzi do dezasemblacji.

Nakładający się kod w niezaciemnionych plikach binarnych

https://chacker.pl/

Warto zauważyć, że nakładające się instrukcje występują nie tylko w celowo zaciemnionym kodzie, ale także w wysoce zoptymalizowanym kodzie zawierającym ręcznie pisane asemblery. Trzeba przyznać, że drugi przypadek jest łatwiejszy w obsłudze i znacznie rzadszy. Poniższy listing przedstawia nakładającą się instrukcję z glibc 2.22.

7b05a: cmp DWORD PTR fs:0x18,0x0

7b063: je 7b066

7b065: lock cmpxchg QWORD PTR [rip+0x3230fa],rcx

W zależności od wyniku instrukcji cmp, je albo przeskakuje na adres 7b066, albo przechodzi na adres 7b065. Jedyna różnica polega na tym, że ten drugi adres odpowiada instrukcji lock cmpxchg, a ten pierwszy odpowiada cmpxchg. Innymi słowy, skok warunkowy służy do wyboru między zablokowanym a niezablokowanym wariantem tej samej instrukcji poprzez opcjonalne przeskoczenie bajtu prefiksu blokady. glibc to biblioteka GNU C. Jest używana w praktycznie wszystkich programach C kompilowanych na platformach GNU/Linux i dlatego jest wysoce zoptymalizowana.

Argument za niestandardowym dezasemblowaniem: Zaciemniony kod

https://chacker.pl/

Niestandardowy przebieg dezasemblacji jest przydatny, gdy trzeba analizować pliki binarne, które naruszają standardowe założenia dezasemblera, takie jak złośliwe oprogramowanie, zaciemnione lub ręcznie stworzone pliki binarne, lub pliki binarne wyodrębnione ze zrzutów pamięci lub oprogramowania układowego. Co więcej, niestandardowe przebiegi dezasemblacji pozwalają na łatwą implementację specjalistycznych analiz binarnych, które skanują w poszukiwaniu określonych artefaktów, takich jak wzorce kodu wskazujące na potencjalne luki w zabezpieczeniach. Są one również przydatne jako narzędzia badawcze, umożliwiając eksperymentowanie z nowatorskimi technikami dezasemblacji. Jako pierwszy konkretny przypadek użycia niestandardowego dezasemblowania rozważmy konkretny typ zaciemnienia kodu, który wykorzystuje nakładanie się instrukcji. Większość dezasemblerów generuje pojedynczą listę dezasemblacji dla każdego pliku binarnego, ponieważ zakłada się, że każdy bajt w pliku binarnym jest przypisany do co najwyżej jednej instrukcji, każda instrukcja jest zawarta w pojedynczym bloku podstawowym, a każdy blok podstawowy jest częścią pojedynczej funkcji. Innymi słowy, dezasemblery zazwyczaj zakładają, że fragmenty kodu nie nakładają się na siebie. Nakładanie się instrukcji łamie to założenie, aby zmylić dezasemblery, utrudniając inżynierię wsteczną nakładającego się kodu. Nakładanie się instrukcji działa, ponieważ instrukcje na platformie x86 różnią się długością. W przeciwieństwie do niektórych innych platform, takich jak ARM, nie wszystkie instrukcje x86 składają się z tej samej liczby bajtów. W rezultacie procesor nie wymusza żadnego konkretnego wyrównania instrukcji w pamięci, co umożliwia jednej instrukcji zajęcie zestawu adresów kodu już zajętego przez inną instrukcję. Oznacza to, że na x86 można rozpocząć dezasemblację od środka jednej instrukcji, a dezasemblacja da w rezultacie kolejną instrukcję, która częściowo (lub całkowicie) nakłada się na pierwszą. Obfuskatory chętnie wykorzystują nakładające się instrukcje, aby zmylić dezasemblery. Nakładanie się instrukcji jest szczególnie łatwe na x86, ponieważ zestaw instrukcji x86 jest niezwykle gęsty, co oznacza, że ​​prawie każda sekwencja bajtów odpowiada pewnej prawidłowej instrukcji.

Listing  przedstawia przykład nakładania się instrukcji. Oryginalne źródło, z którego pochodzi ten listing, można znaleźć w pliku overlapping_bb.c. Aby zdeasemblować nakładający się kod, można użyć flagi -start-address=<addr> w programie objdump, aby rozpocząć deasemblację od podanego adresu.

Listing : Deasemblacja nachodzącego na siebie_bb (1)

Listing przedstawia prostą funkcję, która przyjmuje jeden parametr wejściowy,o nazwie i (1) i ma zmienną lokalną o nazwie j (2). Po wykonaniu pewnych obliczeń funkcja zwraca j. Po bliższym przyjrzeniu się zauważysz coś dziwnego: instrukcja jne pod adresem 40060a (3) warunkowo przeskakuje do środka instrukcji, zaczynając od adresu 400610, zamiast kontynuować wykonywanie od początku którejkolwiek z wymienionych instrukcji! Większość deasemblerów, takich jak objdump i IDA Pro, deasembluje tylko instrukcje pokazane na Listingu 8-1. Oznacza to, że deasemblery ogólnego przeznaczenia pominęłyby nakładającą się instrukcję pod adresem 400612, ponieważ te bajty są już zajęte przez instrukcję osiągniętą w przypadku przejścia przez jne. Ten rodzaj nakładania się umożliwia ukrycie ścieżek kodu, co może mieć drastyczny wpływ na ogólny wynik programu. Rozważmy na przykład następujący przypadek. W Listingu 8-1, jeśli skok pod adresem 40060a nie zostanie wykonany (i == 0), instrukcje dotarte przez przypadek przejścia są obliczane i zwracają wartość 148 (4). Jeśli jednak skok zostanie wykonany (i != 0), wykonywana jest ścieżka kodu ukryta w Listingu . Spójrzmy na Listing 2, który pokazuje tę ukrytą ścieżkę kodu, aby zobaczyć, jak zwraca ona zupełnie inną wartość.

Listing 2: Deasemblacja nakładki_bb (2)

Listing 2 przedstawia ścieżkę kodu wykonywaną w przypadku wykonania instrukcji jne (1). W takim przypadku przeskakuje ona o dwa bajty (400610 i 400611) do adresu 0x400612 (2), który znajduje się w środku instrukcji xor osiągniętej w przypadku przejścia przez jne. Powoduje to inny strumień instrukcji. W szczególności operacje arytmetyczne wykonywane na j są teraz inne, co powoduje, że funkcja zwraca i + 4 (3) zamiast 148. Jak można sobie wyobrazić, tego rodzaju zaciemnianie utrudnia zrozumienie kodu, zwłaszcza jeśli jest stosowane w więcej niż jednym miejscu. Zazwyczaj można nakłonić dezasemblery do ujawnienia ukrytych instrukcji, restartując dezasemblację od innego przesunięcia, tak jak zrobiłem to z flagą -start-address w objdump w poprzednich listingach. Jak widać na Listingu 8-2, ponowne uruchomienie deasemblacji pod adresem 400612 ujawnia ukrytą tam instrukcję. Jednak wykonanie tej czynności powoduje ukrycie instrukcji pod adresem 400610. Niektóre zaciemnione programy są przepełnione nakładającymi się sekwencjami kodu, takimi jak ta pokazana w tym przykładzie, co sprawia, że ​​kod jest niezwykle żmudny i trudny do ręcznego zbadania. Przykład z Listingów 1 i 2 pokazuje, że zbudowanie wyspecjalizowanego narzędzia do deasemblacji, które automatycznie „rozplątuje” nakładające się instrukcje, może znacznie ułatwić inżynierię wsteczną. Zwłaszcza jeśli często musisz odwracać zaciemnione pliki binarne, wysiłek włożony w zbudowanie narzędzia do deasemblacji opłaca się na dłuższą metę. W dalszej części dowiesz się, jak zbudować rekurencyjny deasembler, który poradzi sobie z nakładającymi się blokami podstawowymi, takimi jak te pokazane w poprzednich Listingach.

Po co pisać niestandardowy przebieg dezasemblacji?

https://chacker.pl/

Większość znanych dezasemblerów, takich jak IDA Pro, została zaprojektowana z myślą o wspomaganiu ręcznej inżynierii wstecznej. Są to wydajne silniki dezasemblacji, które oferują rozbudowany interfejs graficzny, niezliczone opcje wizualizacji zdezasemblowanego kodu oraz wygodne sposoby nawigacji po dużych zbiorach instrukcji asemblera. Jeśli Twoim celem jest po prostu zrozumienie działania pliku binarnego, dezasembler ogólnego przeznaczenia sprawdzi się doskonale, ale narzędzia ogólnego przeznaczenia nie zapewniają elastyczności niezbędnej do zaawansowanej, zautomatyzowanej analizy. Chociaż wiele dezasemblerów posiada funkcjonalność skryptową do postprocessingu zdezasemblowanego kodu, nie oferują one opcji modyfikowania samego procesu dezasemblacji i nie są przeznaczone do wydajnego przetwarzania wsadowego plików binarnych. Dlatego, gdy chcesz przeprowadzić specjalistyczną, zautomatyzowaną analizę binarną wielu plików binarnych jednocześnie, potrzebujesz niestandardowego dezasemblera.

DOSTOSOWYWANIE DEZASEMBLACJI

https://chacker.pl/

Do tej pory omówiłem podstawowe techniki analizy binarnej i dezasemblacji. Jednak te podstawowe techniki nie są przeznaczone do obsługi zaciemnionych plików binarnych, które naruszają standardowe założenia dezasemblera lub analizy specjalistyczne, takie jak skanowanie podatności. Czasami nawet funkcjonalność skryptowa oferowana przez dezasemblery nie wystarcza, aby temu zaradzić. W takich przypadkach można zbudować własny, wyspecjalizowany silnik dezasemblacji, dostosowany do własnych potrzeb. W tym rozdziale dowiesz się, jak zaimplementować niestandardowy dezasembler za pomocą Capstone, frameworka do dezasemblacji, który daje pełną kontrolę nad całym procesem analizy. Rozpoczniesz od zapoznania się z API Capstone, używając go do zbudowania niestandardowego dezasemblera liniowego i dezasemblera rekurencyjnego. Następnie nauczysz się implementować bardziej zaawansowane narzędzie, a mianowicie skaner gadżetów Return-Oriented Programming (ROP), którego można używać do tworzenia exploitów ROP.