E-book
27.3
drukowana A5
39.99
drukowana A5
Kolorowa
67.18
Asembler x64. Laboratorium

Bezpłatny fragment - Asembler x64. Laboratorium


Objętość:
242 str.
ISBN:
978-83-8245-703-2
E-book
za 27.3
drukowana A5
za 39.99
drukowana A5
Kolorowa
za 67.18

Maksymalnie skondensowana dawka wiedzy na temat programowania w języku Asembler x86—64 (MASM x64). Kurs poprzedza opis architektury procesorów x86/x64 oraz systemu Windows. Poznaj Asembler — język wirusów komputerowych!


Wszelkie znaki towarowe i nazwy zastrzeżone zostały użyte jedynie w celach informacyjnych i należą wyłącznie do ich prawnych właścicieli. Autor w czasie tworzenia tej treści nie jest zawodowo powiązany z firmami, których technologie i produkty opisuje. Autor w tej publikacji nie działa też w imieniu tych firm.

O autorze…


Postać Mr. At nawiązuje do gier z gatunku roguelike/ASCII RPG w których do wyświetlania grafiki korzysta się ze znaków tekstowych. Znak @ (At) w grach roguelike przeważnie oznacza postać bohatera.

Rozdział 1. Wprowadzenie

Elementy architektury procesorów i systemu Windows.

1.1. Rejestry procesora x64

Rejestry można sobie wyobrazić jako specjalna, wewnętrzna pamięć w procesorze. Niektórzy nie lubią tu określenia „pamięć”, gdyż rejestrów się nie adresuje. Mimo wszystko można je porównać do „pojemników na dane komputerowe”. I to takich do których jest bardzo szybki dostęp, więc operacje z ich użyciem wykonywane są bardzo szybko.

Bezpośredni dostęp do rejestrów jako programiści możemy uzyskać w języku Asembler. Odwołuje się do nich poprzez ich nazwy (np. rax, rbx, rcx…).

Jeśli na przykład chcemy wykonać dodawanie to wpisujemy dwie wartości do dwóch różnych rejestrów i wykonujemy na nich operację sumowania odpowiednią instrukcją procesora (wynik też będzie w określonym rejestrze).

Zatem za pomocą rejestrów procesora możemy przekazywać dane do przetworzenia, ale też np. uzyskać dane wynikowe, które zwraca określona instrukcja czy funkcja.

Jeśli dane nie mieszczą się w rejestrze, czyli np. jest to długi ciąg tekstu, to wykonujemy wtedy adresowanie. Fragment tekstu do przetworzenia umieszczamy w zmiennej (takiej „komórce” pamięci) o wymyślonej nazwie, a do rejestru podajemy tylko adres do tego miejsca w pamięci (przeważnie jest to nazwa tej zmiennej poprzedzona specjalną dyrektywą pobierającą adres).

1.2. Rejestry ogólnego przeznaczenia

Rejestry ogólnego przeznaczenia mogą być używane do różnych celów. Przy wprowadzeniu ich nadano im domyślne użycie np. akumulator przechowuje wyniki obliczeń, a licznik służy do tworzenia pętli. Mimo to możemy tych rejestrów używać różnie. Schematy tych rejestrów przedstawiono na rysunku 1.1.

Rysunek 1.1

1.3. Rejestr znaczników — flag (RFLAGS)

Rejestr znaczników, nazywany też rejestrem flag procesora przechowuje bieżący stan procesora. Można w nim wyróżnić flagi kontrolne, statusu oraz systemowe.

W architekturze 16 bitowej rejestr ten miał nazwę FLAGS i miał rozmiar 16 bitów. Natomiast w 32-bitowej był już EFLAGS. A tryb 64 bitowy rozszerzył ten rejestr do 64 bitów i nazywany jest RFLAGS.

Poszczególne znaczniki to po prostu bity. Określenie flagi pasuje tutaj, gdyż określony bit tak jak tradycyjna flaga może być podniesiony (wartość 1) lub opuszczony (wartość 0).

Każda flaga coś sygnalizuje. Można to też porównać do diody, która jest zaświecona lub zgaszona. Schemat rejestru flag przedstawiono na rysunku 1.2. Bity zaznaczone na szaro są zarezerwowane i jeśli się je odczyta to będą miały wartość zero.

Rysunek 1.2

Przy programowaniu typowych aplikacji w języku Asembler należy znać następujące flagi:

