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é proTLSproměnné) - offset
0x60-ProcessEnvironmentBlock(ukazatel na strukturuPEB) - offset
0x68-LastErrorValue(kód poslední chyby, který interně využívá WinAPIGetLastError())
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ý.exesoubor programu, následovanýntdll.dlla 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í funkceDllMain). 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 jakontdll.dllakernel32.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 seznamuBlink– 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í
TEBje přístupný přesGS:[0x60]a obsahuje ukazatel naPEBPEBna offsetu0x18obsahuje ukazatel naPEB_LDR_DATAPEB_LDR_DATAna offsetu0x20obsahuje hlavu seznamuInMemoryOrderModuleList- Každý uzel seznamu je ve skutečnosti struktura
LDR_DATA_TABLE_ENTRYs bázovou adresou a názvem modulu - Procházením
Flinkukazatelů 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
- [1] TEB (Thread Environment Block) - geoffchappell.com
- [2] struct NT_TIB - Nirsoft
- [3] PEB (Process Environment Block) - Microsoft Learn
- [4] PEB - geoffchappell.com
- [5] strukct PEB - geoffchappell.com
- [6] struct PEB_LDR_DATA - geoffchappell.com
- [7] struct LIST_ENTRY - Microsoft Learn
- [8] struct LDR_DATA_TABLE_ENTRY - geoffchappell.com
- [9] struct UNICODE_STRING - geoffchappell.com