純C#實(shí)現(xiàn)Hook功能詳解
發(fā)布一個(gè)自己寫(xiě)的用于Hook .Net方法的類(lèi)庫(kù),代碼量不大,完全的C#代碼實(shí)現(xiàn),是一個(gè)比較有趣的功能,分享出來(lái)希望能和大家共同探討
安裝:Install-Package DotNetDetour
源碼:http://xiazai.jb51.net/201701/yuanma/DotNetDetour_jb51.rar
1.為何想做這個(gè)
說(shuō)到hook大家都應(yīng)該不陌生,就是改變函數(shù)的執(zhí)行流程,讓本應(yīng)該執(zhí)行的函數(shù)跑到另一個(gè)函數(shù)中執(zhí)行,這是個(gè)很有用也很有趣的功能(例如獲取函數(shù)參數(shù)信息,改變函數(shù)執(zhí)行流程,計(jì)算函數(shù)執(zhí)行時(shí)間等等),殺軟中主防的原理就是hook,通過(guò)hook攔截函數(shù)獲取參數(shù)信息來(lái)判斷是否是危險(xiǎn)行為,但這類(lèi)程序大多是C++的,一直以來(lái)我都想實(shí)現(xiàn)可以hook .net函數(shù)的庫(kù),網(wǎng)上搜索了很多,但都不理想,所以想自己實(shí)現(xiàn)一個(gè)
2.實(shí)現(xiàn)原理
我采用的是inline hook的方式,因?yàn)槲覍?duì).net虛擬機(jī)以及一些內(nèi)部的結(jié)構(gòu)并不是很熟悉,并且有些東西的確找不到任何文檔,所以就采用原生代碼的inline hook的方式來(lái)實(shí)現(xiàn)。
首先說(shuō)一下inline hook的基本原理,它是通過(guò)修改函數(shù)的前5字節(jié)指令為jmp xxxxxxxx來(lái)實(shí)現(xiàn)的,例如一個(gè)C#方法:
用windbg調(diào)試查看方法信息:
查看已經(jīng)jit了的原生代碼:
這里的地址(0x008c0640)可以通過(guò)MethodInfo.MethodHandle.GetFunctionPointer().ToPointer()方法獲取
到了這里,我們就知道了修改從push ebp開(kāi)始的5個(gè)字節(jié)為jmp跳轉(zhuǎn)指令,跳入我們自己的函數(shù)就可以達(dá)到hook的目的,但執(zhí)行到我們的函數(shù)后,如果我們并不是要攔截執(zhí)行流程,那么我們最終是需要再調(diào)用原函數(shù)的,但原函數(shù)已經(jīng)被修改了,這會(huì)想到的辦法就是恢復(fù)那修改的5字節(jié)指令,但這又會(huì)引發(fā)另一個(gè)問(wèn)題,就是當(dāng)我們恢復(fù)時(shí),正好另一個(gè)線程調(diào)用到這個(gè)函數(shù),那么程序?qū)?huì)崩潰,或者說(shuō)漏掉一次函數(shù)調(diào)用,修改時(shí)暫停其他線程并等待正跑在其中的CPU執(zhí)行完這5字節(jié)再去恢復(fù)指令也許是個(gè)不錯(cuò)的辦法,但感覺(jué)并不容易實(shí)現(xiàn),而且影響性能,所以我放棄了這種辦法
那么如何才能調(diào)用修改前的函數(shù)呢,我首先想到是C中寫(xiě)裸函數(shù)的方式,即自己用匯編拼出來(lái)一個(gè)原函數(shù)再執(zhí)行:
原函數(shù)前5字節(jié)指令+jmp跳轉(zhuǎn)指令
但其實(shí)這也是不可行的,聰明的人已經(jīng)發(fā)現(xiàn),圖中所示的函數(shù)的前5字節(jié)并不是一個(gè)完整的匯編指令,不同的函數(shù),長(zhǎng)度都不一樣,.net的函數(shù)并不像某些原生函數(shù)那樣,會(huì)預(yù)留mov edi,edi這樣的正好5字節(jié)的指令,我先想到的是復(fù)制函數(shù)的所有匯編指令生成新的函數(shù),但這樣也會(huì)出問(wèn)題,因?yàn)橄馝8,E9這樣的相對(duì)跳轉(zhuǎn)指令,如果指令地址變了,那么跳轉(zhuǎn)的位置也就變了,程序就會(huì)崩潰,所以這也不可行。
到了這里,我有些不耐煩了,畢竟我是要hook所有函數(shù)的,而不是某個(gè)固定的函數(shù),而函數(shù)入口的指令又不相同,這可怎么辦,難道我需要計(jì)算出大于等于5字節(jié)的最小完整匯編指令長(zhǎng)度?
按照這個(gè)思路,最終找到了一個(gè)用C寫(xiě)的反匯編庫(kù)(BlackBone),其中提供了類(lèi)似的方法,我稍作了修改后試用了下,的確不錯(cuò),可以準(zhǔn)確求出匯編指令長(zhǎng)度,例如
push ebp
mov ebp,esp
mov eax,dword ptr ds:[33F22ACh]
求出值是9,這樣我根據(jù)求出的值動(dòng)態(tài)拼接一個(gè)函數(shù)出來(lái)即可,哈哈,到了這里,感覺(jué)實(shí)現(xiàn)的差不多了,但沒(méi)想到64位下又給了我當(dāng)頭一棒,之前的原函數(shù)指令可以寫(xiě)成:
大于等于5字節(jié)的最小完整匯編指令+jmp跳轉(zhuǎn)指令即可構(gòu)成我們的原函數(shù)
但我們知道,C#中要想執(zhí)行匯編,是需要用Marshal.AllocHGlobal來(lái)分配非托管空間的,而這樣分配的地址與我們要跳轉(zhuǎn)到的原函數(shù)的地址在64位下是超過(guò)2GB地址范圍的,一般的跳轉(zhuǎn)指令是無(wú)法實(shí)現(xiàn)的,所以想到了用ret指令實(shí)現(xiàn),而64位地址又不能直接push,所以最后寫(xiě)出如下匯編:
push rax
mov rax,target_addr
push rax
mov rax,qword ptr ss:[rsp+8]
ret 8
由于某些C#函數(shù)竟然第一行就是修改rax寄存器的值,所以只能是先保存rax,推入堆棧后再恢復(fù),這里匯編操作就方便多了,之前實(shí)現(xiàn)另一個(gè)東西,用到IL指令,但發(fā)現(xiàn)只有dup這種復(fù)制棧頂元素的指令,卻沒(méi)有獲取堆棧中某個(gè)非棧頂元素值的指令,所以說(shuō)還是匯編?kù)`活啊,想怎么寫(xiě)就怎么寫(xiě),啥都能實(shí)現(xiàn)。
最后就是這個(gè)原函數(shù)的調(diào)用過(guò)程了,因?yàn)槭莿?dòng)態(tài)拼接的函數(shù),所以想到的就是用Marshal.GetDelegateForFunctionPointer轉(zhuǎn)成委托來(lái)執(zhí)行,后來(lái)發(fā)現(xiàn)不對(duì),因?yàn)槲译m然拼接的是匯編,而這個(gè)匯編是C#方法jit后的匯編,這個(gè)并不是C方法編譯后的匯編,通過(guò)把非托管指針轉(zhuǎn)換為委托的方式運(yùn)行函數(shù)是會(huì)添加很多不需要的操作的,例如托管類(lèi)型與非托管類(lèi)型的轉(zhuǎn)換,但我拼接出的函數(shù)是不需要這些過(guò)程的,這個(gè)怎么辦,看來(lái)只能用調(diào)用C#普通函數(shù)的方式調(diào)用,這個(gè)怎么實(shí)現(xiàn)呢,其實(shí)很好辦,只需寫(xiě)一個(gè)空殼函數(shù),然后修改這個(gè)函數(shù)的方法表中的原生指令指針即可,具體方法如下:
*((ulong*)((uint*)method.MethodHandle.Value.ToPointer() + 2)) = (ulong)ptr;
method是空殼函數(shù)的MethodInfo,ptr是動(dòng)態(tài)拼接的原函數(shù)的地址
好,到了這里就基本完成核心功能了,最不好處理的就是這個(gè)原函數(shù)調(diào)用,我的完整的64位原函數(shù)指令拼接就實(shí)現(xiàn)了,代碼很少,如下所示:
byte[] jmp_inst = { 0x50, //push rax 0x48,0xB8,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90, //mov rax,target_addr 0x50, //push rax 0x48,0x8B,0x44,0x24,0x08, //mov rax,qword ptr ss:[rsp+8] 0xC2,0x08,0x00 //ret 8 }; protected override void CreateOriginalMethod(MethodInfo method) { uint oldProtect; var needSize = NativeAPI.SizeofMin5Byte(srcPtr); byte[] src_instr = new byte[needSize]; for (int i = 0; i < needSize; i++) { src_instr[i] = srcPtr[i]; } fixed (byte* p = &jmp_inst[3]) { *((ulong*)p) = (ulong)(srcPtr + needSize); } var totalLength = src_instr.Length + jmp_inst.Length; IntPtr ptr = Marshal.AllocHGlobal(totalLength); Marshal.Copy(src_instr, 0, ptr, src_instr.Length); Marshal.Copy(jmp_inst, 0, ptr + src_instr.Length, jmp_inst.Length); NativeAPI.VirtualProtect(ptr, (uint)totalLength, Protection.PAGE_EXECUTE_READWRITE, out oldProtect); RuntimeHelpers.PrepareMethod(method.MethodHandle); *((ulong*)((uint*)method.MethodHandle.Value.ToPointer() + 2)) = (ulong)ptr; }
3.類(lèi)庫(kù)開(kāi)發(fā)所用到的語(yǔ)言
之前我說(shuō),我的這個(gè)庫(kù)是完全用C#實(shí)現(xiàn)的,但其中的確用到了一個(gè)C寫(xiě)的反匯編庫(kù),于是我用C#把那個(gè)庫(kù)重寫(xiě)了一遍,說(shuō)來(lái)也簡(jiǎn)單,C的代碼粘過(guò)來(lái),C#啟用unsafe代碼,改了10分鐘就好了,真心是非常方便,畢竟C#是支持指針和結(jié)構(gòu)體的,而且基礎(chǔ)類(lèi)型非常豐富,這里得給C#點(diǎn)個(gè)贊!
4.具體使用
使用非常簡(jiǎn)單,首先新建控制臺(tái)程序并添加一個(gè)類(lèi),繼承接口IMethodMonitor,Get是你自己的函數(shù),Ori是原函數(shù)會(huì)在運(yùn)行時(shí)動(dòng)態(tài)生成,在Get中你可以干你想干的任何事情
public class CustomMonitor : IMethodMonitor //自定義一個(gè)類(lèi)并繼承IMethodMonitor接口 { [Monitor("TargetNamespace", "TargetClass")] //你要hook的目標(biāo)方法的名稱(chēng)空間,類(lèi)名 public string Get() //方法簽名要與目標(biāo)方法一致 { return "B" + Ori(); } [MethodImpl(MethodImplOptions.NoInlining)] [Original] //原函數(shù)標(biāo)記 public string Ori() //方法簽名要與目標(biāo)方法一致 { return null; //這里寫(xiě)什么無(wú)所謂,能編譯過(guò)即可 } }
然后定義目標(biāo)函數(shù),例如
public string Get() { return "A"; }
最后調(diào)用Monitor.Install()安裝監(jiān)視器,例如:
Console.WrtieLine(Get()); Monitor.Install() Console.WrtieLine(Get());
你會(huì)發(fā)現(xiàn)第一次調(diào)用Get輸出的值是"A",第二次是"BA"
當(dāng)然這個(gè)庫(kù)只是hook,但hook一般都需要dll注入來(lái)配合,因?yàn)閔ook自身進(jìn)程沒(méi)什么意義,hook別人的進(jìn)程才有意義,我之后會(huì)發(fā)布一個(gè)用于.net程序遠(yuǎn)程注入的類(lèi)庫(kù),注入的是.net的dll哦,不是C++的
好了,講了這么多,其實(shí)這個(gè)庫(kù)代碼量并不大,但主要是自己研究的一個(gè)成果,很多東西都是自己琢磨出來(lái)的,所以覺(jué)得這個(gè)過(guò)程很有意思,也希望高手能指出改進(jìn)方案,畢竟感覺(jué)目前這種方法雖然實(shí)現(xiàn)了功能,但是并不是很好,總覺(jué)得以hook .net虛擬機(jī)的方式來(lái)實(shí)現(xiàn)會(huì)更簡(jiǎn)單一些,或者網(wǎng)絡(luò)上已經(jīng)有了現(xiàn)成的解決方案我沒(méi)有找到,總之,拋磚引玉,希望大家能共同探討
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- iOS開(kāi)發(fā)中實(shí)現(xiàn)hook消息機(jī)制的方法探究
- CI框架中通過(guò)hook的方式實(shí)現(xiàn)簡(jiǎn)單的權(quán)限控制
- C++實(shí)現(xiàn)修改函數(shù)代碼HOOK的封裝方法
- Python利用pyHook實(shí)現(xiàn)監(jiān)聽(tīng)用戶(hù)鼠標(biāo)與鍵盤(pán)事件
- Inline Hook(ring3)的簡(jiǎn)單C++實(shí)現(xiàn)方法
- C++實(shí)現(xiàn)inline hook的原理及應(yīng)用實(shí)例
- 基于C#實(shí)現(xiàn)的HOOK鍵盤(pán)鉤子實(shí)例代碼
- python中使用pyhook實(shí)現(xiàn)鍵盤(pán)監(jiān)控的例子
- python使用pyhook監(jiān)控鍵盤(pán)并實(shí)現(xiàn)切換歌曲的功能
相關(guān)文章
C#使用TensorFlow.NET訓(xùn)練自己的數(shù)據(jù)集的方法
這篇文章主要介紹了C#使用TensorFlow.NET訓(xùn)練自己的數(shù)據(jù)集的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03C#并發(fā)實(shí)戰(zhàn)記錄之Parallel.ForEach使用
這篇文章主要給大家介紹了關(guān)于C#并發(fā)實(shí)戰(zhàn)記錄之Parallel.ForEach使用的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用C#具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08WPF+DiffPlex實(shí)現(xiàn)文本比對(duì)工具
現(xiàn)行的文本編輯器大多都具備文本查詢(xún)的能力,但是并不能直觀的告訴用戶(hù)兩段文字的細(xì)微差異,所以對(duì)比工具在某種情況下,就起到了很便捷的效率。本文將利用DiffPlex實(shí)現(xiàn)簡(jiǎn)易的文本比對(duì)工具,需要的可以參考一下2022-11-11Unity編輯器資源導(dǎo)入處理函數(shù)OnPostprocessAudio使用案例
這篇文章主要為大家介紹了Unity編輯器資源導(dǎo)入處理函數(shù)OnPostprocessAudio使用案例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08C#.Net基于正則表達(dá)式抓取百度百家文章列表的方法示例
這篇文章主要介紹了C#.Net基于正則表達(dá)式抓取百度百家文章列表的方法,結(jié)合實(shí)例形式分析了C#獲取百度百家文章內(nèi)容及使用正則表達(dá)式匹配標(biāo)題、內(nèi)容、地址等相關(guān)操作技巧,需要的朋友可以參考下2017-08-08winform關(guān)閉窗體FormClosing事件用法介紹
這篇文章介紹了winform關(guān)閉窗體FormClosing事件的用法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03C# DataGridView中實(shí)現(xiàn)勾選存儲(chǔ)數(shù)據(jù)和右鍵刪除數(shù)據(jù)(示例代碼)
這篇文章主要介紹了C# DataGridView中實(shí)現(xiàn)勾選存儲(chǔ)數(shù)據(jù)和右鍵刪除數(shù)據(jù)的示例代碼,通過(guò)示例代碼給大家展示運(yùn)行效果圖,需要的朋友可以參考下2021-07-07