日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

win32 调试 API 学习总结

發布時間:2025/4/14 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 win32 调试 API 学习总结 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Win32調試API原理

來自《軟件技術加密內幕》和chm版本不太一樣?

在Win32中自帶了一些API函數,它們提供了相當于一般調試器的大多數功能,這些函數統稱為Win32調試API(Win32 Debug API)。利用這些API可以做到加載一個程序或捆綁到一個正在運行的程序上以供調試;可以獲得被調試的程序的底層信息,例如進程ID、進入地址、映像基址等;甚至可以對被調試的程序進行任意的修改,包括進程的內存、線程的運行環境等。
簡而言之,讀者可以用這些API寫一個進程調試器。就像現在流行的調試器Visual C++調試器、WinDBG、OllyDbg等一樣。當然除了能寫調試器外,利用調試API還能做很多不同尋常的工作。

3.1 ?Win32調試API原理

3.1.1 ?調試相關函數簡要說明

Windows提供了一組Win32 Debug API,其具體定義如下。

(1)ContinueDebugEvent函數

說明:此函數允許調試器恢復先前由于調試事件而掛起的線程。
語法:BOOL ContinueDebugEvent(DWORD dwProcessId,DWORD dwThreadId, DWORD dwContinueStatus )
參數:
dwProcessId ? ? ? ? DWORD ?被調試進程的進程標識符
dwThreadId ? ? ? ? ? DWORD ?欲恢復線程的線程標識符
dwContinueStatus ?DWORD ?此值指定了該線程將以何種方式繼續,包含兩個定義值DBG_CONTINUE和DBG_EXCEPTION_NOT_HANDLED
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數成功,則返回非零值;如果失敗,則返回零
?

(2)DebugActiveProcess ??

說明:此函數允許將調試器捆綁到一個正在運行的進程上。
語法:BOOL DebugActiveProcess(DWORD dwProcessId )
參數:
dwProcessId ? ? ? ? DWORD ?欲捆綁進程的進程標識符
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數成功,則返回非零值;如果失敗,則返回零

(3)DebugActiveProcessStop

說明:此函數允許將調試器從一個正在運行的進程上卸載。
語法:BOOL DebugActiveProcessStop(DWORD dwProcessId )
參數:
dwProcessId ? ? ? ? DWORD ?欲卸載的進程的進程標識符
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數成功,則返回非零值;如果失敗,則返回零
注意:Windows 9x內核不支持此函數。

(4)DebugBreak

說明:在當前進程中產生一個斷點異常,如果當前進程不是處在被調試狀態,那么這個異常將被系統例程接管,多數情況下會導致當前進程被終止。
語法:VOID DebugBreak(VOID)
參數:無
其他:其實這個函數的用處與在程序中直接插入INT 3的效果是一樣的,如果反編譯Windows 98的KERNEL32.dll,讀者可以發現這個函數只包含兩句,一句是INT 3,一句是RET。

(5)DebugBreakProcess

說明:在指定進程中產生一個斷點異常。
語法:VOID DebugBreakProcess (HANDLE hProcess)
參數:
hProcess ? ? ? ? ? ? ? HANDLE ?進程的句柄
返回值 ? ? ? ? ? ? ? ? ?無

(6)FatalExit

說明:此函數將使調用進程強制退出,將控制權轉移至調試器。與ExitProcess不同的是,在退出前會先調用一個INT 3斷點。
語法:VOID FatalExit(int ExitCode)
參數:
ExitCode ? ? ? ? ? ? ? int ?退出碼
返回值 ? ? ? ? ? ? ? ? ?無

(7)FlushInstructionCache

說明:刷新指令高速緩存。
語法:BOOL FlushInstructionCache(HANDLE hProcess, LPCVOID lpBassAddress, SIZE_T dwSize)
參數:
hProcess ? ? ? ? ? ? ? HANDLE ?進程的句柄
lpBassAddress ? ? ? ?LPCVOID ?欲刷新區域的基地址
dwSize ? ? ? ? ? ? ? ? ?SIZE_T ?欲刷新區域的長度
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數成功,則返回非零值;如果失敗,則返回零

(8)GetThreadContext

