V předchozím dílu jsme si vysvětlili proč shellcode nemůže používat staticky zapsané adresy Windows API funkcí. Řešení spočívá ve strukturách které Windows udržuje přímo v paměti každého procesu. Dnes se na ně podíváme zblízka.

Předpoklady

Před čtením tohoto dílu je vhodné přečíst a pochopit předchozí díl. Zároveň je velmi vhodné mít alespoň základní představu, co je virtuální paměť a ukazatel (pointer).


TEB – Thread Environment Block

Každé vlákno (thread) běžícího procesu má v paměti vyhrazenou strukturu nazvanou TEB (Thread Environment Block)[1]. Jedná se o klíčovou datovou strukturu, která ukládá kontextové informace o konkrétním vláknu. Windows ji udržuje automaticky a aktualizuje za běhu a je přístupná přímo z uživatelského režimu. Společnost Microsoft považuje tuto strukturu za interní, čemuž odpovídá i skutečnost, že pouze část této struktury má garantovaný tvar. Tato veřejná část se nachází na samém začátku struktury TEB a nese označení NT_TIB[2]. Slouží primárně pro účely nízkoúrovňových vývojových nástrojů a aplikací, které bez nejnovějších informací o vláknu nedovedou pracovat (například kompilátory pro obsluhu výjimek).

typedef struct _NT_TIB									// size 0x38
{
     PEXCEPTION_REGISTRATION_RECORD ExceptionList;		// offset 0x00
     PVOID StackBase;									// offset 0x08
     PVOID StackLimit;									// offset 0x10
     PVOID SubSystemTib;								// offset 0x18
     union
     {
          PVOID FiberData;								// offset 0x20
          ULONG Version;								// offset 0x20
     };
     PVOID ArbitraryUserPointer;						// offset 0x28
     PNT_TIB Self;										// offset 0x30
} NT_TIB, *PNT_TIB;

Aby byla situace trošku komplikovanější, existuje mezi offsety 0x38 a 0x68 takzvaná “šedá zóna”. Členové struktury v tomto rozsahu tvoří součást interní struktury, kterou Microsoft dlouho tajil, ale kvůli zpětné kompatibilitě a vývojářům systémových nástrojů musel jejich rozvržení nechat fixní. Jsou pro stabilitu aplikací natolik kruciální, že není možné je svévolně měni. Jedná se o následující členy:

  • offset 0x38 - EnvironmentPointer (ukazatel na prostředí vlákna)
  • offset 0x40 - ClientId (struktura obsahující Process ID - PID - a Thread ID - TID)
  • offset 0x50 - ActiveRpcHandle (handle aktivně probíhajícího RPC volání)
  • offset 0x58 - ThreadLocalStoragePointer (ukazatel na lokální paměť vlákna - klíčové pro TLS proměnné)
  • offset 0x60 - ProcessEnvironmentBlock (ukazatel na strukturu PEB)
  • offset 0x68 - LastErrorValue (kód poslední chyby, který interně využívá WinAPI GetLastError())

Na architektuře x64 je TEB aktuálního vlákna vždy přístupný přes segment registr GS. Z výše uvedených členů struktury je pro nás důležitý hlavně offset 0x60, kde se nachází ukazatel na strukturu PEB. Zmíněný offset 0x60 představuje relativní vzdálenost od začátku struktury TEB, nikoliv absolutní adresu v paměti.

mov rax, gs:[0x60]   ; rax = načte 8bajtovou adresu PEB struktury z TEB

V praxi se můžete setkat ještě s jednou ekvivalentní variantou získání PEB, která vyžaduje dvě instrukce:

mov rax, gs:[0x30]    ; Načte z NT_TIB ukazatel "Self" (lineární adresa TEB)
mov rax, [rax + 0x60] ; K této adrese přičte offset 0x60 a načte PEB

