Tutorial 29: Win32 Debug API Teil 2
Wir fahren mit dem Thema Win32 Debug API fort. In diesem Tutorial lernen wir, wie man den debuggenden Prozess modifiziert.
Laden Sie das Beispiel hier herunter.
Theorie:
Im letzten Tutorial haben wir gelernt, wie man das zu debuggende Programm lädt und Debug-Ereignisse handlet, die während des Prozess auftreten. Um nützlich zu sein, muss unser Programm in der Lage sein, den zu debuggenden Prozess zu modifizieren. Es gibt verschiedene APIs nur für dieses Vorhaben.
-
ReadProcessMemory
Diese Funktion erlaubt Ihnen Speicher in dem spezifizierten Prozess zu lesen. Der Funktions-Prototyp ist wie folgt:
ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD,
lpNumberOfBytesRead:DWORD
hProcess
ist das Handle des Prozesses den Sie lesen wollen.
lpBaseAddress ist die Adresse im Ziel-Prozess, den Sie lesen wollen. Wenn Sie zum Beispiel 4 Bytes aus dem zu debuggenden Programm ab 401000h lesen wollen, muss der Wert dieses Parameters 401000h sein.
lpBuffer ist die Adresses des Buffers, der die erhaltenen Bytes aus dem Prozess aufnimmt.
nSize ist die Anzahl der Bytes die Sie lesen wollen.
lpNumberOfBytesRead ist die Adresse der DWord-Variable, die die Anzahl der tatsächlich gelesenen Bytes enthält. Wenn Sie das nicht interessiert, können Sie NULL angeben.
-
WriteProcessMemory
ist der Pendant zu ReadProcessMemory.
Es ermöglicht Ihnen in den Speicher des Ziel-Prozesses zu schreiben. Die Parameter sind genau die selben wie von ReadProcessMemory
Die nächsten beiden API-Funktionen benötigen ein wenig Hintergrundwissen. Unter einem Multitasking Betriebssystem wie Windows, können mehrere Programme gleichzeitig laufen. Windows gibt jedem Thread eine Zeitscheibe. Wenn die Zeitscheibe abgelaufen ist, friert Windows den aktuellen Thread ein und wächselt zum nächsten Thread, der die höchste Priorität hat. Kurz bevor zum anderen Thread gewechselt wird, sichert Windows die Register des aktuellen Threads, so dass er zu gegebenen Zeit fortgesetzt werden kann, Windows kann somit die letzte *Umgebung* des Threads wiederherstellen. Die gesicherten Werten der Register werden kollektiv Kontext genannt.
Zurück zu unserem Thema. Wenn ein Debug-Ereignis auftritt, hält Windows das zu debuggende Programm an. Dessen Kontext wird gespeichert. Da das Programm unterbrochen ist, können wir davon ausgehen, dass die Werte in dem Kontext unverändert bleiben. Wir können die Werte aus dem Kontext mit GetThreadContext erhalten und wir können sie mit SetThreadContext ändern.
Diese beiden APIs sind sehr mächtig. Mit ihnen haben Sie die VxD-ähnliche Macht über das zu debuggende Programm: Sie können die gesicherten Werte verändern und genau bevor das debuggende Programm fortgesetzt wird, werden die Werte aus dem Kontext zurück in die Register geschrieben. Jede Änderung die Sie im Kontext gemacht haben, wird reflektiert an das debuggende Programm. Denken Sie mal darüber nach: Sie können sogar den Wert des EIP-Registers ändern und den Ablauf irgendwo fortsetzen wo Sie wollen! Unter normalen Umständen könnten Sie das nicht.
GetThreadContext
proto hThread:DWORD, lpContext:DWORD
hThread
ist das Handle des Threads, von dem Sie den Kontext erhalten wollen.
lpContext ist die Adresse der CONTEXT
Struktur die gefüllt wird, wenn die Funktion erfolgreich war.
SetThreadContext
hat exakt die selben Parameter. Schauen wir uns an, wie die CONTEXT Struktur aussieht:
- CONTEXT STRUCT
- ContextFlags dd ?
;----------------------------------------------------------------------------------------------------------
; Diese Sektion wird zurückgeliefert, wenn ContextFlags den Wert CONTEXT_DEBUG_REGISTERS enthält
- ;-----------------------------------------------------------------------------------------------------------
iDr0 dd ?
iDr1 dd ?
iDr2 dd ?
iDr3 dd ?
iDr6 dd ?
iDr7 dd ?
- ;----------------------------------------------------------------------------------------------------------
; Diese Sektion wird zurückgeliefert, wenn ContextFlags den Wert CONTEXT_FLOATING_POINT enthält.
- ;-----------------------------------------------------------------------------------------------------------
- FloatSave FLOATING_SAVE_AREA
- ;----------------------------------------------------------------------------------------------------------
; Diese Sektion wird zurückgeliefert, wenn ContextFlags den Wert CONTEXT_SEGMENTS enthält.
- ;-----------------------------------------------------------------------------------------------------------
- regGs dd ?
regFs dd ?
regEs dd ?
regDs dd ?
- ;----------------------------------------------------------------------------------------------------------
; Diese Sektion wird zurückgeliefert, wenn ContextFlags den Wert CONTEXT_INTEGER enthält.
- ;-----------------------------------------------------------------------------------------------------------
- regEdi dd ?
regEsi dd ?
regEbx dd ?
regEdx dd ?
regEcx dd ?
regEax dd ?
- ;----------------------------------------------------------------------------------------------------------
; Diese Sektion wird zurückgeliefert, wenn ContextFlags den Wert CONTEXT_CONTROL enthält.
- ;-----------------------------------------------------------------------------------------------------------
- regEbp dd ?
regEip dd ?
regCs dd ?
regFlag dd ?
regEsp dd ?
regSs dd ?
- ;----------------------------------------------------------------------------------------------------------
; Diese Sektion wird zurückgeliefert, wenn ContextFlags den Wert CONTEXT_EXTENDED_REGISTERS enthält.
- ;-----------------------------------------------------------------------------------------------------------
-
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION
dup(?) CONTEXT ENDS
Wie Sie beobachten können, die Elemente dieser Struktur sind Abbilder der realen Prozessor Register. Bevor Sie diese Struktur benutzen können, müssen Sie spezifizieren, welche Gruppe von Registern Sie lesen/schreiben wollen und zwar im ContextFlags Element.
Wenn Sie zum Beispiel alle Register lesen/schreiben wollen, müssen Sie CONTEXT_FULL
in ContextFlags angeben. Wenn Sie nur regEbp, regEip, regCs, regFlag, regEsp oder regSs lesen/schreiben wollen, müssen Sie
CONTEXT_CONTROL in ContextFlags angeben.
Etwas was Sie beachten müssen, wenn Sie die CONTEXT Struktur benutzen: sie muss an einer DWord Grenze ausgerichtet sein, ansonsten erhalten Sie eigenartige Ergebnisse unter NT. Sie müssen nur die Zeile
"align dword" direkte über den Zeilen, wo Sie sie deklarieren, einfügen, ungefähr so:
align dword
MyContext CONTEXT
Beispiel:
Das erste Beispiel demonstriert die Benutzung von DebugActiveProcess. Als erstes müssen Sie ein Ziel-Programm starten, namens win.exe, welches in eine Endlosschleife eintritt, genau vor dem Anzeigen des Fensters. Dann starten Sie das Beispiel, welches sich selbst an win.exe anfügt und den Code von win.exe so modifiziert, dass win.exe die Endlosschleife verlässt und sein eigenes Fenster zeigt.
.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.2",0
ClassName db "SimpleWinClass",0
SearchFail db "Cannot find the target process",0
TargetPatched db "Target patched!",0
buffer dw 9090h
.data?
DBEvent DEBUG_EVENT
ProcessId dd ?
ThreadId dd ?
align dword
context CONTEXT
.code
start:
invoke FindWindow, addr ClassName, NULL
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
invoke MessageBox, 0, addr TargetPatched, 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
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.else
invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR .endif
invoke ExitProcess, 0
end start
;--------------------------------------------------------------------
; Der Code-Auszug unseres zu debuggenden Programms, win.asm. Tatsächlich ist es
; das einfache Fenster-Beispiel aus Tutorial 2, mit einer eingefügten Endlosschleife
; bevor die Message Loop betreten wird.
;----------------------------------------------------------------------
......
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax
jmp $
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp
Analyse:
invoke FindWindow, addr ClassName, NULL
Unser Programm muss sich selbst an das zu debuggende Programm anhängen und zwar mit der DebugActiveProcess Funktion welche die Prozess-ID des zu debuggenden Programms benötigt. Wir können die ID erhalten, indem wir GetWindowThreadProcessId aufrufen, welche wiederum das Fenster-Handle als Parameter benötigt. Demnach müssen wir erst das Fenster-Handle ermitteln.
Mit FindWindow können wir den Namen der Fenster-Klasse, die wir benötigen, spezifizieren. Sie gibt das Handle des Fensters zurück, welches durch die Fenster-Klasse erzeugt wurde. Wenn sie NULL zurückliefertm ist kein Fenster dieser Fensterklasse verfügbar.
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
Nachdem wir die Prozess-ID ermittelt haben, können wir DebugActiveProcess aufrufen. Dann betreten wir die Debug-Schleife und warten auf die Debug-Ereignisse.
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
Wenn wir CREATE_PROCESS_DEBUG_INFO erhalten, bedeutet das, dass das zu debuggende Programm unterbrochen ist, bereit um den Prozess zu behandeln. In diesem BEsipeil werden wir die Opcodes der Endlosschleife (0EBh 0FEh) in dem Programm mit NOPs (90h 90h) überschreiben.
Als erstes benötigen wir die Adresse der Befehle. Da das zu debuggende Programm bereits in der Endlosschleife ist, wenn unser Programm angefügt wird, zeigt EIP immer auf diese Befehle. Alles was wir machen müssen, ist den Wert aus EIP auslesen. Wir benutzen GetThreadContext um das zu erreichen. Wir setzen das ContextFlags Element auf CONTEXT_CONTROL
um GetThreadContext mitzuteilen, dass wir die "Kontroll" Register Elemente der
CONTEXT Struktur füllen wollen.
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
Nun, da wir den Wert von EIP haben, können wir WriteProcessMemory aufrufen, um den "jmp $" Befehl mit NOPs zu überschreiben, was effektiv hilft, das zu debuggende Programm aus der Endlosschleife zu holen. Danach zeigen wir eine Nachricht dem Benutzer an und rufen ContinueDebugEvent auf, um das zu debuggende Programm fortzusetzen. Da der "jmp $" Befehl mit NOPs überschrieben ist, ist das zu debuggende Programm in der Lage fortzufahren, das Fenster anzuzeigen und die Message Loop zu betreten. Der Beweis dafür ist, dass wir das Fenster sehen werden.
Das andere Beispiel benutzt eine etwas andere Vorgehensweise um das zu debuggende Programm in der Endlosschleife zu unterbrechen.
.......
.......
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
add context.regEip,2
invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION
.......
.......
Es ruft immer noch GetThreadContext auf, um den aktuellen Wert von EIP herauszufinden, aber statt "jmp
$" zu überschreiben, wird der Wert von regEip um 2 inkrementiert. Das Ergebnis ist, dass das Programm nach "jmp
$" fortgesetzt wird.
Nun können Sie sehen, wir mächtig Get/SetThreadContext ist. Sie können auch andere Register-Abbilder modifizieren und so an das zu debuggende Programm weitergeben. Sie können sogar einen int 3h Befehl einfügen, um Breakpoints zu setzen.
Deutsche Übersetzung: Joachim Rohde
Die original Win32Asm-Tutorials stammen von Iczelion's Win32 Assembly HomePage