原文出處:http://www./system/hooksys.asp 攔截win32 API
調(diào)用對于多數(shù)windows開發(fā)人員來說都一直是很有挑戰(zhàn)性的課題,,我承認(rèn),,這也是我感興趣的一個課題。鉤子機(jī)制就是用一種底層技術(shù)控制特定代碼段的執(zhí)
行,它同時提供了一種直觀的方法,,很容易就能改變操作系統(tǒng)的行為,,而并不需要涉及到代碼。這跟一些第三方產(chǎn)品類似,。
許多系統(tǒng)都通過攔截技術(shù)(spying techniques)利用現(xiàn)有windows應(yīng)用程序,。而攔截的一個重要目的,并不只是為應(yīng)用程序提供更高級功能,,而是為完成調(diào)試,。 與老式操作系統(tǒng)(如dos,win3.xx)不同,,現(xiàn)有操作系統(tǒng)(如WINNT/2K和win9X)使用了成熟的機(jī)制來分隔各進(jìn)程的地址空間,。這種架構(gòu)提 供了真正的內(nèi)存保護(hù),因此任何應(yīng)用程序都不能破壞屬于其它進(jìn)程的地址空間,,更不可能破壞操作系統(tǒng)本身。這使得開發(fā)系統(tǒng)相關(guān)的鉤子(system- aware hooks)變得十分困難,。 我寫這篇文章就是要探討一種簡單實(shí)用的鉤子機(jī)制,,它提供了一個簡單的接口,用來攔截不同的API調(diào)用,。它也示范了一些技巧,,可以幫助你開發(fā)出自己的api 攔截程序(spying system)。同時它還提供了一系列在WIN2K/NT和WIN98/ME(下面簡稱9X)等windows上攔截WIN32 API 的方法,。為了簡化我的描述,,我沒有引入UNICODE的相關(guān)內(nèi)容。但你只需對代碼作一些微小改動就能支持UNICODE,。 攔截應(yīng)用程序(Spying of applications)有許多好處: 1.監(jiān)視API函數(shù)
有助于控制API調(diào)用,,也讓開發(fā)人員在API調(diào)用期間跟蹤到應(yīng)用程序特定的“不可見”動作。它有助于開發(fā)人員全面掌握程序的細(xì)節(jié) (comprehensive validation of parameters),,也有助于發(fā)現(xiàn)潛在問題,。例如,有時候,,它能便于監(jiān)視內(nèi)存管理API引起的資源泄漏,。 2.調(diào)試和逆向工程
除了一般的調(diào)試方法,API鉤子也是一種值得稱道的非常流行的調(diào)試方式,。許多開發(fā)人員用鉤子來區(qū)分不同組件的執(zhí)行以及它們之間的關(guān)聯(lián),。因此它也用于獲取二進(jìn)制可執(zhí)行文件的信息。 3.深入操作系統(tǒng)內(nèi)部
通常開發(fā)人員都熱衷于深入了解操作系統(tǒng)并扮演著“調(diào)試者”的角色,。鉤子機(jī)制也是用于解碼未公開的或不為人知的API的有力技術(shù),。 4.?dāng)U展已有功能
可以向外部的windows應(yīng)用程序嵌入自定義模塊、增強(qiáng)原有函數(shù)的功能,這需要借助鉤子來重定向原有代碼的執(zhí)行序列(讓系統(tǒng)在執(zhí)行原有代碼過程中執(zhí)行用 戶自定義代碼),,從而擴(kuò)展現(xiàn)有模塊的功能,。例如,許多第三方軟件產(chǎn)品并不遵循指定的安全規(guī)則而只滿足用戶特定的使用需求,。攔截應(yīng)用程序允許開發(fā)者在原有 API執(zhí)行之前或之后添加屬于用戶自己的代碼,。這有助于改變已經(jīng)編譯好的代碼的行為。 對攔截系統(tǒng)的功能需求
在實(shí)現(xiàn)任何形式的API攔截系統(tǒng)之前,,都必須先做一些慎重考慮,。首先,你要決定是開發(fā)對單個程序的鉤子還是全局鉤子,。例如,,假設(shè)你只希望攔截一個程序,就
不必安裝全局鉤子了,;但如果要監(jiān)視一切對TerminateProcess()
和WriteProcessMemory()的調(diào)用,,唯一的辦法就是使用全局鉤子。選用何種方法都取決于特定的環(huán)境和要解決的問題,。
API攔截架構(gòu)的概要設(shè)計(jì) 通常攔截系統(tǒng)由至少兩部分組成——一個鉤子服務(wù)器(Hook
Server)和一個驅(qū)動(Driver),。鉤子服務(wù)器用于在合適時機(jī)把驅(qū)動注入目標(biāo)進(jìn)程,它也可以管理驅(qū)動,,甚至可以通過注入點(diǎn)獲取驅(qū)動的工作情況,。這
樣的設(shè)計(jì)比較粗略,很明顯它并未涉及所有可能的實(shí)現(xiàn)方式,。但這已經(jīng)能夠描述API攔截的框架了,。
如果需要實(shí)現(xiàn)特定的鉤子架構(gòu),應(yīng)該慎重考慮下面幾點(diǎn): a 要攔截什么程序
b 如何向目標(biāo)進(jìn)程注入DLL或者說應(yīng)用何種注入技術(shù)
c 使用何種攔截機(jī)制
希望讀者可以從以下章節(jié)找到答案,。
注入技術(shù) 1.注冊表
如果要向加載了USER32.DLL的進(jìn)程注入DLL,,只需向如下注冊表鍵寫入DLL的名稱:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
上述表鍵的值可包含單個或成組用逗號(,)或空格分隔的DLL名稱。根據(jù)MSDN文檔[參考7],,所有包含在上述鍵值內(nèi)的DLL,,都會被任何運(yùn)行在當(dāng)前用
戶登陸空間(current logon
session)的windows應(yīng)用程序所加載。有趣的是,,實(shí)際上,,這些DLL的加載過程其實(shí)是USER32初始化過程的一部分。USER32讀取上述
鍵值并為這些DLL的入口調(diào)用LoadLibrary(),。但這種方法只適用于那些加載了USER32.DLL的程序,。另外一種限制是,這種內(nèi)置的機(jī)制只
適用于windows2k/nt系統(tǒng),。這是一種安全的DLL注入方法,,但有以下缺點(diǎn):
a. 激活或撤銷進(jìn)程注入必須重啟windows
b. 被注入的DLL只被映射到那些加載了USER32.DLL的進(jìn)程,,所以這種方法至少不能注入控制臺程序,因?yàn)樗鼈兏静槐貙?dǎo)入USER32的函數(shù),。 c. 另外一方面,,注入方不可能控制注入過程。就是說,,DLL被注入了所有GUI程序,,不管注入方是否有這樣的需求。在只需要攔截少量程序的情況下,,這樣會顯得多余,。更多信息請參考[參考2]“利用注冊表注入DLL”。 2.全局Windows鉤子 的確,,另外一種很流行的DLL注入方法來自windows鉤子,。MSDN指出這種鉤子是系統(tǒng)消息處理機(jī)制中添加的陷阱。應(yīng)用程序可以通過安裝鉤子來監(jiān)視系統(tǒng)中的消息流(message traffic),,并在消息到達(dá)特定窗口過程之前處理它們,。
根據(jù)系統(tǒng)底層要求,這種全局鉤子一般在DLL內(nèi)實(shí)現(xiàn),。它的基本原理是,,鉤子回調(diào)過程在被攔截進(jìn)程的地址空間內(nèi)被調(diào)用。通過調(diào)用
SetWindowHookEx()并加入合適的參數(shù)來安裝一個鉤子,。一旦這種全局鉤子安裝好,操作系統(tǒng)就會把DLL映射到目標(biāo)進(jìn)程的地址空間,。此
時,,DLL內(nèi)的全局變量就變成局限于單個進(jìn)程(per-process),不能被各目標(biāo)進(jìn)程共享,。因此,,所有需要共享的變量應(yīng)該被放置在共享數(shù)據(jù)段。下圖
展示了一個例子:鉤子服務(wù)器注冊一個鉤子并將其注入到名為“Application one”和“Application two”的進(jìn)程的地址空間,。
圖1
每當(dāng)SetWindowsHookEx()執(zhí)行,,全局鉤子就會被注冊一次。一切正確時函數(shù)將返回該鉤子的句柄,。在用戶自定義的鉤子回調(diào)過程末尾調(diào)用
CallNextHookEx()時將要用到上述句柄,。成功調(diào)用SetWindowsHookEx()后,操作系統(tǒng)就會自動把這個DLL注入到符合要求的
所有進(jìn)程的地址空間,,但并不一定是立即注入,。下面具體來看過濾WH_GETMESSAGE消息的函數(shù)體:
//---------------------------------------------------------------------------
// GetMsgProc // // Filter function for the WH_GETMESSAGE - it's just a dummy function //--------------------------------------------------------------------------- LRESULT CALLBACK GetMsgProc( int code, // hook code WPARAM wParam, // removal option LPARAM lParam // message ) { // We must pass the all messages on to CallNextHookEx. return ::CallNextHookEx(sg_hGetMsgHook, code, wParam, lParam); }
全局鉤子被多個不共享相同地址空間的進(jìn)程加載。例如,,鉤子句柄sg_hGetMsgHook會被SetWindowsHookEx()函數(shù)獲取并作為參數(shù)
用于CallNextHookEx(),,它必須在各進(jìn)程的地址空間中使用。就是說,該句柄的值必須被客戶進(jìn)程和攔截服務(wù)器共享,。因此,,鉤子句柄應(yīng)該放置在
共享數(shù)據(jù)段中。
以下例子調(diào)用了#pragma data_seg()編譯預(yù)處理語句(使用共享數(shù)據(jù)段),。在此,,我要提醒一下,共享數(shù)據(jù)段內(nèi)的變量必須初始化,,否則它們將會被放置在默認(rèn)數(shù)據(jù)段,,同時#pragma data_seg()語句也會失效。
//---------------------------------------------------------------------------
// Shared by all processes variables //--------------------------------------------------------------------------- #pragma data_seg(".HKT") HHOOK sg_hGetMsgHook = NULL; BOOL sg_bHookInstalled = FALSE; // We get this from the application who calls SetWindowsHookEx()'s wrapper HWND sg_hwndServer = NULL; #pragma data_seg() 同時應(yīng)該添加SECTIONS語句到DLL的DEF文件,,如下所示: SECTIONS
.HKT Read Write Shared 或使用
#pragma comment(linker, "/section:.HKT, rws")
一旦鉤子DLL被加載到目標(biāo)進(jìn)程的地址空間,,要卸載該鉤子的話除了攔截服務(wù)器調(diào)用UnhookWindowHookEx()函數(shù)或客戶進(jìn)程退出就沒有其他 辦法了。當(dāng)攔截服務(wù)器調(diào)用UnhookWindowHookEx()函數(shù)時,,操作系統(tǒng)就會掃描一個列表,,這個列表包含了所有加載了鉤子dll的進(jìn)程。這時 操作系統(tǒng)會(根據(jù)列表內(nèi)進(jìn)程數(shù)量)相應(yīng)遞減鉤子DLL的鎖定計(jì)數(shù),,當(dāng)這個計(jì)數(shù)變?yōu)?,,DLL就會從對應(yīng)進(jìn)程的地址空間刪除。 下面列舉了上述方法的一些好處,。
a.這種機(jī)制都被WINNT/2K和9X系列的操作系統(tǒng)所支持,,后繼版本的windows系統(tǒng)也有望支持這種機(jī)制。
b.跟注冊表注入機(jī)制不同,,當(dāng)攔截服務(wù)器不再需要鉤子DLL時,,可以調(diào)用UnhookWindowsHookEx()卸載鉤子。
盡管我認(rèn)為windows鉤子方便實(shí)用,,但它不可避免存在下列缺點(diǎn):
a.windows鉤子會明顯減低windows的性能,,因?yàn)樗黾恿讼到y(tǒng)處理每個消息時所需開銷。
b.要調(diào)試全局鉤子很麻煩,。但如果你同時運(yùn)行多于一個vc++實(shí)例,,就會發(fā)現(xiàn)在復(fù)雜情況下這樣調(diào)試反而簡單些。
c.最后一個不容忽視的問題,,這種鉤子會影響整個系統(tǒng),,在特定環(huán)境下(鉤子中有bug),需要重啟系統(tǒng)來恢復(fù)它,。
3.用CreateRemoteThread()函數(shù)遠(yuǎn)程注入DLL 這是我最喜歡的一種注入方式,。不幸的是只有NT和2K系統(tǒng)支持。該函數(shù)的獨(dú)特之處在于,,它也可以在windows9x上被執(zhí)行,,但只返回NULL而不做任何操作,。
遠(yuǎn)程注入DLL是Jeffrey Ritcher發(fā)明的,并記錄在他的文章[參考9] "Load Your 32-bit DLL into Another Process's Address Space Using INJLIB"中,。
基本原理簡單且巧妙,。任何進(jìn)程都可以調(diào)用LoadLibrary()來動態(tài)加載DLL。問題是當(dāng)我們?nèi)狈δ繕?biāo)(外部)進(jìn)程子線程的訪問權(quán)限時,,如何根據(jù)
自己的意愿強(qiáng)制外部進(jìn)程調(diào)用這個函數(shù),?這里要用到CreateRemoteThread()函數(shù)了,它可以遠(yuǎn)程產(chǎn)生線程,。這里有個竅門——且看線程體函數(shù)
的原型,,它的指針(類型為LPTHREAD_START_ROUTINE)被作為參數(shù)傳遞給了CreateRemoteThread():
DWORD WINAPI ThreadProc(LPVOID lpParameter);
下面是LoadLibrary()的原型:
HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName);
它們都有相似之處。它們都使用相同的WINAPI調(diào)用約定,,它們都接受一個參數(shù),,且返回值的長度也是一樣的。上面的比較告訴我們,,可以把LoadLibrary()作為線程體,,這樣它就可以在遠(yuǎn)程線程產(chǎn)生后被執(zhí)行。接下來看下面的示例代碼:
hThread = ::CreateRemoteThread(
hProcessForHooking, NULL, 0, pfnLoadLibrary, "C:HookTool.dll", 0, NULL); 調(diào)用GetProcAddress()可以獲取LoadLibrary()的地址,。很巧妙的一點(diǎn)是,,KERNEL32.DLL總是被映射到進(jìn)程地址空間的 相同位置,因此在每個進(jìn)程中,,LoadLibrary()的地址總是相同的,。這就保證了CreateRemoteThread()接收到的參數(shù)總是一個有 效指針。 我們用DLL的絕對路徑作為線程體函數(shù)的實(shí)參,,并轉(zhuǎn)換成LPVOID類型,。遠(yuǎn)程線程運(yùn)行時,它會把DLL的路徑傳遞給線程體函數(shù)(LoadLibrary),。上述就是用遠(yuǎn)程線程注入dll的全部竅門,。
在此需要慎重考慮的是,,是否使用CreateRemoteThread()實(shí)現(xiàn)遠(yuǎn)程注入,。每當(dāng)注入程序訪問目標(biāo)進(jìn)程的虛擬地址空間之前都要調(diào)用
CreateRemoteThread(),它首先會用OpenProcess()打開目標(biāo)進(jìn)程,,并傳遞PROCESS_ALL_ACCESS標(biāo)志作為實(shí)
參,,這樣對目標(biāo)進(jìn)程會有最高訪問權(quán)限。這種情況下,,OpenProcess()對于某些低ID(low
ID)進(jìn)程會返回NULL,。這是因?yàn)椋M管使用合法的進(jìn)程ID,,但注入程序的上下文所具有的權(quán)限還不足以訪問目標(biāo)進(jìn)程,。稍思考片刻,,你立即會發(fā)現(xiàn)這其實(shí)很
必要。所有被嚴(yán)格限制訪問的進(jìn)程都是操作系統(tǒng)的一部分,,因此普通進(jìn)程不應(yīng)該訪問它們,。如果一些存在bug的進(jìn)程突然試圖終止一個操作系統(tǒng)的進(jìn)程會發(fā)生什么
事情呢?為了避免操作系統(tǒng)上發(fā)生這些問題,,應(yīng)用程序需要具備足夠的特權(quán)才能調(diào)用那些改變操作系統(tǒng)行為的API,,要通過OpenProcess()訪問操作
系統(tǒng)資源(例如smss.exe,winlogon.exe,,services.exe等),,你必須具有調(diào)試特權(quán)級(debug
privilege)。這是一種非常強(qiáng)大的功能,,它提供一種訪問操作系統(tǒng)資源的途徑,,這通常是被限制的。調(diào)整進(jìn)程特權(quán)級的過程比較麻煩,,可以描述如下:
a.用目標(biāo)特權(quán)級所需訪問許可打開進(jìn)程記號(processs token)
b.為了指定特權(quán)級名稱“SeDebugPrivilege”,,必須定位它的本地LUID映射。各個特權(quán)級都被冠以名稱并可以在平臺SDK的winnt.h中找到,。 c.調(diào)用AdjustTokenProvileges()函數(shù)以調(diào)整進(jìn)程記號(token),,這樣就使得“SeDebugPrivilege”特權(quán)生效。 d. 關(guān)閉通過OpenProcessToken()函數(shù)獲得的進(jìn)程記號句柄,。 關(guān)于改變特權(quán)級的更多信息可以參考[參考10]“using privilege”,。
4.通過BHO插件注入 有時候我們只想把自定義代碼注入Inernet
Explorer。幸運(yùn)的是,,微軟公司為這樣的需求提供了一種簡單且有詳細(xì)文檔記錄的解決方法——瀏覽器輔助對象(BHO),。BHO用COM
DLL實(shí)現(xiàn),而且一旦正確注冊,,以后每當(dāng)IE加載,,所有實(shí)現(xiàn)了IobjectWithSite接口的COM組件都會隨之一起加載。
5.微軟Office插件 與BHO插件類似,,如果要向微軟office系列應(yīng)用程序注入用戶自定義代碼,,只需借助微軟提供的高級機(jī)制來實(shí)現(xiàn)office插件,以達(dá)到上述目的,。很多現(xiàn)成代碼展示了如何實(shí)現(xiàn)這類插件,。
攔截機(jī)制
向外部進(jìn)程注入DLL是攔截系統(tǒng)的關(guān)鍵環(huán)節(jié)。它提供了控制外部線程活動的極好機(jī)會,。盡管如此,,這樣還不足以攔截API調(diào)用。
本部分將會對現(xiàn)實(shí)世界中的API攔截方法作一個概述,,并著眼于每種方法的要點(diǎn),,同時揭示它們各自的優(yōu)點(diǎn)和缺點(diǎn),。 根據(jù)所使用鉤子的層次,可以把攔截API的鉤子機(jī)制分為兩種——內(nèi)核級和用戶級,。要更好的理解這兩種層次,,就必須掌握win32子系統(tǒng)API和內(nèi)部 API(Native API)之間的關(guān)系。下圖解釋了不同層次的鉤子所在的位置,,并說明了在Windows 2k系統(tǒng)上,,各個模塊的關(guān)系以及它們的依賴性。 圖2
在實(shí)現(xiàn)上主要的不同點(diǎn)在于內(nèi)核級攔截引擎用內(nèi)核模式驅(qū)動程序?qū)崿F(xiàn),,而用戶級鉤子通常以用戶模式DLL實(shí)現(xiàn),。
1.NT內(nèi)核級鉤子
在內(nèi)核模式中有幾種方法攔截NT系統(tǒng)服務(wù)。最流行的方法在Mark Russinovich和Bryce Cogswell的文章[參考3]"Windows NT System-Call Hooking"中有詳細(xì)描述,。其基本思想是,,在用戶模式下實(shí)現(xiàn)監(jiān)視NT系統(tǒng)調(diào)用的攔截機(jī)制。這種技術(shù)非常強(qiáng)大,,它提供了一種靈活的方法,,在系統(tǒng)內(nèi)核處理 用戶線程請求前將其攔截下來。 你可以在 "Undocumented Windows 2000 Secrets" 中找到上述機(jī)制的極好設(shè)計(jì)和實(shí)現(xiàn),。在這本書中,,Sven Schreiber 解釋了如何從零開始建立一個內(nèi)核級鉤子框架[參考5]。
另外一個全面分析和高明的實(shí)現(xiàn)來自Prasad Dabak所著的[參考17]"Undocumented Windows NT",。盡管如此,,所有上述的攔截策略都超出了本文的討論范圍。
2.Win32用戶級鉤子
A. 窗口子類
這種方法適用于那些會根據(jù)不同窗口過程的實(shí)現(xiàn)而具有不同行為的應(yīng)用程序,。要完成上述工作(通過更改窗口過程來執(zhí)行用戶自定義代碼),,只需對該特定窗口簡單
調(diào)用SetWindowLongPtr(),傳遞GWLP_WNDPROC和用戶自定義窗口過程的指針作為實(shí)參即可,。一旦建立好用戶自定義窗口過程,,以后
windows每次分發(fā)消息到目標(biāo)窗口時,都會調(diào)用用戶自定義的窗口過程了,。
這種機(jī)制的缺點(diǎn)是,,子類只在指定進(jìn)程(當(dāng)前進(jìn)程)邊界范圍內(nèi)(the boundaries of a specific process)有效。就是說,,應(yīng)用程序不能為其它進(jìn)程創(chuàng)建的窗口建立窗口子類,。
通常,,這種方法適用于通過插件攔截應(yīng)用程序,,這樣就能夠取得要替換窗口過程的窗口的句柄了。
例如,,以前我寫過一個簡單的IE插件(BHO),,它通過窗口子類把IE的浮動菜單替換掉,。
B. 代理DLL(DLL木馬)
攔截API的另一種簡單方法是,用具有相同名稱,、相同導(dǎo)出符號的DLL替換掉應(yīng)用程序原來的DLL,。借助函數(shù)導(dǎo)出節(jié)(function
forwarders)實(shí)現(xiàn)這種技術(shù)會很容易。從根本上說,,函數(shù)導(dǎo)出節(jié)就是DLL入口處的導(dǎo)出節(jié),,它代表本模塊與其它DLL的函數(shù)調(diào)用關(guān)系。
你可以簡單使用#pragma comment完成以上工作:
#pragma comment(linker, "/export:DoSomething=DllImpl.ActuallyDoSomething")
如果你決定使用這種方法,,你應(yīng)該自行處理庫新舊版本之間的兼容性問題,。更多信息請參考[參考13a]“Export forwarding”和[參考2]“Function Forwarders”。
C. 代碼重寫(Code Overwriting) 很多函數(shù)攔截的方法都基于代碼重寫,。其中一種通過改變call指令的目標(biāo)地址實(shí)現(xiàn)代碼重寫,。這種方法使用困難,而且容易出錯,?;舅枷胧牵瑪r截內(nèi)存中所有的call指令并以用戶提供的地址替換其原來的函數(shù)地址,。
代碼重寫的另一種方法實(shí)現(xiàn)起來更復(fù)雜,。簡單說,基本思想就是先定位原有API函數(shù)地址,,然后通過jmp指令改變函數(shù)體前幾個字節(jié)來重定向到用戶自定義的
API函數(shù)去執(zhí)行,。這種方法需要極強(qiáng)技巧性,并涉及到對每個call調(diào)用的一系列恢復(fù)和攔截操作,。要指出的一點(diǎn)是,,如果函數(shù)處于未被攔截的狀態(tài)
(unhooked mode)并且該函數(shù)正被調(diào)用,則將不能攔截到對該函數(shù)的下一次調(diào)用,。
上述方法的主要問題是,,它跟多線程環(huán)境中的線程規(guī)則相沖突。
盡管如此,,還是有巧妙解決方法的,,它解決了一些問題并提供了可以基本實(shí)現(xiàn)API攔截的成熟的方法。如果對上述問題有興趣,,可以查看[參考12]的Detours解決方案,。
D.通過調(diào)試器攔截api調(diào)用
另一個替代的方法是在目標(biāo)函數(shù)內(nèi)插入斷點(diǎn)。但這種方法也有些缺點(diǎn),。主要問題是拋出調(diào)試異常時(debugging
exceptions)會掛起當(dāng)前應(yīng)用程序所有子線程的執(zhí)行,。還需要一個調(diào)試線程處理這個異常。另一個問題是,,當(dāng)調(diào)試過程(debugger)完成
時,,windows就會把調(diào)試器關(guān)閉。
E.通過改變導(dǎo)入地址表攔截api調(diào)用
這種技術(shù)最初由 Matt Pietrek 公布,,后來由 Jeffrey Ritcher
([參考2] “通過操作模塊的導(dǎo)入節(jié)實(shí)現(xiàn)API攔截”)和 John Robbins
([參考4]“攔截導(dǎo)入函數(shù)”)加以詳細(xì)描述,。這是種強(qiáng)大簡單而且容易實(shí)現(xiàn)的方法,,也滿足在winNT/2k和9x上運(yùn)行攔截系統(tǒng)的大部分需求。這種技術(shù)
基于windows的可執(zhí)行文件結(jié)構(gòu),。要理解這種方法的工作過程,,必須熟悉PE文件結(jié)構(gòu),,它是通用文件對象格式(COFF)的擴(kuò)展,。Matt
Pietrek
在[參考6]“深入PE格式”以及[參考13a/b]“win32PE格式深度透視”中詳述了PE格式的相關(guān)細(xì)節(jié)。我將給出PE格式的概述,,旨在讓讀者明
白通過操作導(dǎo)入表實(shí)現(xiàn)api攔截的思想,。
通常來說,一個PE二進(jìn)制文件的格式是經(jīng)過組織的,,因此它具有代碼節(jié)(code
sections)和數(shù)據(jù)節(jié)(data
sections),這與可執(zhí)行文件在內(nèi)存中的格式是一致的,。PE文件格式在邏輯上由幾個節(jié)組成,,每個節(jié)維護(hù)特定的數(shù)據(jù),并符合操作系統(tǒng)程序加載器的特定
要求,。
請注意 .idata節(jié),,它包含導(dǎo)入地址表的信息,。這部分信息對于一個更改IAT攔截api調(diào)用的系統(tǒng)相當(dāng)重要。
符合PE格式的可執(zhí)行文件都具有下圖所述的結(jié)構(gòu):
圖3
應(yīng)用程序加載器負(fù)責(zé)把應(yīng)用程序以及與它相鏈接的DLL加載到內(nèi)存,由于這些DLL被加載到的內(nèi)存地址是不可預(yù)料的,,因此加載器不能斷定各個導(dǎo)入函數(shù)的實(shí)際
地址。加載器必須做一些額外工作來保證應(yīng)用程序能成功調(diào)用每個導(dǎo)入函數(shù),。但是對于內(nèi)存中的每個可執(zhí)行文件映像,逐個修改它們的每個導(dǎo)入函數(shù)會花費(fèi)大量的處
理器時間并導(dǎo)致性能下降,。那么,加載器是如何處理這個問題的呢,?關(guān)鍵在于,,對同一個導(dǎo)入函數(shù)的每次調(diào)用都指向相同地址,那就是函數(shù)代碼駐留在內(nèi)存的位置,。
實(shí)際上對導(dǎo)入函數(shù)的調(diào)用都是間接調(diào)用,,即通過一條間接JMP指令并結(jié)合IAT實(shí)現(xiàn)尋址。這樣的好處是加載器不需要掃描整個可執(zhí)行映像,。這種方法看上去很簡
單,,它僅僅是改變IAT內(nèi)導(dǎo)入函數(shù)的地址,。這里是一個簡單win32程序的PE結(jié)構(gòu)的示例,并借助了[參考8]的PEView
工具,。可以看到TestApp導(dǎo)入表包含了兩個由GDI32.DLL導(dǎo)出的函數(shù):TextOutA() 和 GetStockObject(),。
圖4 實(shí)際上攔截一個導(dǎo)入函數(shù)并沒有那么難,。總之,,一個通過修改IAT來攔截api的攔截系統(tǒng)需要找出IAT中存儲的導(dǎo)入函數(shù)地址,并用自定義函數(shù)的地址覆蓋它,。這個用戶自定義的函數(shù)必須和被替換函數(shù)的原形一致,,這點(diǎn)很重要。下面是替換的步驟:
1,、對于目標(biāo)進(jìn)程以及它加載的每個DLL,,都要通過IAT定位導(dǎo)入節(jié)的位置。
2,、找到導(dǎo)出目標(biāo)函數(shù)的DLL的IMAGE_IMPORT_DESCRIPTOR 束,。實(shí)際上,我們通過DLL的名稱來找到這個入口,。
3,、找到含有目標(biāo)函數(shù)地址的IMAGE_THUNK_DATA 束,。
4、用自定義函數(shù)的地址替換原有地址,。
要改變IAT內(nèi)導(dǎo)入函數(shù)的地址,,我們必須保證所有對目標(biāo)函數(shù)的調(diào)用都被重定向(re-routed)到鉤子函數(shù),。
還有一點(diǎn)是,,需要改寫的 .idata 節(jié)不一定都是可寫的,這就要求我們保證 .idata 節(jié)可寫,??梢哉{(diào)用 VirtualProtect() 來實(shí)現(xiàn),。
另一點(diǎn)值得注意的是,,GetProcAddr()
函數(shù)在win9x系統(tǒng)上的行為。當(dāng)一個程序在非調(diào)試模式調(diào)用這個API,,它會返回目標(biāo)函數(shù)的指針;但如果在調(diào)試時調(diào)用,,它會返回一個與上述指針不同的地
址,。這是由于在調(diào)試的時候,,GetProcAddr()
返回的是指向目標(biāo)函數(shù)指針的指針,。這個指針指向一條帶有目標(biāo)函數(shù)地址的PUSH指令。就是說,,在win9x系統(tǒng)上迭代
(IMAGE_THUNK_DATA)束時,,我們必須檢查這個函數(shù)指針是否帶有PUSH指令(在x86平臺上是0x86),,并相應(yīng)地獲取函數(shù)的實(shí)際地址。
Win9x并不支持寫時拷貝,,因此操作系統(tǒng)會嘗試阻止調(diào)試器訪問2GB邊界以上的函數(shù)。這就是GetProcAddr() 返回調(diào)試指針(debug thunk)而不是實(shí)際地址的原因,。John Robbins 在[參考4]“攔截導(dǎo)入函數(shù)”中討論了這個問題,。
注入攔截DLL的時機(jī)
在此前已經(jīng)討論過,選擇的注入機(jī)制并不是操作系統(tǒng)本身的固有機(jī)制時開發(fā)人員所面臨的困難,。例如,,當(dāng)使用內(nèi)置的windows鉤子注入DLL時,,注入機(jī)制就
不是開發(fā)人員所要關(guān)心的問題,。強(qiáng)制每個符合要求的進(jìn)程加載DLL[參考18],,這是操作系統(tǒng)需要完成的工作,。事實(shí)上,,windows跟蹤所有新建立的進(jìn)程
并強(qiáng)制它們加載鉤子DLL。通過注冊表來管理dll注入與windows 鉤子類似,。這些方法的最大的好處是它們本身就是操作系統(tǒng)的一部分,。
與上述注入機(jī)制不同的是,利用CreateRemoteThread()
的注入方法還要求維護(hù)當(dāng)前運(yùn)行的進(jìn)程列表,。如果沒有及時注入,,攔截系統(tǒng)將會丟失一些原本需要攔截的api調(diào)用。每當(dāng)一個新進(jìn)程開始或關(guān)閉時,,鉤子服務(wù)器
(Hook Server)都要使用一種巧妙的方法來接收相關(guān)的通知,這很重要,。其中一種方法,,是通過攔截并監(jiān)視CreateProcess()
系列的API函數(shù)的調(diào)用(來獲得進(jìn)程開啟或關(guān)閉的通知)。這時當(dāng)用戶自定義函數(shù)被調(diào)用,,它就可以通過添加CREATE_SUSPENDED標(biāo)志調(diào)用原來的
CreateProcess(),。這意味著目標(biāo)進(jìn)程的主線程將被掛起,同時鉤子服務(wù)器將有機(jī)會利用手寫的機(jī)器碼指令把DLL注入到目標(biāo)進(jìn)程,,然后使用
ResumeThread() 喚醒目標(biāo)進(jìn)程,。細(xì)節(jié)可以參考[參考2]“利用CreateProcess()遠(yuǎn)程注入代碼”。
另一種檢測進(jìn)程執(zhí)行的方法,,是基于驅(qū)動程序的,。值得關(guān)注的是它極高的靈活性。Windows
nt/2k 提供了一個由NTOSKRNL導(dǎo)出的名為 PsSetCreateProcessNotifyRoutine()
的函數(shù),,這個函數(shù)允許增加一個回調(diào)函數(shù),,每當(dāng)有進(jìn)程產(chǎn)生或終止時這個回調(diào)函數(shù)都被調(diào)用。更多細(xì)節(jié)請參見[參考11]和[參考15],。
枚舉進(jìn)程和模塊
有時候我們希望利用CreateRemoteThread()
API來注入DLL,,特別是當(dāng)攔截系統(tǒng)運(yùn)行在windows
NT/2K上,。這種情況下,,鉤子服務(wù)器啟動后都會枚舉所有活動進(jìn)程并把DLL注入它們的地址空間。Windows 9x和windows2k
都內(nèi)置了這種進(jìn)程枚舉機(jī)制(Tool Help
庫,,輔助庫)的實(shí)現(xiàn)(由Kernel32.dll實(shí)現(xiàn)),。另一方面,windowsNT使用PSAPI庫達(dá)到相同目的,。于是,,我們需要一種能夠令鉤子服務(wù)
器正確運(yùn)行并動態(tài)決定當(dāng)前可用“幫助庫”的途徑。因此,,攔截系統(tǒng)被設(shè)計(jì)成可以判斷當(dāng)前操作系統(tǒng)支持何種幫助庫,,并相應(yīng)采用合適的API。
我將要展示一個以面向?qū)ο鬄榛A(chǔ)的簡單框架,,它用于在windows9x以上操作系統(tǒng)枚舉進(jìn)程和模塊,。我的設(shè)計(jì)允許用戶根據(jù)自己的需求來擴(kuò)展框 架的功能??蚣艿膶?shí)現(xiàn)也是相當(dāng)簡單明了的,。
CTaskManager類實(shí)現(xiàn)了整個枚舉子系統(tǒng)的處理核心,,它負(fù)責(zé)生成一個進(jìn)程枚舉庫對象(例如CPsapiHandler 或
CToolhelpHandler)來調(diào)用正確的進(jìn)程信息提供庫(例如在9x和2k上分別是psapi和toolHelp32)。
CTaskManager也負(fù)責(zé)產(chǎn)生和維護(hù)一個記錄當(dāng)前所有進(jìn)程列表的容器對象,。在CTaskManager實(shí)例化之后,,攔截系統(tǒng)調(diào)用
Populate()函數(shù)。這個函數(shù)強(qiáng)制性地枚舉系統(tǒng)中所有進(jìn)程和dll庫并把它們的信息保存在CTaskManager的成員m_pProcesses
中,。
下面的uml圖展示了上述子系統(tǒng)各個類之間的關(guān)系:
圖5
此處要著重指出的一條是,,事實(shí)上,windowsNT的Kernel32.dll并沒有實(shí)現(xiàn)任何的ToolHelp32函數(shù),。因此我們必須使用運(yùn)行時動態(tài)
鏈接的方式額外鏈接這些函數(shù),。在windowsNT上如果使用靜態(tài)鏈接,那么不論應(yīng)用程序是否曾經(jīng)試圖調(diào)用任何ToolHelp32函數(shù),,代碼都會運(yùn)行失
敗,。更多信息請參見我的文章“在windows9x/2k和windowsNT上枚舉進(jìn)程和模塊的單一接口”。
建立鉤子工具系統(tǒng)(Hook Tool System)的必要條件
目前我已經(jīng)對攔截過程中用到的各種原理概念作了簡短介紹,,現(xiàn)在是時候來確定建立一個攔截系統(tǒng)的必要條件,,并研究其詳細(xì)設(shè)計(jì)了。下面是有關(guān)這個系統(tǒng)的一些總結(jié):
1)提供用戶級攔截系統(tǒng),,對通過名字導(dǎo)入的win32 api函數(shù)實(shí)施攔截
2)提供一種方法,,可以用windows鉤子 或遠(yuǎn)程線程把攔截驅(qū)動注入到所有正在運(yùn)行的進(jìn)程。攔截系統(tǒng)應(yīng)該提供ini文件來選擇使用何種方式
3)使用一種基于更改iat的攔截方法
4)攔截系統(tǒng)的架構(gòu)是基于面向?qū)ο蠹夹g(shù)的,、可重用的,、可擴(kuò)展的和分層的
5)用一種高效的可擴(kuò)展的機(jī)制來攔截api函數(shù)
6)(整個系統(tǒng))能夠達(dá)到預(yù)期的性能要求
7)在攔截驅(qū)動和鉤子服務(wù)器之間使用一種可靠的數(shù)據(jù)傳輸機(jī)制
8)實(shí)現(xiàn)TextOutA()、TextOutW()以及ExitProcess()的攔截
9)把攔截過程發(fā)生的事件都記錄在日志中
10)攔截系統(tǒng)可以運(yùn)行在任何基于Intel x86架構(gòu)的windows9x或以上的操作系統(tǒng)
設(shè)計(jì)和實(shí)現(xiàn)
本部分將討論鉤子架構(gòu)的關(guān)鍵部分以及它們之間的通訊方式,。這樣的架構(gòu)能夠攔截任何通過名稱導(dǎo)入的api函數(shù),。
在概述攔截系統(tǒng)的設(shè)計(jì)之前,請留意幾種注入和攔截方式,。
首先,,要選擇一種能夠把dll注入系統(tǒng)所有進(jìn)程的方式。因此我設(shè)計(jì)了一個抽象基類,,并實(shí)現(xiàn)兩種注入技術(shù),,根據(jù)ini文件的設(shè)置和操作系統(tǒng)版本(例如
windowsNT/2k或windows9x)來選擇兩種注入方法中的一種。這兩種方法分別是全局windows鉤子和遠(yuǎn)程線程,。在示例代碼中,,同時使
用了windows鉤子機(jī)制和遠(yuǎn)程線程的方式在windowsNT/2k系統(tǒng)上注入dll??梢酝ㄟ^修改一個包含攔截系統(tǒng)所有設(shè)置的ini文件來選擇注入
方式,。
另一個重點(diǎn)是選擇攔截機(jī)制,不必驚奇,我將使用修改iat來作為攔截win32api的最有效方法,。為了達(dá)到預(yù)期的效果,,我設(shè)計(jì)的鉤子框架將包含如下組件和文件:
1)TestApp.exe – 一個用于測試的簡單win32應(yīng)用程序,它僅僅使用TextOut()簡單的輸出了一行文本,。它的目的只為了展示api的攔截過程,。
2)HookSvr.exe - 控制dll注入和鉤子安裝的程序。
3)HookTool.dll –用win32 dll方式實(shí)現(xiàn)的鉤子庫
4)HookTool.ini – 配置文件
5)NTProcDrv.sys – 一個小型的用于監(jiān)視進(jìn)程產(chǎn)生和撤銷的windowsNT/2k內(nèi)核模式驅(qū)動,。這個組件是可選的,,它僅用于在windowsNT以上系統(tǒng)監(jiān)視進(jìn)程。
HookSrv是一個簡單的控制程序,。它主要用來加載HookTool.dll并激活攔截引擎,。加載dll以后,鉤子服務(wù)器傳遞一個隱藏窗口的句柄同時調(diào)
用InstallHook(),,HookTool.dll把所有消息都發(fā)送到這個窗口上,。HookTool.dll實(shí)現(xiàn)了攔截驅(qū)動且還是攔截系統(tǒng)的核心。
它實(shí)現(xiàn)了真正的攔截操作并攔截了TextOutA(),、TextOutW() 和 ExitProcess(),。
盡管文章著眼于windows內(nèi)部機(jī)制因而沒必要使用面向?qū)ο蠓椒ǎ胰允褂每芍赜胏++類封裝了相關(guān)操作,。這樣可以提供更多的靈活性并使系統(tǒng)易于被擴(kuò)展,。開發(fā)者也可以在本工程外的其它工程使用其中獨(dú)立的類,這對他們有好處,。
下面的UML圖解釋了HookTool dll所實(shí)現(xiàn)的各個類之間的關(guān)系,。
圖6
此處請大家留意HookTool.dll的類架構(gòu)。其中設(shè)計(jì)各個類的功能是開發(fā)過程的重要一環(huán),。每個類實(shí)現(xiàn)一個特定功能并對外表現(xiàn)為一個獨(dú)立的邏輯整體,。
CmoduleScope是整個系統(tǒng)的基類。它用Singleton模式實(shí)現(xiàn)且是線程安全(thread-safe)的,。它的構(gòu)造函數(shù)接受3個在共享數(shù)據(jù)
段聲明的指針,,這些指針將被所有進(jìn)程共享?;谏鲜龇椒ǎ陬愔羞@些變量可以很容易被維護(hù),,而不會破壞類封裝的原則,。
當(dāng)一個應(yīng)用程序加載HookTool庫時,dll在接收到DLL_PROCESS_ATTACH消息后便產(chǎn)生一個CModuleScope實(shí)例,。這一步初
始化了CmoduleScope的唯一實(shí)例,。CmoduleScope對象構(gòu)造的重要一環(huán)是產(chǎn)生一個合適的dll注入器對象。而選擇合適的注入器是在解釋
HookTool.ini文件并判斷[Scope]節(jié)下的UseWindowsHook參數(shù)后發(fā)生的,。當(dāng)攔截系統(tǒng)運(yùn)行在windows9x時,,這個參數(shù)的
值將不會被解釋,,因?yàn)閣indows9x不支持遠(yuǎn)程線程的注入方式。
上述實(shí)例化步驟完成后,,接著就會調(diào)用ManageModuleEnlistment() ,。以下是該函數(shù)的一個簡化版本:
// Called on DLL_PROCESS_ATTACH DLL notification
BOOL CModuleScope::ManageModuleEnlistment() ...{ BOOL bResult = FALSE; // Check if it is the hook server if (FALSE == *m_pbHookInstalled) ...{ // Set the flag, thus we will know that the server has been installed *m_pbHookInstalled = TRUE; // and return success error code bResult = TRUE; } // and any other process should be examined whether it should be // hooked up by the DLL else ...{ bResult = m_pInjector->IsProcessForHooking(m_szProcessName); if (bResult) InitializeHookManagement(); } return bResult; } ManageModuleEnlistment() 的實(shí)現(xiàn)簡單明了,通過檢查m_pbHookInstalled指向的變量,,它測試自身是否已經(jīng)被鉤子服務(wù)器調(diào)用過,。如果已經(jīng)被調(diào)用,它就只是簡單的把 sg_bHookInstalled設(shè)為TRUE,,表明鉤子服務(wù)器已經(jīng)啟動了,。 接著,鉤子服務(wù)器通過調(diào)用鉤子dll的導(dǎo)出函數(shù)InstallHook()
來激活鉤子安裝引擎,。實(shí)際上,,該函數(shù)只是簡單調(diào)用了CmoduleScope的InstallHookMethod()
函數(shù)。這個函數(shù)的作用是強(qiáng)制目標(biāo)進(jìn)程加載或卸載HookTool.dll,。HookTool.dll提供了兩種把自身注入外部進(jìn)程空間的方法——一是使用
Windows鉤子另外一種利用CreateRemoteThread()函數(shù),。在該系統(tǒng)的架構(gòu)上,定義了一個抽象類CInjector以及用于注入和卸
載dll的純虛函數(shù),。類CWinHookInjector和CremThreadInjector都從CInjector繼承,。盡管如此,但它們提供了兩
個純虛函數(shù)InjectModuleIntoAllProcesses()和EjectModuleFromAllProcesses() 的不同實(shí)現(xiàn),。
// Activate/Deactivate hooking engine
BOOL CModuleScope::InstallHookMethod(BOOL bActivate, HWND hWndServer) { BOOL bResult; if (bActivate) { *m_phwndServer = hWndServer; bResult = m_pInjector->InjectModuleIntoAllProcesses(); } else { m_pInjector->EjectModuleFromAllProcesses(); *m_phwndServer = NULL; bResult = TRUE; } return bResult; }
CwinHookInjector類用Windows鉤子實(shí)現(xiàn)注入機(jī)制,。它通過如下調(diào)用安裝過濾函數(shù)(Filter
Function):正如你在上面看到的,它向系統(tǒng)注冊了WM_GETMESSAGE類型的鉤子,。鉤子服務(wù)器只調(diào)用這個函數(shù)一次,。
SetWindowsHookEx() 的最后一個參數(shù)是0,,因?yàn)樵谶@里GetMsgProc()
是作為一個全局鉤子來使用的。當(dāng)某個窗口即將接收一條特定消息時,回調(diào)函數(shù)就被調(diào)用,。這里有趣的是,我們僅為回調(diào)函數(shù)GetMsgProc()
提供了幾近空的(什么也不做的)實(shí)現(xiàn),,因?yàn)槲覀儾恍枰O(jiān)控窗口的消息處理,。我們提供該函數(shù)的實(shí)現(xiàn)僅僅是要用到操作系統(tǒng)提供的注入機(jī)制。
// Inject the DLL into all running processes
BOOL CWinHookInjector::InjectModuleIntoAllProcesses() { *sm_pHook = ::SetWindowsHookEx( WH_GETMESSAGE, (HOOKPROC)(GetMsgProc), ModuleFromAddress(GetMsgProc), 0 ); return (NULL != *sm_pHook); } 調(diào)用SetWindowsHookEx()后,,操作系統(tǒng)將檢查導(dǎo)出GetMsgProc()的dll(例如HookTool.dll)是否已經(jīng)被映射到 GUI進(jìn)程的地址空間,。如果未被加載,Windows將強(qiáng)制該進(jìn)程映射它,。有趣的是,,全局鉤子的DllMain()函數(shù)不應(yīng)該返回FALSE。這是因?yàn)? windows會不斷驗(yàn)證DllMain()的返回值并加載該dll直到DllMain()返回TRUE。
另一個完全不同的實(shí)現(xiàn)來自CRemThreadInjector類,。此處的注入機(jī)制基于產(chǎn)生遠(yuǎn)程線程,。CRemThreadInjector通過接收進(jìn)程
產(chǎn)生和終止的消息來擴(kuò)展Windows的進(jìn)程列表維護(hù)機(jī)制。也就是產(chǎn)生一個CNtInjectorThread對象來監(jiān)視進(jìn)程產(chǎn)生,。
CntInjectorThread從內(nèi)核驅(qū)動接收進(jìn)程產(chǎn)生的相關(guān)消息,。因此每次有進(jìn)程產(chǎn)生,CNtInjectorThread
::OnCreateProcess() 都被調(diào)用,,相應(yīng)的,,當(dāng)進(jìn)程終止將自動調(diào)用CNtInjectorThread
::OnTerminateProcess()。與windows鉤子不同的是,,每次有進(jìn)程產(chǎn)生的時候,,采用遠(yuǎn)程線程機(jī)制都要手動向新線程注入dll。這
需要我們提供一種發(fā)送進(jìn)程產(chǎn)生事件的簡便方法,。CntDriverController類實(shí)現(xiàn)了一些列管理服務(wù)和驅(qū)動的api,。它被設(shè)計(jì)成加載和卸載內(nèi)核
模式驅(qū)動NTProcDrv.sys。后面將會討論它的實(shí)現(xiàn),。
成功向特定進(jìn)程注入HookTool.dll后,,該dll的DllMain()將調(diào)用ManageModuleEnlistment()。在前面我已經(jīng)闡
述過了重復(fù)調(diào)用該函數(shù)的原因,。它通過CmoduleScope的成員m_pbHookInstalled檢查共享變量
sg_bHookInstalled,。由于鉤子服務(wù)器在初始化時已經(jīng)把sg_bHookInstalled置TRUE,于是它便檢查當(dāng)前進(jìn)程是否需要被攔
截,,如果需要,,鉤子服務(wù)器將為該進(jìn)程激活攔截引擎。
在CmoduleScope
::InitializeHookManagement()的實(shí)現(xiàn)中,,攔截引擎被激活,。這個函數(shù)用于攔截LoadLibrary()系列的函數(shù)和
GetProcAddress()
函數(shù)。用這種方法,,我們可以在進(jìn)程初始化后監(jiān)視dll的加載,。每當(dāng)一個新的鉤子dll被映射到目標(biāo)進(jìn)程,它都要修改進(jìn)程的導(dǎo)入地址表,,因此我們可以保證系
統(tǒng)不會丟失對目標(biāo)函數(shù)的任何調(diào)用,。
在CmoduleScope ::InitializeHookManagement() 末尾,我們對目標(biāo)函數(shù)的鉤子函數(shù)作了初始化,。
由于示例代碼攔截了多于一個的用戶提供的win32
api函數(shù),,因此我們應(yīng)該對每個被攔截的函數(shù)提供獨(dú)立的鉤子函數(shù)。就是說,,用上述更改iat攔截api的方法,不僅需要把原目標(biāo)函數(shù)在導(dǎo)入表中的地址更改
為一個“通用的”鉤子函數(shù)的地址。攔截系統(tǒng)需要知道某個函數(shù)調(diào)用指向哪個函數(shù),。另外一點(diǎn)很重要的是,,鉤子函數(shù)必須有跟原api函數(shù)相同的函數(shù)原型,否則堆
棧將會崩潰,。例如CModuleScope實(shí)現(xiàn)了MyTextOutA(),、MyTextOutW()和MyExitProcess()3個靜態(tài)函數(shù)。一
旦HookTool.dll被加載到目標(biāo)進(jìn)程,,并且攔截系統(tǒng)被激活,,每次對原TextOutA()的調(diào)用都回變成對CmoduleScope
::MyTextOutA()的調(diào)用。
前面提及的攔截系統(tǒng)是非常靈活高效的,。盡管如此,,該系統(tǒng)一般只適合于攔截那些名稱和原型已知且數(shù)量有限的api函數(shù)。
如果你需要攔截新的函數(shù),,你應(yīng)該簡單聲明并實(shí)現(xiàn)它的鉤子函數(shù),,就如同前面實(shí)現(xiàn)MyTextOutA/w()和MyExitProcess()一樣。然后你應(yīng)該用前面在InitializeHookManagement()中實(shí)現(xiàn)的方法注冊被攔截的函數(shù),。
攔截和跟蹤進(jìn)程產(chǎn)生對實(shí)現(xiàn)那些需要處理外部進(jìn)程的系統(tǒng)十分有用,。獲取進(jìn)程產(chǎn)生過程中的有用信息向來是開發(fā)進(jìn)程監(jiān)視系統(tǒng)和攔截系統(tǒng)過程中的一個經(jīng)典問題。
Win32
api提供了一系列的庫([參考16]PSAPI和ToolHelp庫)用來枚舉當(dāng)前系統(tǒng)中運(yùn)行的進(jìn)程,。盡管它們功能很強(qiáng)大,,卻沒有提供接收進(jìn)程產(chǎn)生/撤
銷事件的方法。幸運(yùn)的是,,windowsNT/2K提供了一系列api,,被詳細(xì)記錄在windowsDDK文檔的“進(jìn)程結(jié)構(gòu)規(guī)則(Process
Structure
Routines)”下,并被NTOSKRNL導(dǎo)出,。其中之一的PsSetCreateProcessNotifyRoutine()允許注冊一個全局回
調(diào)函數(shù),,該函數(shù)在進(jìn)程產(chǎn)生、退出,、終止時被操作系統(tǒng)調(diào)用,。通過寫一個NT內(nèi)核模式驅(qū)動和win32用戶模式控制程序,就可以簡單的利用上述函數(shù)來實(shí)現(xiàn)進(jìn)程
監(jiān)視,。該驅(qū)動的作用是檢測進(jìn)程產(chǎn)生并把相關(guān)事件通知控制程序,。示例代碼中的windows進(jìn)程監(jiān)視器NTProcDrv提供了在基于NT內(nèi)核的操作系統(tǒng)上
監(jiān)視進(jìn)程的最基本功能。更多細(xì)節(jié)請參見文章[參考11]和[參考15],。相關(guān)代碼在NTProcDrv.c中,。由于該驅(qū)動的加載和釋放是動態(tài)的,因此當(dāng)前
登錄用戶必須擁有管理員權(quán)限,。否則驅(qū)動安裝將會失敗同時進(jìn)程監(jiān)視也會出錯,。解決辦法是用管理員權(quán)限手動安裝驅(qū)動,,或使用windows2k提供的“作為其
他用戶運(yùn)行(run as different user)”選項(xiàng)來運(yùn)行HookSrv.exe。
最后要注意的一點(diǎn)是,,可以通過簡單修改ini文件(HookTool..ini)中的相關(guān)設(shè)置來選用上述所有的工具,。這個文件決定了使用windows鉤
子(在win9x和nt/2k系統(tǒng)上)還是遠(yuǎn)程線程(僅在windowsnt/2k上)來做dll注入。也可以在該文件里面指定要監(jiān)視的進(jìn)程和不被監(jiān)視的
進(jìn)程,。如果需要查看被攔截進(jìn)程的詳細(xì)活動,,打開[Trace]節(jié)下面的Enabled選項(xiàng)就可以記錄下攔截系統(tǒng)的所有活動。通過調(diào)用由ClogFile類
導(dǎo)出的方法,,這個選項(xiàng)能讓用戶看到發(fā)生錯誤的內(nèi)容,。實(shí)際上,ClogFile提供了線程安全的實(shí)現(xiàn),,而且訪問共享資源的同步問題也不需要操心了(日志文
件),。更多信息請參看ClogFile類的實(shí)現(xiàn)以及HookTool.ini的內(nèi)容。
示例源代碼 編譯本工程需要vc++6 sp4和Platform
SDK,。在windowsNT平臺上需要提供PSAPI.DLL以運(yùn)行CtaskManager的實(shí)現(xiàn),。在運(yùn)行示例代碼之前必須確保已經(jīng)被設(shè)置好了
HookTool.ini文件。對于那些要擴(kuò)展NTProcDrv代碼和對本工程底層內(nèi)容感興趣的開發(fā)人員,,他們應(yīng)該安裝windowsDDK,。
延伸內(nèi)容 出于簡化問題的目的,在本文中我忽略了一些內(nèi)容:
1)監(jiān)視內(nèi)部的api(Native API)調(diào)用
2)windows9x系統(tǒng)上監(jiān)視進(jìn)程產(chǎn)生的驅(qū)動
3)支持UNICODE,,但你還是可以通過攔截UNICODE導(dǎo)入函數(shù)來達(dá)到相同目的,。
總結(jié) 到目前為止,本文尚未對攔截任意api的問題給出完整的指南,,毫無疑問,,本文遺漏了一些細(xì)節(jié)。盡管如此,,我仍在這少數(shù)的有限篇幅中給出了足夠重要的信息,,也許對那些在win32用戶模式做api攔截的開發(fā)人員有幫助。
參考文章
[1] "Windows 95 System Programming Secrets", Matt Pietrek
[2] "Programming Application for MS Windows" , Jeffrey Richter [3] "Windows NT System-Call Hooking" , Mark Russinovich and Bryce Cogswell, Dr.Dobb's Journal January 1997 [4] "Debugging applications" , John Robbins [5] "Undocumented Windows 2000 Secrets" , Sven Schreiber [6] "Peering Inside the PE: A Tour of the Win32 Portable Executable File Format" by Matt Pietrek, March 1994 [7] MSDN Knowledge base Q197571 [8] PEview Version 0.67 , Wayne J. Radburn [9] "Load Your 32-bit DLL into Another Process's Address Space Using INJLIB" MSJ May 1994 [10] "Programming Windows Security" , Keith Brown [11] "Detecting Windows NT/2K process execution" Ivo Ivanov, 2002 [12] "Detours" Galen Hunt and Doug Brubacher [13a] "An In-Depth Look into the Win32 PE file format" , part 1, Matt Pietrek, MSJ February 2002 [13b] "An In-Depth Look into the Win32 PE file format" , part 2, Matt Pietrek, MSJ March 2002 [14] "Inside MS Windows 2000 Third Edition" , David Solomon and Mark Russinovich [15] "Nerditorium", James Finnegan, MSJ January 1999 [16] "Single interface for enumerating processes and modules under NT and Win9x/2K." , Ivo Ivanov, 2001 [17] "Undocumented Windows NT" , Prasad Dabak, Sandeep Phadke and Milind Borate [18] Platform SDK: Windows User Interface, Hooks 歷史
2002年3月21日 – 修改了源代碼
2002年5月12日 - 修改了源代碼
2002年9月4日 - 修改了源代碼
2002年12月3日 – 修改了源代碼和示例程序
關(guān)于Ivo Ivanov
Ivo是InfoProcess公司的創(chuàng)始人之一和技術(shù)總監(jiān),。該公司位于澳大利亞,,主要從事開發(fā)基于windows的主機(jī)入侵檢測系統(tǒng)。在成立
InfoProcess公司前,,Ivo曾經(jīng)作為首席技術(shù)架構(gòu)師和高級程序員,,任職于包括Professional Advantage
和NICE公司在內(nèi)的業(yè)界領(lǐng)先的軟件解決方案提供商。他精通windows操作系統(tǒng),,面向?qū)ο笤O(shè)計(jì),,以及基于C/C++和C#的串開發(fā)技術(shù)(string
development skills)。
更多細(xì)節(jié)請關(guān)注Http://www.infoProcess.com.au以及http://www./script/profile/whos_who.asp?vt=arts&id=24862,。
譯者后話
之前因?yàn)楣ぷ餍枰?,從codeProject上找到了Ivo先生的這篇文章,,實(shí)在受益匪淺,。文章中Ivo先生幾乎是毫無保留的把所知道的關(guān)于用戶模式
api攔截的內(nèi)容都深入淺出地向讀者展示出來,,基本上文章連同附帶的源代碼就是api攔截方面很全面的入門級指南了,。真的佩服Ivo先生在技術(shù)上無私奉獻(xiàn)
的精神,。 早些時候也在codeProject上找到另外一些相關(guān)文章,。相比之下,其中一篇給我的印象實(shí)在是深刻,。那大作的作者也在試圖介紹一個可能是他自己做的 api攔截系統(tǒng),,但篇幅就少得多,只有短短一兩頁,,也就是簡單說說那系統(tǒng)的功能而以,。而且等我把源代碼下下來才發(fā)現(xiàn),說明文件用的是中文,,內(nèi)容不介紹其 它,,卻是在向讀者聲明,該代碼的升級維護(hù)需要跟他本人聯(lián)系,,還標(biāo)出價格人民幣400元,;而且系統(tǒng)中用到的攔截庫不包含源代碼,如果要代碼,,就給他匯錢,,還 給出銀行賬號,戶名,,中文名字,,XX。這不就是咱中國程序員的大作麼,。 寫程序的都知道,,codeProject是世界各地程序員交流技術(shù)共享資源的地方,講求互助,。那位仁兄不但吝嗇于展示他技術(shù)(這不說,,畢竟人還是有點(diǎn)私心 的);更要命的是,,他竟只會張口要錢,,用的還是堂堂中文。篇幅足有上面的文章長短,。丟臉啊,。真的沒錢嗎?真的窮到要跑到外國人的地方去丟人現(xiàn)眼嗎,?姑且不 評論他代碼寫得怎樣,,但類似文章codeProject上一抓一大把,,且那位仁兄還只懂向別人索取而毫無付出。我只想問,,你有這資本么,?其它國家的同行看 到會怎么想我們國人呢?真丟臉,。只希望其它同行都不懂中文,,或者打開全是亂碼。,。,。呵呵,聊以自我安慰,。 |
|