Na první pohled se to zdá pro shellcode nevýhodné, protože kód naroste o několik bajtů. Pokud však nejsme limitováni velikostí zneužívaného bufferu (máme dostatek místa), je tento přístup velmi vhodný. Instrukce mov rax, gs:[0x30] totiž do registru uloží skutečnou, absolutní adresu struktury TEB v paměti RAM, kterou uživatelský kód jinak ze segmentového registru GS nedokáže přímo vyčíst. To otevírá dveře pro pokročilejší shellcody, které potřebují kromě PEB manipulovat přímo s kontextem vlákna, jako je správa TLS proměnných nebo nízkoúrovňová obsluha výjimek.

Tento postup je klíčový pro veškerý Position Independent Code (PIC) na platformě Windows x64. Jedná se o jakýsi univerzální vstupní bod do světa získání/vyhledání adres za běhu (API resolution / runtime API resolution).

Pomocí WinDbg si můžeme zobrazit strukturu TEB příkazem dt ntdll!_TEB:

0:000> !teb
TEB at 000000a94b722000
    ExceptionList:        0000000000000000
    StackBase:            000000a94b560000
    StackLimit:           000000a94b55b000
    SubSystemTib:         0000000000000000
    FiberData:            0000000000001e00
    ArbitraryUserPointer: 0000000000000000
    Self:                 000000a94b722000
    EnvironmentPointer:   0000000000000000
    ClientId:             0000000000017eb0 . 000000000001b250
    RpcHandle:            0000000000000000
    Tls Storage:          000001b1af317100
    PEB Address:          000000a94b721000
    LastErrorValue:       0
    LastStatusValue:      0
    Count Owned Locks:    0
    HardErrorMode:        0

(Všimněte si, že WinDbg ukazuje absolutní adresu TEB 000000a94b722000. Pokud k této adrese procesor přičte relativní offset 0x60, přečte na daném místě hodnotu 000000a94b721000, což je přesná adresa struktury PEB celého procesu, kterou vidíme na řádku PEB Address)


PEB – Process Environment Block

PEB (Process Environment Block)[3] reprezentuje globální stav celého běžícího procesu. Je tedy pro proces tím, čím je struktura TEB pro jednotlivá vlákna. Zatímco každé vlákno disponuje svým vlastním TEB, všechna vlákna uvnitř jednoho procesu sdílejí jeden a ten stejný PEB. Obsahuje širokou škálu informací: od příznaků přítomnosti debuggeru, přes cestu ke spuštěnému .exe souboru, až po parametry procesu. Pro účely tohoto textu je ale nejdůležitější seznam všech modulů načtených do adresního prostoru procesu.

Situace kolem struktury PEB z pohledu garance jejího rozvržení je velmi podobná jako u TEB, avšak s jedním zajímavým rozdílem. Drtivá většina PEB zůstává oficiálně nedokumentovaná. Microsoft si přesto nemůže dovolit její layout svévolně měnit, protože by tím rozbil obrovské množství existujícího komerčního softwaru, který na ní spoléhá. Ve skutečnosti k interním změnám dochází téměř neustále, avšak týkají se členů na velmi vysokých offsetech, kde již není vývojářům nic garantováno.

Výjimku tvoří právě členy BeingDebugged a Ldr, na kterých nám v tomto textu nejvíc záleží. Ty jsou totiž, na rozdíl od zbytku struktury, oficiálně dokumentované, byť k tomu došlo poněkud nedobrovolně. Microsoft patrně v roce 2002 v rámci vyrovnání v americké antitrustové při zveřejnil hlavičkový soubor winternl.h[4], obsahující ořezanou verzi struktury PEB s členem BeingDebugged na jeho skutečném offsetu, přičemž zbytek tehdy nahradil anonymní výplní Reserved, aby zůstaly zachovány správné pozice v paměti. Členy Ldr a ProcessParameters do této hlavičky přibyly později, konkrétně v SDK pro Windows 7. Tato “oficiální” verze PEB je dodnes plně dohledatelná v dokumentaci Microsoft Learn.

