Iczelions Tutorial 28
DEBUG-API (parte 1)

Data

by "-Death_Reaver-

 

5/10/2005

UIC's Home Page

Published by Quequero

Xor quequero, quequero

mov eax, capodelmondo
xor eax, eax
mov capodelmondo, eax
div eax ; Ops, no SEH :)

mov capodelmondo, Death_Reaver

....

 

....

Difficolt

( )NewBies ()Intermedio (X)Avanzato ( )Master

 



Introduzione

Dopo qualche mesetto di pausa si riparte con la traduzione dei tutorial: Ora siamo arrivati al n 28: le utilissime debug api. Il tutorial si compone di 3 parti: Questa la prima. Per errori/segnalazioni/commenti mandatemi una mail a [email protected].

Tools usati

Masm32

RadAsm

Guida Api

URL o FTP del programma

ALLEGATO

Essay

In questo tutorial impareremo cosa Win32 offre per i programmatori riguardo gli strumenti di debugging. Dopo aver letto questo tutorial avrai imparato come  debuggare un processo.

TEORIA:

Win32 possiede molte API che permettono ai programmatori alcune potenziabilit di un debugger. Sono chiamate Win32 Debug-API o "primitives". Con esse puoi:

         Caricare un programma o attaccarsi a un programma in esecuzione

         Ottenere informazioni di basso livello sul programma che stai debuggando, come per esempio le process ID, indirizzo dell'entry-point, imagebase etc.

         Essere avvisato dagli eventi relativi al debugger come succede quando un processo/thread inizia/esce, le DLL sono caricate/scaricate etc.

         Modificare il processo/thread che stato debuggato

In poche parole, puoi scrivere un semplice debugger con queste API. Siccome l'argomento vasto, stato diviso in parti pi maneggevoli: Il tutorial inizia con la prima parte. Qui si spiegheranno i concetti di base e il framework generale per usare le Debug-API.

Le step da seguire per usare le debug-API sono:

1.  Creare un processo o "attaccare" il tuo programma a un processo in esecuzione. Questo il primo passo per usare le Debug-API. Finch il tuo programma lavora da debugger ti serve un programma da debuggare. Il programma che sta per essere debuggato chiamato "Debuggee". Puoi acquisire il Debuggee in due modi:

         Puoi creare il processo del debuggee da te con CreateProcess. per poter creare un processo da debuggare devi specificare il flag DEBUG_PROCESS. Questo flag fa in modo che Windows sappia che vogliamo debuggare il
processo. Windows manderà delle notificazioni al tuo programma riguardo importanti eventi di debugging(debug-events) che si presenteranno nel debuggee. Il processo debuggee verrà immediatamente sospeso finchè il tuo programma non è pronto. Se il debuggee crea a sua volta processi-figli, Windows notificherà dei debug-events provenienti anche da tutti i processi figli. Di solito non si desidera tale comportamento. Lo puoi disabilitare specificando il flag DEBUG_ONLY_THIS_PROCESS combinato con DEBUG_PROCESS (in masm, si scrive 'DEBUG_ONLY_THIS_PROCESS or DEBUG_PROCESS' NTD).

·         Puoi "attaccare" il tuo programma a un processo già presente con DebugActiveProcess.

2. Aspettare i debug-events. Dopo che il tuo programma ha acquisito il debuggee, il thread primario del debuggee è sospeso e lo rimarrà finchè il tuo programma chiama WaitForDebugEvent. Questa funzione lavora come gli altri WaitFor..... cioè blocca il thread chiamato finchè i eventi attesi si verificheranno. In questo caso, aspetta i debug-events mandati da Windows. Vediamo il suo prototipo:

WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD

lpDebugEvent è l'indirizzo di una struttura DEBUG_EVENT che sarà riempita con informazioni sul debug-event che si è presentato nel debuggee
dwMilliseconds
è il periodo di tempo in millisecondi per il quale questa funzione attenderà la venuta di debug-events. Se il periodo di tempo scade e non ci sono stati debug-events, WaitForDebugEvent ritornerà al caller (ovvero al pezzo di codice che l'ha chiamata NTD). Però se specifichi INFINITE (costante NTD) in questo argomento, la funzione non ritornerà al caller finchè non si verificherà un debug-event. Adesso esaminiamo la struttura DEBUG_EVENT in dettaglio:

DEBUG_EVENT STRUCT
dwDebugEventCode dd ?
dwProcessId dd ?
dwThreadId dd ?
u DEBUGSTRUCT
<>
DEBUG_EVENT ENDS

dwDebugEventCode contiene il valore che specifica il tipo di debug-event arrivato. Brevemente, qui ci possono essere tanti tipi di eventi, e perciò il tuo programma deve controllare il valore di questo campo in modo che sappia quale tipo di evento è arrivato e agire di conseguenza. I valori possibili sono:

Valore
Significato
CREATE_PROCESS_DEBUG_EVENT Quando un processo è creato. Questo evento sarà mandato quando il debuggee è stato appena creato (ma non in eseguzione)o quando il tuo programma si attacca a un processo in eseguzione con DebugActiveProcess. Questo è il primo evento che il tuo programma riceverà
EXIT_PROCESS_DEBUG_EVENT Quando un processo viene chiuso
CREATE_THEAD_DEBUG_EVENT Quando un nuovo thread è creato nel processo del debuggee o quando il tuo programma si attacca a un processo in eseguzione. Notare che non si ricevono questo tipo di eventi quando il thread principale è creato
EXIT_THREAD_DEBUG_EVENT Quando un thread del debuggee viene chiuso. Il tuo programma però non riceverà questo debug-event per il thread principale. In poche parole, puoi vedere il thread principale del debuggee come l'equivalente del processo del debuggee stesso. Quindi quando il tuo programma vede CREATE_PROCESS_DEBUG_EVENT è in realtà CREATE_THREAD_DEBUG_EVENT per il thread principale
LOAD_DLL_DEBUG_EVENT Quando il debuggee carica una DLL., Il programma riceverà questo evento solo quando il PE Loader ha risolto i link alle DLL (scrive la IAT NTD) oppure quando il debuggee chiama LoadLibrary
UNLOAD_DLL_DEBUG_EVENT Quando una DLL è scaricata dal processo del debuggee
EXCEPTION_DEBUG_EVENT Quando viene generata un'eccezione nel processo del debuggee. IMPORTANTE:Questo evento arriverà appena prima che il debuggee inizia a esegure la sua prima istruzione. L'eccezione è attualmente un debug break (int3). Quando vuoi "riesumare" il debuggee chiama ContinueDebugEvent con il flag DBG_CONTINUE. Non usare il flag DBG_EXCEPTION_NOT_HANDLED altrimenti il debuggee si rifiuterà di eseguire su WinNT (e quindi anche su WinXP e Win2k NTD). In Win98 invece va bene.
OUTPUT_DEBUG_STRING_EVENT Questo evento è generato quando il debuggee chiama la funzione DebugOutPutString per mandare una stringa di messaggio al tuo programma
RIP_EVENT Quando arrivano degli errori di debug di sistema (System Debugging errors NTD)

dwProcessId e dwThreadId sono le ID del processo e del thread nel quale arrivano gli eventi di debug. Puoi usare questi valori come identificatori del processo/thread a cui sei interessato. Ricorda che se usi CreateProcess per caricare il debuggee, puoi anche prendere la process ID e la thread ID del debuggee nella struttura PROCESS_INFO. Puoi usare questi valori per distinguere i debug-event del debuggee e quelli dei child-process (nel caso tu non abbia specificato il flag DEBUG_ONLY_THIS_PROCESS)
u è un unione che contiene informazioni sul debug-event. Può essere una delle strutture sottostanti, dipende dal valore di dwDebugEventCode

valore in dwDebugEventCode
Interpretazione di u
CREATE_PROCESS_DEBUG_EVENT Una struttura CREATE_PROCESS_DEBUG_INFO chiamata CreateProcessInfo
EXIT_PROCESS_DEBUG_EVENT Una struttura EXIT_PROCESS_DEBUG_INFO chiamata ExitProcess
CREATE_THEAD_DEBUG_EVENT Una struttura CREATE_THREAD_DEBUG_INFO chiamata CreateThread
EXIT_THREAD_DEBUG_EVENT Una struttura EXIT_THREAD_DEBUG_EVENT chiamata ExitThread
LOAD_DLL_DEBUG_EVENT Una struttura LOAD_DLL_DEBUG_INFO chiamata LoadDll
UNLOAD_DLL_DEBUG_EVENT Una struttura UNLOAD_DLL_DEBUG_INFO chiamata UnloadDll
EXCEPTION_DEBUG_EVENT Una struttura EXCEPTION_DEBUG_INFO chiamata Exception
OUTPUT_DEBUG_STRING_EVENT Una struttura OUTPUT_DEBUG_STRING_INFO chiamata DebugString
RIP_EVENT Una struttura RIP_INFO chiamata RipInfo

Non spiegherò nei dettagli tutte queste strutture in questo tutorial,solo la struttura CREATE_PROCESS_DEBUG_INFO sarà spiegata.
Immaginiamo che il nostro programma chiamasse WaitForDebugEvent e che ritorni. La prima cosa che dovremo fare sarà esaminare il valore in dwDebugEventCode per vedere che tipo di debug-event è accaduto nel processo del debuggee. Per esempio se il valore in dwDebugEventCode è CREATE_PROCESS_DEBUG_EVENT puoi interpretare il membro "u" come CreateProcessInfo e accedervi tramite u.CreateProcessInfo.


3. Fai qualunque cosa il tuo programma voglia fare in risposta ai debug-event. Quando WaitForDebugEvent ritorna, significa che un debug-event è appena accaduto nel processo del debugggee oppure il timeout è scaduto. Il tuo programma deve esaminare il valore in dwDebugEventCode in modo da reagire al debug-event in modo appropiato. In questo senso è come quando si processano i messaggi di Windows: tu scegli di gestirne alcuni e di tralasciarne altri.


4. Fai continuare l'esecuzione del debuggee. Quando arriva un debug-event, Windows sopende il processo del debuggee. Quando hai finito di gestire gli eventi, devi "spingere" il debuggee in modo da farlo ripartire. Puoi farlo chiamando la funzione ContinueDebugEvent.

ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

Questa funzione "riesuma"(fa ripartire, NTD) il thread che è stato precedentemente sospeso a causa della comparsa di un debug-event.
dwProcessId e dwThreadId sono rispettivamente l'ID del processo e del thread che sarà riesumato. Di solito prendi questi valori dai membri dwProcessId e dwThreadId della struttura DEBUG_EVENT. dwContinueStatus specifica come continuare il thread che ha generato il debug-event. Ci sono due possibili valori: DBG_CONTINUE e DBG_EXCEPTION_NOT_HANDLED.Questi due valori fanno la stessa per tutti i debug-event: continuano il thread. L'eccezione è EXCEPTION_DEBUG_EVENT. Se il thread riporta un'eccezione, significa che si è avuta un'eccezione del thread del debuggee. Se specifichi DBG_CONTINUE, il thread ignorerà il proprio gestore di eccezioni (vedi il corso NewBie N°13 NTD) e continuerà l'eseguzione. Per questo, il tuo programma deve esaminare e risolvere le eccezioni prima di riesumare il thread con DBG_CONTINUE oppure l'eccezione arriverà ancora e ancora...Se specifichi DBG_EXCEPTION_NOT_HANDLED, il tuo programma sta dicendo a Windows che è incapace di gestire l'eccezione. Windows dovrà usare il gestore di eccezioni di default del debuggee per poter gestire l'eccezione. In conclusione, se il debug-event si riferisce a un'eccezione avvenuta nel processo del debuggee, dovresti chiamare ContinueDebugEvent con il flag DBG_CONTINUE se il tuo programma ha già risolto l'eccezione. Altrimenti, il tuo programma deve chiamare ContinueDebugEvent con il flag DBG_EXCEPTION_NOT_HANDLED. Dovresti sempre usare DBG_CONTINUE tranne in un caso:il primo EXCEPTION_DEBUG_EVENT che ha il valore EXCEPTION_BREAKPOINT nel membro ExceptionCode. Quando il debuggee sta per eseguire la sua primissima istruzione, il tuo programma riceverà un eccezione (che è un tipo di debug-event NTD) che precisamente è un debug-break (INT 3). Se rispondi chiamando ContinueDebugEvent con il flag DBG_EXCEPTION_NOT_HANDLED, Windows NT (e quindi anche XP e 2000) si rifiuterà di eseguire il debuggee (perchè non ne tiene conto). Devi sempre usare il flag DBG_CONTINUE per dire a Windows che vuoi che il thread vada avanti.

5.Continuare questo ciclo all'infinito finchè il processo del debuggee esce. Il tuo programma deve rimanere in un loop infinito come un message-loop finchè il debuggee non chiude. Il loop dovrebbe essere tipo questo:

.while TRUE
invoke WaitForDebugEvent, addr DebugEvent, INFINITE
.break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
<Gestione dei debug-event>
invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw


Ecco la sorpresa: Una volta che sei partito a debuggare un programma, non puoi staccarti finchè il programma non esce.
Facciamo un breve riepilogo:

1. Creare un processo o "attaccare" il tuo programma a un processo in esecuzione

2. Aspettare i debug-events

3. Fai qualunque cosa il tuo programma voglia fare in risposta ai debug-event.

4. Fai continuare l'esecuzione del debuggee

5.Continuare questo ciclo all'infinito finchè il processo del debuggee esce.

Esempio:

Questo esempio debugga un programma in Win32 e mostra importanti informazioni come l'handle del processo, ID, imagebase etc.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
              db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
             db "Process Handle: %lx",0Dh,0Ah
             db "Thread Handle: %lx",0Dh,0Ah
             db "Image Base: %lx",0Dh,0Ah
             db "Start Address: %lx",0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
.code
start:
mov ofn.lStructSize,sizeof ofn
mov ofn.lpstrFilter, offset FilterString
mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
    invoke WaitForDebugEvent, addr DBEvent, INFINITE
    .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
       invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
       .break
   .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
       invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
       invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION    
   .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
       .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
           invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
          .continue
       .endif
   .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif
   invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
.endif
invoke ExitProcess, 0
end start

ANALISI:
Il programma riempie la struttura OPENFILENAME e chiama GetOpenFileName per far scegliere all'utente il programma da debuggare.

invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer,NULL,NULL,NULL,FALSE,DEBUG_PROCESS + DEBUG_ONLY_THIS_PROCESS,NULL,NULL,addr startinfo,addr pi

Quando l'utente ne sceglie uno, il programma chiama CreateProcess per caricare il programma. Poi chiama GetStartupInfo per riempire la struttura STARTUPINFO con i valori di default. Notare il fatto che stiamo usando DEBUG_PROCESS combinato con DEBUG_ONLY_THIS_PROCESS in modo da debuggare solo il nostro programma e non i processi figli di quest'ultimo.

.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE

Quando il debuggee è caricato, entriamo nel debug-loop infinito, dove viene chiamato WaitForDebugEvent. WaitForDebugEvent non ritornerà finchè non si verifica un debug-event nel debuggee, questo perchè abbiamo specificato il flag INFINITE come suo secondo parametro. Quando si verifica un debug-event, WaitForDebugEvent ritorna e DBEvent (cioè la nostra struttura NTD) è riempita con le informazioni riguardanti il debug-event.

.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
.break

Prima controlliamo il valore di dwDebugEventCode: Se è EXIT_PROCESS_DEBUG_EVENT mostriamo il messaggio che dice "il debuggee è uscito" e quindi usciamo dal
debug-loop.

.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION

Se il valore di dwDebugEventCode è CREATE_PROCESS_DEBUG_EVENT, mostriamo un pò di informazioni sul debuggee, tutto su una message-box. Otteniamo queste informazioni da u.CreateProcessInfo. CreatProcessInfo è una struttura del tipo CREATE_PROCESS_DEBUG_INFO. Puoi prelevare altre informazioni su questa struttura dalla API Reference.

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif

Se il valore di dwDebugEventCode è EXCEPTION_DEBUG_EVENT dobbiamo capire che tipo di eccezione è. E' una lunga serie di strutture nidificate (vedi tutorial N°13 per newbie! NTD) ma puoi ottenere il tipo d'eccezione dal membro ExceptionCode. Sei il valore di ExceptionCode è EXCEPTION_BREAKPOINT e arriva per la prima volta (oppure siamo sicuri che il debuggee non abbia nel codice degli int 3h) possiamo dire senza sbagliare che il debuggee sta per eseguire la sua prima istruzione. Abbiamo finito con il processing (ovvero di controllare i debug-event NTD), dobbiamo chiamare ContinueDebugEvent con il flag DBG_CONTINUE per permettere al debuggee di ripartire. Quindi aspettiamo il prossimo debug-event.

.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
.endif

Se il valore di dwDebugEventCode è CREATE_THREAD_DEBUG_EVENT o EXIT_THREAD_DEBUG_EVENT, mostriamo il testo che lo dice

invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw

Eccetto per il caso di EXCEPTION_DEBUG_EVENT sopra, chiamiamo ContinueDebugEvent con il flag DBG_EXCEPTION_NOT_HANDLED

invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread

Quando il debuggee esce, siamo fuori dal debug-loop e dobbiamo chiudere entrambi i processi e gli handle dei thread del debuggee. Chiuderli non significa chiudere il processo/thread. Significa solo che non vogliamo usare più questi handle per riferirci al processo/thread.

                                                                                                             §-Death_Reaver-

Note finali

Ringrazio tutti, ma soprattutto quequero che visto che è un bravo ragaz...papero correggierà tutti i miei errrori ortogggraficci

Disclaimer

Vorrei ricordare che il software va comprato e  non rubato, dovete registrare il vostro prodotto dopo il periodo di valutazione. Non mi ritengo responsabile per eventuali danni causati al vostro computer determinati dall'uso improprio di questo tutorial. Questo documento stato scritto per invogliare il consumatore a registrare legalmente i propri programmi, e non a fargli fare uso dei tantissimi file crack presenti in rete, infatti tale documento aiuta a comprendere lo sforzo che ogni sviluppatore ha dovuto portare avanti per fornire ai rispettivi consumatori i migliori prodotti possibili.BLA BLA BLA

Reversiamo al solo scopo informativo e per migliorare la nostra conoscenza del linguaggio Assembly. SI SI Jalappi Cia' Cia' Cia 'Ciao