1. 程式人生 > >HOOK技術的一些簡單總結

HOOK技術的一些簡單總結

好久沒寫部落格了, 一個月一篇還是要儘量保證,今天談下Hook技術。 在Window平臺上開發任何稍微底層一點的東西,基本上都是Hook滿天飛, 普通應用程式如此,安全軟體更是如此, 這裡簡單記錄一些常用的Hook技術。 基本上做Windows開發都知道這個API, 它給我們提供了一個攔截系統事件和訊息的機會, 並且它可以將我們的DLL注入到其他程序。 但是隨著64位時代的到來和Vista之後的UAC機制開啟,這個API很多時候不能正常工作了: 首先,32位DLL沒法直接注入到64位的應用程式裡面, 因為他們的地址空間完全不一樣的。當然儘管沒法直接注入,但是在許可權範圍內,系統會盡量以訊息的方式讓你能收到64位程式的訊息事件。 其次,UAC開啟的情況下低許可權程式沒法Hook高許可權程式, 實際上低許可權程式以高許可權程式視窗為Owner建立視窗也會失敗, 低許可權程式在高許可權程式視窗上模擬滑鼠鍵盤也會失敗。 有人說我們可以關閉UAC, Win7下你確實可以,但是Win8下微軟已經不支援真正關閉UAC, 從這裡我們也可以看到微軟技術過渡的方式, 中間會提供一個選項來讓你慢慢適應,最後再把這個選項關掉, UAC和Aero模式都是如此。 那麼我們如何解決這些問題? 對於64位問題 , 解決方法是提供2個DLL,分別可以Hook32和64位程式。 對於許可權問題, 解決方法是提升許可權, 通過註冊系統服務, 由服務程式建立我們的工作程序。這裡為什麼要建立一個其他程序而不直接在服務程序裡幹活? 因為Vista後我們有了Session隔離機制,服務程式執行在Session 0,我們的其他程式執行在Session 1, Session 2等, 如果我們直接在服務程式裡幹活,我們就只能在Session 0裡工作。通過建立程序,我們可以在DuplicateTokenEx後將Token的SessionID設定成目標Session,並且在CreateProcessAsUser時指定目標WinStation和Desktop, 這樣我們就既獲得了System許可權,並且也可以和當前桌面程序互動了。 很多人可能都不知道這個API, 但是這個API其實挺重要的, 看名字就知道它是Hook事件(Event)的, 具體哪些事件可以看這裡. 為什麼說這個API重要, 因為這個API大部分時候沒有SetWindowsHookEx的許可權問題, 也就是說這個API可以讓你Hook到高許可權程式的事件, 它同時支援程序內(WINEVENT_INCONTEXT
)和程序外(WINEVENT_OUTOFCONTEXT)2種Hook方式, 你可以以程序外的方式Hook到64位程式的事件。 為什麼這個API沒有許可權問題, 因為它是給Accessibility用的, 也就是它是給自動測試和殘障工具用的, 所以它要保證有效。 我曾經看到這樣一個程式,當任何程式(無論許可權高低)有視窗拖動(拖標題欄改變位置或是拖邊框改變大小), 程式都能捕獲到, 當時很好奇它是怎麼做到的? Spy了下視窗訊息, 知道有這樣2個訊息:WM_ENTERSIZEMOVE和WM_EXITSIZEMOVE表示進入和退出這個事件, 但是那也只能獲得自己的訊息,其他程式的訊息它是如何捕獲到的?當時懷疑用的是Hook, 卻發現沒有DLL注入。查遍了Windows API 也沒有發現有API可以查詢一個視窗是否在這個拖動狀態。最後發現用的是SetWinEventHook
EVENT_SYSTEM_MOVESIZESTART和EVENT_SYSTEM_MOVESIZEEND。 API Hook 常見的API Hook包括2種, 一種是基於PE檔案的匯入表(IAT), 還有一種是修改前5個位元組直接JMP的inline Hook. 對於基於IAT的方式, 原理是PE檔案裡有個匯入表, 代表該模組呼叫了哪些外部API,模組被載入到記憶體後, PE載入器會修改該表,地址改成外部API重定位後的真實地址, 我們只要直接把裡面的地址改成我們新函式的地址, 就可以完成對相應API的Hook。《Windows核心程式設計》裡第22章有個封裝挺好的CAPIHook類,我們可以直接拿來用。
我曾經用API Hook來實現自動測試,見這裡 API Hook在TA中的應用 對於基於Jmp方式的inline hook, 原理是修改目標函式的前5個位元組, 直接Jmp到我們的新函式。雖然原理挺簡單, 但是因為用到了平臺相關的彙編程式碼, 一般人很難寫穩定。真正在專案中用還是要求穩定, 所以我們一般用微軟封裝好的Detours, 對於Detours的原理,這裡有篇不錯的文章  比較一下2種方式:  IAT的方式比較安全簡單, 但是隻適用於Hook匯入函式方式的API。 Inline Hook相對來說複雜點, 但是它能Hook到任何函式(API和內部函式),但是它要求目標函式大於5位元組, 同時把握好修改時機或是Freeze其他執行緒, 因為多執行緒中改寫可能會引起衝突。