Z toho důvodu dnes velká část nízkoúrovňových vývojářů namísto volání standardních Windows API funkcí přistupuje raději přímo k hodnotám v této struktuře. Příkladem je právě člen BeingDebugged na offsetu 0x02, ze kterého čte data dokumentovaná funkce IsDebuggerPresent() z kernel32.dll. U zbytku skrytých polí lze stálost rozvržení označit jako “nechtěnou” záruku zpětné kompatibility ze strany společnosti Microsoft. U těchto dvou členů je ale “záruka” zcela oficiální.

Protože je struktura PEB v reálu mimořádně obsáhlá[5], uvedeme si pouze členy relevantní pro tento dokument:

typedef struct  _PEB
{
	...
	UCHAR BeingDebugged;		// offset 0x02
	...
	PVOID ImageBaseAddress;		// offset 0x10
	PPEB_LDR_DATA Ldr;			// offset 0x18
	....
} PEB, *PPEB;

Pomocí WinDbg si můžeme zobrazit strukturu PEB příkazem dt ntdll!_PEB.

Na klíčovém offsetu 0x18 se nachází hledaný ukazatel na strukturu PEB_LDR_DATA.


PEB_LDR_DATA a seznamy modulů

Struktura PEB_LDR_DATA[6] je jedním z nejdůležitějších členů struktury PEB. Přes tento uzel zavaděč programů v ntdll.dll spravuje a eviduje všechny dynamické knihovny, které daný proces načetl do svého adresního prostoru. Oficiálně je tato struktura dokumentovaná jen částečně, ale její vnitřní rozložení je na architektuře x64 kvůli kompatibilitě stabilní.

typedef struct _PEB_LDR_DATA
{
     ULONG Length;									// offset 0x00
     UCHAR Initialized;								// offset 0x04
     PVOID SsHandle;								// offset 0x08
     LIST_ENTRY InLoadOrderModuleList;				// offset 0x10
     LIST_ENTRY InMemoryOrderModuleList;			// offset 0x20
     LIST_ENTRY InInitializationOrderModuleList;	// offset 0x30
     PVOID EntryInProgress;							// offset 0x40
} PEB_LDR_DATA, *PPEB_LDR_DATA;

Pomocí WinDbg si můžeme zobrazit strukturu PEB_LDR_DATA příkazem dt ntdll!_PEB_LDR_DATA.

Z výše uvedených členů nás zajímají tři obousměrné propojované seznamy obsahující načtené moduly. Každý z těchto seznamů řadí stejné moduly v jiném pořadí:

  • InLoadOrderModuleList řadí knihovny podle toho, jak je exekutor systému načítal. Na prvních místech bývá samotný .exe soubor programu, následovaný ntdll.dll a dalšími závislostmi.
  • InMemoryOrderModuleList řadí knihovny podle jejich reálného rozmístění v adresním prostoru paměti RAM.
  • InInitializationOrderModuleList řadí knihovny podle pořadí, v jakém byly spuštěny jejich inicializační funkce (tedy volání funkce DllMain). Protože hlavní spustitelný soubor aplikace (.exe) se jako DLL neinicializuje, v tomto seznamu ho vůbec nenajdete. Na prvních pozicích zde kvůli závislostem obvykle trůní čistě systémové knihovny jako ntdll.dll a kernel32.dll.

Pro naše účely je nejvhodnější využít InMemoryOrderModuleList, protože byl historicky nejkonzistentnější napříč verzemi Windows a zároveň nám umožňuje velmi snadno určit první dvě knihovny v paměti. Těmi jsou ntdll.dll (druhý modul v pořadí, hned za samotnou spuštěnou aplikací) a kernel32.dll (třetí modul v pořadí). Každý z těchto seznamů je tvořen jako kruhový obousměrný seznam, kde každý uzel reprezentuje strukturu LIST_ENTRY[7]:

typedef struct _LIST_ENTRY
{
   struct _LIST_ENTRY *Flink;	// offset 0x00
   struct _LIST_ENTRY *Blink;	// offset 0x08
} LIST_ENTRY, *PLIST_ENTRY;

