本節書摘來自異步社群出版社《c++ 黑客程式設計揭秘與防範(第2版)》一書中的第6章,第6.6節,作者:冀雲,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
c++ 黑客程式設計揭秘與防範(第2版)
windows中有些api函數是專門用來進行調試的,被稱作debug api,或者是調試api。利用這些函數可以進行調試器的開發,調試器通過建立有調試關系的父子程序來進行調試,被調試程序的底層資訊、即時的寄存器、指令等資訊都可以被擷取,進而用來分析。
上面介紹的ollydbg調試器的功能非常強大,雖然有衆多的功能,但是其基礎的實作就是依賴于調試api。調試api函數的個數雖然不多,但是合理使用會産生非常大的作用。調試器依賴于調試事件,調試事件有着非常複雜的結構體。調試器有着固定的流程,由于實時需要等待調試事件的發生,其過程是一個調試循環體,非常類似于sdk開發程式中的消息循環。無論是調試事件還是調試循環,對于調試或者說調試器來說,其最根本、最核心的部分是中斷,或者說其最核心的部分是可以捕獲中斷。
6.6.1 常見的3種斷點方法
在前面介紹od的時候提到過,産生中斷的方法是設定斷點。常見的産生中斷的斷點方法有3種,分别是中斷斷點、記憶體斷點和硬體斷點。下面介紹這3種斷點的不同。
中斷斷點,這裡通常指的是彙編語言中的int 3指令,cpu執行該指令時會産生一個斷點,是以也常稱之為int3斷點。現在示範如何使用int 3來産生一個斷點,代碼如下:
可以看出,這裡給的address是一個va(虛拟位址),用od打開這個程式,直接按f9鍵運作,如圖6-65和圖6-66所示。

