揭示Win32 API攔截細節/API hooking revealed (2)

2.Win32用戶級鉤子
A. 窗口子類
    這種方法適用於那些會根據不同窗口過程的實現而具有不同行爲的應用程序。要完成上述工作(通過更改窗口過程來執行用戶自定義代碼),只需對該特定窗口簡單調用SetWindowLongPtr(),傳遞GWLP_WNDPROC和用戶自定義窗口過程的指針作爲實參即可。一旦建立好用戶自定義窗口過程,以後windows每次分發消息到目標窗口時,都會調用用戶自定義的窗口過程了。
    這種機制的缺點是,子類只在指定進程(當前進程)邊界範圍內(the boundaries of a specific process)有效。就是說,應用程序不能爲其它進程創建的窗口建立窗口子類。
    通常,這種方法適用於通過插件攔截應用程序,這樣就能夠取得要替換窗口過程的窗口的句柄了。
    例如,以前我寫過一個簡單的IE插件(BHO),它通過窗口子類把IE的浮動菜單替換掉。
B. 代理DLL(DLL***)
    攔截API的另一種簡單方法是,用具有相同名稱、相同導出符號的DLL替換掉應用程序原來的DLL。藉助函數導出節(function forwarders)實現這種技術會很容易。從根本上說,函數導出節就是DLL入口處的導出節,它代表本模塊與其它DLL的函數調用關係。
    你可以簡單使用#pragma comment完成以上工作:
      #pragma comment(linker, "/export:DoSomething=DllImpl.ActuallyDoSomething")
    如果你決定使用這種方法,你應該自行處理庫新舊版本之間的兼容性問題。更多信息請參考[參考13a]“Export forwarding”和[參考2]“Function Forwarders”。

C. 代碼重寫(Code Overwriting)
    很多函數攔截的方法都基於代碼重寫。其中一種通過改變call指令的目標地址實現代碼重寫。這種方法使用困難,而且容易出錯。基本思想是,攔截內存中所有的call指令並以用戶提供的地址替換其原來的函數地址。
    代碼重寫的另一種方法實現起來更復雜。簡單說,基本思想就是先定位原有API函數地址,然後通過jmp指令改變函數體前幾個字節來重定向到用戶自定義的API函數去執行。這種方法需要極強技巧性,並涉及到對每個call調用的一系列恢復和攔截操作。要指出的一點是,如果函數處於未被攔截的狀態(unhooked mode)並且該函數正被調用,則將不能攔截到對該函數的下一次調用。
    上述方法的主要問題是,它跟多線程環境中的線程規則相沖突。
    儘管如此,還是有巧妙解決方法的,它解決了一些問題並提供了可以基本實現API攔截的成熟的方法。如果對上述問題有興趣,可以查看[參考12]的Detours解決方案。

D.通過調試器攔截api調用
    另一個替代的方法是在目標函數內插入斷點。但這種方法也有些缺點。主要問題是拋出調試異常時(debugging exceptions)會掛起當前應用程序所有子線程的執行。還需要一個調試線程處理這個異常。另一個問題是,當調試過程(debugger)完成時,windows就會把調試器關閉。
E.通過改變導入地址表攔截api調用
    這種技術最初由 Matt Pietrek 公佈,後來由 Jeffrey Ritcher ([參考2] “通過操作模塊的導入節實現API攔截”)和 John Robbins ([參考4]“攔截導入函數”)加以詳細描述。這是種強大簡單而且容易實現的方法,也滿足在winNT/2k和9x上運行攔截系統的大部分需求。這種技術基於windows的可執行文件結構。要理解這種方法的工作過程,必須熟悉PE文件結構,它是通用文件對象格式(COFF)的擴展。Matt Pietrek 在[參考6]“深入PE格式”以及[參考13a/b]“win32PE格式深度透視”中詳述了PE格式的相關細節。我將給出PE格式的概述,旨在讓讀者明白通過操作導入表實現api攔截的思想。
    通常來說,一個PE二進制文件的格式是經過組織的,因此它具有代碼節(code sections)和數據節(data sections),這與可執行文件在內存中的格式是一致的。PE文件格式在邏輯上由幾個節組成,每個節維護特定的數據,並符合操作系統程序加載器的特定要求。
    請注意 .idata節,它包含導入地址表的信息。這部分信息對於一個更改IAT攔截api調用的系統相當重要。
    符合PE格式的可執行文件都具有下圖所述的結構:


