Copy Link
Add to Bookmark
Report
6x03 PEB Dll Hooking
PEB Dll hooking
S verom u Boga, deroko/ARTeam
Ovo je nova tehnika hookovanja APIja koju ja do sada nisam sureo
nigde. Upravo zbog toga sto je metoda koju sam ja poceo prvi da koristim,
namerio sam da napisem ovaj tutorijal kako bih vam pokazao moc koja se
iza ovog metoda krije, ali ujedno i zasto je ona veoma korisna ako nameravate
da reversujete neki protektor koji virtuelno mapira neke dllove i odatle
poziva APIje. Trik je u tome da takvi protektori pokusavaju da sakriju od
debuggera koji API pozivaju kako bi se debugovanje ucinilo mnogo mnogo
tezim. Upravo iz tih razloga ja sam razvio ovu metodu hookovanja da bih
mogao da vidim koji se API odmah poziva, a ne da pokusavam da zakljucim
na osnovu samog APIja sta je u pitanju. To bi bio kratak uvod. Protektor
koji za sada jedino koristi ovu metodu skrivanja APIja je TheMida, o cemu
ce ovde takodje biti reci, ali takodje i jedan VM protektor koji sam video
na www.crackmes.de koristi slicnu metodu, njega necu pokriti ovde.
Pre svega ideja koja stoji iza ovog trika jeste da kad god protektor
pozove GetProcAddress bilo API bilo svoju implementaciju, on ce skenirati .dll
za APIjem. Dobro, to nije nista novo, uobicajena procedura kao sto vidite,
medjutim zamislimo situaciju gde protektor skenira moj .dll i iz njega vadi
potrebne APIje, vec tu bih ja mogo da hookujem sta mi se iste bez po muke,
i lako mogu videti sta se kad i gde desava. Drugim recima ja cu prakticno
naterai protektor da misli da je moj injectovan .dll ustvari .dll koji on
zeli.
Da bi protektor ili bilo koja aplikacija dobila baznu adresu nekog
.dll on ce uvek koristiti GetModuleHandleA/W ili LoadLibraryA/W. U oba slucaja
ce API skenirati PEB u potrazi za zeljenim .dll-om dok ce kod LoadLibraryA, ako
.dll nije najden u PEBu ucitati .dll i ubaciti referencu ka njemu u PEB. Moj
trik se ovde sastoji od prostog hookovanja .dll-a preko PEBa tako da GetMod i
LoadLibA umesto da vrate baznu adresu pravog .dll, vracaju adresu mog .dll-a.
PEB iliti Process Environment Block ima dosta korisnih podataka o procesu, ali
takodje cuva i podatke o ucitanim .dllovima. PEB se nalazi na ofsetu 30h u
TEBu (Thread Environment Block) na koji pokazuje FS selektor. Pristupanje
PEBu se svodi na jednu prostu ASM instrukciju:
mov eax, dword ptr fs:[30h]
Na ofsetu +0ch u PEBu nalazi se PEB_LDR_DATA:
kd> dt nt!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged : UChar
+0x003 SpareBool : UChar
+0x004 Mutant : Ptr32 Void
+0x008 ImageBaseAddress : Ptr32 Void
+0x00c Ldr : Ptr32 _PEB_LDR_DATA
kd> dt nt!_PEB_LDR_DATA
+0x000 Length : Uint4B
+0x004 Initialized : UChar
+0x008 SsHandle : Ptr32 Void
+0x00c InLoadOrderModuleList : _LIST_ENTRY
+0x014 InMemoryOrderModuleList : _LIST_ENTRY
+0x01c InInitializationOrderModuleList : _LIST_ENTRY
+0x024 EntryInProgress : Ptr32 Void
kd>
kd> dt nt!_LIST_ENTRY
+0x000 Flink : Ptr32 _LIST_ENTRY
+0x004 Blink : Ptr32 _LIST_ENTRY
Ovde u PEB_LDR_DATA kao sto vidite imamo 3 liste koje pokazuje na
LDR_DATA_TABLE_ENTRY ili skraceno LDR_MODULE:
kd> dt nt!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY
+0x008 InMemoryOrderLinks : _LIST_ENTRY
+0x010 InInitializationOrderLinks : _LIST_ENTRY
+0x018 DllBase : Ptr32 Void
+0x01c EntryPoint : Ptr32 Void
+0x020 SizeOfImage : Uint4B
+0x024 FullDllName : _UNICODE_STRING
+0x02c BaseDllName : _UNICODE_STRING
+0x034 Flags : Uint4B
+0x038 LoadCount : Uint2B
+0x03a TlsIndex : Uint2B
+0x03c HashLinks : _LIST_ENTRY
+0x03c SectionPointer : Ptr32 Void
+0x040 CheckSum : Uint4B
+0x044 TimeDateStamp : Uint4B
+0x044 LoadedImports : Ptr32 Void
+0x048 EntryPointActivationContext : Ptr32 Void
+0x04c PatchInformation : Ptr32 Void
kd>
Ako pazljivo pogledate u PEB_LDR_DATA i LDR_MODULE strukture videcete
3 liste koje sluze za linkovanje module, idu u krug i pokazujeu na sledeci
modul u zavisnosti od tipa liste. Tako InLoadOrderModuleList linkuje module
po redosledu njihovog ucitavanja (program -> ntdll -> k32 itd), sledeca
InMemoryOrder linkuje module prema njihovom mestu u memoriji dok poslednja
InInitializationOrder lista linkuje module po redosledu njihove inicijalizacije,
tj. kad je modul kompletno ucitan, u tom slucaju prvi je ntdll.dll zatim kernel32.dll
i potom ostali .dllovi koji se inizijalizuju posle ova 2. No kojom god listom
da krenete uvek cete se vracati na ovu istu strukturu, tako da kada .dll hookujemo
preko jedne liste one je hookovan i na drugim listama. Ako ne znate sta su liste,
pogledajte neku C knjigu, to je uvek u njima objasnjeno.
Dobro, sad da vidimo sta ce GetModuleHandleA uraditi kad mu trazimo
adresu od kernel32.dll:
1. pretvorice ANSI string u UNICODE jer su imena .dlla u PEBu sacuvana kao
UNICODE
2. pocece da vrti po listama dok u BaseDllName ne nadje kernel32.dll
3. kad ga nadje, pogledace u DllBase i vratiti taj rezultat korisniku
koji ce onda koristiti GetProcAddress da ucita neki API.
E sad ide moj scenario gde ja prebrisem DllBase od kernel32.dll u ibacim adresu
mog .dlla, tako da GetModuleHandleA vraca adresu mog .dll-a, i kad protektor
pozove GetProcAddress on ce skenirati moj .dll i naci ce API u njemu. U tom
trnutku ja mogu da hookujem API i da proverim sta me zanima, ili pak, mogu
jednostavno da taj API u mom .dll preusmerima na API u pravom .dll-u.
Mana ove metode jeste da vas .dll mora da ima iste exporte kao i originalni .dll,
svakako da moze da ima i vise Exporta od originalnog .dll, recimo APIje koji
vama trebaju, no u svakom slucaju on MORA da ima exporte kao i originalni .dll
jer u suprotnom GetProcAddress nece uspeti i protektor ce izaci sa greskom da
ne moze da nadje "xxx API u zzz.dll". Normalno prilikom pisanja ovakvog hooking
.dlla necete pisati rucno sve Exporte jer moze glava da zaboli kad treba da
prekucate preko 100 Exporta. U tom slucaju treba da napravite program koji ce
da napravi Template .dll-a kao sto to radi moj dllcreator.c i onda bi vas
skelet dlla trebalo ovako nekako da izgleda:
public my_ActivateActCtx
my_ActivateActCtx:
jmp ActivateActCtx
retn
public my_AddAtomA
my_AddAtomA:
jmp AddAtomA
retn
public my_AddAtomW
my_AddAtomW:
jmp AddAtomW
retn
a vas .def fajl ovako:
EXPORTS
ActivateActCtx=my_ActivateActCtx
AddAtomA=my_AddAtomA
AddAtomW=my_AddAtomW
itd, itd...
Sada mirne duse mozete dodati svoj DllEntrypoint i u njemu obaviti
hookovanje .dll-a:
public C start
start proc
arg imagebase
arg reason
arg reserved
Ako razlog ucitavanja ovog DLL-a nije PROCESS_ATTACH prosto izlazimo
odavde.
pusha
cmp reason, 1
jne __e_dllinit
Ovde trazimo adresu InLoadOrderModuleList:
mov eax, dword ptr fs:[30h]
mov eax, [eax+0ch]
mov esi, [eax+0ch]
Nalazimo adresu pravog kernel32.dll-a. LoadLibraryA je neophodan jer cemo
posle videti kako da hookujemo kernel32.dll i kad on jos nije ucitan, tako da
mi moramo nekako da ga ucitamo.
call LoadLibraryA, offset szkernel32
mov old_dll_base, eax
xchg eax, ebx
E sad prosto idemo kroz strukturu LDR_MODULE i trazimo LDR_MODULE od kernel32.dll
i mog .dlla koji je upravo ucitan:
__find_dll: cmp [esi.lm_baseaddress], ebx
je __esiedi
lodsd
xchg eax, esi
jmp __find_dll
__esiedi: cmp ebx, imagebase
je __hook
mov edi, esi
mov ebx, imagebase
jmp __find_dll
U ovom trenutku ESI pokazuje na LDR_MODULE od mog hooking.dll dok EDI pokazuje
na LDR_MODULE od originalnog kernel32.dll, sad samo trebamo promeniti imagebase,
entrypoint i sizeofimage i to je cela mudrost ovog hookvoanja:
__hook: mov eax, ebx
xchg eax, [edi.lm_baseaddress]
mov [esi.lm_baseaddress], eax
add ebx, [ebx+3ch]
mov eax, [ebx.pe_addressofentrypoint]
add eax, imagebase
xchg eax, [edi.lm_entrypoint]
mov [esi.lm_entrypoint], eax
mov eax, [ebx.pe_sizeofimage]
xchg eax, [edi.lm_sizeofimage]
mov [esi.lm_sizeofimage], eax
__e_dllinit: popa
mov eax, 1
leave
retn 0ch
endp
To je to, sad kad protektor pozove GetModuleHandleA("kernel32.dll") on ce dobiti
adresu mog .dll-a i skenirace moj .dll u potrazi za hookovanim APIjem. Ovaj metod
je jos dobar jer postavljanjem BPX-a u SoftICEu na APIjima izbegavamo skeniranje
za int 3h instrukcijama tako da bez po muke mozemo da pratimo tok i rad protektora.
Za olly je ova metoda malko neadekvatna jer nece naci APIje iz hookovanog dlla.
SoftICE rulz...
Ovaj dll treba injectovati pre entrypointa programa, ali pitanje koje za nas
sad nastupa jeste kako da nateramo da se import tablica protektora popuni sa
nasim fake_k32.dll, jer je vise nego jasno da su importi popunjeni pre nego
sto se dostigne entrypoint. Takodje ne mozemo da redirektujemo primary thread
na nas kod kad koristimo CREATE_SUSPENDED jer logika windows startovanja procesa
nam ne omogucuje da to uradimo. Zbog toga sam pristupio analizi startovanja
procesa prateci sta se i kad desava.
Ako pratimo CreateProcessW mozemo videti kako se interno poziva NtCreateProcess,
ovaj native API je zaduzen za mapiranje fajla u memoriju ali takodje i za mapiranje
ntdll za taj proces. Tako da u tom momentu kad se API zavrsi prakticno imamo samo
fajl mapiran u memoriji i ntdll sa njim.
Prateci dalje sta se desava vidimo kako se poziva NtCreateThread koji prakticno
pravi novi thread u contextu novog procesa.
Pogledajmo kako to izgleda u IDA:
.text:7C818EE1 push dword ptr [ebp-8E4h]
.text:7C818EE7 push ebx
.text:7C818EE8 push dword ptr [ebp-8D0h]
.text:7C818EEE push dword ptr [ebp-670h]
.text:7C818EF4 push dword ptr [ebp-804h]
.text:7C818EFA or esi, 0FFFFFFFFh
.text:7C818EFD push esi
.text:7C818EFE push dword ptr [ebp-838h]
.text:7C818F04 push 1F0FFFh
.text:7C818F09 lea eax, [ebp-66Ch]
.text:7C818F0F push eax
.text:7C818F10 call ds:__imp__NtCreateProcessEx@36
Argumenti koji su prosledjeni nisu bitni za nas sad, vec je bitno da uhvatimo
logiku koda da bih znali sta se desava, potom imamo pripremanje Contexta za
thread koji ce biti kreiran u tom procesu:
.text:7C819A3C call _BaseInitializeContext@20 <-- sredi Context
.text:7C819A41 push ebx
.text:7C819A42 push dword ptr [ebp-8C8h]
.text:7C819A48 lea eax, [ebp-7CCh]
.text:7C819A4E push eax
.text:7C819A4F call _BaseFormatObjectAttributes@12
.text:7C819A54 mov [ebp-838h], eax
.text:7C819A5A cmp [ebp-799h], bl
.text:7C819A60 jz short loc_7C819A6E
.text:7C819A62 cmp [ebp-83Ch], ebx
.text:7C819A68 jnz loc_7C838025
.text:7C819A6E
.text:7C819A6E loc_7C819A6E:
.text:7C819A6E push 1
.text:7C819A70 lea eax, [ebp-7B4h]
.text:7C819A76 push eax
.text:7C819A77 lea eax, [ebp-664h]
.text:7C819A7D push eax
.text:7C819A7E lea eax, [ebp-6E8h]
.text:7C819A84 push eax
.text:7C819A85 push dword ptr [ebp-66Ch]
.text:7C819A8B push dword ptr [ebp-838h]
.text:7C819A91 push 1F03FFh
.text:7C819A96 lea eax, [ebp-67Ch]
.text:7C819A9C push eax
.text:7C819A9D call ds:__imp__NtCreateThread@32
EIP u novom Threadu je postavljen ovde:
.text:7C81059D _BaseInitializeContext@20 proc near
.text:7C81059D mov edi, edi
.text:7C81059F push ebp
.text:7C8105A0 mov ebp, esp
.text:7C8105A2 mov eax, [ebp+arg_4]
.text:7C8105A5 mov ecx, [ebp+arg_C]
.text:7C8105A8 and [eax+CONTEXT.SegGs], 0
.text:7C8105AF cmp [ebp+arg_14], 1
.text:7C8105B3 mov [eax+CONTEXT.Eax], ecx
.text:7C8105B9 mov ecx, [ebp+arg_8]
.text:7C8105BC mov [eax+CONTEXT.Ebx], ecx
.text:7C8105C2 push 20h
.text:7C8105C4 pop ecx
.text:7C8105C5 mov [eax+CONTEXT.SegEs], ecx
.text:7C8105CB mov [eax+CONTEXT.SegDs], ecx
.text:7C8105D1 mov [eax+CONTEXT.SegSs], ecx
.text:7C8105D7 mov ecx, [ebp+arg_10]
.text:7C8105DA mov [eax+CONTEXT.SegFs], 38h
.text:7C8105E4 mov [eax+CONTEXT.SegCs], 18h
.text:7C8105EE mov [eax+CONTEXT.EFlags], 3000h
.text:7C8105F8 mov [eax+CONTEXT.Esp], ecx
.text:7C8105FE jnz loc_7C814D67
.text:7C810604 mov [eax+CONTEXT.Eip], offset _BaseThreadStartThunk@8 <--- EIP
.text:7C81060E loc_7C81060E:
.text:7C81060E add ecx, 0FFFFFFFCh
.text:7C810611 mov [eax+CONTEXT.ContextFlags], 10007h
.text:7C810617 mov [eax+CONTEXT.Esp], ecx
.text:7C81061D pop ebp
.text:7C81061E retn 14h
.text:7C81061E _BaseInitializeContext@20 endp
.text:7C81061E
A EIP pokazuje na :
.text:7C80B4D4 _BaseThreadStart@8:
.text:7C80B4D4 push 10h
.text:7C80B4D6 push offset dword_7C80B518
.text:7C80B4DB call __SEH_prolog
.text:7C80B4E0 and dword ptr [ebp-4], 0
.text:7C80B4E4 mov eax, large fs:18h
.text:7C80B4EA mov [ebp-20h], eax
.text:7C80B4ED cmp dword ptr [eax+10h], 1E00h
.text:7C80B4F4 jnz short loc_7C80B505
.text:7C80B4F6 cmp _BaseRunningInServerProcess, 0
.text:7C80B4FD jnz short loc_7C80B505
.text:7C80B4FF call ds:__imp__CsrNewThread@0
.text:7C80B505 loc_7C80B505:
.text:7C80B505 push dword ptr [ebp+0Ch]
.text:7C80B508 call dword ptr [ebp+8] <--- call EntryPoint
.text:7C80B50B push eax
.text:7C80B50C loc_7C80B50C:
.text:7C80B50C call _ExitThread@4
Kao sto se vidi iz svega prilozenog novi thread ce samo da pozove EntryPoint, ali
nama je bitno da uhvatimo kad se popunjavaju importi kako bismo mogli da hookujemo.
Takodje postavlja se isto pitanje kako se izvrsi TLS callback ako je ocigledno
da se odavde ne poziva?
Odogvor lezi upravo u ntoskrnl!NtCreateThread. Naime prilikom kreiranja threada
sam NtCreateThread ce koristiti APC (Asynchonious Procedure Calls) kako bi izvrsio
neki kod pre pokretanja threada. Ako pratite ntoskrnl.exe i NtCreateThread uvidecete
kako se pravi APC kao ntdll!LdrInitilaizeThunk. Osobina APCa je da se kod koji se
preko njega prosledjuje izvrsava pre threada koji se nalazi u stanju cekanja. Kad
dati thread bude spreman za izvrsavanje prvo ce se izvrsiti APC pa tek onda EIP na
koji je postavljen thread. Ujedno kod koji se izvrsava preko APCa tj. LdrInitilaizeThunk
iz ntdll.dll ce ujedno pozvati TLS callback, ali takodje ce i popuniti importe.
Buduci da se APC izvrsava tek kad je thread spreman da krene mi mozemo hookovati
ntdll.dll loader dok je nas thread u SUSPENDED stanju i na taj nacin isforsirati
ucitavanje naseg fake_k32.dll sa kojim ce biti popunjena import tablica, postavivsi
BPX na ntdll!LdrInitializeThunk mogo sam mirne duse da debugujem APC i da vidim
gde je to mesto kad se ucitava kernel32.dll:
.text:7C9222FD mov word ptr [ebp+var_100+2], 1Ah
.text:7C922306 mov [ebp+var_FC], offset aKernel32_dll ; "kernel32.dll"
.text:7C922310 call _LdrpLoadDll@24
Oki sad znamo sta da patchujemo u ntdll.dll a to je ime stringa kernel32.dll koji
cemo zameniti sa "fake_k32.dll". Zapamtite process je kreiran u SUSPENDED stanju, tako
da APC jos nije izvrsen. Ja licno nisam naso bolji nacin za nalazenje stringa kernel32.dll
u ntdll.dll buduci da ih ima nekoliko, a takodje ne mogu da koristim ni neki byte
search buduci da se u sledecoj verziji ili pak ranijim verzijama ntdll.dlla mogu
promeniti instrukcije itd... Tako da cete morati rucno da nadjete adresu za svoj
loader.
Evo kako to sad izgleda kad smo hookovali preko LdrInitializeThunk-a, kao primer
vam dajem UPX aplikaciju jer je on izuzetno prost a sa druge strane on importuje
APIje za svoj rad:
001B:00413F77 CALL [ESI+000140A8]
001B:00413F7D OR EAX,EAX
001B:00413F7F JZ 00413F88
001B:00413F81 MOV [EBX],EAX
001B:00413F83 ADD EBX,04
001B:00413F86 JMP 00413F69
Pa onda ulecemo u API iz hookovanog k32.dll:
fake_k32!GetProcAddress
001B:003A1CD2 JMP [KERNEL32!GetProcAddress]
001B:003A1CD8 RET
i idemo na originalan API:
KERNEL32!GetProcAddress
001B:7C80AC28 MOV EDI,EDI
001B:7C80AC2A PUSH EBP
001B:7C80AC2B MOV EBP,ESP
001B:7C80AC2D PUSH ECX
Fino, fino, sad jos da vidimo kako izgleda import tablica naseg
fajla na OEPu:
001B:00401000 JMP 00401012 (JUMP )
001B:00401002 BOUND DI,[EDX]
001B:00401005 INC EBX
001B:00401006 SUB EBP,[EBX]
001B:00401008 DEC EAX
001B:00401009 DEC EDI
001B:0040100A DEC EDI
001B:0040100B DEC EBX
001B:0040100C NOP
001B:0040100D JMP 0080A12E
001B:00401012 MOV EAX,[0040910F]
001B:00401017 SHL EAX,02
001B:0040101A MOV [00409113],EAX
001B:0040101F PUSH EDX
001B:00401020 PUSH 00
001B:00401022 CALL fake_k32!GetModuleHandleA
001B:00401027 MOV EDX,EAX
001B:00401029 CALL 004020BC
001B:0040102E POP EDX
001B:0040102F CALL 00401458
001B:00401034 CALL 004020C0
001B:00401039 PUSH 00
001B:0040103B CALL 00402CD8
001B:00401040 POP ECX
001B:00401041 PUSH 004090B8
001B:00401046 PUSH 00
001B:00401048 CALL fake_k32!GetModuleHandleA
001B:0040104D MOV [00409117],EAX
Sjajno zar ne? Eto vam kako na prost i jednostavan nacin da pisete
mocne hooking engine. E sad rekoh da cu vam objasniti moc ovog na
TheMida licno. Da da TheMida nas najbolji prijatelj kad trebaju da se
razvijaju nove ideje =)
Dobro ovo cu vam samo ukratko ispricati da biste shvatili moc ovog
pristupa kad ga koristite sa SoftICE, naime, TheMida tokom svog otpakovanja
mapira u svojoj memoriji kernel32.dll kao RAW (samo ucitava njegov
sadrzaj u memorijski buffer koji je jednak velicini kernel32.dll na
disku), zatim ce koristiti poseban GetProcAddress koji je normalno
rucne izrade da nadje APIje tako sto ce koristi RAW offset iz section
hedera. Kad nadje tako API onda ce TheMida pozvati takav API, normalno kako
je kernel32.dll kompajliran za baznu adresu 78c00000h na win XP sp2 onda
ce sve reference ka kernel32 u takvom ucitanom bufferu biti validne.
Sad nastupamo mi sa nasim pristupom, naime da bi nas dll ucinili validanim
za TheMida prvo moramo da znamo na kojoj ce adresi nas fake_k32.dll biti
ucitan. To proveravamo prostim ucitavanjem fake_k32.dll i rekompjliranjem
naseg sourca za baznu adresu na kojoj je fake_k32.dll bio ucitan u TheMida.
Takodje mi moramo hookovai CreateFileA tako da kad god themida pokusa da
otvori kernel32.dll mi cemo je redirektovati da otvori fake_k32.dll, evo
kako sad TheMidini AntiAPISpyr postaje sasvim lak za pracenje i tracovanje.
Sad bez po muke mozete postavljati BPXove u kernel32.dll i pratiti kako se
izvrsava TheMida kad je virtuelno mapiran kernel32.dll...
Pa to je to, nemam vise sta da dodam niti da oduzmem, nadam se da vam se
ovaj princip svidja... sve u svemu:
S verom u Boga, deroko/ARTeam
Zahvalnice/Pohvalnice: moj ortacima u ARTeam koji ne znaju srpski citati :P
ap0x, LaFarge, Vrane, Kaca, miniC, TwistedX i ostalima
sa RL, Shatterhand, Winter aka Aca, Somile, gdje je onaj
sunnis!?!? argv, Limp i ostali moji pajtosi koje sam
zaboravio a cinili su ex-blackhatz :P