從圖6-65中可以看到,程式執行停在了00401029位置處。從圖6-66看到,int3指令位于00401028位置處。再看一下圖6-64中address後面的值,為00401028。這也就證明了在系統的錯誤報告中可以給出正确的出錯位址(或産生異常的位址)。這樣在以後寫程式的過程中可以很容易地定位到自己程式中有錯誤的位置。
注:在od中運作自己的int 3程式時,可能od不會停在00401029位址處,也不會給出類似圖6-65的提示。在實驗這個例子的時候需要對od進行設定,在菜單中選擇“選項”->“調試設定”,打開“調試選項”對話框,選擇“異常”頁籤,取消“int3 中斷”複選框的選中狀态,這樣就可以按照該例子進行測試了。
回到中斷斷點的話題上,中斷斷點是由int 3産生的,那麼要如何通過調試器(調試程序)在被調試程序中設定中斷斷點呢?看圖6-65中00401028位址處,在位址值的後面、反彙編代碼的前面,中間那一列的内容是彙編指令對應的機器碼。可以看出,int3對應的機器碼是0xcc。如果想通過調試器在被調試程序中設定int3斷點的話,那麼隻需要把要中斷的位置的機器碼改為0xcc即可。當調試器捕獲到該斷點異常時,修改為原來的值即可。
記憶體斷點的方法同樣是通過異常産生的。在win32平台下,記憶體是按頁進行劃分的,每頁的大小為4kb。每一頁記憶體都有其各自的記憶體屬性,常見的記憶體屬性有隻讀、可讀寫、可執行、可共享等。記憶體斷點的原理就是通過對記憶體屬性的修改,本該允許進行的操作無法進行,這樣便會引發異常。
在od中關于記憶體斷點有兩種,一種是記憶體通路,另一種是記憶體寫入。用od随便打開一個應用程式,在其“轉存視窗”(或者叫“資料視窗”)中随便選中一些資料點後單擊右鍵,在彈出的菜單中選擇“斷點”指令,在“斷點”子指令下會看到“記憶體通路”和“記憶體寫入”兩種斷點,如圖6-67所示。
圖6-67 記憶體斷點類型
下面通過簡單例子來看如何産生一個記憶體通路異常,代碼如下:
bool createprocess(
lpctstr lpapplicationname, // name of executable module
lptstr lpcommandline, // command line string
lpsecurity_attributes lpprocessattributes, // sd
lpsecurity_attributes lpthreadattributes, // sd
bool binherithandles, // handle inheritance option
dword dwcreationflags, // creation flags
lpvoid lpenvironment, // new environment block
lpctstr lpcurrentdirectory, // current directory name
lpstartupinfo lpstartupinfo, // startup information
lpprocess_information lpprocessinformation // process information
);<code>`</code>
現在要做的是建立一個被調試程序。createprocess()函數有一個dwcreationflags參數,其取值中有兩個重要的常量,分别為debug_process和debug_only_this_process。debug_
process的作用是被建立的程序處于調試狀态。如果一同指定了debug_only_ this_process的話,那麼就隻能調試被建立的程序,而不能調試被調試程序建立出來的程序。隻要在使用createprocess()函數時指定這兩個常量即可。
除了createprocess()函數以外,還有一種建立調試關系的方法,該方法用的函數如下:
winbaseapi
bool
winapi
debugactiveprocessstop(
__in dword dwprocessid
);<code>`</code>
該函數隻有一個參數,就是被調試程序的程序id号。使用該函數可以在不影響調試器程序和被調試程序的正常運作的情況下,将兩者的關系解除。但是有一個前提,被調試程序需要處于運作狀态,而不是中斷狀态。如果被調試程序處于中斷狀态時和調試程序解除調試關系,由于被調試程序無法運作而導緻退出。
2.判斷程序是否處于被調試狀态
很多程式都要檢測自己是否處于被調試狀态,比如遊戲、病毒,或者加殼後的程式。遊戲為了防止被做出外挂而進行反調試,病毒為了給反病毒工程師增加分析難度而反調試。加殼程式是專門用來保護軟體的,當然也會有反調試的功能(該功能僅限于加密殼,壓縮殼一般沒有反調試功能)。
本小節不是要介紹反調試,而是介紹一個簡單的函數,這個函數是判斷自身是否處于被調試狀态,函數名為isdebuggerpresent(),其定義如下:
<code>bool isdebuggerpresent(void);</code>
該函數沒有參數,根據傳回值來判斷是否處于被調試狀态。這個函數也可以用來進行反調試。不過由于這個函數的實作過于簡單,很容易就能夠被分析者突破,是以現在也沒有軟體再使用該函數來進行反調試了。
下面通過一個簡單的例子來示範isdebuggerpresent()函數的使用,代碼如下:
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;<code>`</code>
這個結構體非常重要,這裡有必要詳細地介紹。
dwdebugeventcode:該字段指定了調試事件的類型編碼。在調試過程中可能産生的調試事件非常多,是以要根據不同的類型碼進行不同的響應處理。常見的調試事件如圖6-74所示。
圖6-74 dwdebugeventcode的取值
dwprocessid:該字段指明了引發調試事件的程序id号。
dwthreadid:該字段指明了引發調試事件的線程id号。
u:該字段是一個聯合體,其取值由dwdebugeventcode指定。該聯合體包含很多個結構體,包括exception_debug_info、create_thread_ debug_info、create_pro cess_debug_
info、exit_thread_debug_info、exit_process_debug_info、load_dll_debug_
info、unload_dll_debug_info和output_debug_string_info。
在以上衆多的結構體中,特别要介紹一下exception_debug_info,因為這個結構體包含關于異常相關的資訊;而其他幾個結構體的使用比較簡單,讀者可以參考msdn。
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;<code>`</code>
exceptioncode:異常碼。該值在msdn中的定義非常多,不過這裡需要使用的值隻有3個,分别是exception_access_violation(通路違例)、exception_ breakpoint(斷點異常)和exception_single_step(單步異常)。這3個值中的前兩個值對于讀者來說應該是非常熟悉的,因為在前面已經介紹過了;最後一個單步異常想必讀者也非常熟悉。使用od快捷鍵的f7鍵、f8鍵時就是在使用單步功能,而單步異常就是由exception_single _step來表示的。
exceptionrecord:指向一個exception_record的指針,異常記錄是一個連結清單,其中可能儲存着很多異常資訊。
exceptionaddress:異常産生的位址。
調試事件這個結構體debug_event看似非常複雜,其實也隻是嵌套得比較深而已。隻要讀者仔細體會每個結構體、每層嵌套的含義,自然就覺得它沒有多麼複雜。
5.調試循環
調試器不斷地對被調試目标程序進行捕獲調試資訊,有點類似于win32應用程式的消息循環,但是又有所不同。調試器在捕獲到調試資訊後進行相應的處理,然後恢複線程,使之繼續運作。
用來等待捕獲被調試程序調試事件的函數是waitfordebugevent(),其定義如下:
bool continuedebugevent(
dword dwprocessid, // process to continue
dword dwthreadid, // thread to continue
dword dwcontinuestatus // continuation status
dwprocessid:該參數表示被調試程序的程序辨別符。
dwthreadid:該參數表示準備恢複挂起線程的線程辨別符。
dwcontinuestatus:該參數指定了該線程以何種方式繼續執行,其取值為dbg_excepti on_not_handled和dbg_continue。對于這兩個值來說,通常情況下并沒有什麼差别。但是當遇到調試事件中的調試碼為exception_debug_event時,這兩個常量就會有不同的動作。如果使用dbg_exception_not_handled,調試器程序将會忽略該異常,windows會使用被調試程序的異常處理函數對異常進行處理;如果使用dbg_continue的話,那麼需要調試器程序對異常進行處理,然後繼續運作。
由上面兩個函數配合調試事件結構體,就可以構成一個完整的調試循環。以下這段調試循環的代碼摘自msdn:
//
// context frame
// this frame has a several purposes: 1) it is used as an argument to
// ntcontinue, 2) is is used to constuct a call frame for apc delivery,
// and 3) it is used in the user level thread creation routines.
// the layout of the record conforms to a standard call frame.
typedef struct _context {
//
// the flags values within this flag control the contents of
// a context record.
// if the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. if the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
// if the context record is used as an in out parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
// the context record is never used as an out only parameter.
dword contextflags;
// this section is specified/returned if context_debug_registers is
// set in contextflags. note that context_debug_registers is not
// included in context_full.
dword dr0;
dword dr1;
dword dr2;
dword dr3;
dword dr6;
dword dr7;
// this section is specified/returned if the
// contextflags word contians the flag context_floating_point.
floating_save_area floatsave;
// contextflags word contians the flag context_segments.
dword seggs;
dword segfs;
dword seges;
dword segds;
// contextflags word contians the flag context_integer.
dword edi;
dword esi;
dword ebx;
dword edx;
dword ecx;
dword eax;
// contextflags word contians the flag context_control.
dword ebp;
dword eip;
dword segcs; // must be sanitized
dword eflags; // must be sanitized
dword esp;
dword segss;
// this section is specified/returned if the contextflags word
// contains the flag context_extended_registers.
// the format and contexts are processor specific
byte extendedregisters[maximum_supported_extension];
} context;<code>`</code>
這個結構體看似很大,隻要了解彙編語言其實也并不大。前面章節中介紹了關于彙編語言的知識,對于結構體中的各個字段,讀者應該非常熟悉。關于各個寄存器的介紹,這裡就不重複了,這需要讀者翻看前面的内容。這裡隻介紹contextflags字段的功能,該字段用于控制getthreadcontext()和setthreadcontext()能夠擷取或寫入的環境資訊。contextflags的取值也隻能在winnt.h頭檔案中找到,其取值如下:
bool getthreadcontext(
handle hthread, // handle to thread with context
lpcontext lpcontext // context structure
);
bool setthreadcontext(
handle hthread, // handle to thread
const context *lpcontext // context structure
這兩個函數的參數基本一樣,hthread表示線程句柄,而lpcontext表示指向context的指針。所不同的是,getthreadcontext()是用來擷取線程環境的,setthreadcontext()是用來設定線程環境的。需要注意的是,在擷取或設定線程的上下文時,請将線程暫停後進行,以免發生“不明現象”。