CF (ang. carry flag) — flaga przeniesienia. Ustawiana jest, gdy ostatnia operacja arytmetyczna spowodowała przeniesienie (ang. carry) poza najstarszy bit wyniku lub pożyczenie (ang. borrow) z najstarszego bitu wyniku. W przeciwnym wypadku jest zerowana.

PF (ang. parity flag) — flaga parzystości. Ustawiana jest, gdy liczba bitów o wartości jeden w najmłodszym bajcie rezultatu niektórych operacji jest parzysta. W przeciwnym wypadku flaga jest zerowana.

AF (ang. auxiliary carry flag) — flaga przeniesienia pomocniczego. Ustawiana jest, gdy ostatnia operacja arytmetyczna spowodowała przeniesienie (ang. carry) poza trzeci bit wyniku lub pożyczenie (ang. borrow) z trzeciego bitu wyniku. W przeciwnym wypadku jest zerowana.

ZF (ang. zero flag) — flaga zerowa. Ustawiana jest, gdy ostatnia operacja arytmetyczna lub logiczna zwróciła w wyniku wartość zero. W przeciwnym wypadku flaga jest zerowana.

SF (ang. sign flag) — flaga znaku. Ustawiana jest, gdy ostatnia operacja arytmetyczna zwróciła liczbę ujemną. W przeciwnym wypadku jest zerowana. W prostych słowach flaga ta jest ustawiana zgodnie z najstarszym bitem wyniku (bitem znaku).

DF (ang. direction flag) — flaga kierunku. Ustawiana jest przez programistę, zależnie od tego w którym kierunku chce przetwarzać ciągi znaków (napisy). Związana jest z rozkazami operującymi na napisach.

OF (ang. overflow flag) — flaga przepełnienia. Ustawiana jest, gdy ostatnia operacja dała w wyniku wartość, która jest zbyt duża lub zbyt mała, aby zmieścić się w operandzie docelowym. W przeciwnym wypadku jest zerowana.


Pozostałe flagi przedstawione na rysunku 1.2. to flagi systemowe i raczej nie używa się ich w typowych aplikacjach.

1.4. Rejestr wskaźnika instrukcji (RIP)

Rejestr RIP (rysunek 1.3) zawiera adres następnej instrukcji do wykonania. Chciałbym tutaj wspomnieć dodatkowo o adresowaniu relatywnym do wskaźnika instrukcji, który jest dostępny w trybie 64-bitowym. Jest to bardzo przydatne przy tworzeniu kodu relokowalnego/wstrzykiwalnego, który musi być niezależny od miejsca w pamięci. Jeśli adresujemy względnie do rejestru RIP, to po zmianie miejsca kodu, który wstrzykniemy do pamięci nasze odwołania (adresy) nie zostaną zniszczone (nie będą nieprawidłowe).

W architekturze 32-bitowej z tego rodzaju adresowania korzystało się przy wykonywaniu skoków (JMP) czy wywołań procedur (CALL). Z wejściem trybu 64-bitowego możliwy jest dodatkowo swobodny dostęp do miejsc w pamięci korzystając z rejestru wskaźnika instrukcji RIP.

Rysunek 1.3

1.5. Rejestry segmentowe

W trybie 64-bitowym dostępne są tylko rejestry segmentowe CS, FS GS (rysunek 1.4). Nie używa się tutaj już segmentowego modelu pamięci. Można sobie wyobrazić, że pamięć jest ciągłą przestrzenią. Mimo to czasem używa się segmentów GS FS jako bazę adresową. Warto zaznaczyć, że z tych rejestrów korzysta się m.in. tworząc kod wstrzykiwalny (shellcode) w systemach Windows. W trybie 64-bitowym za pomocą rejestru GS (w trybie 32-bitowym za pomocą FS) możemy się dostać do struktury Thread Environment Block (TEB), gdzie po wykonaniu określonych kroków możemy uzyskać adresy funkcji Windows API potrzebne dla naszego shellcode/payload.

Rysunek 1.4

1.6. Rejestry koprocesora (FPU / x87)

Nazwa koprocesor pochodzi z dawnych czasów i oznacza osobny element wspomagający procesor w obliczeniach. W tym rozdziale przedstawiono rejestry koprocesora arytmetycznego nazywanego też jednostką zmiennoprzecinkową (ang. floating-point unit, FPU) czy krótko x87. Mimo, że określenie koprocesor pozostało, to aktualnie jest on zawarty w układzie procesora, a nie oddzielnie. Koprocesor arytmetyczny powstał, aby przeprowadzać obliczenia na liczbach z większą dokładnością. Główne typy danych używane przy operacjach zmiennoprzecinkowych to:

— Wartość pojedynczej precyzji o rozmiarze 32 bitów,

— Wartość podwójnej precyzji o rozmiarze 64 bitów,

— Wartość o rozszerzonej precyzji o rozmiarze 80 bitów.

Rejestry koprocesora (x87) zachowują się jak stos. Korzystając z jego instrukcji możliwe jest wykonywanie obliczeń relatywnie od wierzchołka stosu, ale nic nie stoi na przeszkodzie, aby odwoływać się do poszczególnych rejestrów indywidualnie (po nazwie np. st0, st1, st2, …, st7).


Rejestry do przechowywania danych dla jednostki zmiennoprzecinkowej (koprocesora x87) w formie schematu przedstawiono na rysunku 1.5.

Rysunek 1.5

Przy opisywaniu koprocesora x87 warto wspomnieć o rejestrze statusu nazywanym FPU Status Word (rysunek 1.6). To w nim są informacje o aktualnym stanie jednostki zmiennoprzecinkowej. Bity od 13 do 11 w tym rejestrze wskazują na wierzchołek stosu określany jako TOP. Jak wcześniej wspomniano rejestry FPU są w formie stosu. Bity określane jako TOP zawierają fizyczny indeks wierzchołka stosu rejestrów (rysunek 1.5).

Schemat rejestru statusu dla jednostki zmiennoprzecinkowej przedstawiono na rysunku 1.6. Oprócz TOP wspomnę też o bitach C0…C3. Otóż są one ustawiane przez rozkazy porównania wartości. Pozostałe znaczniki (IE, DE, ZE etc.) dotyczą wyjątków (ang. exceptions).

Rysunek 1.6

Oprócz rejestru statusu istnieje też rejestr kontroli jednostki zmiennoprzecinkowej (rysunek 1.7). Znacznik PC w tym rejestrze określa dokładność:

00b — pojedyncza precyzja (24 bity),

01b — zarezerwowane,

10b — podwójna precyzja (53 bity)

11b — rozszerzona podwójna precyzja (64 bity).


Natomiast RC dotyczy zaokrąglania, a jego możliwe wartości to:


00b — zaokrąglanie do najbliższej wartości lub wartości parzystej w przypadku równej odległości,

01b — zaokrąglanie w dół,

10b — zaokrąglanie w górę,

11b — zaokrąglanie w kierunku zera.


Pozostałe znaczniki to maski wyjątków. Określają one czy ma zostać rzucony wyjątek danego rodzaju. Wyjątki są domyślnie wyciszone (maskowane). Możemy zatem ustawić, które wyjątki chcemy aktywować.

Rysunek 1.7

1.7. Rejestry MMX

Procesor o architekturze x86—64 posiada osiem rejestrów związanych z technologią MMX. Rozmiar każdego z tych rejestrów to 64 bity. A ich nazwy to MM0, MM1, MM2…, aż do MM7. Rozszerzenie to powstało, aby sprawniej i wydajniej obsługiwać multimedia. Niektórzy rozwijają skrót MMX jako Multimedia eXtensions.

Rejestry MMX są nałożone (mapowane) na młodszą (dolną) 64-bitową część 80-bitowych rejestrów koprocesora (FPU, ang. floating-point unit). Z tego powodu wykonywanie operacji przez rozkazy z tego zestawu instrukcji powoduje też modyfikacje rejestrów jednostki zmiennoprzecinkowej (koprocesora x87).

Rozszerzenie MMX posiada własny zestaw instrukcji do transferu danych, konwersji, operacji arytmetycznych, logicznych etc. Należy też zaznaczyć, że rozszerzenie to korzysta z techniki nazywanej Single Instruction Multiple Data (SIMD) co w tłumaczeniu z języka angielskiego oznacza „pojedyncza instrukcja — wiele danych”.

Dzięki technice SIMD możliwe jest np. dodanie kilku wartości z operandu źródłowego do innych kilku wartości z operandu docelowego w sposób równoległy. Oznacza to wyraźne przyspieszenie przetwarzania danych przez aplikacje, które używają rozkazów typu SIMD.

Rejestry rozszerzenia MMX w formie prostego schematu przedstawiono na rysunku 1.8.

Rysunek 1.8

