win32 调试 API 学习总结
Win32調(diào)試API原理
來自《軟件技術(shù)加密內(nèi)幕》和chm版本不太一樣?在Win32中自帶了一些API函數(shù),它們提供了相當于一般調(diào)試器的大多數(shù)功能,這些函數(shù)統(tǒng)稱為Win32調(diào)試API(Win32 Debug API)。利用這些API可以做到加載一個程序或捆綁到一個正在運行的程序上以供調(diào)試;可以獲得被調(diào)試的程序的底層信息,例如進程ID、進入地址、映像基址等;甚至可以對被調(diào)試的程序進行任意的修改,包括進程的內(nèi)存、線程的運行環(huán)境等。
簡而言之,讀者可以用這些API寫一個進程調(diào)試器。就像現(xiàn)在流行的調(diào)試器Visual C++調(diào)試器、WinDBG、OllyDbg等一樣。當然除了能寫調(diào)試器外,利用調(diào)試API還能做很多不同尋常的工作。
3.1 ?Win32調(diào)試API原理
3.1.1 ?調(diào)試相關(guān)函數(shù)簡要說明
Windows提供了一組Win32 Debug API,其具體定義如下。(1)ContinueDebugEvent函數(shù)
說明:此函數(shù)允許調(diào)試器恢復先前由于調(diào)試事件而掛起的線程。語法:BOOL ContinueDebugEvent(DWORD dwProcessId,DWORD dwThreadId, DWORD dwContinueStatus )
參數(shù):
dwProcessId ? ? ? ? DWORD ?被調(diào)試進程的進程標識符
dwThreadId ? ? ? ? ? DWORD ?欲恢復線程的線程標識符
dwContinueStatus ?DWORD ?此值指定了該線程將以何種方式繼續(xù),包含兩個定義值DBG_CONTINUE和DBG_EXCEPTION_NOT_HANDLED
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數(shù)成功,則返回非零值;如果失敗,則返回零
?
(2)DebugActiveProcess ??
說明:此函數(shù)允許將調(diào)試器捆綁到一個正在運行的進程上。語法:BOOL DebugActiveProcess(DWORD dwProcessId )
參數(shù):
dwProcessId ? ? ? ? DWORD ?欲捆綁進程的進程標識符
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數(shù)成功,則返回非零值;如果失敗,則返回零
(3)DebugActiveProcessStop
說明:此函數(shù)允許將調(diào)試器從一個正在運行的進程上卸載。語法:BOOL DebugActiveProcessStop(DWORD dwProcessId )
參數(shù):
dwProcessId ? ? ? ? DWORD ?欲卸載的進程的進程標識符
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數(shù)成功,則返回非零值;如果失敗,則返回零
注意:Windows 9x內(nèi)核不支持此函數(shù)。
(4)DebugBreak
說明:在當前進程中產(chǎn)生一個斷點異常,如果當前進程不是處在被調(diào)試狀態(tài),那么這個異常將被系統(tǒng)例程接管,多數(shù)情況下會導致當前進程被終止。語法:VOID DebugBreak(VOID)
參數(shù):無
其他:其實這個函數(shù)的用處與在程序中直接插入INT 3的效果是一樣的,如果反編譯Windows 98的KERNEL32.dll,讀者可以發(fā)現(xiàn)這個函數(shù)只包含兩句,一句是INT 3,一句是RET。
(5)DebugBreakProcess
說明:在指定進程中產(chǎn)生一個斷點異常。語法:VOID DebugBreakProcess (HANDLE hProcess)
參數(shù):
hProcess ? ? ? ? ? ? ? HANDLE ?進程的句柄
返回值 ? ? ? ? ? ? ? ? ?無
(6)FatalExit
說明:此函數(shù)將使調(diào)用進程強制退出,將控制權(quán)轉(zhuǎn)移至調(diào)試器。與ExitProcess不同的是,在退出前會先調(diào)用一個INT 3斷點。語法:VOID FatalExit(int ExitCode)
參數(shù):
ExitCode ? ? ? ? ? ? ? int ?退出碼
返回值 ? ? ? ? ? ? ? ? ?無
(7)FlushInstructionCache
說明:刷新指令高速緩存。語法:BOOL FlushInstructionCache(HANDLE hProcess, LPCVOID lpBassAddress, SIZE_T dwSize)
參數(shù):
hProcess ? ? ? ? ? ? ? HANDLE ?進程的句柄
lpBassAddress ? ? ? ?LPCVOID ?欲刷新區(qū)域的基地址
dwSize ? ? ? ? ? ? ? ? ?SIZE_T ?欲刷新區(qū)域的長度
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數(shù)成功,則返回非零值;如果失敗,則返回零
(8)GetThreadContext
說明:獲取指定線程的執(zhí)行環(huán)境。語法:BOOL GetThreadContext(HANDLE hThread, LPCONTEXT lpContext )
參數(shù):
hThread ? ? ? ? ? ? ? ?HANDLE ?欲獲取執(zhí)行環(huán)境的線程的句柄
lpContext ? ? ? ? ? ? ? LPCONTEXT ?指向CONTEXT結(jié)構(gòu)的指針
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數(shù)成功,則返回非零值;如果失敗,則返回零
(9)GetThreadSelectorEntry
說明:此函數(shù)返回指定選擇器和線程的描述符表的入口地址。語法:BOOL GetThreadSelectorEntry( HANDLE hThread,DWORD dwSelector, ? ?LPLDT_ENTRY lpSelectorEntry )
參數(shù):
hThread ? ? ? ? ? ? ? ?HANDLE ?包含指定選擇器的線程的句柄
dwSelector ? ? ? ? ? ?DWORD ?選擇器數(shù)目
lpSelectorEntry ? ? ?LPLDT_ENTRY ?指向用來接收描述符表的結(jié)構(gòu)的指針
返回值 ? ? ? ? ? ? ? ? ?如果函數(shù)成功,則返回非零值,此外lpSelectorEntry指向的結(jié)構(gòu)中將被填入接收到的描述符表;如果失敗,則返回零
(10)IsDebuggerPresent
說明:此函數(shù)用來判斷調(diào)用進程是否處于被調(diào)試環(huán)境中。語法:BOOL IsDebuggerPresent(VOID)
參數(shù):
返回值 ? ? ? ? ? ? ? ? ? BOOL:如果進程處在被調(diào)試狀態(tài),則返回非零值,不是處在被調(diào)試狀態(tài)則返回零
(11)OutputDebugString
說明:將一個字符串傳遞給調(diào)試器顯示。語法:VOID OutputDebugString(LPCYSTR lpOutputString)
參數(shù):
lpOutputString ? ? ? LPCYSTR:指向要顯示的以“00”結(jié)尾的字符串的指針
返回值 ? ? ? ? ? ? ? ? ?無
(12)ReadProcessMemory
說明:讀取指定進程的某區(qū)域內(nèi)的數(shù)據(jù)。語法:BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBassAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesRead)
參數(shù):
hProcess ? ? ? ? ? ? ?HANDLE ?進程的句柄
lpBassAddress ? ? ? ?LPCVOID ?欲讀取區(qū)域的基地址
lpBuffer ? ? ? ? ? ? ? ? LPVOID ?保存讀取數(shù)據(jù)的緩沖的指針
nSize ? ? ? ? ? ? ? ? ? ? SIZE_T ?欲讀取的字節(jié)數(shù)
lpNumberOfBytesRead ? SIZE_T ?存儲已讀取字節(jié)數(shù)的地址指針
返回值 ? ? ? ? ? ? ? ? ?BOOL:如果函數(shù)成功,則返回非零值;如果失敗,則返回零
(13)SetThreadContext
說明:設置指定線程的執(zhí)行環(huán)境。語法:BOOL SetThreadContext(HANDLE hThread, LPCONTEXT lpContext )
參數(shù):
hThread ? ? ? ? ? ? ? ?HANDLE ?欲設置執(zhí)行環(huán)境的線程的句柄
lpContext ? ? ? ? ? ? ? LPCONTEXT ?指向CONTEXT結(jié)構(gòu)的指針
返回值 ? ? ? ? ? ? ? ? ?BOOL:如果函數(shù)成功,則返回非零值;如果失敗,則返回零
(14)WaitForDebugEvent
說明:此函數(shù)用來等待被調(diào)試進程發(fā)生調(diào)試事件。語法:BOOL WaitForDebugEvent(LPDEBUG_ENENT lpDebugEvent, DWORD dwMilliseconds)
參數(shù):
lpDebugEvent ? ? ? ?LPDEBUG_ENENT ?指向接收調(diào)試事件信息的DEBUG_ ENENT結(jié)構(gòu)的指針
dwMilliseconds ? ? ?DWORD ?該函數(shù)用來等待調(diào)試事件發(fā)生的毫秒數(shù),如果這段時間內(nèi)沒有調(diào)試事件發(fā)生,函數(shù)將返回調(diào)用者;如果將該參數(shù)指定為INFINITE,函數(shù)將一直等待直到調(diào)試事件發(fā)生
返回值 ? ? ? ? ? ? ? ? ?BOOL:如果函數(shù)成功,則返回非零值;如果失敗,則返回零
(15)WriteProcessMemory
說明:在指定進程的某區(qū)域內(nèi)寫入數(shù)據(jù)。語法:BOOL WriteProcessMemory(HANDLE hProcess, LPCVOID lpBassAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesRead)
參數(shù):
hProcess ? ? ? ? ? ? ?HANDLE ?進程的句柄
lpBassAddress ? ? ? ?LPCVOID ?欲寫入?yún)^(qū)域的基地址
lpBuffer ? ? ? ? ? ? ? ? LPVOID ?保存欲寫入數(shù)據(jù)的緩沖的指針
nSize ? ? ? ? ? ? ? ? ? ? SIZE_T ?欲寫入的字節(jié)數(shù)
lpNumberOfBytesRead ? SIZE_T ?存儲已寫入字節(jié)數(shù)的地址的指針
返回值 ? ? ? ? ? ? ? ? ?BOOL:如果函數(shù)成功,則返回非零值;如果失敗,則返回零
3.1.2 ?調(diào)試事件
作為調(diào)試器,監(jiān)視目標進程的執(zhí)行、對目標進程發(fā)生的每一個調(diào)試事件做出應有的反應是它的主要工作。當目標進程發(fā)生一個調(diào)試事件后,系統(tǒng)將會通知調(diào)試器來處理這個事件。調(diào)試器將利用WaitForDebugEvent函數(shù)來獲取目標進程的相關(guān)環(huán)境信息。可能存在的調(diào)試事件類型如表3-1所示。表3-1 ?調(diào)試事件
調(diào)試事件
含 ? ?義
CREATE_PROCESS_DEBUG_EVENT
進程被創(chuàng)建。當調(diào)試的進程剛被創(chuàng)建(還未運行)或調(diào)試器開始調(diào)試已經(jīng)激活的進程時,就會生成這個事件
CREATE_THEAD_DEBUG_EVENT
在調(diào)試進程中創(chuàng)建一個新的進程或調(diào)試器開始調(diào)試已經(jīng)激活的進程時,就會生成這個調(diào)試事件。要注意的是當調(diào)試的主線程被創(chuàng)建時不會收到該通知
EXCEPTION_DEBUG_EVENT
在調(diào)試的進程中出現(xiàn)了異常,就會生成該調(diào)試事件
EXIT_PROCESS_DEBUG_EVENT
每當退出調(diào)試進程中的最后一個線程時,產(chǎn)生這個事件
EXIT_THREAD_DEBUG_EVENT
調(diào)試中的線程退出時事件發(fā)生,調(diào)試的主線程退出時不會收到該通知
LOAD_DLL_DEBUG_EVENT
每當被調(diào)試的進程裝載DLL文件時,就生成這個事件。當PE裝載器第一次解析出與DLL文件有關(guān)的鏈接時,將收到這一事件。調(diào)試進程使用了LoadLibrary時也會發(fā)生。每當DLL文件裝載到地址空間中去時,都要調(diào)用該調(diào)試事件
OUTPUT_DEBUG_STRING_EVENT
當調(diào)試進程調(diào)用DebugOutputString函數(shù)向程序發(fā)送消息字符串時該事件發(fā)生
UNLOAD_DLL_DEBUG_EVENT
每當調(diào)試進程使用FreeLibrary函數(shù)卸載DLL文件時,就會生成該調(diào)試事件。僅當最后一次從過程的地址空間卸載DLL文件時,才出現(xiàn)該調(diào)試事件(也就是說DLL文件的使用次數(shù)為0時)
RIP_EVENT
只有Windows 98檢查過的構(gòu)件才會生成該調(diào)試事件。該調(diào)試事件是報告錯誤信息
當WaitForDebugEvent接收到一個調(diào)試事件時,它將把調(diào)試事件的信息填寫入DEBUG_EVENT結(jié)構(gòu)中并返回。這個結(jié)構(gòu)定義如下:
typedef struct _DEBUG_EVENT { ??
DWORD dwDebugEventCode; ?
DWORD dwProcessId; ?
DWORD dwThreadId; ?
union { ?
EXCEPTION_DEBUG_INFO Exception; ?
CREATE_THREAD_DEBUG_INFO CreateThread; ?
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; ?
EXIT_THREAD_DEBUG_INFO ExitThread; ?
EXIT_PROCESS_DEBUG_INFO ExitProcess; ?
LOAD_DLL_DEBUG_INFO LoadDll; ?
UNLOAD_DLL_DEBUG_INFO UnloadDll; ?
OUTPUT_DEBUG_STRING_INFO DebugString; ? ? ? ? ?
RIP_INFO RipInfo; ?
} u; ?
} DEBUG_EVENT; ?
?
在dwDebugEventCode中的值標記了所發(fā)生的調(diào)試事件的類型。dwProcessId的值是調(diào)試事件所發(fā)生的進程的標識符。dwThreadId的值是調(diào)試事件所發(fā)生的線程的標識符。u結(jié)構(gòu)包含了關(guān)于調(diào)試事件的更多信息,根據(jù)上面dwDebugEventCode的不同,它可以是表3-2中所示的結(jié)構(gòu)。
表3-2 ?事件信息與u結(jié)構(gòu)成員
dwDebugEventCode
u的解釋
CREATE_PROCESS_DEBUG_EVENT
名為CreateProcessInfo的CREATE_PROCESS_DEBUG_INFO結(jié)構(gòu)
EXIT_PROCESS_DEBUG_EVENT
名為ExitProcess的EXIT_PROCESS_DEBUG_INFO結(jié)構(gòu)
CREATE_THREAD_DEBUG_EVENT
名為CreateThread的CREATE_THREAD_DEBUG_INFO結(jié)構(gòu)
EXIT_THREAD_DEBUG_EVENT
名為ExitThread的EXIT_THREAD_DEBUG_EVENT 結(jié)構(gòu)
LOAD_DLL_DEBUG_EVENT
名為LoadDll的LOAD_DLL_DEBUG_INFO 結(jié)構(gòu)
UNLOAD_DLL_DEBUG_EVENT
名為UnloadDll的UNLOAD_DLL_DEBUG_INFO結(jié)構(gòu)
EXCEPTION_DEBUG_EVENT
名為Exception的EXCEPTION_DEBUG_INFO結(jié)構(gòu)
OUTPUT_DEBUG_STRING_EVENT
名為DebugString的OUTPUT_DEBUG_STRING_INFO 結(jié)構(gòu)
RIP_EVENT
名為RipInfo的RIP_INFO 結(jié)構(gòu)
那么如何訪問這些數(shù)據(jù)呢?假設程序調(diào)用了WaitForDebugEvent函數(shù)并返回,那么要做的第一件事就是檢查dwDebugEventCode字段中的值,根據(jù)它來判斷debugger進程中發(fā)生了哪種類型的調(diào)試事件。比如說,如果dwDebugEventCode字段的值為 CREATE_PROCESS_DEBUG_EVENT,就可認為u的成員為CreateProcessInfo 并可通過u.CreateProcessInfo來訪問。
下面是常用的CREATE_PROCESS_DEBUG_INFO結(jié)構(gòu)的簡要說明。
CREATE_PROCESS_DEBUG_INFO結(jié)構(gòu)定義:
typedef struct _CREATE_PROCESS_DEBUG_INFO { ?
? HANDLE hFile; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 進程文件的句柄,利用它可對文件進行操作 ?
? HANDLE hProcess; ? ? ? ? ? ? ? ? ? ? ? ? // 進程的句柄,在進程空間中進行讀寫操作時要用到它 ?
? HANDLE hThread; ? ? ? ? ? ? ? ? ? ? ? ? ?// 主線程的句柄,在讀取、設置線程環(huán)境時都要用到它 ?
? LPVOID lpBaseOfImage; ? ? ? ? ? ? ? ? ?// 進程執(zhí)行的映像基地址 ?
? DWORD dwDebugInfoFileOffset; ? ? ? ?
? DWORD nDebugInfoSize; ?
? LPVOID lpThreadLocalBase; ?
? LPTHREAD_START_ROUTINE lpStartAddress; ?
? LPVOID lpImageName; ?
? WORD fUnicode; ?
} CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO; ?
?
3.1.3 ?如何在調(diào)試時創(chuàng)建并跟蹤一個進程
這是使用Win32調(diào)試API的第一步。可以通過以下方式創(chuàng)建進程。1. 如何創(chuàng)建一個新進程以供調(diào)試
通過CreateProcess創(chuàng)建新進程時,如果在dwCreationFlags標志字段中設置了DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS標志,將創(chuàng)建一個用以調(diào)試的新進程。如果是以DEBUG_PROCESS標志創(chuàng)建新進程,調(diào)試器將會接收到目標進程及由目標進程創(chuàng)建的所有子進程發(fā)生的所有調(diào)試事件,一般來說這是沒有必要的。建議可以指定DEBUG_ONLY_THIS_PROCESS和DEBUG_PROCESS的組合標志來禁止它,如果設置了DEBUG_ONLY_THIS_PROCESS標志,調(diào)試器將只會收到目標進程的調(diào)試事件,而對其子進程的調(diào)試事件不予理睬。當進程創(chuàng)建成功后,可以通過查看PROCESS_INFORMATION結(jié)構(gòu)來獲取被創(chuàng)建進程及其主線程的進程標識符和線程標識符。
由于操作系統(tǒng)將調(diào)試對象標記為在特殊模式下運行,所以,可以使用IsDebuggerPresent函數(shù)查看進程是否在調(diào)試器下運行。
2. 如何將調(diào)試器捆綁到一個正在運行的進程上
利用DebugActiveProcess函數(shù)可以將調(diào)試器捆綁到一個正在運行的進程上,如果執(zhí)行成功,則效果類似于利用DEBUG_ONLY_THIS_PROCESS標志創(chuàng)建的新進程。
要注意的是,在NT內(nèi)核下當試圖通過DebugActiveProcess函數(shù)將調(diào)試器捆綁到一個創(chuàng)建時帶有安全描述符的進程上時,將被拒絕。在Windows 9x中則簡單得多,只有當指定了一個無效的進程標識符時調(diào)用才會失敗。所以看上去NT內(nèi)核的系統(tǒng)要更安全。
將調(diào)試器捆綁到一個進程上一般是比較好的一種做法,但是有時除了利用CreateProcess函數(shù)來載入進程外沒有其他的辦法。那么到底該用哪種方法呢?這涉及讀者將要進行的工作。比如要做一個簡單的游戲修改器時,用臨時捆綁調(diào)試器的方法可能比較好,如果要做一些不同尋常的工作的話,利用載入的方法可能更好,因為它獲得目標進程及其線程的所有控制權(quán),這樣就可以為所欲為了。
3.1.4 ?調(diào)試循環(huán)體
用調(diào)試API建立一個簡單的調(diào)試程序是非常簡單的,所有要做的只是創(chuàng)建一個用來調(diào)試的新進程,然后執(zhí)行相關(guān)代碼來監(jiān)視所有的調(diào)試事件。筆者把監(jiān)視所有的調(diào)試事件的這部分代碼稱為“調(diào)試循環(huán)體”,為什么呢?因為它的實現(xiàn)非常簡單,看上去就像一個“while”循環(huán),所需要做的只是使用WaitForDebugEvent和ContinueDebugEvent函數(shù)。就像上面說的,WaitForDebugEvent在一段時間內(nèi)等待目標進程中調(diào)試事件的發(fā)生,如果在這段時間沒有調(diào)試事件發(fā)生,那么函數(shù)將返回FALSE。如果在指定時間內(nèi)調(diào)試事件發(fā)生了,那么函數(shù)將返回TRUE,并且它會把所發(fā)生的調(diào)試事件及其相關(guān)信息填寫入一個DEBUG_EVENT結(jié)構(gòu)。然后調(diào)試器會檢查這些信息,并據(jù)此做出相應的反應。在對這些事件做出相應的操作后,就可以使用ContinueDebugEvent函數(shù)來恢復線程的執(zhí)行,并等待下一個調(diào)試事件的發(fā)生。要注意的一點是WaitForDebugEvent只能使用在創(chuàng)建的或是捆綁上的進程中的某個線程上。WaitForDebugEvent – ContinueDebugEvent循環(huán)的C語言示例:
PROCESS_INFORMATION ? ? ? pi; ?
STARTUP_INFO ? ? ? ? ? ? ? ? ? ? ?si; ?
DEBUG_EVENT ? ? ? ? ? ? ? ? ? ? ? devent; ?
If ? ? ? ?(CreateProcess( 0 , "target.exe" ? ?, 0 , 0 ,FALSE ,DEBUG_ONLY_THIS_PROCESS , 0 ,0 ,&si , π)) ?
{ ?
? ? ? ? ?while(TRUE) ? ? ? ? ??
? ? ? ? ? ? ? ? ? ?{ ?
? ? ? ? ? ? ? ? ? ?if ? ? ? (WaitForDebugEvent( &devent , 150)) ? ? ? ? ? ? //在150毫秒內(nèi)等待調(diào)試事件 ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? { ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? switch (devent.dwDebugEventCode) ?
{ ?
case CREATE_PROCESS_DEBUG_EVENT: ?
//在此填入你的處理程序 ?
break; ? ? ? ? ? ?
case EXIT_PROCESS_DEBUG_EVENT: ?
//在此填入你的處理程序 ?
break; ?
case EXCEPTION_DEBUG_EVENT: ? ? ? ? ? ? ? ? ? ? ??
//在此填入你的處理程序 ?
break; ?
} ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ? ? ContinueDebugEvent(devent.dwProcessId , devent.dwThreadId , DBG_CONTINUE); ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? } ?
? ? ? ? ? ? ? ? ? ?else ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? { ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? // 其他一些操作 ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? } ?
? ? ? ? ? ? ? ? ? ?} ?
? ? ? ? ?} ?// while循環(huán)結(jié)束 ?
else ?
? ? ? ? ?{ ?
? ? ? ? ?MessageBox(0,"Unexpected load error","Fatal Error" ,MB_OK); ? ? ? ?
? ? ? ? ?} ?
?
3.1.5 ?如何處理調(diào)試事件
在上一節(jié)的示例中已經(jīng)看到調(diào)試器如何捕獲調(diào)試事件,并且利用C/C++中定義的case/switch語法做出了相應的動作。當每一個調(diào)試事件發(fā)生時,根據(jù)事件的不同類型,都會有一段不同的處理程序來進行處理。關(guān)于調(diào)試事件的更多信息可以根據(jù)DEBUG_EVENT中U結(jié)構(gòu)的相應成員來取得。作為示例,再來看一下EXCEPTION_DEBUG_EVENT結(jié)構(gòu)。選擇它是因為遇到一個異常斷點并追蹤它的來龍去脈是經(jīng)常要做的事情。其他的事件結(jié)構(gòu)請參考API說明。EXCEPTION_DEBUG_EVENT結(jié)構(gòu):
[cpp] view plain copy
typedef struct _EXCEPTION_DEBUG_INFO ?
? ? ? ? ?{ ?
? ? ? ? ? ? ? ? ? ?EXCEPTION_RECORD ExceptionRecord; ?
? ? ? ? ? ? ? ? ? ?DWORD dwFirstChance; ?
? ? ? ? ?} EXCEPTION_DEBUG_INFO; ?
其中EXCEPTION_RECORD結(jié)構(gòu)包含異常的很多信息,內(nèi)容如下: ?
typedef struct _EXCEPTION_RECORD ?
? ? ? ? ? ? ? ? ? ?{ ?
? ? ? ? ? ? ? ? ? ?DWORD ExceptionCode; ?
? ? ? ? ? ? ? ? ? ?DWORD ExceptionFlags; ?
? ? ? ? ? ? ? ? ? ?struct _EXCEPTION_RECORD *ExceptionRecord; ?
? ? ? ? ? ? ? ? ? ?PVOID ExceptionAddress; ?
? ? ? ? ? ? ? ? ? ?DWORD NumberParameters; ?
? ? ? ? ? ? ? ? ? ?DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; ?
? ? ? ? ? ? ? ? ? ?} EXCEPTION_RECORD; ?
? ?
?
? ? ? ? ?結(jié)構(gòu)中字段的定義
ExceptionCode
ExceptionFlags
ExceptionRecord
ExceptionAddress
NumberParameters
ExceptionInformation
DWORD:用來描述異常類型的代碼。
DWORD:零表示可繼續(xù)異常。反之,則值為EXCEPTION_NONCONTINUABLE。
指向_EXCEPTION_RECORD結(jié)構(gòu)的指針。
PVOID:異常發(fā)生的地址。
DWORD:ExceptionInformation隊列中定義的32位參數(shù)的數(shù)目。
額外的32位消息隊列,主要在嵌套異常時使用,對多數(shù)異常情況,它沒有定義。
從這些結(jié)構(gòu)中可找到需要的一切,比如可以發(fā)現(xiàn):發(fā)生異常的類型、是否可以繼續(xù)執(zhí)行、異常發(fā)生的地址等信息。
*注意:在發(fā)生一個EXCEPTION_NONCONTINUABLE異常后如果試圖去繼續(xù)執(zhí)行,那么會產(chǎn)生一個EXCEPTION_NONCONTINUABLE_EXCEPTION異常。
最常見的異常情況是EXCEPTION_BREAKPOINT和EXCEPTION_SINGLE_ STEP。當執(zhí)行時遇到一個INT 3斷點時會產(chǎn)生EXCEPTION_BREAKPOINT異常,如果設置了單步執(zhí)行標志,那么執(zhí)行完一條指令后會發(fā)生一個EXCEPTION_SINGLE_ STEP異常。關(guān)于異常還有一點要知道,當以調(diào)試的方式創(chuàng)建一個進程的時候,在進入進程之前,系統(tǒng)會先執(zhí)行一次DebugBreak函數(shù),這樣會產(chǎn)生一個EXCEPTION_BREAKPOINT異常,如果一切正常,那么這應該是第一個遇到的也是必定會遇到的異常。
可以調(diào)用ContinueDebugEvent函數(shù)來繼續(xù)線程的運行,ContinueDebugEvent函數(shù)的dwContinueStatus參數(shù)有兩個取值,分別是DBG_EXCEPTION_NOT_HANDLED和DBG_CONTINUE。對于大多數(shù)調(diào)試事件,這兩個值沒有什么區(qū)別,都是恢復線程。惟一的例外是EXCEPTION_DEBUG_EVENT,如果線程報告發(fā)生了一個異常調(diào)試事件,就意味著在被調(diào)試的線程中發(fā)生了一個異常。如果指定了DBG_CONTINUE,線程將忽略它自己的異常處理部分并繼續(xù)執(zhí)行。在這種情況下,程序必須在以DBG_CONTINUE恢復線程之前檢查并處理異常,否則異常將不斷地發(fā)生,直至程序被系統(tǒng)終止。如果指定了 DBG_EXCEPTION_NOT_HANDLED值,就是告訴Windows:程序并不處理異常。Windows將使用被調(diào)試線程的默認異常處理函數(shù)來處理異常。這一般發(fā)生在什么時候呢?在進程被載入后發(fā)生的第一個EXCEPTION_DEBUG_EVENT,我們必須以DBG_CONTINUE為標志繼續(xù),在程序中如果調(diào)用了DebugBreak函數(shù),或者插入INT3設置斷點成功,并將內(nèi)存恢復后,都應該使用DBG_CONTINUE為標志繼續(xù)。如果在程序中發(fā)生了不確定的異常,特別是調(diào)試帶殼程序的時候,多半是由于外殼的SEH引起的,此時應該以DBG_EXCEPTION_NOT_HANDLED為標志繼續(xù),以便讓被調(diào)試程序本身的異常處理機制來處理。
在發(fā)生其他調(diào)試事件時,可以利用類似的結(jié)構(gòu)取得線程、進程所調(diào)用的DLL或其他一些事物的信息集合。?
3.1.6 ?線程環(huán)境詳解
在WIN32系統(tǒng)中,進程的概念實際包含了它的私有地址空間、代碼、數(shù)據(jù)和一個主線程。每個進程都有一個最初的主線程,通過這個主線程可以在以后創(chuàng)建在同一地址空間中運行的其他線程。和一般說法不同的是進程并不執(zhí)行代碼,真正執(zhí)行代碼的是線程。每個線程共同分享相同的地址空間和相同的系統(tǒng)資源,但是它們各自又有不同的執(zhí)行環(huán)境,這到底如何理解呢?Windows是一個多任務多線程的操作系統(tǒng),在系統(tǒng)的同一時間里看似運行著多個線程,但事實并非如此。Windows分配給每個線程一小段時間片,這段時間結(jié)束后,Windows將凍結(jié)當前線程并切換到下一個具有最高優(yōu)先級的線程。在切換之前,Windows將把當前線程執(zhí)行狀態(tài)保存到一個名為CONTEXT的結(jié)構(gòu)中。這個環(huán)境包含下面幾部分:● ? ? ? 線程執(zhí)行所用寄存器
● ? ? ? 系統(tǒng)堆棧和用戶堆棧
● ? ? ? 線程所用的描述符表等其他狀態(tài)信息
這樣當該線程再次恢復運行時,Windows就可以恢復最近一次線程運行的“環(huán)境”,好像中間什么都沒有發(fā)生一樣。
CONTEXT結(jié)構(gòu)包含了特定處理器的寄存器數(shù)據(jù),系統(tǒng)使用CONTEXT結(jié)構(gòu)執(zhí)行各種內(nèi)部操作。由于此結(jié)構(gòu)是依賴于硬件的,所以在X86,Alpha等不同的系統(tǒng)上,此結(jié)構(gòu)是不同的。下面是X86系統(tǒng)中此結(jié)構(gòu)的構(gòu)成情況:
[cpp] view plain copy
typedef struct _CONTEXT { ?
? ? DWORD ContextFlags; ? ? ? ?
? ? DWORD ? Dr0; ?
? ? DWORD ? Dr1; ?
? ? DWORD ? Dr2; ?
? ? DWORD ? Dr3; ?
? ? DWORD ? Dr6; ?
? ? DWORD ? Dr7; ?
? ? FLOATING_SAVE_AREA FloatSave; ?
? ? DWORD ? SegGs; ?
? ? DWORD ? SegFs; ?
? ? DWORD ? SegEs; ?
? ? DWORD ? SegDs; ?
? ? DWORD ? Edi; ?
? ? DWORD ? Esi; ?
? ? DWORD ? Ebx; ?
? ? DWORD ? Edx; ?
? ? DWORD ? Ecx; ?
? ? DWORD ? Eax; ?
? ? DWORD ? Ebp; ?
? ? DWORD ? Eip; ?
? ? DWORD ? SegCs; ? ? ? ? ? ? ??
? ? DWORD ? EFlags; ? ? ? ? ? ? ?
? ? DWORD ? Esp; ?
? ? DWORD ? SegSs; ?
} CONTEXT; ?
?
?
其中的FloatSave是指向FLOATING_SAVE_AREA結(jié)構(gòu)的指針,FLOATING_ SAVE_AREA結(jié)構(gòu)定義如下:
[cpp] view plain copy
typedef struct _FLOATING_SAVE_AREA { ?
? ? DWORD ? ControlWord; ?
? ? DWORD ? StatusWord; ?
? ? DWORD ? TagWord; ?
? ? DWORD ? ErrorOffset; ?
? ? DWORD ? ErrorSelector; ?
? ? DWORD ? DataOffset; ?
? ? DWORD ? DataSelector; ?
? ? BYTE ? ? RegisterArea[SIZE_OF_80387_REGISTERS]; ?
? ? DWORD ? Cr0NpxState; ?
} FLOATING_SAVE_AREA; ?
?
另外,其中的ContextFlags字段用于控制GetThreadContext和SetThreadContext處理那些環(huán)境信息。它的定義如下:
CONTEXT_CONTROL ? ? ? ? ? ? ? ? ? ? ?ContextFlags包含此標志時處理Ebp,Eip,Cs, Flages,Esp,Ss
CONTEXT_INTEGER ? ? ? ? ? ? ? ? ? ? ? ?ContextFlags包含此標志時處理 Edi,Esi,Ebx,Edx,Ecx,Eax
CONTEXT_SEGMENTS ? ? ? ? ? ? ? ? ? ?ContextFlags包含此標志時處理 GS,FS,ES,DS
CONTEXT_FLOATING_POINT ? ? ? ? ?ContextFlags包含此標志時處理 FLOATING_SAVE_AREA FloatSave
CONTEXT_DEBUG_REGISTERS ? ? ? ContextFlags包含此標志時處理 Dr0,Dr1,Dr2,Dr3,Dr6,Dr7
CONTEXT_FULL = (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS)
為什么在CONTEXT_FULL中不包含CONTEXT_DEBUG_REGISTERS和CONTEXT_FLOATING_POINT呢?或許這只能去問微軟,不過這看上去確實有點不合情理。
Windows實際上允許查看線程內(nèi)核對象的內(nèi)部情況,以便抓取它的當前一組CPU寄存器。若要進行這項操作,可以調(diào)用GetThreadContext和SetThreadContext函數(shù)。
GetThreadContext用來獲取指定線程的執(zhí)行環(huán)境,語法如下:
語法:BOOL GetThreadContext(HANDLE hThread, LPCONTEXT lpContext )
參數(shù):
hThread ? ? ? ? ? ? ? ? ? ?HANDLE:欲獲取執(zhí)行環(huán)境的線程的句柄
lpContext ? ? ? ? ? ? ? ? LPCONTEXT:指向CONTEXT結(jié)構(gòu)的指針
要注意的是,在使用GetThreadContext函數(shù)之前,必須先將ContextFlags初始化為適當?shù)臉酥?#xff0c;指明想要收回哪些寄存器,并將該結(jié)構(gòu)的地址傳遞給GetThreadContext函數(shù)。例如設置的標志是CONTEXT_CONTROL,只返回Ebp,Eip,Cs,Flages,Esp,Ss這些值。
SetThreadContext用來設置指定線程的執(zhí)行環(huán)境,語法如下:
語法:BOOL SetThreadContext(HANDLE hThread, ? ? ?LPCONTEXT lpContext )
參數(shù):
hThread ? ? ? ? ? ? ? ? ? ?HANDLE:欲設置執(zhí)行環(huán)境的線程的句柄
lpContext ? ? ? ? ? ? ? ? LPCONTEXT:指向CONTEXT結(jié)構(gòu)的指針
與GetThreadContext函數(shù)相類似,SetThreadContext函數(shù)也是通過ContextFlags的值來控制哪些數(shù)據(jù)將被恢復。
這兩個函數(shù)威力非凡。有了它們,對于被調(diào)試進程你就有了上帝的能力。如果改變其寄存器內(nèi)容,那么在被調(diào)試程序恢復運行前,這些值將會寫回寄存器中。在進程環(huán)境中所做的任何改動,都將反映到被調(diào)試程序中。想像一下:甚至可以改變EIP寄存器的內(nèi)容,這樣就可以讓程序運行到想要的任何地方! 在正常情況下是不可能做到這一點的。
在調(diào)用GetThreadContext函數(shù)之前,應該調(diào)用SuspendThread函數(shù),否則線程可能被調(diào)度,而且線程的環(huán)境可能與收回的不同。一個線程實際上有兩個環(huán)境:一個是用戶方式,一個是內(nèi)核方式。GetThreadContext函數(shù)只能返回線程的用戶方式環(huán)境。如果調(diào)用SuspendThread函數(shù)來停止線程的運行,但該線程目前正在用內(nèi)核方式運行,那么,即使SuspendThread實際上尚未暫停該線程的運行,它的用戶方式仍然處于穩(wěn)定狀態(tài)。線程在恢復用戶方式之前,無法執(zhí)行更多的用戶方式代碼,因此可以放心地將線程視為處于暫停狀態(tài),GetThreadContext函數(shù)將能正常運行。
所以,正確的做法應該是先利用SuspendThread函數(shù)暫停一個線程,當設置好環(huán)境后再利用ResumeThread函數(shù)來恢復它。但要注意的是:ResumeThread函數(shù)并不能保證線程真地繼續(xù)執(zhí)行,為什么呢?每一個線程都有一個線程暫停計數(shù)器,當線程正在運行時計數(shù)器為0,當其他線程對此線程使用SuspendThread函數(shù)時計數(shù)器會增加1,調(diào)用ResumeThread會使計數(shù)器減小1,所以當調(diào)用SuspendThread函數(shù)后計數(shù)器變?yōu)?。但是Windows是一個多線程操作系統(tǒng),所以很有可能某個其他的線程也對此線程調(diào)用了SuspendThread,這時計數(shù)器就會變?yōu)?,這時再調(diào)用ResumeThread只會使計數(shù)器變回為1,線程將繼續(xù)暫停,直到計數(shù)器變?yōu)?。那么,如何確定線程是否真地被繼續(xù)執(zhí)行了呢?很簡單,檢查函數(shù)返回值就可以了。如果返回值為0,則表示線程已經(jīng)恢復執(zhí)行了,如果不為0,則表示線程繼續(xù)被暫停,如果為0xffffffff,則說明函數(shù)調(diào)用失敗了。
同樣,在調(diào)用SetThreadContext函數(shù)之前,必須暫停,否則結(jié)果將無法預測。
3.1.7 ?如何在另一個進程中注入代碼
現(xiàn)在讓我們更深入一些來討論,有時候需要將一段代碼注入到某個進程的地址空間中,實際上這并不非常復雜,但在真正開始做之前先得解決一個小小的麻煩:首先得需要一小段地址空間來存放補丁代碼。這似乎很簡單,有的讀者會說,利用VirtualAllocEx不就可以了嗎?遺憾的是,VirtualAllocEx只在Windows NT內(nèi)核下被支持,在Windows 9X內(nèi)核下不被支持,那怎么辦呢?如果注入的代碼很短小,那么可以利用原進程的各個區(qū)塊之間的間隙,甚至可以把代碼注入到原進程文件頭中的DOS stub部分,當然這樣的話執(zhí)行之前先得更改目標進程文件頭的讀寫屬性。如果要注入的部分比較大呢?只能先將目標進程中的某個代碼頁保存,然后注入新的代碼,執(zhí)行完后再將原始的代碼寫回。具體的步驟如下:(1)利用CreateProcess函數(shù)創(chuàng)建一個供調(diào)試的進程。
(2)建立WaitForDebugEvent和ContinueDebugEvent構(gòu)成的調(diào)試循環(huán)體。
(3)利用SuspendThread函數(shù)掛起目標線程。
(4)利用VirtualProtectEx函數(shù)修改目標頁的讀寫權(quán)限。
(5)利用ReadProcessMemory函數(shù)讀取目標頁。
(6)利用GetThreadContext函數(shù)保存線程環(huán)境。
(7)利用WriteProcessMemory函數(shù)寫入新代碼頁。
(8)確認在新指令中的最后一個代碼是INT 3,我們需要利用它在指令執(zhí)行完成后獲得系統(tǒng)控制權(quán)。INT 3產(chǎn)生的異常將會被我們的程序捕獲,要注意的是必須確認這是一個breakpoint異常,并且是在我們放置INT 3的位置。
(9)保存一份CONTEXT結(jié)構(gòu)的臨時拷貝。
(10)在這份臨時拷貝中設置新的EIP值。
(11)恢復原線程的執(zhí)行,它將執(zhí)行我們的代碼,直到INT 3被執(zhí)行,當它被執(zhí)行時會被我們的程序捕獲,目標線程再次被掛起。
(12)利用WriteProcessMemory函數(shù)恢復原始代碼頁。
(13)恢復原始代碼頁的讀寫屬性。
(14)利用SetThreadContext恢復線程原始的環(huán)境。
(15)恢復原線程執(zhí)行。
如果需要讓注入的代碼和進程原始的代碼同時存在于進程空間中,而且準備注入的代碼比較大,則必須為目標進程分配一些地址空間。調(diào)用VirtuallAlloc的代碼是非常短小的,可以先在目標進程中注入調(diào)用VirtuallAlloc的代碼,利用它可以獲取一些額外的地址空間,一般來說幾個KB就足夠了,不要試圖去申請比如10MB的空間,那樣很容易導致執(zhí)行失敗。當然還有另一個辦法,可以在自己的進程中調(diào)用VirtualAllocEx,這也可以為目標進程分配一定的地址空間,可惜的是它只能運行在Windows NT內(nèi)核下。
如果為了某些工作,需要將某個區(qū)塊的相對地址轉(zhuǎn)換為線性虛擬地址,就可以使用函數(shù)GetThreadSelectorEntry。
最后提醒一下,向其他線程注入代碼時,千萬要注意堆棧的平衡問題,如果不注意,可能會產(chǎn)生非常嚴重的錯誤。
Win32一個調(diào)試器的實現(xiàn)
[Win32]一個調(diào)試器的實現(xiàn)(一)調(diào)試事件與調(diào)試循環(huán)
[Win32]一個調(diào)試器的實現(xiàn)(二)調(diào)試事件的處理?
[Win32]一個調(diào)試器的實現(xiàn)(三)異常?
[Win32]一個調(diào)試器的實現(xiàn)(四)讀取寄存器和內(nèi)存?
[Win32]一個調(diào)試器的實現(xiàn)(五)調(diào)試符號?
[Win32]一個調(diào)試器的實現(xiàn)(六)顯示源代碼?
[Win32]一個調(diào)試器的實現(xiàn)(七)斷點
[Win32]一個調(diào)試器的實現(xiàn)(八)單步執(zhí)行
[Win32]一個調(diào)試器的實現(xiàn)(九)符號模型?
[Win32]一個調(diào)試器的實現(xiàn)(十)顯示變量?
[Win32]一個調(diào)試器的實現(xiàn)(十一)顯示函數(shù)調(diào)用棧?
https://www.zhihu.com/question/52553014/answer/136312479
總結(jié)
以上是生活随笔為你收集整理的win32 调试 API 学习总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Perl学习总结
- 下一篇: C# 大型对象堆学习总结