圖3
    應用程序加載器負責把應用程序以及與它相鏈接的DLL加載到內存,由於這些DLL被加載到的內存地址是不可預料的,因此加載器不能斷定各個導入函數的實際地址。加載器必須做一些額外工作來保證應用程序能成功調用每個導入函數。但是對於內存中的每個可執行文件映像,逐個修改它們的每個導入函數會花費大量的處理器時間並導致性能下降。那麼,加載器是如何處理這個問題的呢?關鍵在於,對同一個導入函數的每次調用都指向相同地址,那就是函數代碼駐留在內存的位置。實際上對導入函數的調用都是間接調用,即通過一條間接JMP指令並結合IAT實現尋址。這樣的好處是加載器不需要掃描整個可執行映像。這種方法看上去很簡單,它僅僅是改變IAT內導入函數的地址。這裏是一個簡單win32程序的PE結構的示例,並藉助了[參考8]的PEView 工具。可以看到TestApp導入表包含了兩個由GDI32.DLL導出的函數:TextOutA() 和 GetStockObject()。

圖4
    實際上攔截一個導入函數並沒有那麼難。總之,一個通過修改IAT來攔截api的攔截系統需要找出IAT中存儲的導入函數地址,並用自定義函數的地址覆蓋它。這個用戶自定義的函數必須和被替換函數的原形一致,這點很重要。下面是替換的步驟:
    1、對於目標進程以及它加載的每個DLL,都要通過IAT定位導入節的位置。
    2、找到導出目標函數的DLL的IMAGE_IMPORT_DESCRIPTOR 束。實際上,我們通過DLL的名稱來找到這個入口。
    3、找到含有目標函數地址的IMAGE_THUNK_DATA 束。
    4、用自定義函數的地址替換原有地址。
    要改變IAT內導入函數的地址,我們必須保證所有對目標函數的調用都被重定向(re-routed)到鉤子函數。
    還有一點是,需要改寫的 .idata 節不一定都是可寫的,這就要求我們保證 .idata 節可寫。可以調用 VirtualProtect() 來實現。
    另一點值得注意的是,GetProcAddr() 函數在win9x系統上的行爲。當一個程序在非調試模式調用這個API,它會返回目標函數的指針;但如果在調試時調用,它會返回一個與上述指針不同的地址。這是由於在調試的時候,GetProcAddr() 返回的是指向目標函數指針的指針。這個指針指向一條帶有目標函數地址的PUSH指令。就是說,在win9x系統上迭代(IMAGE_THUNK_DATA)束時,我們必須檢查這個函數指針是否帶有PUSH指令(在x86平臺上是0x86),並相應地獲取函數的實際地址。
    Win9x並不支持寫時拷貝,因此操作系統會嘗試阻止調試器訪問2GB邊界以上的函數。這就是GetProcAddr() 返回調試指針(debug thunk)而不是實際地址的原因。John Robbins 在[參考4]“攔截導入函數”中討論了這個問題。

注入攔截DLL的時機
    在此前已經討論過,選擇的注入機制並不是操作系統本身的固有機制時開發人員所面臨的困難。例如,當使用內置的windows鉤子注入DLL時,注入機制就不是開發人員所要關心的問題。強制每個符合要求的進程加載DLL[參考18],這是操作系統需要完成的工作。事實上,windows跟蹤所有新建立的進程並強制它們加載鉤子DLL。通過註冊表來管理dll注入與windows 鉤子類似。這些方法的最大的好處是它們本身就是操作系統的一部分。
    與上述注入機制不同的是,利用CreateRemoteThread() 的注入方法還要求維護當前運行的進程列表。如果沒有及時注入,攔截系統將會丟失一些原本需要攔截的api調用。每當一個新進程開始或關閉時,鉤子服務器(Hook Server)都要使用一種巧妙的方法來接收相關的通知,這很重要。其中一種方法,是通過攔截並監視CreateProcess() 系列的API函數的調用(來獲得進程開啓或關閉的通知)。這時當用戶自定義函數被調用,它就可以通過添加Create_SUSPENDED標誌調用原來的CreateProcess()。這意味着目標進程的主線程將被掛起,同時鉤子服務器將有機會利用手寫的機器碼指令把DLL注入到目標進程,然後使用ResumeThread() 喚醒目標進程。細節可以參考[參考2]“利用CreateProcess()遠程注入代碼”。
    另一種檢測進程執行的方法,是基於驅動程序的。值得關注的是它極高的靈活性。Windows nt/2k 提供了一個由NTOSKRNL導出的名爲 PsSetCreateProcessNotifyRoutine() 的函數,這個函數允許增加一個回調函數,每當有進程產生或終止時這個回調函數都被調用。更多細節請參見[參考11]和[參考15]。