1.8. Rejestry SSE / AVX / AVX-512

Rozszerzenia SSE (SSE2, SSE3, SSSE3, SSE4…) oraz AVX to kolejny krok do zwiększenia wydajności poprzez przetwarzanie typu SIMD (Single Instruction Multiple Data).

Wartości podawane do instrukcji tego typu mogą być wektorami (ang. vector) lub skalarami (ang. scalar). Operacje na wektorach określane są też jako packed. Warto znać te terminy w języku angielskim, aby łatwiej zrozumieć oficjalne dokumentacje AMD64 Architecture Programmer’s Manual oraz Intel 64 and IA-32 Architectures Software Developer’s Manual.

Wektor z danymi posiada kilka wartości, które jak już było wspomniane przy rozdziale o MMX są przetwarzane równolegle.

Natomiast przy operacjach skalarnych działania wykonywane są na pojedynczych wartościach. Rejestry SSE i AVX przedstawiono na rysunku 1.9. Natomiast rejestry rozszerzenia AVX-512 umieszczono na rysunku 1.10.

Rysunek 1.9


Rysunek 1.10

Na rysunku 1.11 przedstawiono razem rejestry FPU, MMX, SSE, AVX i AVX-512. Warto jeszcze raz zaznaczyć, że fragmenty rejestrów MMX są mapowane na dolną część rejestrów koprocesora (x87).

Rysunek 1.11

1.9. Pamięć operacyjna

Dawne aplikacje dla podsystemu MS-DOS w architekturze 16-bitowej korzystały z segmentowego modelu pamięci (ang. segmented memory model). Aby dostać się wtedy do określonego miejsca w pamięci należało podać selektor segmentu, a następnie przesunięcie. Powodem tego było podzielenie pamięci na osobne obszary. Takie jak np. segment kodu czy danych. W architekturze 32-bitowej (x86) oraz 64-bitowej (x86—64) korzysta się z modelu płaskiego (ang. flat memory model). Pamięć tutaj wygląda jak ciągła przestrzeń, a odwołanie do określonego miejsca wykonuje się poprzez podanie adresu liniowego nazywanego też efektywnym (rysunek 1.12).

Rysunek 1.12

1.10. Stos

W programowaniu określenia stos używa się na strukturę danych typu LIFO (Last In — First Out). Element, który jest odłożony jako ostatni zostanie odczytany w pierwszej kolejności.

Język Asembler posiada dwie główne instrukcje, których przeważnie używa się do odkładania wartości na stos oraz ich zdejmowania. Rozkaz PUSH zmniejsza wartość wskaźnika stosu i odkłada wartość. Natomiast rozkaz POP zwiększa wartość wskaźnika stosu i zdejmuje ostatnio odłożoną wartość. Wskaźnik stosu to wcześniej przedstawiony rejestr o nazwie RSP. W prostych słowach: stos programu jest obszarem pamięci do przechowywania różnych danych związanych często z wywołaniami funkcji (rysunek 1.13). Podstawowe elementy jakie znajdziemy na stosie programu w architekturze x64 to m.in.:

— argumenty funkcji odłożone na stos,

— miejsce na argumenty przekazywane przez rejestry RCX, RDX, R8 oraz R9,

— adres powrotu odłożony przez instrukcję CALL.

Wspomnę tutaj przy okazji o pojęciu, które można spotkać w różnych miejscach i warto je znać. Chodzi o ramkę stosu (ang. stack frame). Ramka stosu to struktura umieszczona na stosie, która przechowuje dane związane z wywołaniem określonego podprogramu (funkcji), czyli wcześniej wymienione argumenty, adres powrotu itd. Ktoś może zapytać dlaczego na stosie znajduje się adres powrotny, miejsca na argumenty przekazywane przez cztery rejestry (RCX, RDX, R8 R9) oraz miejsce na dodatkowe argumenty? Związane jest to z cechami architektury x64 oraz konwencji wywołań funkcji (tutaj Microsoft x64). Zatem ramka i stos nie zawsze muszą wyglądać i być używane tak samo.

Rysunek 1.13

1.11. Adresowanie operandów

W celu uzyskania dostępu do określonego miejsca w pamięci lub rejestru procesora należy zapoznać się z metodami adresowania. Poniżej przedstawiono sposoby adresowania operandów w Asemblerze x64. Dla nie znających jeszcze instrukcji Asemblera wprowadzę tutaj opis instrukcji kopiowania wartości z jednego operandu do drugiego. Jest to instrukcja MOV i ma następującą składnię: MOV operand docelowy, operand źródłowy.