說明:獲取指定線程的執行環境。
語法:BOOL GetThreadContext(HANDLE hThread, LPCONTEXT lpContext )
參數:
hThread ? ? ? ? ? ? ? ?HANDLE ?欲獲取執行環境的線程的句柄
lpContext ? ? ? ? ? ? ? LPCONTEXT ?指向CONTEXT結構的指針
返回值 ? ? ? ? ? ? ? ? ?BOOL ?如果函數成功,則返回非零值;如果失敗,則返回零

(9)GetThreadSelectorEntry

說明:此函數返回指定選擇器和線程的描述符表的入口地址。
語法:BOOL GetThreadSelectorEntry( HANDLE hThread,DWORD dwSelector, ? ?LPLDT_ENTRY lpSelectorEntry )
參數:
hThread ? ? ? ? ? ? ? ?HANDLE ?包含指定選擇器的線程的句柄
dwSelector ? ? ? ? ? ?DWORD ?選擇器數目
lpSelectorEntry ? ? ?LPLDT_ENTRY ?指向用來接收描述符表的結構的指針

返回值 ? ? ? ? ? ? ? ? ?如果函數成功,則返回非零值,此外lpSelectorEntry指向的結構中將被填入接收到的描述符表;如果失敗,則返回零

(10)IsDebuggerPresent

說明:此函數用來判斷調用進程是否處于被調試環境中。
語法:BOOL IsDebuggerPresent(VOID)
參數:
返回值 ? ? ? ? ? ? ? ? ? BOOL:如果進程處在被調試狀態,則返回非零值,不是處在被調試狀態則返回零

(11)OutputDebugString

說明:將一個字符串傳遞給調試器顯示。
語法:VOID OutputDebugString(LPCYSTR lpOutputString)
參數:
lpOutputString ? ? ? LPCYSTR:指向要顯示的以“00”結尾的字符串的指針
返回值 ? ? ? ? ? ? ? ? ?無

(12)ReadProcessMemory

說明:讀取指定進程的某區域內的數據。
語法:BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBassAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesRead)
參數:
hProcess ? ? ? ? ? ? ?HANDLE ?進程的句柄
lpBassAddress ? ? ? ?LPCVOID ?欲讀取區域的基地址
lpBuffer ? ? ? ? ? ? ? ? LPVOID ?保存讀取數據的緩沖的指針
nSize ? ? ? ? ? ? ? ? ? ? SIZE_T ?欲讀取的字節數
lpNumberOfBytesRead ? SIZE_T ?存儲已讀取字節數的地址指針
返回值 ? ? ? ? ? ? ? ? ?BOOL:如果函數成功,則返回非零值;如果失敗,則返回零

(13)SetThreadContext

說明:設置指定線程的執行環境。
語法:BOOL SetThreadContext(HANDLE hThread, LPCONTEXT lpContext )
參數:
hThread ? ? ? ? ? ? ? ?HANDLE ?欲設置執行環境的線程的句柄
lpContext ? ? ? ? ? ? ? LPCONTEXT ?指向CONTEXT結構的指針
返回值 ? ? ? ? ? ? ? ? ?BOOL:如果函數成功,則返回非零值;如果失敗,則返回零

(14)WaitForDebugEvent

說明:此函數用來等待被調試進程發生調試事件。
語法:BOOL WaitForDebugEvent(LPDEBUG_ENENT lpDebugEvent, DWORD dwMilliseconds)
參數:
lpDebugEvent ? ? ? ?LPDEBUG_ENENT ?指向接收調試事件信息的DEBUG_ ENENT結構的指針
dwMilliseconds ? ? ?DWORD ?該函數用來等待調試事件發生的毫秒數,如果這段時間內沒有調試事件發生,函數將返回調用者;如果將該參數指定為INFINITE,函數將一直等待直到調試事件發生
返回值 ? ? ? ? ? ? ? ? ?BOOL:如果函數成功,則返回非零值;如果失敗,則返回零

(15)WriteProcessMemory

說明:在指定進程的某區域內寫入數據。
語法:BOOL WriteProcessMemory(HANDLE hProcess, LPCVOID lpBassAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesRead)
參數:
hProcess ? ? ? ? ? ? ?HANDLE ?進程的句柄
lpBassAddress ? ? ? ?LPCVOID ?欲寫入區域的基地址
lpBuffer ? ? ? ? ? ? ? ? LPVOID ?保存欲寫入數據的緩沖的指針
nSize ? ? ? ? ? ? ? ? ? ? SIZE_T ?欲寫入的字節數
lpNumberOfBytesRead ? SIZE_T ?存儲已寫入字節數的地址的指針
返回值 ? ? ? ? ? ? ? ? ?BOOL:如果函數成功,則返回非零值;如果失敗,則返回零

