很多朋友都夢想有自己的Debugger程序,今天我們就來自己制作一個。作為一個Debugger程序,其最基本的功能框架其實就是完成2件事情:
? 啟動目標程序。
? 實時監控目標程序的運行,并做出相應的應對。
我們要打造自己的Debugger程序,實際上也只需要完成這兩個功能就可以了。當然,要完成這兩個特定的功能,我們不可能從頭開始造輪子,要首先看看操作系統給我們提供了什么樣的基礎設施:
由于我們是在Windows平臺上工作,自然離不開微軟公司提供的文檔大全——MSDN。翻開MSDN,定位到“Debugging and Error Handling”,一些最基本的Windows Debug信息都在這里面。不過與其他欄目相比,這個欄目的信息明顯顯得單薄許多——也許越是底層、強大的技術,微軟越不想公開吧。
初步瀏覽之后,我們可以確定,對于我們的Debugger而言,最重要的Debug API有如下幾個:
? CreateProcess —— 用于創建被調試進程
? WaitForDebugEvent —— Debug Loop(調試循環)的主要構成函數
? ContinueDebugEvent —— 用于構成Debug Loop
? GetThreadContext —— 得到被調試進程的寄存器信息
? SetThreadContext —— 設置被調試進程的寄存器信息
? ReadProcessMemory —— 得到被調試進程的內存內容
? WriteProcessMemory —— 設置被調試進程的內存內容
最重要的數據結構有如下幾個:
? CONTEXT —— 寄存器結構
? STARTUPINFO —— Start信息
? PROCESS_INFORMATION —— 進程相關信息
? DEBUG_EVENT —— Debug Event(調試事件)結構
可以說,我們的Debugger程序就是利用這幾個API函數結合下面的幾個數據結構,完成我們指定的功能。那么下面就讓我們先來看看這幾個API和數據結構的具體含義:
?
Debug API解析
在這里,我們將對上面所述的幾個Debug調試API做一個檢閱式的考察,大概介紹一下每個API的應用領域。而將這些API應用到具體實踐中去,將會在下一部分“實例解析”中,給出詳細的說明。
1.CreateProcess。
函數原型:BOOL CreateProcess(
LPCTSTR lpApplicationName, // 要創建的進程模塊名
LPTSTR lpCommandLine, // 命令行字符串
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 進程安全屬性
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 線程安全屬性
BOOL bInheritHandles, // 句柄繼承選項
DWORD dwCreationFlags, // 進程創建選項
LPVOID lpEnvironment, // 進程環境塊數據指針
LPCTSTR lpCurrentDirectory, // 當前目錄名
LPSTARTUPINFO lpStartupInfo, // 啟動信息
LPPROCESS_INFORMATION lpProcessInformation // 進程信息
);
函數解析:該函數是Windows平臺提供的最基本的創建進程的函數。每當我們雙擊一個EXE可執行文件,Windows內核就會自動調用該函數創建我們雙擊的文件所對應的進程。該函數中,最重要的參數有三個:一個是進程模塊名,指明了要創建哪個進程;一個是進程創建選項,指明了要如何創建目標進程;對于Debugger程序而言,最常用的創建選項就是:DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS。最后還有一個就是進程信息,我們調用CreateProcess創建了進程以后,Windows會將新創建的進程的相關信息全部放到ProcessInfo信息塊中,我們在Debug Loop調試循環中使用進程信息塊中的數據與目標進程交互,監視和控制目標進程的動作。
2.WaitForDebugEvent。
函數原型:BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent, // Debug Event(調試事件指針)
DWORD dwMilliseconds // 超時設置
);
函數解析:該函數構成了Debug Loop調試循環的主體,一個Debugger程序在創建出目標進程后,一般都會緊接著循環調用該函數等待目標進程的各種調試信息,這個循環調用WaitForDebugEvent的過程,我們就稱之為Debug Loop調試循環。調試循環是所有Debugger程序的主體部分,Debugger幾乎所有的監視、控制、調整的功能都是在調試循環內完成的。一般來說,此處的超時設置都設置為-1也就是無窮等待下去,該函數是非阻塞函數,在沒有Debug Event發生,處于等待的過程中,僅消耗極其微小的系統資源。
3.ContinueDebugEvent。
函數原型:BOOL ContinueDebugEvent(
DWORD dwProcessId, // 目標進程ID
DWORD dwThreadId, // 目標線程ID
DWORD dwContinueStatus // 線程繼續的標志
);
函數解析:該函數主要用于Debugger在Debug Loop調試循環中,處理完Debug Event,通知目標進/線程繼續運作。通常情況下,目標進程ID和目標線程ID這兩個參數,都是CreateProcess調用后,ProcessInfo結構中所包含的信息。該函數通過目標進程/線程ID來唯一標識目標進/線程,并且通過設置不同的ContinueStatus來通知目標進/線程繼續運行的動作。最主要的ContinueStatus有兩種選擇:一個是DBG_CONTINUE,表明調試事件已經被Debugger處理完畢,目標進/線程可以照常繼續運行;另一個是DBG_EXCEPTION_NOT_HANDLED,表明Debugger并未處理該調試事件,目標進程收到該標志位后,將會將調試事件沿著Windows異常調用鏈繼續往下發送。直至該調試事件被處理完為止——當然,如果目標進程發出的Debug Event沒有任何調試器能夠處理,那最后Windows只有祭出自己的殺手锏:應用程序XXX異常,即將被關閉。
3.GetThreadContext & SetThreadContext。
函數原型:BOOL GetThreadContext(
HANDLE hThread, // 目標線程句柄
LPCONTEXT lpContext // CONTEXT結構
);
BOOL SetThreadContext(
HANDLE hThread, // 目標線程句柄
CONST CONTEXT *lpContext // CONTEXT結構
);
函數解析:這兩個函數分別用來得到和設置目標線程的寄存器內容。請注意,在Windows操作系統中,操作系統調度的最小單位粒度是線程而不是進程,所以,籠統地說:設置某進程的寄存器內容是錯誤的,因為一個進程可能對應多個線程。因此在和寄存器打交道的時候,一定要指明是哪個線程所對應的寄存器。該函數參數中,由hThread參數也就是線程句柄參數指定目標線程,該參數的來源也通常是CreateProcess調用后所得到的ProcessInfomation中的hThread成員。而CONTEXT結構則是根據所在機器硬件平臺的不同而有不同的定義。Windows操作系統在Intel、MIPS、Alpha和PowerPC平臺上,各有不同的CONTEXT定義,每種定義都忠實而完整地反映出了目標CPU的寄存器分布情況。不過雖然CONTEXT結構在不同的CPU平臺上有不同的表現形式,但是最基本的Intel X86架構在各個CPU上面的表現都是相同的,因此,只要Debugger代碼未牽涉到各個CPU非常Specifc的細節,還是可以跨CPU平臺使用的。
4.ReadProcessMemory & WriteProcessMemory。
函數原型:BOOL ReadProcessMemory(
HANDLE hProcess, // 進程句柄
LPCVOID lpBaseAddress, // 欲讀取內存基地址
LPVOID lpBuffer, // 數據緩沖區指針
SIZE_T nSize, // 欲讀取的內存內容長度
SIZE_T * lpNumberOfBytesRead // 實際讀取的內存內容長度
);
BOOL WriteProcessMemory(
HANDLE hProcess, // 進程句柄
LPVOID lpBaseAddress, // 欲寫入內存基地址
LPCVOID lpBuffer, // 數據緩沖區指針
SIZE_T nSize, // 欲寫入的內存內容長度
SIZE_T * lpNumberOfBytesWritten // 實際寫入的內存內容長度
);
函數解析:這兩個函數分別用于“讀取”和“寫入”目標進程的內存地址空間。與寄存器操作不同,Windows對內存的分配粒度是以進程為單位的。由于自Intel 386以后,所有的Intel x86系列CPU都采用的保護模式,因此在保護模式下,Windows為每一個應用程序——也就是每一個進程都虛擬出一個擁有4GB大小內存的“虛擬機器”, 隸屬于該進程的所有線程都共享這4GB的地址空間。因此,與上面寄存器操作不同,讀取、寫入內存操作時,我們需要的是進程的句柄——當然,該句柄也來源于CreateProcess后得到的ProcessInfomation結構。有了進程句柄以后,我們還需要一個基地址和一個長度參數來確定我們的Debugger程序所需要讀取的內存范圍。當然,此處的基地址數值上應該對應于Windows虛擬出來的那4GB的平坦地址空間內的地址——也就是經過了段選擇和頁選擇過程后的地址數值。
DEBUG_EVENT全面剖析
在整個調試循環中,Debugger和目標進程之間調試信息的交互完全是通過調用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, *LPDEBUG_EVENT;
不過就是一個union聯合域加上三個普通的信息數據??杉毤毻魄弥?,這個結構所實現的功能卻絕對不簡單——讓我們做一個最簡單的思考,作為Debugger,至少應該能夠收到:
? 目標進程啟動
? 目標進程發生異常
? 目標進程退出
這三個最基本的調試信息。而且每個信息所對應的信息類型應該都不太一樣,比如:
? 目標進程啟動時,我們需要的是目標進程的模塊名字,權限設置等啟動信息
? 目標進程異常時,我們應該能夠知道異常的地址,異常的原因(這里面分類又很多),異常的嚴重程度
? 目標進程退出時,我們應該能夠知道進程的退出值,用來確定進程是否屬于正常退出
上面所列出的,還只是一些最基本的要素而已,如果要構成整個Windows Debug API的數據交互層,那情況還要復雜得多。將這么多錯綜復雜的信息全部用一個結構表示,其難度可想而知。
微軟公司在這里選擇的方法是通過dwDebugEventCode標識最基本信息,然后通過union聯合域將各種信息全部包羅進去的方法。這種做法的優點是在WaitForDebugEvent調用時,只需要傳遞一個統一的結構參數即可;缺點就是Debug_Event結構內部信息的異常復雜,給程序設計帶來不小的麻煩。
我們在使用Debug_Event結構時,首先要得到dwDebugEventCode的值,從而確定union聯合域內是什么內容。兩者之間的對應關系如下表所示:
dwDebugEventCode的值
Union聯合域類型 調試信息
EXCEPTION_DEBUG_EVENT EXCEPTION_DEBUG_INFO
應用程序發生異常
CREATE_THREAD_DEBUG_EVENT CREATE_THREAD_DEBUG_INFO
線程創建
CREATE_PROCESS_DEBUG_EVENT CREATE_PROCESS_DEBUG_INFO
進程創建
EXIT_THREAD_DEBUG_EVENT EXIT_THREAD_DEBUG_INFO
線程退出
EXIT_PROCESS_DEBUG_EVENT EXIT_PROCESS_DEBUG_INFO
進程退出
LOAD_DLL_DEBUG_EVENT LOAD_DLL_DEBUG_INFO
Dll載入
UNLOAD_DLL_DEBUG_EVENT UNLOAD_DLL_DEBUG_INFO
Dll卸載
OUTPUT_DEBUG_STRING_EVENT OUTPUT_DEBUG_STRING_INFO
輸出Debug字符串
RIP_EVENT RIP_INFO 系統調試錯誤
上表僅僅是Debug_Event結構數據解析的第一層,當union聯合域取得各個不同的值時,這些第二層的數據結構并不比這一層簡單多少——不過幸虧大部分的時間我們解析Debug_Event結構只需要解析到第二層就可以了。
一般情況下,我們最感興趣的DebugEventCode就是EXCEPTION_DEBUG_EVENT,而不巧的是,EXCEPTION_DEBUG_EVENT所對應的EXCEPTION_DEBUG_INFO結構也是所有union聯合域結構中最復雜的一個,因此,有必要在此再對EXCEPTION_DEBUG_INFO這個二級數據結構作進一步的詳細說明:
typedef struct _EXCEPTION_DEBUG_INFO {
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;
? dwFirstChange:如果為0,表示是該異常從前未被處理過,也就是當前我們的Debugger程序處于Windows異常處理鏈條的頭部。
? ExceptionRecord:該結構內部的信息是EXCEPTION_DEBUG_INFO結構的真實存儲地點。因此,繼續剖析該結構。
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;
首先:要注意到,該結構內部有一個類型為EXCEPTION_RECORD*的數據成員,這是C語言里經典的串聯數據的方式:用一個指針域連接各個數據成員,可以構成經典的“鏈表”數據結構(英文里管這種做法叫做Chain)。
其次:ExceptionCode成員標識了該結構所代表的EXCEPTION_RECORD的類型,Windows內部定義了如下20種異常行為:
值 含義
EXCEPTION_ACCESS_VIOLATION 存取越界
EXCEPTION_ARRAY_BOUNDS_EXCEEDED 由硬件監測到的數組訪問越界
EXCEPTION_BREAKPOINT 觸發斷點
EXCEPTION_DATATYPE_MISALIGNMENT 數據未對齊
EXCEPTION_FLT_DENORMAL_OPERAND 浮點操作數范圍越界(太小或太大)
EXCEPTION_FLT_DIVIDE_BY_ZERO 浮點操作除數為0
EXCEPTION_FLT_INEXACT_RESULT 浮點運算結果不能用小數正常表示.
EXCEPTION_FLT_INVALID_OPERATION 其他未知的浮點數錯誤
EXCEPTION_FLT_OVERFLOW 浮點操作太大溢出
EXCEPTION_FLT_STACK_CHECK 浮點棧溢出.
EXCEPTION_FLT_UNDERFLOW 浮點操作太小溢出
EXCEPTION_ILLEGAL_INSTRUCTION 執行非法指令
EXCEPTION_IN_PAGE_ERROR 存取未存在的內存頁
EXCEPTION_INT_DIVIDE_BY_ZERO 除數為0
EXCEPTION_INT_OVERFLOW 整數操作,最高位溢出
EXCEPTION_INVALID_DISPOSITION 錯誤的異常處理程序地址
EXCEPTION_NONCONTINUABLE_EXCEPTION 遇上不能繼續執行的異常
EXCEPTION_PRIV_INSTRUCTION 當前模式下,不能執行該指令
EXCEPTION_SINGLE_STEP 單步跟蹤斷點觸發
EXCEPTION_STACK_OVERFLOW 線程的??臻g溢出
一般而言,我們的Debugger程序處理得最多的異常情況就是EXCEPTION_BREAKPOINT和EXCEPTION_SINGLE_STEP。例如:我們需要在目標進程運行到0x00400000地址的時候對目標進程進行一些操作,那么我們只要設法讓目標程序運行到0x00400000的時候,向我們的Debugger程序發出異常信號,那么我們的Debugger程序就能收到該信號,并進而通過前面介紹的Set/GetThreadContext和Read/WriteProcessMemory函數對目標進程進行控制操作。
一個最初步的Debugger框架
既然上面所述的內容現在已經足夠我們寫出一個最基本的Debugger了,那么我們就來“實戰演習”一番,先寫出一個最基本的Debugger程序,該程序完成的功能異常簡單:利用CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS作為標志,創建一個進程,并讓目標進程能夠照常地運作起來。
首先跳入我們腦海的代碼大概如下所示:
::CreateProcess (_T("Msg.exe"), NULL, NULL, NULL, NULL, DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,
NULL, NULL, &sif, &pi) ;
//這個下面就是大名鼎鼎的Debug框架!
do
{
::WaitForDebugEvent (&DBEvent, INFINITE) ;
dwState = DBG_EXCEPTION_NOT_HANDLED ;
case EXIT_PROCESS_DEBUG_EVENT :
{
STOP = TRUE ;
break ;
}
if (!STOP)
{
::ContinueDebugEvent(pi.dwProcessId, pi.dwThreadId, dwState) ;
}
} while (!STOP) ;
編譯,雙擊,很不幸!Windows給了我們
為什么會出現上面這種錯誤情況?微軟的文檔里面并沒有對Debugger具體應該如何工作做出詳細的說明。不過通過跟蹤上面的程序,我們發現每次Msg.exe目標應用程序啟動前,都會向我們的Debugger程序發送一個EXCEPTION_BREAKPOINT的斷點信號,而我們的Debug Loop并沒有對該信號進行處理?,F在我們加入對該信號的處理過程,使我們的Debugger不返回DBG_EXCEPTION_NOT_HANDLED而返回DBG_CONTINUE,具體到代碼中,就是在:
case EXIT_PROCESS_DEBUG_EVENT :
這一句之前,加入:
case EXCEPTION_DEBUG_EVENT:
{
switch (DBEvent.u.Exception.ExceptionRecord.ExceptionCode)
{
case EXCEPTION_BREAKPOINT:
{
dwState = DBG_CONTINUE ;
break ;
}
}
break ;
}
編譯、鏈接,試運行,一切OK。Msg.exe很正常地彈出了MessageBox,
從C到C++
從上面那個最簡單的調試框架代碼不難看出,每當我們要增加一個處理判斷的異常類型,都要在日益龐大復雜的switch – case代碼中增加新的選擇路線,程序的代碼很容易就變得臃腫不堪且難以維護。而實際上整個代碼的工作骨架并沒有發生實質性的改變,變化的僅僅是針對各種不同的異常情況我們所需要的不同處理子過程——這不禁讓我們想起了Template設計模式——我們可以將在Debug Loop中分解Debug_Event的代碼放進框架代碼中,然后在框架代碼中調用不同的hook虛函數,這樣,我們要擴展自己的Debugger功能時,只需要從已有的debug_base基類繼承,重寫hook虛方法即可。
隨本文附帶的Debug_Base.h中有debug_base類,該類就實現了如上所述的Debugger Template模板。該類最簡單的使用如下所示:
Debugger::debug_base debugger ;
debugger.run_debug_loop(std::tstring(TEXT("Msg.exe"))) ;
以上這兩段代碼,就實現了上一節所提到的那個最簡單的調試器所完成的功能——載入被調試程序并且照常運行。
Debug_base提供有如下幾個hook虛函數:
hook函數名 函數作用
handle_first_exception Windows內核首次發送EXCEPTION_BREAKPOINT時的處理子過程
handle_exception_breakpoint 調試過程中EXCEPTION_BREAKPOINT的處理子過程
handle_single_step 調試過程中EXCEPTION_SINGLE_STEP的處理子過程
handle_process_create 進程創建處理子過程
handle_process_exit 進程退出處理子過程
handle_thread_create 線程創建處理子過程
handle_thread_exit 線程推出處理子過程
handle_dll_load Dll載入時得到調用
handle_dll_unload Dll卸載的時候得到調用
handle_debug_wstring 被調試程序調用OutputDebugString得到調用
實際的編程過程中,我們只需要從debug_base派生出自己的新類,并重寫其中需要的虛函數即可編譯得到自己的Debugger!
例如:我們想讓我們的Debugger具有如下功能:
? 在程序入口點處中斷,提示已經到達入口點
? 截獲被調試程序的Debug字符串,并輸出
那么我們可以這樣設計我們的新類:
class MyDebugger : public Debugger::debug_base
{
virtual void handle_first_exception(const PROCESS_INFORMATION& pi)
{
::MessageBox(NULL, TEXT("首次中斷"), TEXT("Debugger Worked"), NULL) ;
return ;
}
virtual void handle_debug_wstring(std::wstring& debug_wstring)
{
::MessageBox(NULL, ATL::CW2T(debug_wstring.c_str()), TEXT("Debug String"), NULL) ;
return ;
}
} ;
而主程序并不需要做任何改變,該Debugger運行后,可以接受被調試程序的調試信息,并輸出:
結束語
Windows借助于Debug API,為我們提供了一個功能全面的Ring 3級別的調試平臺,但是由于文檔資料、示例代碼的缺乏,迄今為止,Debug API的應用面還很窄。其實如果能真正把握住動態調試的精髓,利用Debug API打造一個自己的專有調試器調試一些很特殊的程序是能收到非常好的效果的。可以說,任何一款脫殼機或者是內存注冊機都只不過是Debug API的一個具體應用。
當然,由于篇幅所限,本文也不可能更加詳盡深入地探索Windows Debugger的世界,我們的debug_base類還很初步——甚至還不具備在程序運行時增加調試斷點的功能,不過只要理解了Debugger的工作原理,結合MicroSoft和Intel的一些底層的編程資料,各種功能都可以慢慢添加、完善。希望讀者朋友在學習逆向工程的過程中,注意基礎知識的積累、整理、消化,不斷地提升自己對計算機系統的理解水平,最終才能修成一代宗師,打造出made in china的Soft-ice和OllyDbg!