程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> 匯編語言 >> Win32中調試API

Win32中調試API

編輯:匯編語言

在本教程中,我們將學習Win32提供給開發者的用於調試的原語. 在教程的結尾,我們將學習如何調試一個進程.

理論:
Win32有一些供程序員使用的API,它們提供相當於調試器的功能. 他們被稱作Win32調試API(或原語).利用這些API,我們可以:

加載一個程序或捆綁到一個正在運行的程序上以供調試
獲得被調試的程序的低層信息,例如進程ID,進入地址,映像基址等.
當發生與調試有關的事件時被通知,例如進程/線程的開始/結束, DLL的加載/釋放等.
修改被調試的進程或線程
簡而言之,我們可以用這些API寫一個簡單的調試器.由於這個題目有些過大,我把它分為幾部分,而本教程就是它的第一部分.在本教程中,我將講解一些基本概念及Win32調試API的大致框架.
使用Win32調試API的步驟如下:

創建一個進程或捆綁到一個運行中的進程上. 這是使用Win32調試API的第一步.由於我們的程序要扮演調試器的角色,我們要找一個供調試的程序.一個被調試的程序被稱為debuggee.可以通過以下兩種方式獲得debuggee:
通過CreateProcess創建debuggee進程.為了創建被調試的進程,必須指定DEBUG_PROCESS標志.這一標志告訴Windows我們要調試該進程. 當debuggee中發生重要的與調試有關的事件(調試事件)時,Windows 會向我們的程序發送通知.debuggee會立即掛起以等待我們的程序准備好.如果debuggee還創建了子進程,Windows還會為每個子進程中的調試事件向我們的程序發送通知.這一特性通常是不必要的.我們可以通過指定DEBUG_ONLY_THIS_PROCESS與 DEBUG_PROCESS的組合標志來禁止它.
我們也可以用 DebugActiveProcess標志捆綁到一個運行中的進程上.
等待調試事件. 在獲得了一個debuggee進程後,debuggee的主線程被掛起,這種狀況將持續到我們的程序調用WaitForDebugEvent為止.這個函數和其他的WaitForXXX函數相似,比如說,它阻塞調用線程直到等待的事件發生.對這個函數來說, 它等待由Windows發送的調試事件.下面是它的定義:
WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD

lpDebugEvent is the address of a DEBUG_EVENT這個結構將被填入關於debuggee中發生的調試事件的信息.

dwMilliseconds 該函數等待調試事件的時間,以毫秒為單位.如果這段時間沒有調試事件發生, WaitForDebugEvent返回調用者.另一方面,如果將該參數指定為 INFINITE 常數,函數將一直等待直到調試事件發生.

現在我們看一下DEBUG_EVENT 結構.

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

dwDebugEventCode 該值指定了等待發生的調試事件的類型.因為有很多種類型的事件發生,我們的程序要檢查該值,知道要發生事件的類型並做出響應. 該值可能的取值如下:

取值 含義
CREATE_PROCESS_DEBUG_EVENT 進程被創建.當debuggee進程剛被創建(還未運行) 或我們的程序剛以DebugActiveProcess被捆綁到一個運行中的進程時事件發生. 這是我們的程序應該獲得的第一個事件.
EXIT_PROCESS_DEBUG_EVENT 進程退出.
CREATE_THEAD_DEBUG_EVENT 當一個新線程在deuggee進程中創建或我們的程序首次捆綁到運行中的進程時事件發生.要注意的是當debugge的主線程被創建時不會收到該通知.
EXIT_THREAD_DEBUG_EVENT debuggee中的線程退出時事件發生.debugee的主線程退出時不會收到該通知.我們可以認為debuggee的主線程與debugge進程是同義詞. 因此, 當我們的程序看到CREATE_PROCESS_DEBUG_EVENT標志時,對主線程來說,就是CREATE_THREAD_DEBUG_EVENT標志.
LOAD_DLL_DEBUG_EVENT debuggee裝入一個DLL.當PE裝載器第一次分解指向DLL的鏈接時,我們將收到這一事件. (當調用CreateProcess裝入 debuggee時)並且當debuggee調用LoadLibrary時也會發生.
UNLOAD_DLL_DEBUG_EVENT 一個DLL從debuggee中卸載時事件發生.
EXCEPTION_DEBUG_EVENT 在debuggee中發生異常時事件發生. 注意: 該事件僅在debuggee開始它的第一條指令之前發生一次.異常實際上是一個調試中斷(int 3h).如果想恢復debuggee事,以 DBG_CONTINUE 標志調用ContinueDebugEvent 函數. 不要使用DBG_EXCEPTION_NOT_HANDLED 標志否則debuggee會在NT下拒絕運行(Win98下運行得很好).
OUTPUT_DEBUG_STRING_EVENT 當debuggee調用DebugOutputString函數向我們的程序發送消息字符串時該事件發生.
RIP_EVENT 系統調試發生錯誤

