在Window平臺上開發(fā)任何稍微底層一點的東西,基本上都是Hook滿天飛,, 普通應(yīng)用程序如此,,安全軟件更是如此, 這里簡單記錄一些常用的Hook技術(shù),。 SetWindowsHookEx 基本上做Windows開發(fā)都知道這個API,, 它給我們提供了一個攔截系統(tǒng)事件和消息的機(jī)會, 并且它可以將我們的DLL注入到其他進(jìn)程,。 但是隨著64位時代的到來和Vista之后的UAC機(jī)制開啟,,這個API很多時候不能正常工作了: 首先,32位DLL沒法直接注入到64位的應(yīng)用程序里面,, 因為他們的地址空間完全不一樣的,。當(dāng)然盡管沒法直接注入,但是在權(quán)限范圍內(nèi),,系統(tǒng)會盡量以消息的方式讓你能收到64位程序的消息事件,。 其次,UAC打開的情況下低權(quán)限程序沒法Hook高權(quán)限程序,, 實際上低權(quán)限程序以高權(quán)限程序窗口為Owner創(chuàng)建窗口也會失敗,, 低權(quán)限程序在高權(quán)限程序窗口上模擬鼠標(biāo)鍵盤也會失敗。 有人說我們可以關(guān)閉UAC,, Win7下你確實可以,但是Win8下微軟已經(jīng)不支持真正關(guān)閉UAC,, 從這里我們也可以看到微軟技術(shù)過渡的方式,, 中間會提供一個選項來讓你慢慢適應(yīng),最后再把這個選項關(guān)掉,, UAC和Aero模式都是如此,。 那么我們?nèi)绾谓鉀Q這些問題? 對于64位問題 ,, 解決方法是提供2個DLL,,分別可以Hook32和64位程序。 對于權(quán)限問題,, 解決方法是提升權(quán)限,, 通過注冊系統(tǒng)服務(wù), 由服務(wù)程序創(chuàng)建我們的工作進(jìn)程,。這里為什么要創(chuàng)建一個其他進(jìn)程而不直接在服務(wù)進(jìn)程里干活,? 因為Vista后我們有了Session隔離機(jī)制,服務(wù)程序運行在Session 0,我們的其他程序運行在Session 1, Session 2等,, 如果我們直接在服務(wù)程序里干活,,我們就只能在Session 0里工作。通過創(chuàng)建進(jìn)程,,我們可以在DuplicateTokenEx后將Token的SessionID設(shè)置成目標(biāo)Session,,并且在CreateProcessAsUser時指定目標(biāo)WinStation和Desktop, 這樣我們就既獲得了System權(quán)限,,并且也可以和當(dāng)前桌面進(jìn)程交互了,。 SetWinEventHook 很多人可能都不知道這個API, 但是這個API其實挺重要的,, 看名字就知道它是Hook事件(Event)的,, 具體哪些事件可以看這里. 為什么說這個API重要, 因為這個API大部分時候沒有SetWindowsHookEx的權(quán)限問題,, 也就是說這個API可以讓你Hook到高權(quán)限程序的事件,, 它同時支持進(jìn)程內(nèi)(WINEVENT_INCONTEXT)和進(jìn)程外(WINEVENT_OUTOFCONTEXT)2種Hook方式, 你可以以進(jìn)程外的方式Hook到64位程序的事件,。 為什么這個API沒有權(quán)限問題,, 因為它是給Accessibility用的, 也就是它是給自動測試和殘障工具用的,, 所以它要保證有效,。 我曾經(jīng)看到這樣一個程序,當(dāng)任何程序(無論權(quán)限高低)有窗口拖動(拖標(biāo)題欄改變位置或是拖邊框改變大小),, 程序都能捕獲到,, 當(dāng)時很好奇它是怎么做到的? Spy了下窗口消息, 知道有這樣2個消息:WM_ENTERSIZEMOVE和WM_EXITSIZEMOVE表示進(jìn)入和退出這個事件,, 但是那也只能獲得自己的消息,,其他程序的消息它是如何捕獲到的?當(dāng)時懷疑用的是Hook,, 卻發(fā)現(xiàn)沒有DLL注入,。查遍了Windows API 也沒有發(fā)現(xiàn)有API可以查詢一個窗口是否在這個拖動狀態(tài)。最后發(fā)現(xiàn)用的是SetWinEventHook的EVENT_SYSTEM_MOVESIZESTART和EVENT_SYSTEM_MOVESIZEEND,。 API Hook 常見的API Hook包括2種,, 一種是基于PE文件的導(dǎo)入表(IAT), 還有一種是修改前5個字節(jié)直接JMP的inline Hook. 對于基于IAT的方式, 原理是PE文件里有個導(dǎo)入表,, 代表該模塊調(diào)用了哪些外部API,,模塊被加載到內(nèi)存后, PE加載器會修改該表,,地址改成外部API重定位后的真實地址,, 我們只要直接把里面的地址改成我們新函數(shù)的地址,, 就可以完成對相應(yīng)API的Hook?!禬indows核心編程》里第22章有個封裝挺好的CAPIHook類,,我們可以直接拿來用。 我曾經(jīng)用API Hook來實現(xiàn)自動測試,,見這里 API Hook在TA中的應(yīng)用 對于基于Jmp方式的inline hook, 原理是修改目標(biāo)函數(shù)的前5個字節(jié),, 直接Jmp到我們的新函數(shù)。雖然原理挺簡單,, 但是因為用到了平臺相關(guān)的匯編代碼,, 一般人很難寫穩(wěn)定。真正在項目中用還是要求穩(wěn)定,, 所以我們一般用微軟封裝好的Detours, 對于Detours的原理,,這里有篇不錯的文章Detour開發(fā)包之API攔截技術(shù)。 比較一下2種方式: IAT的方式比較安全簡單,, 但是只適用于Hook導(dǎo)入函數(shù)方式的API,。 Inline Hook相對來說復(fù)雜點, 但是它能Hook到任何函數(shù)(API和內(nèi)部函數(shù)),,但是它要求目標(biāo)函數(shù)大于5字節(jié),, 同時把握好修改時機(jī)或是Freeze其他線程, 因為多線程中改寫可能會引起沖突,。 還有一種是Hook被調(diào)用模塊的導(dǎo)出表(EAT),, 但是感覺一般用得用的不多。原理是調(diào)用模塊里IAT中的函數(shù)地址是通過被調(diào)用模塊的EAT獲取的,, 所以你只要修改了被調(diào)用模塊的EAT中的函數(shù)地址,,對方的調(diào)用就自然被你Hook了。但是這里有個時機(jī)問題,, 就是你替換EAT表要足夠早,,要在IAT使用它之前才行。但是感覺這個行為是由PE加載器決定的,, 一般人很難干涉, 不知道大家有什么好方法,? 我們一般可以將IAT Hook和EAT Hook結(jié)合起來使用,, 先枚舉所有模塊Hook IAT,這樣當(dāng)前已有模塊的API都被你Hook了,,然后再Hook EAT,, 這樣后續(xù)的模塊也被你Hook了(因為要通過EAT獲取函數(shù)地址)。 如果你只用Hook IAT而不Hook EAT, 當(dāng)有新模塊加載時,,你要Hook它的IAT,, 所以你就要Hook LoadLibrary(Ex)和GetProcAddress來攔截新模塊的加載,。所以理論上感覺 Hook EAT不是很有必要, 因為單用Hook IAT已經(jīng)可以解決我們所有的問題了,, HOOK IAT還有一種優(yōu)勢是我們可以過濾某個模塊不Hook,,而一旦hook EAT, 它就會影響我們所有調(diào)用該函數(shù)的模塊,。 COM Hook Window上因為有很多開發(fā)包是以COM方式提供的(比如DirectX),, 所以我們就有了攔截COM調(diào)用的COM Hook。 因為COM里面很關(guān)鍵的是它的接口是C++里虛表的形式提供的,, 所以COM的Hook很多是時候其實就是虛表(vtable)的Hook,。 關(guān)于C++ 對象模型和虛表可以看我這篇 探索C++對象模型 對于COMHook,考慮下面2種case: 一種是我們Hook程序先運行,,然后啟動某個游戲程序(DirectX 9),, 我們想Hook游戲的繪畫內(nèi)容。 這種方式下,, 我們可以先Hook API Direct3DCreate9,, 然后我們繼承于IDirect3D9, 自己實現(xiàn)一個COM對象返回回去,, 這樣我們就可以攔截到所有對該對象的操作,,為所欲為了, 當(dāng)然我們自己現(xiàn)實的COM對象內(nèi)部會調(diào)用真正的Direct3DCreate9,,封裝真正的IDirect3D9,。 當(dāng)然有時我們可能不用替代整個COM組件,我們只需要修改其中一個或幾個COM函數(shù),, 這種情況下我們可以創(chuàng)建真正的IDirect3D9對象后直接修改它的虛表,, 把其中某些函數(shù)改成我們自己的函數(shù)地址就可以了。 其實ATL就是用接口替代的方式來調(diào)試和記錄COM接口引用計數(shù)的次數(shù),, 具體可以看我這篇 理解ATL中的一些匯編代碼 還有一種case是游戲程序已經(jīng)在運行了,, 然后才啟動我們的Hook進(jìn)程, 我們怎么樣才能Hook到里面的內(nèi)容,? 這種情況下我們首先要對程序內(nèi)存有比較詳細(xì)的認(rèn)識,, 才能思考創(chuàng)建出來的D3D對象的虛表位置, 從而進(jìn)行Hook,, 關(guān)于程序內(nèi)存布局,,可見我這篇 理解程序內(nèi)存。 理論上說COM對象如果是以C++接口的方式實現(xiàn),, 虛表會位于PE文件的只讀數(shù)據(jù)節(jié)(.rdata), 并且所有該類型的對象都共享該虛表,, 所以我們只要創(chuàng)建一個該類型對象,我們就可以獲得其他人創(chuàng)建的該類型對象的虛表位置,,我們就可以改寫該虛表實現(xiàn)Hook(實際操作時需要通過VirtualProtect修改頁面的只讀屬性才能寫入),。 但是實際上COM的虛表只是一塊內(nèi)存,, 它并不一定是以C++實現(xiàn), 所以它可以存在于任何內(nèi)存的任何地方,。另外對象的虛表也不一定是所有同類型的對象共享同一虛表,, 我們完全可以每個對象都有自己的一份虛表。比如我發(fā)現(xiàn)IDirect3D9是大家共享同一虛表的(存在D3D9.dll),, 但是IDirect3DDevice9就是每個對象都有自己的虛表了(存在于堆heap),。所以如果你要Hook IDirect3DDevice9接口,通過修改虛表實際上沒法實現(xiàn),。 但是盡管有時每個對象的虛表不一樣,,同類型對象虛表里的函數(shù)地址卻都是一樣的, 所以這種情況下我們可以通過inline Hook直接修改函數(shù)代碼,。當(dāng)然有些情況下如果是靜態(tài)鏈接庫,,即使函數(shù)代碼也是每個模塊都有自己的一份, 這種情況下就只能反匯編獲取虛表和函數(shù)的地址了,。 最后,,總結(jié)一下, 上面主要探討了Windows上的各種Hook技術(shù),,通過將這些Hook技術(shù)組起來,, 可以實現(xiàn)很多意想不到的功能, 比如我們完全可以通過Hook D3D實現(xiàn)Win7任務(wù)欄那種Thumbnail預(yù)覽的效果(當(dāng)然該效果可以直接由DWM API實現(xiàn), 但是如果我們可以通過HOOK以動畫的方式實現(xiàn)是不是更有趣 ),。 |
|