Pomocí WinDbg si můžeme zobrazit strukturu LIST_ENTRY příkazem dt ntdll!_LIST_ENTRY.


LDR_DATA_TABLE_ENTRY

Každý načtený modul je reprezentován strukturou LDR_DATA_TABLE_ENTRY[8]. Pro nás jsou relevantní tato pole:

LDR_DATA_TABLE_ENTRY
├── InMemoryOrderLinks      (offset 0x00)  –  LIST_ENTRY (Flink, Blink)
├── ...
├── DllBase                 (offset 0x20)  –  bázová adresa modulu v paměti
├── ...
├── BaseDllName             (offset 0x58)  –  UNICODE_STRING (název souboru)
└── ...

Pomocí WinDbg si můžeme zobrazit strukturu LDR_DATA_TABLE_ENTRY příkazem dt ntdll!_LDR_DATA_TABLE_ENTRY.

InMemoryOrderLinks je struktura LIST_ENTRY obsahující dva ukazatele:

  • Flink – ukazatel na další uzel seznamu
  • Blink – ukazatel na předchozí uzel seznamu

Procházením Flink ukazatelů se dostaneme postupně přes všechny načtené moduly.

; =====================================================================
; Vyhledání kernel32.dll pomocí pevné pozice (indexu) v paměti
; =====================================================================

mov rax, gs:[0x60]       ; RAX = Adresa PEB z registru GS
mov rax, [rax + 0x18]    ; RAX = Adresa Ldr (PEB_LDR_DATA)
mov rsi, [rax + 0x20]    ; RSI = Adresa hlavy InMemoryOrderModuleList

; --- Slepé skákání po uzlech (předpoklad fixního pořadí) ---

lodsq                    ; 1. Samotný .exe soubor procesu
xchg rax, rsi

lodsq                    ; 2. Knihovna ntdll.dll
xchg rax, rsi

lodsq                    ; 3. Předpokládaná pozice kernel32.dll

; --- Výpočet bázové adresy ---

mov rbx, [rax + 0x20]    ; RBX = hodnota DllBase knihovny kernel32.dll

Slepé spoléhání se na pevné pořadí (indexy) modulů – tedy předpoklad, že kernel32.dll bude vždy přesně třetím článkem řetězu – je ideální pro pochopení základního principu a zkrácení ukázkového kódu. V historii Windows se sice toto pořadí měnilo (např. v éře Windows 9x/Millennium, kde byla kernel32.dll občas až pátá nebo šestá), ale na moderních NT systémech jsou tyto klíčové knihovny v paměti přítomny prakticky okamžitě. Knihovnu ntdll.dll mapuje samotné jádro OS jako úplně první a kernel32.dll leží na samém vrcholu importního stromu závislostí.

Přesto i na moderních Windows 10 a 11 hrozí, že slepý shellcode bez kontroly jména havaruje. Zavedení paralelního zavaděče (ntdll!LdrpEnableParallelLoading) totiž způsobuje, že se pořadí dokončení mapování knihoven může drobně zatřást. Mnohem větší riziko však představují bezpečnostní nástroje (EDR/AV) či herní ochrany, které se do nového procesu injektují hned na startu. Tyto cizí knihovny, případně doprovodné moduly jako KERNELBASE.dll nebo kernel.appcore.dll, se mohou v seznamu předběhnout a obsadit právě onu očekávanou třetí pozici.

Pokud chcete, aby byl váš shellcode skutečně spolehlivý (reliable), je nutné do vyhledávací smyčky přidat aktivní kontrolu jména modulu z pole BaseDllName. Začít můžete přímým porovnáváním konkrétních bajtů textového řetězce (např. ověřením celého slova "kernel32" v paměti pomocí 64-bitových registrů).

; =====================================================================
; Robustní vyhledání kernel32.dll přes InMemoryOrderModuleList (x64)
; =====================================================================