枚舉進程和模塊
    有時候我們希望利用CreateRemoteThread() API來注入DLL,特別是當攔截系統運行在windows NT/2K上。這種情況下,鉤子服務器啓動後都會枚舉所有活動進程並把DLL注入它們的地址空間。Windows 9x和windows2k 都內置了這種進程枚舉機制(Tool Help 庫,輔助庫)的實現(由Kernel32.dll實現)。另一方面,windowsNT使用PSAPI庫達到相同目的。於是,我們需要一種能夠令鉤子服務器正確運行並動態決定當前可用“幫助庫”的途徑。因此,攔截系統被設計成可以判斷當前操作系統支持何種幫助庫,並相應採用合適的API。
    我將要展示一個以面向對象爲基礎的簡單框架,它用於在windows9x以上操作系統枚舉進程和模塊。我的設計允許用戶根據自己的需求來擴展框 架的功能。框架的實現也是相當簡單明瞭的。
    CTaskManager類實現了整個枚舉子系統的處理核心,它負責生成一個進程枚舉庫對象(例如CPsapiHandler 或 CToolhelpHandler)來調用正確的進程信息提供庫(例如在9x和2k上分別是psapi和toolHelp32)。CTaskManager也負責產生和維護一個記錄當前所有進程列表的容器對象。在CTaskManager實例化之後,攔截系統調用Populate()函數。這個函數強制性地枚舉系統中所有進程和dll庫並把它們的信息保存在CTaskManager的成員m_pProcesses中。
    下面的uml圖展示了上述子系統各個類之間的關係:


圖5
 
    此處要着重指出的一條是,事實上,windowsNT的Kernel32.dll並沒有實現任何的ToolHelp32函數。因此我們必須使用運行時動態鏈接的方式額外鏈接這些函數。在windowsNT上如果使用靜態鏈接,那麼不論應用程序是否曾經試圖調用任何ToolHelp32函數,代碼都會運行失敗。更多信息請參見我的文章“在windows9x/2k和windowsNT上枚舉進程和模塊的單一接口”。
 
建立鉤子工具系統(Hook Tool System)的必要條件
    目前我已經對攔截過程中用到的各種原理概念作了簡短介紹,現在是時候來確定建立一個攔截系統的必要條件,並研究其詳細設計了。下面是有關這個系統的一些總結:
    1)提供用戶級攔截系統,對通過名字導入的win32 api函數實施攔截
    2)提供一種方法,可以用windows鉤子 或遠程線程把攔截驅動注入到所有正在運行的進程。攔截系統應該提供ini文件來選擇使用何種方式
    3)使用一種基於更改iat的攔截方法
    4)攔截系統的架構是基於面向對象技術的、可重用的、可擴展的和分層的
    5)用一種高效的可擴展的機制來攔截api函數
    6)(整個系統)能夠達到預期的性能要求
    7)在攔截驅動和鉤子服務器之間使用一種可靠的數據傳輸機制
    8)實現TextOutA()、TextOutW()以及ExitProcess()的攔截
    9)把攔截過程發生的事件都記錄在日誌中
    10)攔截系統可以運行在任何基於Intel x86架構的windows9x或以上的操作系統
 
