我們繼續Win32調試API的話題。在本章中,我們將要學習如何修改被調試程序。
理論:
在前面一章中,我們學會了如何裝載被調試的進程以及如何處理進程中發生的事件。為了有實際用途,我們的程序應具有修改被調試程序的能力。有好幾個API函數用於這一目的。
ReadProcessMemory該函數允許你去讀指定的進程的內存。函數原型如下:
ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD
hProcess 待讀進程的句柄.
lpBaseAddress 目標進程中待讀內存起始地址。例如,如果你想要讀目標 進程中從地址401000h開始的4個字節,該參數值應置為401000h。
lpBuffer 接收緩沖區地址
nSize 想要讀的字節數。
lpNumberOfBytesRead 記錄實際讀取的字節數的變量地址。如果對這個值 不關心,填入NULL即可。
WriteProcessMemory 是對應於ReadProcessMemory的函數,通過它 可以寫目標進程的內存。其參數和ReadProcessMemory 相同。
理解接下去的兩個函數需要一些進程上下文的有關背景知識。在象Windows這樣的 多任務操作系統中,同一時間裡可能運行著幾個程序。Windows分配給每個線程一個 時間片,當時間片結束後,Windows將凍結當前線程並切換到下一具有最高優先級的 線程。在切換之前,Windows將保存當前進程的寄存器的 內容,這樣當在該線程再 次恢復運行時,Windows可以恢復最近一次線程運行的*環境*。保存的寄存器內容總 稱為進程上下文。
現在回到我們的主題。當一個調試事件發生時,Windows暫停被調試進程,並保存其 進程上下文。由於進程被暫停運行,我們可以確信其進程上下文內容將保持不變。 可以用GetThreadContext來獲取進程上下文內容,並且也可以用GetThreadContext 來修改進程上下文內容。
這兩個函數威力非凡。有了他們,對被調試進程你就具有象VxD的能力: 如改變其寄 存器內容,而在被調試程序恢復運行前,這些值將會寫回寄存器中。在進程上下文中 所做的任何改動,將都會反映到被調試程序中。想象一下: 甚至可以改變eip寄存器 的內容,這樣你可以讓程序運行到你想要的任何地方! 在正常情況下是不可能做到這 一點的。
GetThreadContext proto hThread:DWORD, lpContext:DWORD
hThread 你想要獲得上下文的線程句柄
lpContext 函數成功返回時用來保存上下文內容的結構指針。
SetThreadContext 參數相同。讓我們來看看上下文的結構:
CONTEXT STRUCT
ContextFlags dd ?
;----------------------------------------------------------------------------------------------------------
;當ContextFlags包含CONTEXT_DEBUG_REGISTERS,返回本部分
;-----------------------------------------------------------------------------------------------------------
iDr0 dd ?
iDr1 dd ?
iDr2 dd ?
iDr3 dd ?
iDr6 dd ?
iDr7 dd ?
;----------------------------------------------------------------------------------------------------------
;當ContextFlags包含CONTEXT_FLOATING_POINT,返回本部分
;-----------------------------------------------------------------------------------------------------------
FloatSave FLOATING_SAVE_AREA <>
;----------------------------------------------------------------------------------------------------------
;當ContextFlags包含CONTEXT_SEGMENTS,返回本部分
;-----------------------------------------------------------------------------------------------------------
regGs dd ?
regFs dd ?
regEs dd ?
regDs dd ?
;----------------------------------------------------------------------------------------------------------
;當ContextFlags包含CONTEXT_INTEGER,返回本部分
;-----------------------------------------------------------------------------------------------------------
regEdi dd ?
regEsi dd ?
regEbx dd ?
regEdx dd ?
regEcx dd ?
regEax dd ?
;----------------------------------------------------------------------------------------------------------
;當ContextFlags包含CONTEXT_CONTROL,返回本部分
;-----------------------------------------------------------------------------------------------------------
regEbp dd ?
regEip dd ?
regCs dd ?
regFlag dd ?
regEsp dd ?
regSs dd ?
;----------------------------------------------------------------------------------------------------------
;當ContextFlags包含CONTEXT_EXTENDED_REGISTERS,返回本部分
;-----------------------------------------------------------------------------------------------------------
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS
可以看出,該結構中的成員是對實際處理器的寄存器的模仿。在使用該結構之前 要在ContextFlags 中指定哪些寄存器組用來讀寫。如要訪問所有的寄存器, 你可以置ContextFlags 為CONTEXT_FULL 。或者只訪問regEbp, regEip, regCs, regFlag, regEsp 或 regSs, 應置ContextFlags 為 CONTEXT_CONTROL 。
在使用結構CONTEXT 時還應記住: 它必須是雙字對齊的,否則在NT下將得 到奇怪的結果。可以在定義前加上"align dword"。例如:
align dword
MyContext CONTEXT <>
例:
第一個例子演示DebugActiveProcess的使用。首先,需要在Windows顯示在屏幕上以前運行一個待調試程序win.exe,該程序將處於無限循環運行狀態中。然後你運行例子程序,它將把自己與win.exe連接起來,並且修改win.exe的代碼,這樣win.exe將退出無限循環狀態而顯示自己的窗口。
.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
;--------------------------------------------------------------------
; The partial source code of win.asm, our debuggee. It's actually
; the simple window example in tutorial 2 with an infinite loop inserted
; just before it enters the message loop.
;----------------------------------------------------------------------
......
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 $ <---- Here's our infinite loop. It assembles to EB FE
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
分析:
invoke FindWindow, addr ClassName, NULL
我們的程序需要用DebugActiveProcess將自己綁定到被調試程序,這需要知道被調試程序的進程Id。用GetWindowThreadProcessId 可以得到該Id,該函數需要窗口句柄作為參數,因此首先需要知道窗口句柄。本文來自編程入門網
用FindWindow, 我們先指定窗口類的名稱,返回的是該類創建的窗口句柄。如 果返回NULL,則表明當前沒有該類的窗口。
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
得到進程Id後,我們調用DebugActiveProcess。這樣就進入等待調試事件的循環中。
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
當得到 CREATE_PROCESS_DEBUG_INFO, 這意味著被調試進程已經被暫停運行了。 我們就可以對該進程動手術了。本例中,我們將用NOPs ( 90h 90h)覆蓋被調試進程中的無 限循環指令(0EBh 0FEh) 。
首先,需要得到該指令的地址。由於在我們的程序綁定到被調試程序時,被調試程序已經 處於循環語句中了,eip總是指向該指令。我們所要做的是得到eip的值。我們將使用 GetThreadContext來達到此目的。將上下文結構成員中ContextFlags設置 為CONTEXT_CONTROL ,這樣告訴GetThreadContext我們需要它去填充上下 文結構的成員中的"控制"寄存器。
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
得到eip的值以後,可以調用WriteProcessMemory來用NOPs覆蓋"jmp $" 指令,這樣將使被調試程序退出無限循環。在向用戶顯示了信息之後,調用ContinueDebugEvent 來恢復被調試程序的運行。由於指令"jmp $"已被Nops覆蓋,被調試程序將繼續 顯示窗口,並進入消息循環。證據是我們在屏幕上觀察到了次窗口。
另一個例子與此稍有不同,它是將被調試程序從無限循環中中斷。
.......
.......
.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
.......
.......
這裡仍調用GetThreadContext來獲取eip值,但沒有去覆蓋"jmp $" 指令,而是將 regEip加2,從而"跳過"該指令。結果是當被調試程序 重新獲得控制權時,將恢復執行在"jmp $"後的指令。
現在你可以體會到Get/SetThreadContext的威力了。你也可以修改其他寄存器映象,這些值將直接反映到被調試程序中。甚至你可以把int 3h指令插入到被調試進程中。產生斷點。