mov rax, gs:[0x60]          ; RAX = Adresa PEB z registru GS
mov rax, [rax + 0x18]       ; RAX = Adresa Ldr (PEB_LDR_DATA)
mov rdi, [rax + 0x20]       ; RDI = Hlavní ukazatel na InMemoryOrderModuleList (začátek/konec)
mov rsi, [rdi]              ; RSI = První uzel seznamu (Flink)

find_dll_loop:
    cmp rsi, rdi            ; Zkontrolovali jsme už celý kruhový seznam?
    je dll_not_found        ; Pokud ano, jsme zpátky na začátku -> konec

    ; --- Kontrola délky řetězce v BaseDllName ---
	; Pole BaseDllName je ve skutečnosti struktura UNICODE_STRING a začíná na offsetu 0x48. 
    ; Prvních 16 bitů (WORD) je pole Length.
    ; Pro řetězec "kernel32.dll" (12 znaků * 2 bajty v UTF-16) musí být délka přesně 24 bajtů (0x18).
	
    mov cx, [rsi + 0x48]    ; CX = BaseDllName.Length
    cmp cx, 0x18            ; Má název délku 24 bajtů ("kernel32.dll")?
    jne next_dll            ; Pokud ne, pokračujeme na další DLL

    ; --- Kontrola samotného obsahu názvu ---
	; Ukazatel na textový řetězec (Buffer) leží na offsetu +0x08 od začátku BaseDllName (celkově 0x50).
    mov rdx, [rsi + 0x50]   ; RDX = Ukazatel na textový Buffer
    
    ; Příprava normalizační masky pro převod na malá písmena
    mov r9, 0x0020002000200020

    ; 1. Kontrola bloku: "kern"
    mov rax, [rdx]          
    or rax, r9              ; Převod všech znaků v RAX na malá písmena
    mov r8, 0x006e00720065006b ; Little Endian pro "kern"
    cmp rax, r8            
    jne next_dll            ; Pokud neshoda, pokračujeme na další DLL

    ; 2. Kontrola bloku: "el32"
    mov rax, [rdx + 8]      
    or rax, r9              ; Převod na malá písmena
    mov r8, 0x00320033006c0065 ; Little Endian pro "el32"
    cmp rax, r8            
    jne next_dll            ; Pokud neshoda, pokračujeme na další DLL

    ; 3. Kontrola bloku: ".dll"
    mov rax, [rdx + 16]     
    or rax, r9              ; Převod na malá písmena
    mov r8, 0x006c006c0064002e ; Little Endian pro ".dll" upravený o posun tečky po operaci OR
    cmp rax, r8            
    je dll_found            ; Pokud sedí délka i všech 24 znormalizovaných bajtů -> nalezeno!

next_dll:
    mov rsi, [rsi]          ; Posun na další uzel
    jmp find_dll_loop

dll_found:
    ; --- Našli jsme správný uzel --- 
    ; Pole DllBase leží na offsetu +0x20 od InMemoryOrderLinks.
    mov rbx, [rsi + 0x20]   ; RBX = Bázová adresa kernel32.dll
    jmp end

dll_not_found:
    xor rbx, rbx            ; Knihovna nebyla nalezena, vynulujeme RBX

end:
    ; V registru RBX máme nyní bezpečně uloženou adresu začátku kernel32.dll

Při psaní vyhledávací smyčky nestačí kontrolovat pouze obsah paměti. Co kdyby v paměti ležel delší řetězec, který shodou okolností začíná stejně – například kernel32.dll.tmp? Naše 64-bitové registry by přečetly prvních 24 bajtů, ohlásily by shodu a ignorovaly by fakt, že řetězec v paměti pokračuje dál. To by vedlo k fatálnímu pádu.

