Praktyczna inżynieria wsteczna. Metody, techniki i narzędzia - ebook
Praktyczna inżynieria wsteczna. Metody, techniki i narzędzia - ebook
Praktyczna Inżynieria Wsteczna. Metody, techniki i narzędzia
Inżynieria wsteczna oprogramowania jest procesem dogłębnej analizy bibliotek, aplikacji i systemów, których kod źródłowy nie jest dostępny dla badacza. Umożliwia ona zarówno odtworzenie i pełne zrozumienie logiki konkretnego programu, jak i poszerzenie wiedzy na temat sposobu działania współczesnych procesorów, kompilatorów, czy całych systemów operacyjnych. Umiejętność ta może zostać użyta do odkrywania i eksploitacji luk bezpieczeństwa, analizy złośliwego oprogramowania, a nawet podejmowania bardziej świadomych decyzji programistycznych i łatwiejszego radzenia sobie z błędami we własnym kodzie.
Książka jest kompilacją publikacji dwunastu polskich autorów z wieloletnim doświadczeniem, którzy na co dzień posługują się przedstawionymi technikami w pracy jako eksperci od bezpieczeństwa i analitycy. Wśród nich znajdziemy wielu specjalistów zatrudnionych w największych firmach informatycznych, laureatów nagrody Pwnie Award, rozpoznawalnych prelegentów uznanych konferencji i członków czołowych zespołów startujących w konkursach security Capture The Flag. Materiały zostały wybrane i zredagowane przez Gynvaela Coldwinda i Mateusza Jurczyka.
Niektóre z poruszanych zagadnień to:
Podstawowe struktury znane z języków C i C++ widziane z perspektywy inżynierii wstecznej. Pliki wykonywalne w formatach ELF oraz PE, w tym zaawansowane triki z nimi związane. Wewnętrzna budowa zabezpieczeń przed wykorzystaniem luk bezpieczeństwa. Inżynieria wsteczna oprogramowania na platformy .NET oraz Python. Metody wstrzykiwania kodu w inne procesy. Projektowanie i analiza zabezpieczeń programów. Metody śledzenia wykonania – programowe i korzystające ze wsparcia oferowanego przez współczesne procesory. Inżynieria wsteczna w analizie bezpieczeństwa oprogramowania. Różnorodność tematów poruszanych przez autorów sprawia, że pozycja ta może zainteresować zarówno osoby początkujące, jak i pasjonatów pragnących poszerzyć swoją wiedzę lub zwiększyć repertuar używanych narzędzi i technik.
Kategoria: | Programowanie |
Zabezpieczenie: |
Watermark
|
ISBN: | 978-83-01-19046-0 |
Rozmiar pliku: | 22 MB |
FRAGMENT KSIĄŻKI
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Tomasz Bukowski (rozdział 1) |
| | |
| | Z wykształcenia i pasji – fizyk. Entuzjasta Linuksa, a jego hobby to programowanie. Przygodę z bezpieczeństwem IT rozpoczął jako administrator sieci w akademiku. Pełną „gotowość bojową” osiągnął podczas pracy w CERT Polska, gdzie zajmował się analizą (również wsteczną) złośliwego oprogramowania, eksplorowaniem (a czasem przejmowaniem) botnetów, informatyką śledczą oraz innymi kwestiami związanymi z ITSEC. Obecnie kieruje zespołem Security Threat Intelligence w banku Millennium. Po godzinach lata ze „smokami” w zespole CTF „Dragon Sector”. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Grzegorz Antoniak (rozdział 2) |
| | |
| | Interesuje się inżynierią oprogramowania w wielu różnych językach oraz inżynierią wsteczną w wielu różnych odmianach: od mechanizmów działania systemu począwszy, poprzez mechanizmy budowy komercyjnych aplikacji oraz ich systemów licencyjnych, analizę zamkniętych formatów binarnych, odzyskiwanie danych, do analizy działania złośliwego oprogramowania i projektowania sposobów jego zwalczania.Od kilku lat pracuje w firmie ESET, próbując zwiększać świadomość o systemach uniksowych w środowiskach Windowsowych, koordynuje także zespół rozszerzający zasięg ochrony przeciwko złośliwemu oprogramowaniu na systemach macOS i Linux. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Tomasz Kwiecień (rozdział 3) |
| | |
| | Od młodzieńczych lat pasjonat inżynierii wstecznej, a w szczególności mechanizmów zabezpieczeń. Zainteresowanie metodami analizy złośliwego oprogramowania doprowadziło go w 2010 roku do krakowskiego oddziału firmy ESET, gdzie pracuje na stanowisku Specialized Researcher. Wicekapitan i współzałożyciel drużyny CTF „Delicious Horse”. W wolnych chwilach „pochłaniacz” filmów i seriali. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Mateusz Krzywicki (rozdział 4) |
| | |
| | Pasjonat inżynierii wstecznej, bezpieczeństwa aplikacji i programowania. Interesuje się technikami deobfuskacji i automatycznej dynamicznej analizy kodu. Przygodę z inżynierią wsteczną zaczynał od analizy działania systemów zabezpieczeń i poznawania wnętrza systemów operacyjnych. Fan skomplikowanych eksploitów wykorzystujących błędy związane z naruszeniem spójności obiektów i struktur w pamięci. Bierze udział w zawodach CTF z zespołem „Delicious Horse”, którego jest współzałożycielem. Pracował w krakowskim oddziale firmy ESET, a aktualnie w firmie Microsoft, na stanowisku Security Software Engineer. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Marcin Hartung (rozdział 5) |
| | |
| | Absolwent Elektroniki i Telekomunikacji na AGH w Krakowie, gdzie okazjonalnie udziela wykładów o inżynierii wstecznej. Autor kilku publikacji, w tym Unpack your troubles: .NET packer tricks and countermeasures zaprezentowanej na konferencji VB2015. Od kilku lat pracuje w firmie ESET jako koordynator zespołu zajmującego się analizą i statycznym rozpakowywaniem archiwów oraz protektorów dla formatów wykonywalnych. Prywatnie – wspinacz i motocyklista. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Gynvael Coldwind (rozdział 6) |
| | |
| | Programista–pasjonat z zamiłowaniem do bezpieczeństwa komputerowego i niskopoziomowych aspektów informatyki, a także autor licznych artykułów, publikacji, podcastów oraz wystąpień poświęconych tym tematom. W roku 2013 odebrał w Las Vegas (wspólnie z Mateuszem Jurczykiem) nagrodę Pwnie Award w kategorii „Najbardziej Innowacyjne Badania Naukowe” z dziedziny bezpieczeństwa komputerowego, przyznaną za wspólną pracę pt. Identifying and Exploiting Windows Kernel Race Conditions via Memory Access Patterns. Kapitan i współzałożyciel „Dragon Sector” – jednej z najlepszych drużyn Security CTF na świecie. Od 2010 r. mieszka w Zurychu, gdzie pracuje dla firmy Google jako Senior Software Engineer / Information Security Engineer. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Hasherezade (rozdział 7) |
| | |
| | Magister inżynier informatyki. Od wieku nastoletniego pasjonuje się programowaniem i inżynierią wsteczną. Aktywnie bierze udział w życiu internetowej społeczności InfoSec, dzieląc się efektami swojej pracy – tworząc m.in. programy freeware/open source oraz publikując artykuły techniczne. Obecnie pracuje jako analityk złośliwego oprogramowania dla firmy Malwarebytes; prowadzi także własną działalność w branży. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Maciej Kotowicz (rozdział 8) |
| | |
| | Miłośnik piwa i tematów pokrewnych. Po godzinach spędza czas w krainie smoków – wraz z resztą drużyny Dragon Sector – biorąc udział (a czasem nawet wygrywając) we wszelkiej maści CTF-ach. Były Wieczny Student i wykładowca w Instytucie Informatyki we Wrocławiu. Bez nagród, ale zdarza mu się występować tu i tam. Na co dzień botnet pwner w CERT Polska, specjalizujący się w analizie złośliwego oprogramowania oraz pisaniu i analizowaniu exploitów. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Michał Kowalczyk (rozdział 9) |
| | |
| | Pasjonat inżynierii wstecznej i analizowania rzeczy tylko po to, aby sprawdzić, jak działają. Interesują go niskopoziomowe aspekty działania komputera, takie jak BIOS oraz system operacyjny, jak również mechanizmy ochrony programów przed inżynierią wsteczną oraz kryptografia. Aktywnie bierze udział w zawodach CTF wraz z zespołem „Dragon Sector”, którego jest wicekapitanem. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Robert Święcki (rozdział 10) |
| | |
| | Badacz problemów z dziedziny bezpieczeństwa komputerowego, szczególnie w zakresie niskopoziomowym oraz systemów operacyjnych. Autor kilku ciekawych narzędzi z tej dziedziny (m.in. fuzzer honggfuzz). Członek polskiej drużyny CTF – „Dragon Sector”. Nominowany w 2016 roku do prestiżowej nagrody Pwnie Award w kategorii ,,Best Privilege Escalation Bug” za upublicznienie błędu bezpieczeństwa w firmwarze procesorów firmy AMD. Pracuje w firmie Google na stanowisku Senior Software Engineer / Information Security Engineer. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Piotr Bania (rozdział 11) |
| | |
| | Interesuje się programowaniem oraz bezpieczeństwem systemów komputerowych. Jako pierwszy Polak otrzymał nagrodę Pwnie Award w kategorii „Najbardziej Innowacyjne Badania Naukowe” z dziedziny bezpieczeństwa komputerowego. Wykonywał projekty dla Agencji Zaawansowanych Projektów Badawczych w Obszarze Obronności Departamentu Obrony Stanów Zjednoczonych (DARPA, program Cyber Fast Track). Miał okazję demonstrować swoje prace w Pentagonie. Autor publikacji, eksploitów i innych narzędzi, z których najbardziej popularnym stał się program „kon-boot” używany przez organy ścigania, służby specjalne, specjalistów z zakresu informatyki śledczej, a także duże firmy informatyczne z całego świata. Obecnie pracuje w firmie Cisco w zespole Talos, na stanowisku Senior Security Researcher. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| | Mateusz Jurczyk (rozdział 12) |
| | |
| | Wicekapitan i współzałożyciel zespołu „Dragon Sector”, drużyny należącej do ścisłej czołówki światowej w zawodach typu Security CTF. Fan przepełnień bufora. Interesuje się bezpieczeństwem systemów i aplikacji klienckich, w szczególności odkrywaniem podatności, ich wykorzystaniem oraz zapobieganiem, ze wskazaniem na jądro systemu Windows. Trzykrotny laureat prestiżowej nagrody Pwnie Award. Występuje na wielu konferencjach dotyczących bezpieczeństwa oprogramowania, takich jak Black Hat, REcon, SyScan, Ruxcon czy 44CON. Na co dzień pracuje dla firmy Google w zespole Project Zero, na stanowisku Senior Software Engineer / Security Engineer. |
+----------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+Wstęp
Inżynieria wsteczna oprogramowania (ang. reverse code engineering, w skrócie RE lub RCE) jest procesem analizy budowy i sposobu działania programów komputerowych, zarówno tych przeznaczonych na serwery, komputery osobiste i urządzenia mobilne, jak i sterujących pracą rozmaitych urządzeń przemysłowych, sieciowych czy AGD. Dziedzina ta ma obecnie bardzo wiele zastosowań.
W wielu przypadkach termin „RCE” jest kojarzony przede wszystkim z analizą tzw. złośliwego oprogramowania, potocznie (choć niekoniecznie poprawnie) nazywanego również „wirusami komputerowymi”. W pracy tej celem analityka jest stwierdzenie, czy analizowany program faktycznie działa w złej wierze (tj. na niekorzyść użytkownika), a w dalszej kolejności – ustalenie dokładnego przebiegu infekcji oraz skutków zarażenia. To z kolei pozwala na tworzenie sygnatur jednoznacznie identyfikujących dany malware, a także narzędzi do jego usunięcia – oba te elementy zwykle wchodzą w skład oprogramowania antywirusowego.
Aby utrudnić pracę analityków, twórcy złośliwego oprogramowania stosują szereg mechanizmów i sztuczek spowalniających analizę – jedną z nich jest oczywiście obfuskacja, która stanowczo zmniejsza czytelność kodu czy wręcz całkowicie ukrywa go za warstwą szyfrowania lub kompresji. Innym często obserwowanym schematem jest wykrywanie faktu dokonywania analizy kodu i jej aktywne utrudnianie np. poprzez przedwczesne zakończenie procesu czy zabicie procesu debuggera. Co ciekawe, dokładnie te same metody są stosowane przez pewną grupę twórców legalnego oprogramowania, którzy starają się chronić w ten sposób zaimplementowane przez siebie mechanizmy, weryfikujące fakt zakupienia przez użytkowników licencji zezwalających na użytkowanie danej aplikacji. Po drugiej stronie barykady w tym przypadku stoją tzw. crackerzy oraz piraci komputerowi, którzy eliminują wspomniane mechanizmy i rozpowszechniają programy „za plecami” producenta.
Powiązaną poniekąd, ale jednak odrębną gałęzią inżynierii wstecznej jest modyfikacja istniejącego oprogramowania z zamiarem naniesienia na niego poprawek, rozszerzenia funkcjonalności, przetłumaczenia interfejsu użytkownika czy umożliwienia współpracy z innymi aplikacjami. Przykładem takiego działania może być zmiana stawki podatku VAT w archaicznym systemie wspomagającym sprzedaż w firmie, którego producent już dawno nie istnieje. Za inne przykłady mogą posłużyć: zautomatyzowanie konwersji bazy danych zapisanej w nieznanym formacie do formatu obsługiwanego przez inne aplikacje, poprawienie błędów uniemożliwiających stabilną pracę aplikacji na nowszej wersji systemu operacyjnego czy też dodanie nowych modułów rozbudowujących lub modyfikujących gry komputerowe.
Wreszcie inżynieria wsteczna jest również stosowana w branży bezpieczeństwa komputerowego, a konkretniej w analizie poprawności implementacji (czyli tzw. vulnerability research, bug hunting), której celem jest poznanie wybranych komponentów oprogramowania na tyle dobrze, aby dogłębnie przebadać je pod kątem występowania błędów bezpieczeństwa. Tak wnikliwa analiza kodu wykonywalnego bardzo często prowadzi do sytuacji, w której badacz zna jego niektóre fragmenty lepiej niż sam twórca.
Można by powiedzieć, że inżynieria wsteczna jest w pewnym stopniu częścią normalnego procesu programowania, gdyż nawet mając dostęp do kodu źródłowego i kompletnej dokumentacji, programiści nierzadko muszą poświęcić czas i energię, aby przeanalizować i zrozumieć nieznane sobie, istotne fragmenty projektu czy nawet własnego kodu, którego szczegóły zdążyły umknąć z pamięci.
Podsumowując: inżynieria wsteczna jest z pewnością fascynującą dziedziną informatyki, w której można znaleźć wiele ciekawych technologii, podejść, trików i niuansów. Z tym większą dumą oddajemy w Państwa ręce niniejszy zbiór publikacji dwunastu autorów – specjalistów z zakresu inżynierii wstecznej z wieloletnim stażem, którzy na co dzień zajmują się analizą złośliwego oprogramowania i badaniem bezpieczeństwa aplikacji, a także biorą udział w turniejach security CTF jako członkowie najlepszych drużyn na świecie.
W tomie tym zebrano materiały dotyczące inżynierii wstecznej przeznaczone zarówno dla początkujących, jak i bardziej zaawansowanych czytelników. Tych pierwszych zainteresują tematy takie jak metody czytania niskopoziomowego kodu, omówienie interesujących aspektów formatów plików wykonywalnych czy charakterystycznych wzorców wynikających z dodawanych przez współczesne kompilatory zabezpieczeń. Osoby od dłuższego czasu związane z tą dziedziną znajdą tu zarówno rozdziały, których tematyka wykracza poza typowy schemat analizy kodu na architekturę x86 – takie jak ciekawostki związane z inżynierią wsteczną mniej standardowych technologii (CPython, .NET), jak i rozdziały zagłębiające się w dobrze znane zagadnienia: śledzenie toku wykonania kodu, sztuczki w formacie ELF, przekierowania API w formacie PE czy techniki związane ze wstrzykiwaniem kodu w inne procesy w systemie Windows. Ostatnie dwa rozdziały książki to studia przypadków, w których omówiono analizę i wykorzystanie niskopoziomowych błędów bezpieczeństwa – najpierw w oprogramowaniu sterującym pracą routera, a następnie w wielu zróżnicowanych zadaniach z turniejów CTF, stworzonych przez autora opracowania.
Życzymy przyjemnej i przede wszystkim interesującej lektury!
Gynvael Coldwind, Mateusz Jurczyk
Wrzesień 2016
Podziękowania
Do powstania niniejszej książki przyczyniło się wiele osób, którym chcielibyśmy w tym miejscu podziękować: Łukasz Łopuszański (wydawca), Tomasz Łopuszański (redaktor), Małgorzata Dąbkowska-Kowalik (redaktor), Mariusz „maryush” Witkowski (recenzent merytoryczny), Paweł „KrzaQ” Zakrzewski (recenzent merytoryczny) i Michał „Redford” Kowalczyk (recenzent merytoryczny).
Ponadto autorzy pragną podziękować następującym osobom: Erik i Alessia, Ange Albertini, Bartłomiej Balcerek, Dawid Bałut, Richard Baranyi, Sergey Bratus, Marta Bukowska, Tadeusz Bukowski, Silvio Cesare, Anton Cherepanov, Arashi Coldwind, Samlis Coldwind, Anna Fałat, Olivier „Fafner ” Ferrand, Piotr Florczyk, Marcin Gabryszewski, Krzysztof Guc, Steven Hunter, Paweł Iwan, Przemek Jaroszewski, Nicolas Joly, Ewa Jurczyk, Jerzy Jurczyk, Paweł Kałuża, Krzysztof Katowicz-Kowalewski, Piotr Kijewski, Krzysztof Kołodziejski, Peter Košinár, Piotr Kramarczyk, Adam „Edisun” Kujawa, Tomasz Kuśmierski, Łukasz Kwiatek, Stanisław Litawa, Michał Ładanowski, Damian Malinowski, Osanda Malith, Marcin Noga, Grażyna Renkiewicz, Jérôme Segura, Tadeusz Składnikiewicz, Elżbieta Składnikiewicz, Tomasz Smolarek, Michał Spadliński, Piotr Szczepański, Kacper Szurek, Gavin Thomas, Julien Vanegue, Damian Wydro, Michał „lcamtuf” Zalewski, Marek Zmysłowski, a także Malware Hunter Team, Google Security Team, Dragon Sector, Vexillium oraz #szpica.Rozdział 1 Funkcje, struktury, klasy i obiekty na niskim poziomie
Tomasz Bukowski
1.1. Wywoływanie funkcji w językach (bardzo) niskiego poziomu 21
1.1.1. CALL, RET i konwencje wywołań
1.1.2. Konwencje wywołań x86
1.1.3. Konwencje wywołań x86-64
1.2. Struktury
1.2.1. „Zgadywanie” wielkości i ułożenia elementów struktury w pamięci
1.2.2. Rozpoznawanie budowy struktur lokalnych i globalnych
1.2.3. Rozpoznawanie budowy struktur dynamicznie alokowanych
1.3. Klasy, obiekty, dziedziczenie i tablice wirtualne
1.3.1. Prosta klasa a struktura
1.3.2. Obiekty = struktury + funkcje + thiscall
1.3.3. Wszystko zostaje w rodzinie, czyli dziedziczenie
1.4. Podsumowanie1.1. Wywoływanie funkcji w językach (bardzo) niskiego poziomu
Zdefiniujmy na początek kilka podstawowych pojęć odnoszących się do architektur x86 i x86-64, którymi będziemy się posługiwać w tym rozdziale:
• wywołanie funkcji – przekazanie kontroli do innego miejsca w kodzie z jednoczesnym odłożeniem na stos adresu następnej instrukcji;
• powrót z funkcji – pobranie ze stosu wartości IP oraz wykonanie skoku pod ten adres;
• caller – wywołujący – określa miejsce wywołania danej funkcji;
• callee – wywoływany – określa funkcję, która jest wywoływana.
1.1.1. CALL, RET i konwencje wywołań
Podczas analizy kodu maszynowego (przetłumaczonego na asembler) trzeba zapomnieć o „dobrodziejstwach”, jakie niosą za sobą języki wyższego poziomu, zaczynając już od pewnych podstawowych pojęć, takich jak funkcje. Dużo łatwiej zrozumieć działanie procesora, jeżeli kod (który jest mu dostarczony do wykonania) potraktujemy jako bliżej niepogrupowany szereg instrukcji. W tym miejscu należy wspomnieć o tym, że prawie każda architektura dostarcza prymitywnych namiastek funkcjonalności „wywoływania funkcji”. W każdym znanym mi przypadku ogranicza się ona do instrukcji CALL lub równoważnej, której działanie można podzielić na dwa etapy: zapisanie (w pewnym miejscu) adresu następnej instrukcji, a następnie skok pod adres podany jako parametr instrukcji. W przypadku x86/64 „miejscem” tym jest stos. Instrukcją komplementarną do CALL jest instrukcja RET. Zdejmuje ona ze stosu wartość rejestru IP, odłożoną tam wcześniej podczas wywoływania funkcji. Dodatkowo, jeżeli dla instrukcji RET został podany parametr N, wskaźnik stosu SP zostanie zwiększony o tę wartość po zdjęciu adresu powrotu (co jest równoznaczne ze zdjęciem N bajtów ze stosu).
W listingu 1.1 przedstawiony został kod źródłowy testowego programu „ex01”. Przełącznik -O0 jest używany przy kompilacji, aby kompilator nie optymalizował kodu. Przełącznik -m32 wymusza z kolei wygenerowanie kodu 32-bitowego.
// Kompilacja: gcc ex01.c -o ex01.exe -O0 -m32
int func1(void){
return 42;
}
int main(void){
return func1();
}
Listing 1.1. Kod programu „ex01”
Rysunek 1.1. Parametr wskazuje nowe miejsce w kodzie przed wywołaniem instrukcji CALL
Nasuwa się pytanie, jak zatem przy użyciu tak „ubogiego” mechanizmu można wywołać funkcję w „nowoczesny” sposób, np:
x = moja_funkcja(1, 2, "string", &wskaznik_gdziekolwiek);
czyli przekazać wiele parametrów różnego typu oraz odczytać zwróconą przez nią wartość?
Można to osiągnąć na wiele sposobów, korzystając z dostępnych narzędzi, czyli rejestrów procesora i pamięci (w szczególności stosu). Problem zaczyna się, kiedy z naszego (binarnego) kodu musi skorzystać ktoś inny. Wtedy należy mu dostarczyć pewien opis interfejsu, tzw. ABI (Application Binary Interface), zawierający informację, w jaki sposób autor kodu oczekuje wywoływania jego funkcji – czyli konwencję wywołań (ang. calling convention). W szczególności taka konwencja zawiera:
Rysunek 1.2. Po wywołaniu adres powrotu jest odłożony na stosie
Rysunek 1.3. Wywołanie funkcji za pomocą instrukcji CALL, widok programu IDA Pro
1. Listę i sposób przekazywania parametrów. W tym celu zazwyczaj wykorzystywane są określone rejestry oraz stos.
2. Sposób zwracania wartości (wyniku działania funkcji).
3. Listę rejestrów procesora, które nie zostaną zmienione po wykonaniu funkcji (tzw. bezpieczne rejestry). Najczęściej tożsame jest to z zapisaniem ich na stosie w prologu funkcji, a następnie zdjęciem w epilogu.
4. Ustalenie, kto „sprząta” (na stosie) po zakończeniu funkcji:
– funkcja wywoływana: najczęściej przez wywołanie instrukcji RET z właściwym parametrem, co powoduje odpowiednie zwiększenie wartości wskaźnika stosu;
– kod wywołujący funkcję: po instrukcji CALL następuje explicite „poprawienie” wartości wskaźnika stosu o odpowiednią wartość.
Jak można się domyślić, na większości platform istnieją już dobrze zdefiniowane konwencje, rozwiązujące ten problem za programistów, którzy dzięki temu mogą skupić się na właściwej logice kodu. W poniższych punktach omówiono szczegółowo konwencje obowiązujące w popularnych systemach działających pod kontrolą procesorów z rodziny x86 i x86-64.
1.1.2. Konwencje wywołań x86
Ponieważ platforma x86 rozwijała się bardzo burzliwie – od wczesnych lat 90. powstało wiele systemów operacyjnych, a jeszcze więcej kompilatorów – nic więc dziwnego, że nie ma jednej idealnej (i preferowanej przez wszystkich) konwencji wywołań. W tabeli 1.1 przedstawiono pięć najbardziej popularnych z nich. Kolumny zawierają kolejno opis:
1. Przyjętej nazwy potocznej.
2. Sposobu przekazywania parametrów poprzez rejestry oraz stos, w szczególności:
a. W konwencji języka C – C-style („od tyłu”) – argumenty odkładane są na stos od ostatniego do pierwszego:
f(a1, a2) -> push a2, push a1, call
b. W konwencji Pascala – Pascal-style („od przodu”) – argumenty odkładane są na stos od pierwszego do ostatniego.
3. Kto sprząta stos: sama funkcja, czy też kod wywołujący.
We wszystkich przedstawionych konwencjach uznaje się, że funkcja wynik swojego działania zapisuje na koniec w rejestrze EAX (lub ST0 w przypadku wartości zmiennoprzecinkowej). Przyjmuje się również, że funkcja podczas działania może w pełni korzystać z wszystkich rejestrów, z tym zastrzeżeniem, że EBX, ESI, EDI oraz EBP po zakończeniu funkcji muszą mieć przywróconą oryginalną wartość (wartość przed rozpoczęciem wykonania kodu funkcji).
Tabela 1.1. Spis najbardziej popularnych konwencji wywołań na platformie x86
---------- ------------------------ --------------------- ------------------ ---------------------------------------------------------------------------------------------------------------------------------------------------------
Nazwa Parametry w rejestrach Parametry na stosie Kto sprząta stos Opis
cdecl brak C-style caller Nazwa pochodzi od C style function declaration. Pozwala na zmienną liczbę argumentów (tzw. variadic functions), ponieważ to wywołujący kod sprząta stos
pascal brak Pascal-style callee
stdcall brak C-style callee
fastcall ECX, EDX C-style callee Pierwsze dwa parametry są przekazywane w rejestrach, a pozostałe (jeśli istnieją) na stosie
thiscall ECX C-style callee Używana do wywoływania metod obiektów. W ECX znajduje się wskaźnik na obiekt, którego metodę wywołano
---------- ------------------------ --------------------- ------------------ ---------------------------------------------------------------------------------------------------------------------------------------------------------
// Kompilacja: gcc ex02.c -o ex02.exe -O0 -m32
int __cdecl func1(int a, int b, int c){ return a+b+c; }
int __stdcall func2(int a, int b, int c){ return a+b+c; }
int __fastcall func3(int a, int b, int c){ return a+b+c; }
int main(void){
int r=0;
r += func1(1, 2, 3);
r += func2(4, 5, 6);
r += func3(7, 8, 9);
return r;
}
Listing 1.2. Kod programu „ex02” przedstawiającego trzy różne konwencje wywołań
Rysunek 1.4. Deasemblacja kodu funkcji w konwencji cdecl
Rysunek 1.5. Deasemblacja kodu funkcji w konwencji stdcall; widoczne „sprzątanie” stosu w epilogu
Rysunek 1.6. Deasemblacja kodu funkcji w konwencji fastcall; strzałkami zaznaczono dwa parametry przekazane przez rejestry
Rysunek 1.7. Kod „ex02” skompilowany na platformę x86; widoczne różne konwencje wywołań (1 – cdecl, 2 – stdcall, 3 – fastcall)
1.1.3. Konwencje wywołań x86-64
Konwencje wywołań obowiązujące na 64-bitowej architekturze x86-64 przedstawia tabela 1.2.
Tabela 1.2. Spis konwencji wywołań na platformie x86-64
+-------------+----------------------------+---------------------+------------------+-----------------------------+
| System | Parametry w rejestrach | Parametry na stosie | Kto sprząta stos | Rejestry bezpieczne |
+-------------+----------------------------+---------------------+------------------+-----------------------------+
| Windows | RCX, RDX, R8, R9 | C-style | caller | RBX, RSI, RDI, RBP, R12-R15 |
+-------------+----------------------------+---------------------+------------------+-----------------------------+
| Linux, BSD, | RDI, RSI, RDX, RCX, R8, R9 | C-style | caller | RBX, RBP, R12-R15 |
| | | | | |
| OS X | | | | |
+-------------+----------------------------+---------------------+------------------+-----------------------------+
W celach testowych możemy ponownie skompilować program „ex02” na rozważaną architekturę:
> gcc ex02.c -o ex02_64.exe -O0 -m64
> file *exe
ex02.exe: PE32 executable (console) Intel 80386, for MS Windows
ex02_64.exe: PE32+ executable (console) x86-64, for MS Windows
W tym przypadku, dla wszystkich trzech wywołań funkcji (niezależnie od zdefiniowanych dla nich atrybutów) został wygenerowany ten sam kod, co widać na rysunku 1.8.
Rysunek 1.8. 64-bitowy kod programu „ex02”, w którym wywołania mają tę samą formę1.2. Struktury
Jak widać, w języku niskiego poziomu implementacja tak prostego i podstawowego zagadnienia jak wieloargumentowe funkcje niekoniecznie jest prosta. Przejdźmy teraz do bardziej złożonego problemu, a mianowicie obsługi struktur. Należy w tym miejscu powtórzyć: dla procesora cała dostępna pamięć wygląda „płasko”. Nie rozumie on (czytaj: nie potrafi interpretować) bloków kodu jako funkcji – jak również organizowania grup komórek pamięci (bajtów) w złożone struktury.
struct st001{
int first;
char blob;
int x;
char y;
unsigned long int z;
int last;
};
Listing 1.3. Przykładowa definicja struktury w języku C zawierająca elementy o różnych typach
Pamięć potrzebna do przechowywania struktury może być zarezerwowana na dwa sposoby:
1. poprzez deklarację globalnej lub lokalnej zmiennej danego typu:
a. zmienna lokalna – pamięć jest automatycznie rezerwowana na stosie podczas rozpoczęcia bloku funkcji,
b. zmienna globalna – pamięć rezerwowana jest podczas inicjalizacji programu;
2. poprzez bezpośrednią alokację miejsca na strukturę (na przykład funkcją malloc).
Listingi 1.4, 1.5 i 1.6 oraz odpowiadające im rysunki 1.9, 1.10 i 1.11 przedstawiają zależność pomiędzy lokalizacją struktury w pamięci (w pamięci globalnej, na stosie i stercie) a odwołującym się do nich kodem asemblera.
#include "stru.h"
struct st001 global;
void func1(void){
global.x = 0xbadf00d;
}
int main(void){
func1();
return 0;
}
Listing 1.4. Przykładowy kod w języku C odwołujący się do pola struktury umieszczonej w pamięci statycznej
Rysunek 1.9. Wpisanie stałej pod odpowiedni adres w pamięci statycznej
#include "stru.h"
void func1(void){
struct st001 local;
local.x = 0xbadf00d;
}
int main(void){
func1();
return 0;
}
Listing 1.5. Przykładowy kod w języku C odwołujący się do pola struktury umieszczonej w lokalnej ramce stosu
Rysunek 1.10. Alokacja struktury w ramce stosu i wpisanie stałej pod odpowiednie przesunięcie (ang. offset)
#include "stru.h"
#include
void func1(void){
struct st001* ptr_stru;
ptr_stru = (struct st001*)malloc(sizeof(struct st001));
ptr_stru->x = 0xbadf00d;
}
int main(void){ func1(); return 0; }
Listing 1.6. Przykładowy kod w języku C alokujący strukturę na stercie i odwołujący się do jej elementu
Rysunek 1.11. Alokacja struktury na stercie za pomocą funkcji malloc, a następnie odwołanie do odpowiedniego przesunięcia w otrzymanym wskaźniku
1.2.1. „Zgadywanie” wielkości i ułożenia elementów struktury w pamięci
Jak można zauważyć w powyższych przykładach, w przypadku programu, w którym wykorzystano struktury, czekają na nas dwa wyzwania:
1. Identyfikacja wielkości struktury – sytuacja jest stosunkowo prosta, jeżeli pamięć, w której przechowywane są dane, jest dynamicznie alokowana. Nieco trudniej jest, jeżeli mamy do czynienia z lokalną lub globalną zmienną.
2. Poznanie układu struktury – czyli identyfikacja lokalizacji poszczególnych pól. Zadanie to ułatwia nieco występowanie wielu zmiennych zajmujących pamięć „w okolicy” naszej struktury.
1.2.2. Rozpoznawanie budowy struktur lokalnych i globalnych
W listingu 1.7 znajduje się przykładowy kod ilustrujący wymienione powyżej problemy oraz etapy rozpoznania.
#include "stru.h"
void func1(void){
int before; // rozmiar = 4B
struct st001 local; // nieznany rozmiar
int after; // rozmiar = 16B
before = 0x1111;
after = 0x2222;
local.first = 0xaaaa;
local.last = 0xffff;
}
int main(void){
func1();
return 0;
}
Listing 1.7. Kod w języku C operujący na lokalnej strukturze
Załóżmy, że wiemy, iż lokalnie występują dwie zmienne o znanym rozmiarze oraz struktura „pomiędzy” nimi (w rozumieniu układu pamięci). Pierwsza zmienna ma wielkość czterech bajtów (sizeof(int)), a druga 16 (4 × sizeof(int)). Kiedy przeanalizujemy kod asemblera wygenerowany na podstawie przytoczonych źródeł w języku C (patrz rys. 1.12), możemy dostrzec następujące etapy wykonania funkcji func1:
1. Alokacja miejsca na stosie na zmienne lokalne, gdzie 0xF0 = 240 bajtów. Stąd można oszacować wielkość pamięci zarezerwowanej na strukturę: 240 − (4 + 16) = 220 bajtów.
2. Przypisanie stałej do zmiennej before.
3. Przypisanie stałej do elementu after.
4. Przypisanie stałej do pierwszego pola struktury. Bardziej czytelny byłby tutaj zapis . Przykład ten pokazuje, że z punktu widzenia kodu niskopoziomowego pojęcie struktury (jako organizacji pamięci) nie istnieje.
5. Przypisanie stałej do ostatniego pola struktury, znajdującego się na przesunięciu 0xD8. Zapis ten jest widoczny tylko dlatego, że zmienną var2 oznaczono (w programie do deasemblacji) jako tablicę 220 bajtów na stosie.
Rysunek 1.12. Kod asemblera funkcji func1 przedstawionej w listingu 1.7
Układ pamięci na stosie funkcji pokazano na rysunku 1.13. Stos rośnie w dół, więc zmienne lokalne odłożone są w odwrotnej kolejności. Zmienne o znanym rozmiarze to var1 (1) oraz var3 (3). Element var2 (kandydat na poszukiwaną strukturę) został oznaczony jako tablica o długości 220 bajtów (rozmiar „odgadnięty” na podstawie powyższej analizy miejsca zarezerwowanego na stosie).
Rysunek 1.13. Układ ramki stosu funkcji func1
Na rysunku 1.14 widzimy fragment kodu funkcji func1 bez opisów ułatwiających analizę układu pamięci (zmiennych lokalnych na stosie). Odwołania do zmiennych lokalnych są podane względem adresu aktualnej ramki stosu (rejestr EBP). W tabeli 3 znajduje się dokładny układ zmiennych na stosie.
Rysunek 1.14. Przypisania stałych do komórek na stosie bez podanych opisów pomocniczych
Tabela 1.3. Dokładny układ obiektów na stosie
Nazwa
Przesunięcie
Opis
var3
240
Pierwszy element tablicy var3
…
Kolejne elementy tablicy
var2
224
Pierwsze pole struktury
…
Kolejne pola struktury
8
Ostatnie pole struktury
var1
4
Zmienna var1
1.2.3. Rozpoznawanie budowy struktur dynamicznie alokowanych
W przypadku kodu, w którym występuje dynamiczna alokacja pamięci dla struktury i opcjonalnie inicjalizacja jej pól, sytuacja wygląda dużo prościej – z dwóch powodów:
1. Jak już opisano wcześniej – rozmiar struktury można poznać poprzez zlokalizowanie parametru przekazywanego funkcji alokującej pamięć (np. malloc).
2. Po wykonaniu alokacji program otrzymuje adres regionu pamięci, więc kod tak naprawdę operuje na wskaźniku do struktury. Operacje na poszczególnych polach widoczne są jako odwołania do wskaźnika z pewnym przesunięciem.
W listingu 1.8 przedstawiony jest kod z użyciem dedykowanej funkcji (new_struct), która alokuje pamięć na strukturę oraz ustawia wartości jej niektórych pól. Funkcja func1 wykorzystuje otrzymany wskaźnik do przeprowadzania kolejnej operacji na strukturze.
#include
#include "stru.h"
struct st001* new_struct(void){
struct st001* ps;
ps = (struct st001*)malloc(sizeof(struct st001));
ps->first = 0x11;
ps->last = 0xFF;
return ps;
}
void func1(void){
struct st001* ptr;
ptr = new_struct();
ptr->x = 0x1234;
}
int main(void){
func1();
return 0;
}
Listing 1.8. Dynamiczna alokacja struktury i dostęp do jej elementów w języku C
Wynik deasemblacji funkcji new_struct jest widoczny na rysunku 1.15. Oto, co możemy wywnioskować z kodu asemblera:
1. Alokacja 220 bajtów pamięci ujawnia rozmiar struktury.
2. Wskaźnik do zaalokowanej pamięci przechowywany jest w zmiennej lokalnej.
3. Następuje odwołanie do pierwszego elementu struktury o rozmiarze czterech bajtów. Wskaźnik na strukturę przechowywany jest w rejestrze EAX. Bardziej czytelny zapis mógłby wyglądać tak: .
4. Następuje odwołanie do ostatniego elementu struktury (przesunięcie 216), również o szerokości czterech bajtów.
Rysunek 1.15. Wynikowy kod funkcji new_struct1.4. Podsumowanie
Wykonując analizę wsteczną skompilowanego kodu obiektowego, powinniśmy kierować się poniższymi wskazówkami:
1. Szukamy konstruktora. Pozwoli on oszacować wielkość struktury obiektu.
a. Jeżeli do pierwszego pola struktury w konstruktorze została przypisana wartość wskazująca na tablicę wskaźników funkcji – mamy do czynienia z vtable, a nasz obiekt zawiera metody wirtualne.
2. Szukamy i analizujemy metody funkcji, patrząc, gdzie przekazywany jest wskaźnik na obiekt z wykorzystaniem rejestru ECX (w przypadku kodu 32-bitowego). Jeżeli obiekt zawiera vtable – funkcje te możemy znaleźć właśnie w tej tablicy. Analiza metod obiektu pozwoli na lepsze dopasowanie konkretnych pól klasy.
3. Jeżeli wskaźnik do pewnej funkcji znajduje się w więcej niż jednej tabeli metod wirtualnych – mamy do czynienia z dziedziczeniem.Rozdział 2 Środowisko uruchomieniowe na systemach GNU/Linux
Grzegorz Antoniak
2.1. Wstęp
2.2. Pliki wykonywalne ELF
2.2.1. Identyfikacja systemu i architektury docelowej
2.2.2. Segmenty
2.2.3. Segment PT_LOAD
2.2.4. Segment PT_DYNAMIC
2.2.5. Sekcja .dynamic
2.2.5.1. Deklaracja bibliotek zależnych
2.2.5.2. Wczesna inicjalizacja programu
2.3. Środowisko uruchomieniowe
2.3.1. Kod PIC
2.3.2. Tablice GOT i PLT
2.3.3. Program ładujący ld.so
2.3.3.1. Zmienne środowiskowe
2.3.3.2. LD_LIBRARY_PATH
2.3.3.3. LD_PRELOAD
2.3.3.4. LD_AUDIT
2.3.4. Zrzucanie pamięci procesów
2.3.4.1. System plików /proc
2.3.4.2. Pliki specjalne w /proc/pid
2.3.4.3. Pliki specjalne maps i mem
2.3.4.4. VDSO
2.3.4.5. Wektory inicjalizacyjne
2.3.5. Wstrzykiwanie kodu
2.3.5.1. Wersja ptrace(2)
2.3.6. Samomodyfikujący się kod
2.4. Podsumowanie
Bibliografia2.1. Wstęp
Systemy operacyjne oparte na jądrze Linuksa oferują wiele metod ułatwiających analizę oprogramowania pod kątem jego działania. Większość popularnych mechanizmów wykorzystywanych z powodzeniem na systemach Windows dostępnych jest też i tutaj, choć często w nieco innej formie.
W tym rozdziale będziemy stosować system nazewnictwa narzędzi typu nazwa(N), gdzie N to numer lub identyfikator. Po napotkaniu takiej nazwy, można szybko przejść do dokumentacji tego narzędzia przy użyciu programu man, poprzez wykonanie polecenia:
$ man N nazwa
Na przykład aby uzyskać informacje na temat narzędzia readelf(1), należy wykonać polecenie:
$ man 1 readelf
Taka lista argumentów powoduje otwarcie podręcznika dotyczącego narzędzia readelf z sekcji pierwszej, czyli programów narzędziowych.