還有一種是Hook被呼叫模組的匯出表(EAT), 但是感覺一般用得用的不多。原理是呼叫模組裡IAT中的函式地址是通過被呼叫模組的EAT獲取的, 所以你只要修改了被呼叫模組的EAT中的函式地址,對方的呼叫就自然被你Hook了。但是這裡有個時機問題, 就是你替換EAT表要足夠早,要在IAT使用它之前才行。但是感覺這個行為是由PE載入器決定的, 一般人很難干涉, 不知道大家有什麼好方法?  我們一般可以將IAT Hook和EAT Hook結合起來使用, 先列舉所有模組Hook IAT,這樣當前已有模組的API都被你Hook了,然後再Hook EAT, 這樣後續的模組也被你Hook了(因為要通過EAT獲取函式地址)。  如果你只用Hook IAT而不Hook EAT, 當有新模組載入時,你要Hook它的IAT, 所以你就要Hook LoadLibrary(Ex)和GetProcAddress來攔截新模組的載入。所以理論上感覺 Hook EAT不是很有必要, 因為單用Hook IAT已經可以解決我們所有的問題了, HOOK IAT還有一種優勢是我們可以過濾某個模組不Hook,而一旦hook EAT, 它就會影響我們所有呼叫該函式的模組。
COM Hook Window上因為有很多開發包是以COM方式提供的(比如DirectX), 所以我們就有了攔截COM呼叫的COM Hook。 因為COM裡面很關鍵的是它的介面是C++裡虛表的形式提供的, 所以COM的Hook很多是時候其實就是虛表(vtable)的Hook。 關於C++ 物件模型和虛表可以看我這篇  對於COMHook,考慮下面2種case: 一種是我們Hook程式先執行,然後啟動某個遊戲程式(DirectX 9), 我們想Hook遊戲的繪畫內容。 這種方式下, 我們可以先Hook API Direct3DCreate9, 然後我們繼承於IDirect3D9, 自己實現一個COM物件返回回去, 這樣我們就可以攔截到所有對該物件的操作,為所欲為了, 當然我們自己現實的COM物件內部會呼叫真正的Direct3DCreate9,封裝真正的IDirect3D9。 當然有時我們可能不用替代整個COM元件,我們只需要修改其中一個或幾個COM函式, 這種情況下我們可以建立真正的IDirect3D9物件後直接修改它的虛表, 把其中某些函式改成我們自己的函式地址就可以了。 其實ATL就是用介面替代的方式來除錯和記錄COM介面引用計數的次數, 具體可以看我這篇  還有一種case是遊戲程式已經在運行了, 然後才啟動我們的Hook程序, 我們怎麼樣才能Hook到裡面的內容? 這種情況下我們首先要對程式記憶體有比較詳細的認識, 才能思考創建出來的D3D物件的虛表位置, 從而進行Hook, 關於程式記憶體佈局,可見我這篇  理論上說COM物件如果是以C++介面的方式實現, 虛表會位於PE檔案的只讀資料節(.rdata), 並且所有該型別的物件都共享該虛表, 所以我們只要建立一個該型別物件,我們就可以獲得其他人建立的該型別物件的虛表位置,我們就可以改寫該虛表實現Hook(實際操作時需要通過VirtualProtect修改頁面的只讀屬性才能寫入)。 但是實際上COM的虛表只是一塊記憶體, 它並不一定是以C++實現, 所以它可以存在於任何記憶體的任何地方。另外物件的虛表也不一定是所有同類型的物件共享同一虛表, 我們完全可以每個物件都有自己的一份虛表。比如我發現IDirect3D9是大家共享同一虛表的(存在D3D9.dll), 但是IDirect3DDevice9就是每個物件都有自己的虛表了(存在於堆heap)。所以如果你要Hook IDirect3DDevice9介面,通過修改虛表實際上沒法實現。 但是儘管有時每個物件的虛表不一樣,同類型物件虛表裡的函式地址卻都是一樣的, 所以這種情況下我們可以通過inline Hook直接修改函式程式碼。當然有些情況下如果是靜態連結庫,即使函式程式碼也是每個模組都有自己的一份, 這種情況下就只能反彙編獲取虛表和函式的地址了。 最後,總結一下, 上面主要探討了Windows上的各種Hook技術,通過將這些Hook技術組起來, 可以實現很多意想不到的功能, 比如我們完全可以通過Hook D3D實現Win7工作列那種Thumbnail預覽的效果(當然該效果可以直接由DWM API實現, 但是如果我們可以通過HOOK以動畫的方式實現是不是更有趣 )