Abychom tomu zabránili, musíme využít vlastnosti struktury UNICODE_STRING[9], ze které je pole BaseDllName složeno. Tato struktura má na svém úplném začátku (offset 0x58) 16-bitové pole Length, které uchovává přesnou délku názvu v bajtech. Protože k listu přistupujeme přes InMemoryOrderModuleList, nachází se toto pole na relativním offsetu 0x48. A protože víme, že "kernel32.dll" má přesně 12 znaků, v kódování UTF-16LE musí mít délku přesně 24 bajtů (0x18). Prvním krokem naší smyčky je tedy bleskové ověření této délky. Pokud se neshoduje, kód rovnou přeskakuje na další modul, aniž by ztrácel čas porovnáváním textu. Tím získáváme absolutní jistotu ukončení řetězce.

Windows navíc ignoruje velikost písmen, takže v paměti můžeme narazit na kernel32.dll, KERNEL32.DLL nebo Kernel32.Dll. Pokud porovnáváme celé 64-bitové registry naráz, zdá se to jako neřešitelný problém. Přece nebudeme řetězec rozebírat znak po znaku?

Naštěstí v tabulce ASCII/Unicode platí pro velké a malé písmeno identický binární zápis až na jeden jediný bit (s hodnotou 0x20). Pokud na načtený registr aplikujeme instrukci logického součtu OR s maskou 0x0020002000200020, u všech velkých písmen tento bit vynutíme na hodnotu 1. Z velkého K se stane malé k, zatímco již malá písmena zůstanou netknutá.

Co se stane s čísly a tečkou v příponě .dll? Logická operace OR samozřejmě ovlivní binární hodnotu některých těchto znaků a pro lidské oko text znetvoří. To ale vůbec nevadí, protože my totiž v assembleru nehledáme lidsky čitelný text. Nám stačí, že tato operace vytvoří v registru naprosto konstantní a předvídatelný otisk bez ohledu na to, zda byl původní název zadán velkými či malými písmeny. Naši porovnávací konstantu v registru R8 jsme upravili tak, aby s tímto výsledným stavem po operaci OR už dopředu počítala. Porovnáváme tak sice vizuálně deformovaná, ale zato stoprocentně unifikovaná data, a to v rámci jediného procesorového taktu bez jakéhokoliv větvení kódu.

Ještě pokročilejší a v praxi nejrozšířenější variantou je pak využití hashování. Místo ukládání celých textových názvů knihoven (které zbytečně zabírají místo a přitahují pozornost antivirů) shellcode v paměti za běhu spočítá krátký číselný hash názvu každé knihovny a porovná ho s předem připravenou hodnotou. Pouze tak zajistíme stoprocentní stabilitu shellcodu.

; =====================================================================
; Vyhledání kernel32.dll pomocí hashovacího algoritmu ROR13
; =====================================================================

mov rax, gs:[0x60]          ; RAX = Adresa PEB z registru GS
mov rax, [rax + 0x18]       ; RAX = Adresa Ldr (PEB_LDR_DATA)
mov rdi, [rax + 0x10]       ; RDI = Hlavní ukazatel na InLoadOrderModuleList (začátek/konec)
mov rsi, [rdi]              ; RSI = První uzel seznamu (Flink)

find_dll_loop:
    cmp rsi, rdi            ; Zkontrolovali jsme už celý kruhový seznam?
    je dll_not_found        ; Pokud ano, jsme zpátky na začátku -> konec

    ; --- Kontrola platnosti ukazatele na název ---
    ; Na offsetu +0x60 od začátku uzlu leží ukazatel na textový řetězec (Buffer).
    mov rdx, [rsi + 0x60]   ; RDX = Ukazatel na Unicode textový řetězec (Buffer)
    test rdx, rdx           ; Je ukazatel na Buffer nulový (NULL)?
    jz next_dll             ; Pokud ano, modul přeskočíme (obrana proti pádu)

    ; --- Příprava na výpočet hashe z BaseDllName ---
    ; Pole BaseDllName začíná na offsetu 0x58. 
    ; Prvních 16 bitů (WORD) je pole Length, které udává délku v bajtech.
    movzx ecx, word [rsi + 0x58] ; ECX = BaseDllName.Length (počet bajtů)
    xor r9, r9              ; R9 = Zde budeme akumulovat výsledný hash (začínáme na 0)