設計和實現 
    本部分將討論鉤子架構的關鍵部分以及它們之間的通訊方式。這樣的架構能夠攔截任何通過名稱導入的api函數。
    在概述攔截系統的設計之前,請留意幾種注入和攔截方式。
    首先,要選擇一種能夠把dll注入系統所有進程的方式。因此我設計了一個抽象基類,並實現兩種注入技術,根據ini文件的設置和操作系統版本(例如windowsNT/2k或windows9x)來選擇兩種注入方法中的一種。這兩種方法分別是全局windows鉤子和遠程線程。在示例代碼中,同時使用了windows鉤子機制和遠程線程的方式在windowsNT/2k系統上注入dll。可以通過修改一個包含攔截系統所有設置的ini文件來選擇注入方式。
    另一個重點是選擇攔截機制,不必驚奇,我將使用修改iat來作爲攔截win32api的最有效方法。爲了達到預期的效果,我設計的鉤子框架將包含如下組件和文件:
    1)TestApp.exe – 一個用於測試的簡單win32應用程序,它僅僅使用TextOut()簡單的輸出了一行文本。它的目的只爲了展示api的攔截過程。
    2)HookSvr.exe - 控制dll注入和鉤子安裝的程序。
    3)HookTool.dll –用win32 dll方式實現的鉤子庫
    4)HookTool.ini – 配置文件
    5)NTProcDrv.sys – 一個小型的用於監視進程產生和撤銷的windowsNT/2k內核模式驅動。這個組件是可選的,它僅用於在windowsNT以上系統監視進程。
    HookSrv是一個簡單的控制程序。它主要用來加載HookTool.dll並激活攔截引擎。加載dll以後,鉤子服務器傳遞一個隱藏窗口的句柄同時調用InstallHook(),HookTool.dll把所有消息都發送到這個窗口上。HookTool.dll實現了攔截驅動且還是攔截系統的核心。它實現了真正的攔截操作並攔截了TextOutA()、TextOutW() 和 ExitProcess()。
    儘管文章着眼於windows內部機制因而沒必要使用面向對象方法,但我仍使用可重用c++類封裝了相關操作。這樣可以提供更多的靈活性並使系統易於被擴展。開發者也可以在本工程外的其它工程使用其中獨立的類,這對他們有好處。
    下面的UML圖解釋了HookTool dll所實現的各個類之間的關係。


圖6

    此處請大家留意HookTool.dll的類架構。其中設計各個類的功能是開發過程的重要一環。每個類實現一個特定功能並對外表現爲一個獨立的邏輯整體。
    CmoduleScope是整個系統的基類。它用Singleton模式實現且是線程安全(thread-safe)的。它的構造函數接受3個在共享數據段聲明的指針,這些指針將被所有進程共享。基於上述方法,在類中這些變量可以很容易被維護,而不會破壞類封裝的原則。
    當一個應用程序加載HookTool庫時,dll在接收到DLL_PROCESS_ATTACH消息後便產生一個CModuleScope實例。這一步初始化了CmoduleScope的唯一實例。CmoduleScope對象構造的重要一環是產生一個合適的dll注入器對象。而選擇合適的注入器是在解釋HookTool.ini文件並判斷[Scope]節下的UseWindowsHook參數後發生的。當攔截系統運行在windows9x時,這個參數的值將不會被解釋,因爲windows9x不支持遠程線程的注入方式。
    上述實例化步驟完成後,接着就會調用ManageModuleEnlistment() 。以下是該函數的一個簡化版本:

1. // Called on DLL_PROCESS_ATTACH DLL notification  
2. BOOL CModuleScope::ManageModuleEnlistment()  
3. ......{  
4.           BOOL bResult = FALSE;  
5.           // Check if it is the hook server   
6.           if (FALSE == *m_pbHookInstalled)  
7.           ......{  
8.                     // Set the flag, thus we will know that the server has been installed  
9.                     *m_pbHookInstalled = TRUE;  
10.                    // and return success error code  
11.                    bResult = TRUE;  
12.          }  
13.          // and any other process should be examined whether it should be  
14.          // hooked up by the DLL  
15.          else  
16.          ......{  
17.                    bResult = m_pInjector->IsProcessForHooking(m_szProcessName);  
18.                    if (bResult)  
19.                               InitializeHookManagement();  
20.          }  
21.          return bResult;  
22.}  

    ManageModuleEnlistment() 的實現簡單明瞭,通過檢查m_pbHookInstalled指向的變量,它測試自身是否已經被鉤子服務器調用過。如果已經被調用,它就只是簡單的把sg_bHookInstalled設爲TRUE,表明鉤子服務器已經啓動了。
    接着,鉤子服務器通過調用鉤子dll的導出函數InstallHook() 來激活鉤子安裝引擎。實際上,該函數只是簡單調用了CmoduleScope的InstallHookMethod() 函數。這個函數的作用是強制目標進程加載或卸載HookTool.dll。HookTool.dll提供了兩種把自身注入外部進程空間的方法——一是使用Windows鉤子另外一種利用CreateRemoteThread()函數。在該系統的架構上,定義了一個抽象類CInjector以及用於注入和卸載dll的純虛函數。類CWinHookInjector和CremThreadInjector都從CInjector繼承。儘管如此,但它們提供了兩個純虛函數InjectModuleIntoAllProcesses()EjectModuleFromAllProcesses() 的不同實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章