Copy Link
Add to Bookmark
Report
3x03 Debugging Programs On Win32
...................
...::: phearless zine #3 :::...
....................>---[ Debugging Programs On Win32 ]---<...................
............................>---[ by deroko ]---<.............................
deroko[at]gmail[dot]com
1. Blah
2. Osnova
3. Muvanje CONTEXTa
4. Pisanje/Citanje memorije procesa
5. Tracovanje kroz single step
6. SEH i HideDebugger
7. Mogucnosti ovakvog tracovanja...
8. Reference
///////////////////////////////////////////////////////////////////////////
--==<[ 1. Blah
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Debugovanje na win32 moze da koristi ako zelite da pisete svoj unpacker ili
de-protektor prilikom reversanja nekih meta. Normalno na raspolaganju vam stoji
gomila opcija. Takodje ovo mozete koristiti i kako biste pisali protektore tipa
Armadilo... Najvaznije je ovde da mozete lako naci neko mesto u kodu koje ce
biti izvrseno i tu ubaciti viri... veoma zgodna stvar... Emulator ce morati da
emulira ceo kod ne bi li naso vas kod, a jos bolje ovde mozemo nauciti i neke
anti-emu trikove... Pa da pocnemo...
///////////////////////////////////////////////////////////////////////////
--==<[ 2. Osnova
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Posto radimo debugovanje programa, moramo pokrenuti proces. Naime debugovanje
se vrsi kroz seriju DEBUG_EVENT-ova koje program salje nasem debuggeru. O tome
ce da bude reci malo dole. No sad se moramo usredsrediti na CreateProcess API.
CreateProcessA/W ima argument koji se zove -> dwCreationFlags i u ovom slucaju
tu moramo da predamo sledece flagove DEBUG_PROCESS i DEBUG_ONLY_THIS_PROCESS
DEBUG_PROCESS flag ce reci da je proces koji pravimo debugovan i da se svi EVENT
salju nasem debugeru.
DEBUG_ONLY_THIS_PROCESS - sluzi da ne bi debugovali i procese koje pravi proces
koji debugujemo. Shodno tome usredsredjuemo se na nas
proces.
*Poziv funkciji izgleda ovako:
push offset pinfo ;
push offset sinfo ;
push 0 ;
push 0 ;
push DEBUG_ONLY_THIS_PROCESS + DEBUG_PROCESS ;
push 0 ;
push 0 ;
push 0 ;
push 0 ;
push offset fname ;
callW CreateProcessA ; create process for
test eax, eax ; debuging
jz error ;
Ok, nas proces je spreman za debugovanje sad moramo da procesiramo EVENTove koje
mi zelimo. Za tako nesto nam sluze 2 APIja:
WaitForDebugEvent i ContinueDebugEvent...
WaitForDebugEvent bi trebalo da sledi odmah posle CreateProcess i ima sledecu
sintaksu:
BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent, // address of structure for event information
DWORD dwMilliseconds // number of milliseconds to wait for event
);
DEBUG_EVENT je struktura koja izgleda ovako:
DEBUG_EVENT STRUCT
de_code DWORD ?
de_ProcessId DWORD ?
de_ThreadId DWORD ?
de_u DEBUGSTRUCT <>
DEBUG_EVENT ENDS
de_code - govori nam koji je EVENT u pitanju i moze biti jedan od ovih kodova:
EXCEPTION_DEBUG_EVENT - u programu se javio neki EXCEPTION
CREATE_THREAD_DEBUG_EVENT - startovan je novi thread u programu
CREATE_PROCESS_DEBUG_EVENT - proces je startovan
EXIT_THREAD_DEBUG_EVENT - neki thread je izasao
EXIT_PROCESS_DEBUG_EVENT - ovaj EVENT se javlja kad program izadje
LOAD_DLL_DEBUG_EVENT - javlja se kad se ucitava DLL (prilikom startovanja)
ili poziva LoadLibrary()
UNLOAD_DLL_DEBUG_EVENT - javlja se kad se oslobadjaju DLLovi, kraj programa ili
FreeLibrary()
OUTPUT_DEBUG_STRING_EVENT - program je u sebi upotrebio OutputDebugStringA
RIP_EVENT - ????
de_ProcessId - je ID procesa
de_ThreadId - je ID Threada u kom se odigrao event
de_u - je UNIJA sledeceg oblika:
DEBUGSTRUCT UNION
DU_Exception EXCEPTION_DEBUG_INFO <>
DU_CreateThread CREATE_THREAD_DEBUG_INFO <>
DU_CreateProcessInfo CREATE_PROCESS_DEBUG_INFO <>
DU_ExitThread EXIT_THREAD_DEBUG_INFO <>
DU_ExitProcess EXIT_PROCESS_DEBUG_INFO <>
DU_LoadDll LOAD_DLL_DEBUG_INFO <>
DU_UnloadDll UNLOAD_DLL_DEBUG_INFO <>
DU_DebugString OUTPUT_DEBUG_STRING_INFO <>
DU_RipInfo RIP_INFO <>
DEBUGSTRUCT ENDS
Naime u zavisnosti od Eventa koristicete neku od ovih struktura. Nama je za
tracovanje najvazniji EXCEPTION_DEBUG_EVENT, CREATE_PROCESS_DEBUG_EVENT i
EXIT_PROCESS_DEBUG_EVENT jer ce nam oni sluziti za tracovanje i debugovanje
koda, shodno tome oni zasluzuju da se detaljnije objasne.
CREATE_PROCESS_DEBUG_INFO STRUCT
CPID_hFile DWORD ?
CPID_hProcess DWORD ?
CPID_hThread DWORD ?
CPID_BaseOfImage DWORD ?
CPID_dwDebugInfoFileOffset DWORD ?
CPID_nDebugInfoSize DWORD ?
CPID_lpThreadLocalBase DWORD ?
CPID_StartAddress DWORD ?
CPDI_lpImageName DWORD ?
CPDI_fUnicode WORD ?
CREATE_PROCESS_DEBUG_INFO ENDS
Ovde nam je znacajno sledece ->
BaseOfImage - je bazna adresa procesa
StartAddress - je ustvari adresa entrypointa
EXIT_PROCESS_DEBUG_EVENT:
ovde samo treba znati da kad se odigra moramo da zavrsimo sa tracovanjem
EXCEPTION_DEBUG_EVENT:
Uhhh ovaj je najvazniji za tracovanje i to iz vise razloga. Kad tracujemo
program mi to radimo preko single stepinga (TF) i putem int3h sto nije nista vise
do exception u kodu koji ce nas debuger da primi i da ih procesira svaki za sebe.
Struktura koja je ovde u pitanju je :
EXCEPTION_DEBUG_INFO STRUCT
EDI_ExceptionRecord EXCEPTION_RECORD <>
EDI_FirstChance dd ?
EXCEPTION_DEBUG_INFO ENDS
EXCEPTION_RECORD STRUC
ER_ExceptionCode DD ?
ER_ExceptionFlags DD ?
ER_ExceptionRecord DD EXCEPTION_RECORD PTR ?
ER_ExceptionAddress DD BYTE PTR ? ; CODE PTR
ER_NumberParameters DD ?
ER_ExceptionInformation DD EXCEPTION_MAXIMUM_PARAMETERS DUP (?)
EXCEPTION_RECORD ENDS
no da vidimo kako to radi -> poceli smo sa tracovanjem i dobili smo prvi event:
cmp de.de_code, EXCEPTION_DEBUG_EVENT ; process exceptions...
jne __skip3 ; no exception? check next posibility
cmp de.de_u.ER_ExceptionCode, EXCEPTION_SINGLE_STEP; check if singlstep
je process_single_step ;
;
cmp de.de_u.ER_ExceptionCode, EXCEPTION_BREAKPOINT;
jne other_exceptions ; nah, not int 3h...
cmp first, 0 ; 1st int3h?
jne restore_and_set ;
mov first, 1 ;
call HideDebugger ; hide debugger from process
jmp continue_dbg ;
ok prvo proveravamo da li je u pitanju EXCEPTION_DEBUG_EVENT? jeste, sad je red
da proverimo koji je Exception u pitanju preko de.de_u.ER_ExceptionCode, ja pratim
samo EXCEPTION_BREAKPOINT (int 3h) i EXCEPTION_SINGLE_STEP (int1 iliti TF) i u
zavisnosti od njih ja procesiram te exceptione dok sve druge exceptione predajem
SEHu programa koje debugujem (setimo se SHIFT+F7/F8/F9 u Olly-ju)...
Ok kad procesiramo exceptione na ovaj nacin moramo da nastavimo sa izvrsavanjem
programa putem ContinueDebugEvent, ova funkcija je jako bitna i prima 3 parametra:
BOOL ContinueDebugEvent(
DWORD dwProcessId,
DWORD dwThreadId,
DWORD dwContinueStatus
);
dwProcessId je ID procesa koji debugujemo
dwThreadID je ID threda koji debugujemo...
dwContinueStatus moze biti DBG_CONTINUE, DBG_EXCEPTION_NOT_HANDLED
DBG_CONTINUE - kaze nasem traceru da nastavi dalje sa tracovanjem
DBG_EXCEPTION_NOT_HANDLED - sluz da bi reko programu da mi nismo handlovali
ovaj exception i da program pozove svoj SEH...
push DBG_EXCEPTION_NOT_HANDLED ;
push pinfo.pi_dwThreadId ;
push pinfo.pi_dwProcessId ;
callW ContinueDebugEvent ;
jmp begin_dbg ;
u slucaju da ovaj Exception ja ne obradjujem ili :
push DBG_CONTINUE ; continue_dbg
push pinfo.pi_dwThreadId ; Id of thread
push pinfo.pi_dwProcessId ; Id of process
callW ContinueDebugEvent ; and continue debug event
jmp begin_dbg ; go to WaitForDebugEvent
DBG_CONTINUE ide posto smo procesirali Exception, doduse ovo se koristi za svaki
drugi EVENT (LODA/UNLOAD_DLL, OUTPUTSTRING, itd...)
Teoretski prost tracer bi ovako izgledao:
CreateProcess();
bla:
WaitForDebugEvent();
<ovde ide procesiranje EVENTova>
ContinueDebugEvent();
jmp bla
///////////////////////////////////////////////////////////////////////////
--==<[ 3. Muvanje CONTEXTa
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Ovde nam trebaju 2 funckije GetThreadContext i SetThreadContext, sa ovim APIjima
mozemo uvek da dobijemo sadrzaj registra threada koji debugujemo, a takodje mozemo
da namestamo registre procesa.
Sintaksa je krajnje prosta oba uzimaju 2 argumenta:
1. handle threada koji debugujemo
2. pointer na CONTEXT strukturu (tj. struktura gde su sadrzani registri)
Prost primer upotrebe evo ga ovde:
;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
;get_ctx...
;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
get_ctx proc ;
mov ctx.CONTEXT_ContextFlags, CONTEXT_FULL;
push offset ctx ;
push pinfo.pi_hThread ;
callW GetThreadContext ;
ret ;
get_ctx endp ;
;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
;set_ctx
;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
set_ctx proc ;
push offset ctx ;
push pinfo.pi_hThread ;
callW SetThreadContext ;
ret ;
set_ctx endp ;
i onda se manipulise sa contextom threada ovako:
call get_ctx ;
mov ctx.CONTEXT_Eax, 0 ;
call set_ctx ;
U ovom slucaju eax ce imati 0 kad thread krene sa svojim izvrsavanjem
///////////////////////////////////////////////////////////////////////////
--==<[ 4. Pisanje/Citanje memorije procesa
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Za ovo Windows ima dve sjajne funkcije :
WriteProcessMemory i ReadProcessMemory... Njihova sintaksa je krajnje laka,
recimo da hocemo da citamo nesto iz procesa koji debugujemo:
get_buffer proc address:dword, nsize:dword ;
push 0 ; stavite 0
push nsize ; koliko?
push offset buffer ; gde?
push address ; odakle?
push pinfo.pi_hProcess ; handle procesa...
callW ReadProcessMemory ;
ret ;
get_buffer endp ;
address je adresa u okvirus procesa sa koje citamo, a nsize je kolicina podataka
koje zelimo da procitamo... posle ove funkcije program ce u buffer imati podatke
sa address i odgovarajucu kolicinu podataka... isto je i sa WriteProcessMemory,
isti argumenti i sve, samo sto pise na adresu.
///////////////////////////////////////////////////////////////////////////
--==<[ 5. Tracovanje kroz single step
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Ok tu su bile sve funkcije koje nam trebaju, sad ono sto nas zanima je tracovanje
instrukcija.
Ovde moramo da postavimo Trap Flag u CONTEXTu... to radimo prosto ovako:
set_tf proc ;
pushad ;
call get_ctx ;
;
or ctx.CONTEXT_EFlags, 100h ;
;
call set_ctx ;
popad ;
ret ;
set_tf endp ;
Sledeci EVENT koji ce se odigrati posle ContinueDebugEvent je :
EXCEPTION_DEBUG_EVENT sa exceptionCode - EXCEPTION_SINGLE_STEP, sad slobodno
mozete uraditi sta hocete, proveriti raspon EIP, procitati insturkciju, analizirati
registre posle svake instrukcije, to je vasa stvar, samo ne zaboravite da, pre nego
sto pozovete ContinueDebugEvent, postavite opet trap flag jer se on cisti kad god je
to moguce...
Sad bismo mogli uopsteno da napisemo nas mali tracer:
CreateProcess()
blah:
WaitForDebugEvent();
if (EXCEPTION_DEBUG_EVENT)
if (EXCEPTION_SINGLE_STEP)
set_tf()
ContinueDebugEvent();
jmp blah
Ovakva konstrukcija debugera ce uciniti da se nas kod ceo prati od pocetka, sto
ce reci EntryPointa DLLova, pa kroz pozive DLLovima itd... sto moze da traje
poprilicno. Da bismo sve to izbegli mozemo se posluziti trikom svakog normalnog
debugera a to je seldece:
- ako detektujemo da je EIP izvan opsega naseg programa
postavicemo INT 3h iza call koji nas je tamo uputio
- ocisticemo trap_flag i cekamo da se generise EXCEPTION_BREAKPOINT
- opet postavljamo TF i nastavljam dalje tracovanje...
Ok sacemo da analiziramo ove korake:
- Ovo mozemo raditi na prost nacin analizom PE hedera i nalazenjem SizeOfImage
i proveravanjem da li je EIP iznad ili ispod.
- ako je iznad (znaci da smo uleteli u neki DLL), normalno ulaskom u dll
koji se vrsi preko call na staku ostaje EIP od sledece instrukcije.
Shodno tome moramo uraditi sledece:
pozvati GetThreadContext i dobiti adresu ESP, onda cemo koristit
ReadProcessMemory da bismo sa te adrese uzeli 4 byte sto je nista drugo
do sacuvan EIP, sa tako dobijene adrese cemo procitati 1 byte (velicina
int 3h) i onda cemo na tu istu adresu staviti int 3h (0CCh opcode),
sad samo treba da ocistimo TrapFlag i da nastavimo sa izvrsavanjem.
- kad dobijemo EXCEPTION_DEBUG_EVENT i EXCEPTION_BREAKPOINT znamo da smo
izasli iz dll-a i sve sto sad treba jeste da vratimo sacuvani opcode
sa WriteProcessMemory i da postavimo trap_flag opet i da nastavimo sa
tracovanjem.
Veoma je bitno da program prvo izbacuje DebugBreak koji ce generisati int 3h
exception, shodno tome moramo zaobici PRVI INT3h i samo nastaviti dalje sa
izvrsavanjem, ali logicno se namece pitanje kako da se zaustavimo na entrypointu
jer postavljanje TRAP_FLAG-a ovde ce izazvati tracovanje svih DLLova na pocetku.
Trik je ovde jako prost, kad dobijemo EVENT CREATE_PROCESS_DEBUG_EVENT, postavi-
cemo int 3h na EntryPoint... Posto znamo da kad popijemo ovaj EVENT da ce:
de.de_u.CPID_StartAddress imati tu adresu, nista lakse, tu postavljamo
breakpoint i nastavljamo sa izvrsavanjem koda dok ne dobijemo BREAK_POINT, ali
zapamtite da prvi EXCEPTION_BREAKPOINT moramo ignorisati... teoretski to ovako
izgleda:
CreateProcess()
blah:
WaitForDebugEvent();
if (EXCEPTION_DEBUG_EVENT)
if (EXCEPTION_SINGLE_STEP)
set_tf()
jmp cont
else
if (EXCEPTION_BREAKPOINT)
if (first)
jmp cont
else
get_eip
restor_opcode()
set_tf()
cont:
ContinueDebugEvent();
jmp blah
To je cela mudrost tracovanje...
///////////////////////////////////////////////////////////////////////////
--==<[ 6. SEH i HideDebugger
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Ok, za debugovanje postoji jos jedna bitna funkcija a to je :
GetThreadSelectorEntry koja ima sledecu sintaksu:
BOOL GetThreadSelectorEntry(
HANDLE hThread,
DWORD dwSelector,
LPLDT_ENTRY lpSelectorEntry
);
hThread je handle naseg threada
dwSelector je vrednost Segment registra koji trazimo u nasem slucaju FS
lpSelectoryEntry je pointer na ovo strukturu:
LDT_ENTRY STRUCT
ldt_LimitLow dw ?
ldt_BaseLow dw ?
ldt_BaseMid db ?
ldt_Flags1 db ?
ldt_Flags2 db ?
ldt_BaseHi db ?
LDT_ENTRY ENDS
pri cemu je - ldt_BaseLow donjih 16 bita (0-15)
- ldt_BaseMid je (16-23)
- ldt_BaseHi je (24-31)
Pomocu ove funkcije cemo dobiti adresu FS selektora i mozemo je analizirati
kako da bi pronasli BeingDebugged ili SEH-chain...
Primer upotrebe:
callW get_ctx
push offset ldt
push ctx.CONTEXT_SegFs
push pinfo.pi_hThread
callW GetThreadSelectorEntry...
sad moramo da konstruisemo adresu iz LDT strukture:
mov al, ldt.ldt_BaseMid ; u al srednjih 8 bita
mov ah, ldt.ldt_BaseHi ; u ah gornjih 8 bita
shl eax, 16 ; sad ih pomeramo na gornjih 16 bita
mov ax, ldt.ldt_BaseLow ; i stavljamo donjih 16 bita u ax
Sad smo formirali celu adresu na koju pokazuje FS selektor...
E sad kako da sakrijemo od naseg procesa da je debugovan:
BeingDebugged se nalazi u PEB + 2, standardna fora za proveru da li ste u
debuggeru je :
mov eax, FS:[30h] ; PEB
movzx eax, byte ptr[eax+2] ; BeingDebugged
Sad sto moramo uciniti je:
- na EAX dodati 30h i odatle procitati 4 byte kako bi dobili adresu PEB-a
- procitamo 4 byte odatle i treci postavimo na 0
- zapisemo tih 4 byte na tu adresu
(normalno da na PEB+2) mozete odmah pisati 0, ali ja sam nesto probavao
pa je ovako ostalo u mom traceru/debugeru.
SEH je prica za sebe, on nam moze zadati problem, a to je da ukoliko dobijemo
Exception koji procesiramo preko DBG_EXCEPTION_NOT_HANDLED onda ce SEH ukloniti
trap_flag i mozemo izgubimo kontrolu nad programom. Ok, trik ovde je, prost,
preko gore pomenutog APIja nadjemo FS i analiziramo SEH chain, nadjemo gde je
lociran seh handle i na njega postavimo int 3h, posle toga ocistimo trap_flag
i cekamo da se popije nas Int 3h i posle nastavljamo normalno izvrsavanje kroz
trap_flag, bas kao i kad izlazimo iz APIja...
Pa to je to...
///////////////////////////////////////////////////////////////////////////
--==<[ 7. Mogucnosti ovakvog tracovanja...
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Hmmm, mogucnosti i primene su velike, na ovaj nacin mozete napisati svoj unpacker.
Koji ces ocas posla i bez muke da otpakuje neki program.
Recimo : tacujemo instrukciju po instrukciju i dog god je EIP u poslednjem sectionu
mi radimo singlestep, kad EIP izadje iz poslednje sectiona, mozemo zaustaviti
tracovanje i sa ReadProcessMemory i malo popravljanje PointerToRawData u section
headeru da dobijemo dumpovan fajl bez po muke, doduse i dalje ostaje problem sa
fiksiranjem Importa, ali to mozete i sa ImportRec da uradite na dumpovanom file.
Druga mogucnost je tracovanje procedura prilikom pravljenja OEPa, sto da ne,
tracujete program i nadjete gde da ubacite vas OEP. U planu mi je da ovu metodu
kombinujem sa dizzy enginom kako bi lako i efikasno naso valjane procedure, a
mozda takodje da jednom i za vjek i vjekova sjebemo AV kompanije koje samo lazu
jadne musterije... ko ce ga znati, zivi bili pa videli...
8. Reference
-------------------------------------------------------------------------------
www.win32asm.cjb.net --- (3 tutoriala)
Tracing under win32 z0mbie --- http://vx.netlux.org/lib/vzo28.html
MSDN --- http://msdn.microsoft.com
9. Primer
-------------------------------------------------------------------------------
Primer je sadrzan uz tekst (debug.exe + src)
10. Greets
-------------------------------------------------------------------------------
#blackhat
hmmm, to je to...
-------------------------------------------------------------------------------
deroko - www.blackhatz.net/~deroko/