3.1.2 ?調試事件

作為調試器,監視目標進程的執行、對目標進程發生的每一個調試事件做出應有的反應是它的主要工作。當目標進程發生一個調試事件后,系統將會通知調試器來處理這個事件。調試器將利用WaitForDebugEvent函數來獲取目標進程的相關環境信息。可能存在的調試事件類型如表3-1所示。
表3-1 ?調試事件
調試事件
含 ? ?義
CREATE_PROCESS_DEBUG_EVENT
進程被創建。當調試的進程剛被創建(還未運行)或調試器開始調試已經激活的進程時,就會生成這個事件
CREATE_THEAD_DEBUG_EVENT
在調試進程中創建一個新的進程或調試器開始調試已經激活的進程時,就會生成這個調試事件。要注意的是當調試的主線程被創建時不會收到該通知
EXCEPTION_DEBUG_EVENT
在調試的進程中出現了異常,就會生成該調試事件
EXIT_PROCESS_DEBUG_EVENT
每當退出調試進程中的最后一個線程時,產生這個事件
EXIT_THREAD_DEBUG_EVENT
調試中的線程退出時事件發生,調試的主線程退出時不會收到該通知
LOAD_DLL_DEBUG_EVENT
每當被調試的進程裝載DLL文件時,就生成這個事件。當PE裝載器第一次解析出與DLL文件有關的鏈接時,將收到這一事件。調試進程使用了LoadLibrary時也會發生。每當DLL文件裝載到地址空間中去時,都要調用該調試事件
OUTPUT_DEBUG_STRING_EVENT
當調試進程調用DebugOutputString函數向程序發送消息字符串時該事件發生
UNLOAD_DLL_DEBUG_EVENT
每當調試進程使用FreeLibrary函數卸載DLL文件時,就會生成該調試事件。僅當最后一次從過程的地址空間卸載DLL文件時,才出現該調試事件(也就是說DLL文件的使用次數為0時)
RIP_EVENT
只有Windows 98檢查過的構件才會生成該調試事件。該調試事件是報告錯誤信息
當WaitForDebugEvent接收到一個調試事件時,它將把調試事件的信息填寫入DEBUG_EVENT結構中并返回。這個結構定義如下:

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中的值標記了所發生的調試事件的類型。dwProcessId的值是調試事件所發生的進程的標識符。dwThreadId的值是調試事件所發生的線程的標識符。u結構包含了關于調試事件的更多信息,根據上面dwDebugEventCode的不同,它可以是表3-2中所示的結構。
表3-2 ?事件信息與u結構成員
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 結構
那么如何訪問這些數據呢?假設程序調用了WaitForDebugEvent函數并返回,那么要做的第一件事就是檢查dwDebugEventCode字段中的值,根據它來判斷debugger進程中發生了哪種類型的調試事件。比如說,如果dwDebugEventCode字段的值為 CREATE_PROCESS_DEBUG_EVENT,就可認為u的成員為CreateProcessInfo 并可通過u.CreateProcessInfo來訪問。
下面是常用的CREATE_PROCESS_DEBUG_INFO結構的簡要說明。
CREATE_PROCESS_DEBUG_INFO結構定義:

typedef struct _CREATE_PROCESS_DEBUG_INFO { ?
? HANDLE hFile; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 進程文件的句柄,利用它可對文件進行操作 ?
? HANDLE hProcess; ? ? ? ? ? ? ? ? ? ? ? ? // 進程的句柄,在進程空間中進行讀寫操作時要用到它 ?
? HANDLE hThread; ? ? ? ? ? ? ? ? ? ? ? ? ?// 主線程的句柄,在讀取、設置線程環境時都要用到它 ?
? LPVOID lpBaseOfImage; ? ? ? ? ? ? ? ? ?// 進程執行的映像基地址 ?
? 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 ?如何在調試時創建并跟蹤一個進程

這是使用Win32調試API的第一步。可以通過以下方式創建進程。
1. 如何創建一個新進程以供調試
通過CreateProcess創建新進程時,如果在dwCreationFlags標志字段中設置了DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS標志,將創建一個用以調試的新進程。如果是以DEBUG_PROCESS標志創建新進程,調試器將會接收到目標進程及由目標進程創建的所有子進程發生的所有調試事件,一般來說這是沒有必要的。建議可以指定DEBUG_ONLY_THIS_PROCESS和DEBUG_PROCESS的組合標志來禁止它,如果設置了DEBUG_ONLY_THIS_PROCESS標志,調試器將只會收到目標進程的調試事件,而對其子進程的調試事件不予理睬。當進程創建成功后,可以通過查看PROCESS_INFORMATION結構來獲取被創建進程及其主線程的進程標識符和線程標識符。
由于操作系統將調試對象標記為在特殊模式下運行,所以,可以使用IsDebuggerPresent函數查看進程是否在調試器下運行。
2. 如何將調試器捆綁到一個正在運行的進程上
利用DebugActiveProcess函數可以將調試器捆綁到一個正在運行的進程上,如果執行成功,則效果類似于利用DEBUG_ONLY_THIS_PROCESS標志創建的新進程。
要注意的是,在NT內核下當試圖通過DebugActiveProcess函數將調試器捆綁到一個創建時帶有安全描述符的進程上時,將被拒絕。在Windows 9x中則簡單得多,只有當指定了一個無效的進程標識符時調用才會失敗。所以看上去NT內核的系統要更安全。
將調試器捆綁到一個進程上一般是比較好的一種做法,但是有時除了利用CreateProcess函數來載入進程外沒有其他的辦法。那么到底該用哪種方法呢?這涉及讀者將要進行的工作。比如要做一個簡單的游戲修改器時,用臨時捆綁調試器的方法可能比較好,如果要做一些不同尋常的工作的話,利用載入的方法可能更好,因為它獲得目標進程及其線程的所有控制權,這樣就可以為所欲為了。

3.1.4 ?調試循環體

用調試API建立一個簡單的調試程序是非常簡單的,所有要做的只是創建一個用來調試的新進程,然后執行相關代碼來監視所有的調試事件。筆者把監視所有的調試事件的這部分代碼稱為“調試循環體”,為什么呢?因為它的實現非常簡單,看上去就像一個“while”循環,所需要做的只是使用WaitForDebugEvent和ContinueDebugEvent函數。就像上面說的,WaitForDebugEvent在一段時間內等待目標進程中調試事件的發生,如果在這段時間沒有調試事件發生,那么函數將返回FALSE。如果在指定時間內調試事件發生了,那么函數將返回TRUE,并且它會把所發生的調試事件及其相關信息填寫入一個DEBUG_EVENT結構。然后調試器會檢查這些信息,并據此做出相應的反應。在對這些事件做出相應的操作后,就可以使用ContinueDebugEvent函數來恢復線程的執行,并等待下一個調試事件的發生。要注意的一點是WaitForDebugEvent只能使用在創建的或是捆綁上的進程中的某個線程上。
WaitForDebugEvent – ContinueDebugEvent循環的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毫秒內等待調試事件 ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? { ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? 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循環結束 ?
else ?
? ? ? ? ?{ ?
? ? ? ? ?MessageBox(0,"Unexpected load error","Fatal Error" ,MB_OK); ? ? ? ?
? ? ? ? ?} ?
?

3.1.5 ?如何處理調試事件

在上一節的示例中已經看到調試器如何捕獲調試事件,并且利用C/C++中定義的case/switch語法做出了相應的動作。當每一個調試事件發生時,根據事件的不同類型,都會有一段不同的處理程序來進行處理。關于調試事件的更多信息可以根據DEBUG_EVENT中U結構的相應成員來取得。作為示例,再來看一下EXCEPTION_DEBUG_EVENT結構。選擇它是因為遇到一個異常斷點并追蹤它的來龍去脈是經常要做的事情。其他的事件結構請參考API說明。
EXCEPTION_DEBUG_EVENT結構:
[cpp] view plain copy
typedef struct _EXCEPTION_DEBUG_INFO ?
? ? ? ? ?{ ?
? ? ? ? ? ? ? ? ? ?EXCEPTION_RECORD ExceptionRecord; ?
? ? ? ? ? ? ? ? ? ?DWORD dwFirstChance; ?
? ? ? ? ?} EXCEPTION_DEBUG_INFO; ?
其中EXCEPTION_RECORD結構包含異常的很多信息,內容如下: ?
typedef struct _EXCEPTION_RECORD ?
? ? ? ? ? ? ? ? ? ?{ ?
? ? ? ? ? ? ? ? ? ?DWORD ExceptionCode; ?
? ? ? ? ? ? ? ? ? ?DWORD ExceptionFlags; ?
? ? ? ? ? ? ? ? ? ?struct _EXCEPTION_RECORD *ExceptionRecord; ?
? ? ? ? ? ? ? ? ? ?PVOID ExceptionAddress; ?
? ? ? ? ? ? ? ? ? ?DWORD NumberParameters; ?
? ? ? ? ? ? ? ? ? ?DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; ?
? ? ? ? ? ? ? ? ? ?} EXCEPTION_RECORD; ?
? ?
?
? ? ? ? ?結構中字段的定義
ExceptionCode
ExceptionFlags
ExceptionRecord
ExceptionAddress
NumberParameters
ExceptionInformation
DWORD:用來描述異常類型的代碼。
DWORD:零表示可繼續異常。反之,則值為EXCEPTION_NONCONTINUABLE。
指向_EXCEPTION_RECORD結構的指針。
PVOID:異常發生的地址。
DWORD:ExceptionInformation隊列中定義的32位參數的數目。
額外的32位消息隊列,主要在嵌套異常時使用,對多數異常情況,它沒有定義。
從這些結構中可找到需要的一切,比如可以發現:發生異常的類型、是否可以繼續執行、異常發生的地址等信息。
*注意:在發生一個EXCEPTION_NONCONTINUABLE異常后如果試圖去繼續執行,那么會產生一個EXCEPTION_NONCONTINUABLE_EXCEPTION異常。
最常見的異常情況是EXCEPTION_BREAKPOINT和EXCEPTION_SINGLE_ STEP。當執行時遇到一個INT 3斷點時會產生EXCEPTION_BREAKPOINT異常,如果設置了單步執行標志,那么執行完一條指令后會發生一個EXCEPTION_SINGLE_ STEP異常。關于異常還有一點要知道,當以調試的方式創建一個進程的時候,在進入進程之前,系統會先執行一次DebugBreak函數,這樣會產生一個EXCEPTION_BREAKPOINT異常,如果一切正常,那么這應該是第一個遇到的也是必定會遇到的異常。
可以調用ContinueDebugEvent函數來繼續線程的運行,ContinueDebugEvent函數的dwContinueStatus參數有兩個取值,分別是DBG_EXCEPTION_NOT_HANDLED和DBG_CONTINUE。對于大多數調試事件,這兩個值沒有什么區別,都是恢復線程。惟一的例外是EXCEPTION_DEBUG_EVENT,如果線程報告發生了一個異常調試事件,就意味著在被調試的線程中發生了一個異常。如果指定了DBG_CONTINUE,線程將忽略它自己的異常處理部分并繼續執行。在這種情況下,程序必須在以DBG_CONTINUE恢復線程之前檢查并處理異常,否則異常將不斷地發生,直至程序被系統終止。如果指定了 DBG_EXCEPTION_NOT_HANDLED值,就是告訴Windows:程序并不處理異常。Windows將使用被調試線程的默認異常處理函數來處理異常。這一般發生在什么時候呢?在進程被載入后發生的第一個EXCEPTION_DEBUG_EVENT,我們必須以DBG_CONTINUE為標志繼續,在程序中如果調用了DebugBreak函數,或者插入INT3設置斷點成功,并將內存恢復后,都應該使用DBG_CONTINUE為標志繼續。如果在程序中發生了不確定的異常,特別是調試帶殼程序的時候,多半是由于外殼的SEH引起的,此時應該以DBG_EXCEPTION_NOT_HANDLED為標志繼續,以便讓被調試程序本身的異常處理機制來處理。
在發生其他調試事件時,可以利用類似的結構取得線程、進程所調用的DLL或其他一些事物的信息集合。?

3.1.6 ?線程環境詳解

在WIN32系統中,進程的概念實際包含了它的私有地址空間、代碼、數據和一個主線程。每個進程都有一個最初的主線程,通過這個主線程可以在以后創建在同一地址空間中運行的其他線程。和一般說法不同的是進程并不執行代碼,真正執行代碼的是線程。每個線程共同分享相同的地址空間和相同的系統資源,但是它們各自又有不同的執行環境,這到底如何理解呢?Windows是一個多任務多線程的操作系統,在系統的同一時間里看似運行著多個線程,但事實并非如此。Windows分配給每個線程一小段時間片,這段時間結束后,Windows將凍結當前線程并切換到下一個具有最高優先級的線程。在切換之前,Windows將把當前線程執行狀態保存到一個名為CONTEXT的結構中。這個環境包含下面幾部分:
● ? ? ? 線程執行所用寄存器
● ? ? ? 系統堆棧和用戶堆棧
● ? ? ? 線程所用的描述符表等其他狀態信息
這樣當該線程再次恢復運行時,Windows就可以恢復最近一次線程運行的“環境”,好像中間什么都沒有發生一樣。
CONTEXT結構包含了特定處理器的寄存器數據,系統使用CONTEXT結構執行各種內部操作。由于此結構是依賴于硬件的,所以在X86,Alpha等不同的系統上,此結構是不同的。下面是X86系統中此結構的構成情況:


[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結構的指針,FLOATING_ SAVE_AREA結構定義如下:
[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處理那些環境信息。它的定義如下:
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實際上允許查看線程內核對象的內部情況,以便抓取它的當前一組CPU寄存器。若要進行這項操作,可以調用GetThreadContext和SetThreadContext函數。
GetThreadContext用來獲取指定線程的執行環境,語法如下:
語法:BOOL GetThreadContext(HANDLE hThread, LPCONTEXT lpContext )
參數:
hThread ? ? ? ? ? ? ? ? ? ?HANDLE:欲獲取執行環境的線程的句柄
lpContext ? ? ? ? ? ? ? ? LPCONTEXT:指向CONTEXT結構的指針
要注意的是,在使用GetThreadContext函數之前,必須先將ContextFlags初始化為適當的標志,指明想要收回哪些寄存器,并將該結構的地址傳遞給GetThreadContext函數。例如設置的標志是CONTEXT_CONTROL,只返回Ebp,Eip,Cs,Flages,Esp,Ss這些值。
SetThreadContext用來設置指定線程的執行環境,語法如下:
語法:BOOL SetThreadContext(HANDLE hThread, ? ? ?LPCONTEXT lpContext )
參數:
hThread ? ? ? ? ? ? ? ? ? ?HANDLE:欲設置執行環境的線程的句柄
lpContext ? ? ? ? ? ? ? ? LPCONTEXT:指向CONTEXT結構的指針
與GetThreadContext函數相類似,SetThreadContext函數也是通過ContextFlags的值來控制哪些數據將被恢復。
這兩個函數威力非凡。有了它們,對于被調試進程你就有了上帝的能力。如果改變其寄存器內容,那么在被調試程序恢復運行前,這些值將會寫回寄存器中。在進程環境中所做的任何改動,都將反映到被調試程序中。想像一下:甚至可以改變EIP寄存器的內容,這樣就可以讓程序運行到想要的任何地方! 在正常情況下是不可能做到這一點的。
在調用GetThreadContext函數之前,應該調用SuspendThread函數,否則線程可能被調度,而且線程的環境可能與收回的不同。一個線程實際上有兩個環境:一個是用戶方式,一個是內核方式。GetThreadContext函數只能返回線程的用戶方式環境。如果調用SuspendThread函數來停止線程的運行,但該線程目前正在用內核方式運行,那么,即使SuspendThread實際上尚未暫停該線程的運行,它的用戶方式仍然處于穩定狀態。線程在恢復用戶方式之前,無法執行更多的用戶方式代碼,因此可以放心地將線程視為處于暫停狀態,GetThreadContext函數將能正常運行。
所以,正確的做法應該是先利用SuspendThread函數暫停一個線程,當設置好環境后再利用ResumeThread函數來恢復它。但要注意的是:ResumeThread函數并不能保證線程真地繼續執行,為什么呢?每一個線程都有一個線程暫停計數器,當線程正在運行時計數器為0,當其他線程對此線程使用SuspendThread函數時計數器會增加1,調用ResumeThread會使計數器減小1,所以當調用SuspendThread函數后計數器變為1。但是Windows是一個多線程操作系統,所以很有可能某個其他的線程也對此線程調用了SuspendThread,這時計數器就會變為2,這時再調用ResumeThread只會使計數器變回為1,線程將繼續暫停,直到計數器變為0。那么,如何確定線程是否真地被繼續執行了呢?很簡單,檢查函數返回值就可以了。如果返回值為0,則表示線程已經恢復執行了,如果不為0,則表示線程繼續被暫停,如果為0xffffffff,則說明函數調用失敗了。
同樣,在調用SetThreadContext函數之前,必須暫停,否則結果將無法預測。

3.1.7 ?如何在另一個進程中注入代碼

現在讓我們更深入一些來討論,有時候需要將一段代碼注入到某個進程的地址空間中,實際上這并不非常復雜,但在真正開始做之前先得解決一個小小的麻煩:首先得需要一小段地址空間來存放補丁代碼。這似乎很簡單,有的讀者會說,利用VirtualAllocEx不就可以了嗎?遺憾的是,VirtualAllocEx只在Windows NT內核下被支持,在Windows 9X內核下不被支持,那怎么辦呢?如果注入的代碼很短小,那么可以利用原進程的各個區塊之間的間隙,甚至可以把代碼注入到原進程文件頭中的DOS stub部分,當然這樣的話執行之前先得更改目標進程文件頭的讀寫屬性。如果要注入的部分比較大呢?只能先將目標進程中的某個代碼頁保存,然后注入新的代碼,執行完后再將原始的代碼寫回。具體的步驟如下:
(1)利用CreateProcess函數創建一個供調試的進程。
(2)建立WaitForDebugEvent和ContinueDebugEvent構成的調試循環體。
(3)利用SuspendThread函數掛起目標線程。
(4)利用VirtualProtectEx函數修改目標頁的讀寫權限。
(5)利用ReadProcessMemory函數讀取目標頁。
(6)利用GetThreadContext函數保存線程環境。
(7)利用WriteProcessMemory函數寫入新代碼頁。
(8)確認在新指令中的最后一個代碼是INT 3,我們需要利用它在指令執行完成后獲得系統控制權。INT 3產生的異常將會被我們的程序捕獲,要注意的是必須確認這是一個breakpoint異常,并且是在我們放置INT 3的位置。
(9)保存一份CONTEXT結構的臨時拷貝。
(10)在這份臨時拷貝中設置新的EIP值。
(11)恢復原線程的執行,它將執行我們的代碼,直到INT 3被執行,當它被執行時會被我們的程序捕獲,目標線程再次被掛起。
(12)利用WriteProcessMemory函數恢復原始代碼頁。
(13)恢復原始代碼頁的讀寫屬性。
(14)利用SetThreadContext恢復線程原始的環境。
(15)恢復原線程執行。
如果需要讓注入的代碼和進程原始的代碼同時存在于進程空間中,而且準備注入的代碼比較大,則必須為目標進程分配一些地址空間。調用VirtuallAlloc的代碼是非常短小的,可以先在目標進程中注入調用VirtuallAlloc的代碼,利用它可以獲取一些額外的地址空間,一般來說幾個KB就足夠了,不要試圖去申請比如10MB的空間,那樣很容易導致執行失敗。當然還有另一個辦法,可以在自己的進程中調用VirtualAllocEx,這也可以為目標進程分配一定的地址空間,可惜的是它只能運行在Windows NT內核下。
如果為了某些工作,需要將某個區塊的相對地址轉換為線性虛擬地址,就可以使用函數GetThreadSelectorEntry。

最后提醒一下,向其他線程注入代碼時,千萬要注意堆棧的平衡問題,如果不注意,可能會產生非常嚴重的錯誤。


Win32一個調試器的實現

[Win32]一個調試器的實現(一)調試事件與調試循環

[Win32]一個調試器的實現(二)調試事件的處理?

[Win32]一個調試器的實現(三)異常?

[Win32]一個調試器的實現(四)讀取寄存器和內存?

[Win32]一個調試器的實現(五)調試符號?

[Win32]一個調試器的實現(六)顯示源代碼?

[Win32]一個調試器的實現(七)斷點

[Win32]一個調試器的實現(八)單步執行

[Win32]一個調試器的實現(九)符號模型?

[Win32]一個調試器的實現(十)顯示變量?

[Win32]一個調試器的實現(十一)顯示函數調用棧?

https://www.zhihu.com/question/52553014/answer/136312479


總結

以上是生活随笔為你收集整理的win32 调试 API 学习总结的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。