Adresowanie natychmiastowe polega na podaniu jako operand wartości liczbowej nazywanej właśnie wartością natychmiastową. Zatem zapis MOV RAX, 7 wpisuje wartość 7 do rejestru akumulatora RAX.


Adresowanie rejestrowe polega na podaniu jako operand nazwy rejestru np. MOV RAX, RCX. Zapis ten kopiuje wartość rejestru RCX do rejestru RAX.


Adresowanie pośrednie polega na podaniu jako operand adresu w pamięci spod którego ma być pobrana wartość np. MOV RAX, qword ptr [zmienna].


Adresowanie pośrednie rejestrowe polega na podaniu jako operand rejestru w którym jest adres komórki w pamięci np. MOV RAX, qword ptr [RCX].


Adresowanie pośrednie z przesunięciem polega na podaniu dodatkowo wartości przesunięcia (ang. offset) np. MOV RAX, qword ptr [RCX+2] czy też MOV RAX, qword ptr [zmienna+2].


Adresowanie bazowo-indeksowe polega na podaniu bazy i dodania do niej indeksu. Indeks może być pomnożony przez skalę 1, 2, 4 czy 8 zależnie od rozmiaru elementu np. MOV RAX, qword ptr [RDX+RDI*8]. Opcjonalnie też przesunięcie.


Warto dodać, że architektura x64 wprowadziła adresowanie relatywne do wskaźnika instrukcji (RIP). Pozwala to łatwiej tworzyć kod niezależny od miejsca w pamięci (relokowalny czy wstrzykiwalny). Asembler MASM x64 korzysta z adresowania relatywnego do wskaźnika instrukcji w sposób domniemany (ang. implicit).

1.12. Kod maszynowy

Typowy program w formie pliku wykonywalnego (np. exe) dla systemu Windows posiada instrukcje zapisane w kodzie maszynowym. Rozkazy do wykonania przez procesor są kodowane według specjalnego schematu i zapisane jako wartości liczbowe. Taki zapis kodu jest bardzo trudny do przeczytania i zrozumienia przez człowieka. Narzędzie, które przetwarza kod programisty np. C++ do gotowego pliku wykonywalnego nazywane jest kompilatorem. Kod wysokiego poziomu abstrakcji w postaci tekstu tłumaczony jest na Asembler, a następnie właśnie na kod maszynowy (ciągi bajtów — liczb). Przedstawia to schemat na rysunku 1.14.

Nieczytelność kodu maszynowego dla człowieka nie powoduje wcale niemożliwości analizy programów w formie plików *.exe. Istnieją programy, które dokonują odwrotnej czynności niż kompilacja, a jest do dekompilacja nazywana też deasemblacją. Narzędzia typu deasembler (disassembler) tłumaczą kod maszynowy na instrukcje języka Asembler, co pozwala na analizę kodu i sprawdzenie „co robi” określony plik wykonywalny. Dekompilatory idą o krok dalej i próbują uzyskać z pliku *.exe kod wyższego poziomu abstrakcji niż Asembler. Może to być np. pseudokod podobny do języka C, który oferuje nieco starszy już dekompilator REC Studio.

Rysunek 1.14

1.13. Format kodowania rozkazów procesora

Rozkaz procesora w formie tekstowej (np. MOV, ADD, XOR…) nazywany jest mnemonikiem. Natomiast zakodowana wartość liczbowa instrukcji to kod operacyjny (opkod). Na rysunku 1.15 przedstawiono poszczególne elementy zakodowanej instrukcji procesora w formie schematu. Nie wszystkie składowe są wymagane. Istnieją instrukcje (NOP itp.), które mają tylko sam opkod, czyli ich zakodowana forma to jeden bajt o określonej wartości.

Rysunek 1.15

Poniżej przedstawiono opis poszczególnych elementów formatu kodowania instrukcji procesora x64.


Prefiksy (w tym prefiks REX) — prefiks REX pozwala na dostęp do rozszerzonych oraz dodatkowych rejestrów procesora, czyli tych wprowadzonych w architekturze x64. Inne prefiksy związane są np. z operacjami na napisach czy też modyfikacją rozmiaru operandu oraz adresu.


Kod operacyjny (opkod) — wartość przypisana określonej instrukcji o rozmiarze od jednego do trzech bajtów.