hash_loop:
    ; --- Samotný ROR13 algoritmus ---
    ror r9d, 13             ; Rotujeme dosavadní hash o 13 bitů doprava

    ; Načteme 1 bajt (Unicode znak má 2 bajty, čtením prvního bajtu děláme ASCII konverzi)
    movzx eax, byte [rdx]   
    
    ; --- Převod na malá písmena (Case Insensitivity) ---
    ; Protože Windows může název vrátit jako "KERNEL32.DLL" i "kernel32.dll",
    ; převedeme velká písmena (A-Z) na malá (a-z) přičtením 0x20.
    cmp al, 'A'
    jl skip_lowercase
    cmp al, 'Z'
    jg skip_lowercase
    add al, 0x20            ; Převod na malé písmeno

skip_lowercase:
    add r9d, eax            ; Přičteme aktuální znormalizovaný znak k hashi
    
    add rdx, 2              ; Posuneme se v Unicode řetězci na další znak (+2 bajty)
    sub ecx, 2              ; Odečteme 2 bajty z celkové délky názvu
    jnz hash_loop           ; Opakujeme, dokud neprojdeme všechny bajty délky

hash_finished:
    ; --- Porovnání výsledného hashe ---
    ; Hodnota 0x8FECD63F je předem spočítaný ROR13 hash pro řetězec "kernel32.dll"
    cmp r9d, 0x8FECD63F     
    je dll_found            ; Pokud hashe sedí, našli jsme kernel32.dll!

next_dll:
    mov rsi, [rsi]          ; Posun na další uzel (RSI = aktuální_uzel->Flink)
    jmp find_dll_loop       ; Opakování hlavní smyčky

dll_found:
    ; --- Našli jsme správný uzel ---
    ; V seznamu InLoadOrderModuleList leží bázová adresa (DllBase) na offsetu 0x30.
    mov rbx, [rsi + 0x30]   ; RBX = Bázová adresa kernel32.dll
    jmp end

dll_not_found:
    xor rbx, rbx            ; Knihovna nebyla nalezena, vynulujeme RBX

end:
    ; V registru RBX máme nyní bezpečně uloženou adresu začátku kernel32.dll

Poznámka k UNICODE_STRING

Názvy modulů jsou uloženy ve formátu UNICODE_STRING, tedy jako UTF-16LE řetězec. Struktura má následující definici:

typedef struct _UNICODE_STRING {
    USHORT Length;        // Aktuální délka řetězce v bajtech (ne ve znacích!)
    USHORT MaximumLength; // Maximální kapacita vyhrazené paměti v bajtech
    PWSTR  Buffer;        // Ukazatel na samotné pole širokých znaků (WCHAR*)
} UNICODE_STRING, *PUNICODE_STRING;

Pomocí WinDbg si můžeme zobrazit strukturu UNICODE_STRING příkazem dt ntdll!_UNICODE_STRING.


Shrnutí

  • TEB je přístupný přes GS:[0x60] a obsahuje ukazatel na PEB
  • PEB na offsetu 0x18 obsahuje ukazatel na PEB_LDR_DATA
  • PEB_LDR_DATA na offsetu 0x20 obsahuje hlavu seznamu InMemoryOrderModuleList
  • Každý uzel seznamu je ve skutečnosti struktura LDR_DATA_TABLE_ENTRY s bázovou adresou a názvem modulu
  • Procházením Flink ukazatelů najdeme libovolný načtený modul v paměti

Co přijde v příštím dílu

Víme, jak najít bázovou adresu modulu. Ale bázová adresa samotná nestačí – potřebujeme adresu konkrétní funkce. V příštím dílu se podíváme na PE strukturu a Export Directory, kde Windows uchovává tabulku exportovaných funkcí každého modulu.


Reference