dwProcessId 和dwThreadId發生調試事件的進程和線程Id.我們可以用這些值作為我們感興趣的進程或線程的標志符.記住如果我們使用CreateProcess來裝載debuggee,我們仍可在PROCESS_INFO結構中獲得debuggee的進程和線程.我們可以用這些值來區別調試事件是發生在debuggee中還是它的子進程中(當沒有指定 DEBUG_ONLY_THIS_PROCESS 標志時).

u 是一個聯合,包含了調試事件的更多信息.根據上面dwDebugEventCode的不同,它可以是以下結構:

dwDebugEventCode u的解釋
CREATE_PROCESS_DEBUG_EVENT 名為CreateProcessInfo的CREATE_PROCESS_DEBUG_INFO結構
EXIT_PROCESS_DEBUG_EVENT 名為ExitProcess的EXIT_PROCESS_DEBUG_INFO結構
CREATE_THREAD_DEBUG_EVENT 名為CreateThread的CREATE_THREAD_DEBUG_INFO結構
EXIT_THREAD_DEBUG_EVENT 名為ExitThread的EXIT_THREAD_DEBUG_EVENT 結構
LOAD_DLL_DEBUG_EVENT 名為LoadDll的LOAD_DLL_DEBUG_INFO 結構
UNLOAD_DLL_DEBUG_EVENT 名為UnloadDll的UNLOAD_DLL_DEBUG_INFO結構
EXCEPTION_DEBUG_EVENT 名為Exception的EXCEPTION_DEBUG_INFO結構
OUTPUT_DEBUG_STRING_EVENT 名為DebugString的OUTPUT_DEBUG_STRING_INFO 結構
RIP_EVENT 名為RipInfo的RIP_INFO 結構

我不會在這一個教程裡講所有這些結構的細節,這裡只詳細講一下CREATE_PROCESS_DEBUG_INFO 結構.
假設我們的程序調用了WaitForDebugEvent函數並返回,我們要做的第一件事就是檢查dwDebugEventCode中的值來看debuggee進程中發生了那種類型的調試事件.比如說,如果dwDebugEventCode的值為 CREATE_PROCESS_DEBUG_EVENT,就可認為u的成員為CreateProcessInfo 並用u.CreateProcessInfo來訪問.

在我們的程序中做對調試事件的響應. 當WaitForDebugEvent 返回時,這意味著在debuggee進程中發生了調試事件或者發生了超時.所以我們的程序要檢查dwDebugEventCode 來作出適當的反應.這裡有些象處理Windows消息:由用戶來選擇和忽略消息.
繼續運行debuggee. 當調試事件發生時, Windows掛起了debuggee,所以當我們處理完調試事件,還要讓debuggee繼續運行.調用ContinueDebugEvent 函數來完成這一過程.
ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