ModR/M — bajt ten jest związany z typem adresowania oraz rodzajem operandów.


SIB — związany z bajtem ModR/M. Czasami użyte adresowanie wymaga tego bajtu. Zawiera trzy pola: skala, indeks oraz baza. Adres efektywny (liniowy) jest obliczany jako: baza + (indeks * skala) + ewentualne przesunięcie.


Przesunięcie (ang. offset) — nazywane też jako przemieszczenie (ang. displacement) to wartość liczbowa, która jest dodawana do podstawy adresu.


Wartość natychmiastowa (ang. immediate) — jeśli rozkaz procesora używa wartości natychmiastowej jako operandu to znajduje się ona w tym polu.


Przykładowe rozkodowane instrukcje procesora przedstawiono na rysunku 1.16.

Rysunek 1.16

1.14. Procesy i wątki

Proces w systemie Windows to uruchomiona w pamięci kopia aplikacji. Program, który jest plikiem wykonywalnym może być uruchomiony wielokrotnie i zostanie wtedy utworzone wiele procesów. Każdy proces musi mieć przynajmniej jeden wątek. Informacje o określonym procesie przechowuje struktura EProcess. Należy pamiętać, że budowa tej struktury oraz struktur i pól w niej zawartych może zmieniać się wraz z wersjami systemu Windows.

Aby nie tworzyć tutaj „teoretycznego opowiadania” przejdę do zaprezentowania metody pozwalającej na przeglądanie i inspekcję tych struktur.

Sklepie z aplikacjami dla systemu Windows firmy Microsoft dostępna jest odświeżona wersja narzędzia WinDbg. Program WinDbg Preview to ulepszona wersja znanego debuggera o nazwie WinDbg.


Po zainstalowaniu narzędzia WinDbg Preview oraz uruchomieniu go powinno wyświetlić się główne okno programu, które wita nas komunikatem: „Debugee not connected” (rysunek 1.17).

Rysunek 1.17

Wczytajmy do debuggera przykładowy plik wykonywalny. Może to być systemowy Kalkulator, którego domyślna ścieżka dostępu to C:\Windows\System32\calc. exe. W tym celu z górnego menu należy wybrać Plik (file) / Start debugging / Launch executable.

Rysunek 1.18

W celu wyświetlenia określonego typu danych posłużymy się poleceniem dt, którego skrót oznacza Display Type.

Cała składnia polecenia, które należy wpisać prezentuje się następująco: dt -a -b -v _EPROCESS

Parametr -a pozwala wyświetlić każdy element tablicy w nowej linii. Parametr -b rozwija (wyświetla) zawartość pól, które są strukturami. Natomiast parametr -v pozwala uzyskać więcej informacji takich jak rozmiar struktury oraz ilość jej elementów.

Rysunek 1.19

Kompletny opis składni polecenia dt oraz innych można znaleźć w dokumentacji firmy Microsoft.


W strukturze EPROCESS znajduje się inna struktura określana jako Process Environment Block (PEB), która pozwala otrzymać wiele informacji o analizowanym procesie (rysunek 1.20).

Rysunek 1.20

Struktura Process Environment Block (PEB) zawiera m.in.:

— wskaźnik do początku obrazu pliku wykonywalnego (ImageBaseAddress)

— informacje o bibliotekach DLL używanych przez program, a znajdziemy je w strukturze Ldr (_PEB_LDR_DATA).

— informacje o parametrach z jakimi uruchomiono proces (ProcessParameters),

— i inne.


Tak jak dla procesów tak i dla wątków istnieje podobna struktura, a jej nazwa to Thread Environment Block (TEB).


Znajomość tych struktur na pewno jest przydatna i ma niejedno zastosowanie. Gdy następuje potrzeba przeanalizowania określonego procesu i/lub wykonania zrzutu pamięci (ang. memory dump) to te struktury dostarczą na pewno wiele informacji. Inne zastosowanie to tworzenie shellcode/payload. Kod wstrzykiwalny typu shellcode potrzebuje adresów do funkcji systemowych, aby wykonać zamiary hakera. Adresy te od dawna nie są stałymi wartościami i nie można ich na stałe wpisać w kod programu. Dlatego kod wstrzykiwalny często przeszukuje strukturę PEB, a dokładnie jej gałąź Ldr, aby otrzymać adres załadowanych modułów DLL takich jak kernel32.dll czy inne.

1.15. Poziomy uprawnień