該函數恢復由於調試事件而掛起的線程.
dwProcessId和dwThreadId是要恢復的線程的進程ID和線程ID,通常這兩個值從 DEBUG_EVENT結構的dwProcessId 和dwThreadId成員獲得.
dwContinueStatus顯示了如何繼續報告調試事件的線程.可能的取值有兩個: DBG_CONTINUE 和DBG_EXCEPTION_NOT_HANDLED. 對大多數調試事件,這兩個值都一樣:恢復線程.唯一的例外是EXCEPTION_DEBUG_EVENT,如果線程報告發生了一個異常調試事件,這意味著在debuggee的線程中發生了一個異常.如果指定了DBG_CONTINUE,線程將忽略它自己的異常處理部分並繼續執行.在這種情況下,我們的程序必須在以DBG_CONTINUE恢復線程之前檢查並處理異常,否則異常將生生不息地不斷發生....如果我們指定了 DBG_EXCEPTION_NOT_HANDLED值,就是告訴Windows我們的程序並不處理異常:Windows將使用debuggee的默認異常處理函數來處理異常.
總而言之,如果我們的程序沒有考慮異常,而調試事件又指向debuggee進程中的一個異常的話,就應調用含DBG_CONTINUE標志的ContinueDebugEvent函數.否則,我們的程序就必須以DBG_EXCEPTION_NOT_HANDLED調用 ContinueDebugEvent.但在下面這種情況下必須使用DBG_CONTINUE標志:第一個在ExceptionCode成員中有值EXCEPTION_BREAKPOINT的 EXCEPTION_DEBUG_EVENT事件.當debuggee開始執行它的第一條指令時,我們的函數將接受到異常調試事件.它事實上是一個調試中斷(int 3h).如果我們以DBG_EXCEPTION_NOT_HANDLED調用ContinueDebugEvent 來響應調試事件, Windows NT會拒絕執行debuggee(因為它沒有異常處理).所以在這種情況下,要用DBG_CONTINUE標志告訴Windows我們希望該線程繼續執行.

繼續上面的步驟循環直到debuggee進程退出. 我們的程序必須在一個很象消息循環的無限循環中直到debuggee結束.該循環大體如下:
.while TRUE
invoke WaitForDebugEvent, addr DebugEvent, INFINITE
.break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
<調試事件處理>
invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw


就是說,當開始調試程序時,我們的程序不能和debuggee分開直到它結束.

我們再來總結一下這些步驟:

創建一個進程或捆綁我們的程序到運行中的進程上.
等待調試事件
響應調試事件.
繼續執行debuggee.
繼續這一無盡循環直到debuggee進程結束
例子:
這個例子調試一個win32程序並顯示諸如進程句柄,進程Id,映象基址等.

.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

分析:
程序首先填充OPENFILENAME結構,調用GetOpenFileName讓用戶選擇要調試的程序.

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

當接收用戶選擇後,調用CreateProcess裝載程序.並調用GetStartupInfo以默認值填充STARTUPINFO結構.注意我們將DEBUG_PROCESS標志與DEBUG_ONLY_THIS_PROCESS標志組合來僅調試這個程序,不包括子進程.

.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE


在debuggee被裝入後,我們調用WaitForDebugEvent進入無盡的調試循環,WaitForDebugEvent在debuggee中發生調試事件時返回,因為我們指定了INFINITE作為第二個參數.當調試事件發生時, WaitForDebugEvent 返回並填充DBEvent結構.

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

我們要先檢查dwDebugEventCode的值, 如果是EXIT_PROCESS_DEBUG_EVENT,用一個消息框顯示"The debuggee exits" 並退出調試循環.

.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

如果dwDebugEventCode 的值為CREATE_PROCESS_DEBUG_EVENT,我們就在消息框中顯示一些感興趣的底層信息.這些信息從u.CreateProcessInfo獲得. CreateProcessInfo是一個CREATE_PROCESS_DEBUG_INFO類型的結構體.你可以查閱Win32 API獲得它的更多信息e.

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

如果dwDebugEventCode 的值為EXCEPTION_DEBUG_EVENT,我們就要更進一步檢查異常類型.它是一大堆的結構嵌套,但我們可以從ExceptionCode成員獲得異常類型.如果ExceptionCode的值為 EXCEPTION_BREAKPOINT並且是第一次發生(或者我們已知道deuggee中沒有int 3h指令),我們可以安全地假定在debuggee要執行第一條指令時發生這一異常.在我們完成這些處理後,就可以用 DBG_CONTINUE調用ContinueDebugEvent來繼續執行debuggee.接著我們繼續等待下一個調試事件的發生.(編程入門網)