Architektura systemu Windows zapewnia izolację programów użytkownika od tych działających w trybie jądra systemu. Możliwe jest to dzięki zaimplementowanym w procesorze zabezpieczeniom (rysunek 1.22), które udostępniają cztery poziomy uprzywilejowania (ang. rings) dla wykonywanego kodu. Najbardziej uprzywilejowany jest poziom zerowy w którym to działają komponenty krytyczne dla systemu operacyjnego jak np. sterowniki. Dwa kolejne poziomy (ring 1 oraz ring 2) są o umiarkowanym poziomie uprzywilejowania. Natomiast poziom użytkownika (ring 3) to tryb w którym działają standardowe programy, które nie mają bezpośredniego dostępu do krytycznych zasobów systemowych czy sprzętowych. W dokumentacji procesorów AMD64 oraz Intel 64 bieżący poziom uprzywilejowania oznaczany jest skrótem CPL od Current Privilege Level. Można go odczytać z rejestru segmentu kodu CS (ang. code segment) — rysunek 1.21.

Rysunek 1.21


Rysunek 1.22

1.16. Konwencja wywoływania funkcji Microsoft x64

Wywołanie funkcji w Asemblerze x64 nazywane też wywołaniem podprogramu przenosi sterowanie do innego miejsca w kodzie. Gdy blok kodu określany funkcją się wykona, to następuje powrót, który jest możliwy poprzez odłożony wcześniej na stosie programu adres powrotny. Funkcje wywołuje się instrukcją procesora call. To ona odkłada na stos wspomniany wcześniej adres powrotny i przekazuje kontrolę do wywoływanego podprogramu.


Nie ma jednego uniwersalnego sposobu na wywołanie funkcji. Zależne jest to od architektury, a to jak działa wywołanie i związane z nim operacje określają konwencje wywołania (ang. calling conventions).


Przypomnijmy sobie, że w Asemblerze MASM32 dla architektury x86—64 korzysta się z konwencji stdcall, która jest domyślna dla API systemu Windows. Wyczyszczenie stosu programu jest obowiązkiem funkcji, która jest wywoływana (ang. callee), czyli programista korzystający z takiej funkcji ma spokój z czyszczeniem stosu. Argumenty (nazywane też parametrami) przekazywane są poprzez stos „od końca”, czyli od prawej do lewej strony. Jeśli funkcja zwraca jakiś rezultat, to znajdzie się on w rejestrze akumulatora EAX. Niektóre funkcje, gdy wynik jest większy niż 32-bity zwracają wynik w parze rejestrów EDX: EAX. W konwencji stdcall, jeśli chcemy (w naszej funkcji) modyfikować wartości rejestrów ESI, EDI, EBP EBX, to powinniśmy zachować ich wartości np. na stosie, a następnie je przywrócić przed powrotem do Windows.

Rysunek 1.23

Asembler MASM64 dla architektury x86—64 (w skrócie x64) korzysta z konwencji wywoływania funkcji nazwanej Microsoft x64. Wyczyszczenie stosu programu jest obowiązkiem funkcji wywołującej (ang. caller). Argumentów nie przekazuje się tylko przez stos, ale przez wybrane rejestry takie jak: R9, R8, RDX, RCX. Ze stosu korzysta się, gdy argumentów jest więcej niż cztery i odkłada się je „od końca”, czyli od prawej do lewej strony. Rezultat funkcji, jeśli ma rozmiar mniejszy niż 64-bity lub równy, zwracany jest w rejestrze akumulatora RAX.


W konwencji Microsoft x64, jeśli chcemy (w naszej funkcji) modyfikować wartości rejestrów RBP, RBX, RDI, RSI, RSP, R12, R13, R14 R15, to powinniśmy zachować ich wartości np. na stosie, a następnie je przywrócić przed powrotem do Windows. Należy również pamiętać o wyrównaniu stosu do okrągłych 16 bajtów. Ilość miejsca rezerwowanego na stosie wraz z adresem powrotnym powinna być podzielna przez 16 bez reszty.

Rysunek 1.24

1.17. Win32 API, Native API, WinRT

„Cebula ma warstwy, ogry mają warstwy, cebula ma warstwy.”

Shrek (film)

Interfejs programowania aplikacji (w skrócie API) w Windows to w uproszczeniu zestaw funkcji zawartych w bibliotekach, które są przeważnie częścią systemu. Udostępniane są one programistom, aby mogli ich używać w swoich programach. Istniejące od bardzo dawna Windows API (nazywane też Win32 API) składa się z warstw (podobnie jak cebula czy ogry) i często jedne funkcje są „opakowaniami” na znajdujące się niżej (biorąc pod uwagę poziom abstrakcji) wywołania. Podstawowe funkcje dostępne są w bibliotekach kernel32.dll oraz user32.dll. Na przykład: Ktoś tworzy program korzystający z Win32 API i potrzebuje utworzyć plik. Może do tego celu skorzystać z funkcji CreateFile. Co jednak dzieje się wewnątrz funkcji? Znajdziemy tam m.in. różne rozkazy procesora, ale wykonanie zmierza do wywołania funkcji NtCreateFile. Funkcja NtCreateFile znajduje się w bibliotece ntdll. dll i jest abstrakcyjnie niżej niż Win32 API, a zbiór funkcji z przedrostkiem Nt tam zawartych określa się jako Native API. Interfejs ten jest mostem pomiędzy aplikacjami użytkownika, a wnętrznościami systemu Windows. Native API zawiera też elementy, które dotyczą trybu jądra (ang. kernel mode, ring 0) takie jak np. wersje funkcji z przedrostkiem Zw (np. ZwCreateFile). Korzysta się z nich podczas tworzenia programów działających w kernel mode.


Warto zaznaczyć, że od pewnego czasu dawne Win32 API programowane głównie w języku C powoli zastępuje nowy interfejs nazywany Windows Runtime (w skrócie WinRT).

1.18. Wywołania systemowe SYSCALL

Poprzedni rozdział opisywał czym jest Windows API oraz „leżące niżej” Native API. Jednak analiza wnętrza wywołań funkcji systemowych zatrzymała się na Native API (funkcje z przedrostkiem Nt). Jako, że Native API jest mostem pomiędzy trybem użytkownika (ang. user mode, ring 3), a trybem jądra systemu (ang. kernel mode, ring 0), to powinno zawierać jakiś mechanizm przekazywania wywołań do jądra systemu.

Wykonywane jest to za pomocą rozkazu procesora SYSCALL. A najlepszym wyjaśnieniem będzie podejrzenie wnętrza wybranej funkcji Native API. Przykładowym programem może być systemowy Kalkulator (rysunek 1.25).

Rysunek 1.25

Poprzez wydanie polecenia u ntdll! NtCreateFile możliwe jest wyświetlenie wnętrza funkcji NtCreateFile w języku Asembler. Otrzymany kod po deasemblacji przedstawia rysunek 1.26.

Rysunek 1.26

Polecenie u (przypis) w narzędziu WinDbg pozwala dokonać deasemblacji, czyli otrzymania listingu w języku Asembler wybranego fragmentu kodu (np. funkcji czy adresu w pamięci).

We wnętrzu funkcji NtCreateFile przedstawionym jako kod w Asemblerze na rysunku 1.26 można zauważyć między innymi:


— przekazanie argumentu z rejestru RCX do R10 (rozkaz SYSCALL niszczy zawartość rejestru RCX),

— wpisanie numeru wywołania do rejestru EAX (tutaj 55h),

— instrukcje sprawdzające (TEST, JNE),

— wywołanie systemowe poprzez rozkaz SYSCALL,

(…)


Dla porównania można wyświetlić wnętrze innej funkcji np. NtQuerySystemInformation (rysunek 1.27). Tutaj numer wywołania SYSCALL w rejestrze EAX to 36h.

Rysunek 1.27

Na rysunkach 1.26 oraz 1.27 przedstawiono jak wygląda wywołanie systemowe za pomocą rozkazu procesora SYSCALL na przykładzie wyświetlenia wnętrza dwóch wybranych funkcji Native API.


Należy pamiętać, że numery wywołań (wartość przekazywana przez rejestr EAX) mogą zmieniać się wraz w wersjami systemu Windows, gdyż są to mechanizmy wewnętrzne i nie jest tu wymagane zapewnianie kompatybilności. Dla programistów aplikacji jest Win32 API, natomiast Native API oraz mechanizm SYSCALL są stosowane przez specjalistyczne programy wymagające tego lub wirusy komputerowe.

Rozdział 2. Asembler x86/x64, czyli poznaj język wirusów

Przeczytałeś bezpłatny fragment.
Kup książkę, aby przeczytać do końca.
E-book
za 27.3
drukowana A5
za 39.99
drukowana A5
Kolorowa
za 67.18