.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

如果dwDebugEventCode 的值為CREATE_THREAD_DEBUG_EVENT或EXIT_THREAD_DEBUG_EVENT, 我們的程序顯示一個消息框.

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

除了上面討論過的 EXCEPTION_DEBUG_EVENT,用DBG_EXCEPTION_NOT_HANDLED標志調用ContinueDebugEvent函數恢復debuggee的執行.

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

當debuggee結束時,我們就跳出了調試循環,這時要關閉 debuggee的線程和進程句柄.關閉這些句柄並不意味著要關閉這些進程和線程.只是說不再用這些句柄罷了.

理論:
在前面一章中,我們學會了如何裝載被調試的進程以及如何處理進程中發生的事件。為了有實際用途,我們的程序應具有修改被調試程序的能力。有好幾個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指令插入到被調試進程中。產生斷點。

理論:
如果你以前使用過調試器,那麼你應對跟蹤比較熟悉。當"跟蹤"一個程序時,程序在每執行一條指令後將會停止,這使你有機會去檢查寄存器/內存中的值。這種單步運行的官方定義為跟蹤(tracing)。
單步運行的特色是由CPU本身提供的。標志寄存器的第8位稱為陷阱標志trap flag。如果該位設置,則CPU運行於單步模式。CPU將在每條指令後產生一個debug異常。當debug 異常產生後,陷阱標志自動清除。利用win32調試api,我們也可以單步運行被調試程序。方法如下:

調用GetThreadContext, 指定 ContextFlags為CONTEXT_CONTROL, 來獲得標志寄存器的值
設置CONTEXT結構成員標志寄存器regFlag中的陷阱標志位
調用 SetThreadContext
等待調式事件。被調試程序將按單步模式執行,在每執行一條指令後,我們將得到調試 事件,u.Exception.pExceptionRecord.ExceptionCode值為EXCEPTION_SINGLE_STEP
如果要跟蹤下一條指令,需要再次設置陷阱標志位。
例:
.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.4",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0Dh,0Ah
db "Total Instructions executed : %lu",0
TotalInstruction dd 0

.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
context CONTEXT <>

.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 wsprintf, addr buffer, addr ExitProc, TotalInstruction
invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context
or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
inc TotalInstruction
invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.endif
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
invoke ExitProcess, 0
end start

分析:
該程序先顯示一個打開文件對話框,當用戶選擇了一個可執行文件,它將單步執行該程序,並記錄執行的指令數,直到被調試程序退出運行。

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT

利用該機會來設置被調試程序為單步運行模式。記住,在執行被調試程序的第一條指令前 windows將發送一個EXCEPTION_BREAKPOINT消息。

mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context

調用GetThreadContext,以被調試程序的當前寄存器內容來填充CONTEXT 結構 特別地,我們需要標志寄存器的當前值。

or context.regFlag,100h

設置標志寄存器映象的陷阱位(第8位)

invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue

然後調用SetThreadContext去覆蓋CONTEXT的值。再以DBG_CONTINUE調用 ContinueDebugEvent 來恢復被調試程序的運行。

.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
inc TotalInstruction

當調試程序中一條指令執行後,我們將接收到EXCEPTION_DEBUG_EVENT的調試事件, 必須要檢查u.Exception.pExceptionRecord.ExceptionCode的值。如果該值為 EXCEPTION_SINGLE_STEP,那麼,該調試事件是單步運行模式造成的。在這種情況 下,TotalInstruction加一,因為我們確切地知道此時被調試程序執行了一條指令。

invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
.continue


由於陷阱標志在debug異常後自動清除了,如果我們需要繼續保持單步運行模式,則必須設置陷阱標志位。
警告: 不要用本教程中的此例子來調試大程序: 跟蹤是很慢的。你或許需要等待10 多分鐘才能關閉被調試程序。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved