純C#實現Hook功能

發佈一個本身寫的用於Hook .Net方法的類庫,代碼量不大,徹底的C#代碼實現,是一個比較有趣的功能,分享出來但願能和你們共同探討

安裝:Install-Package DotNetDetour
源碼:https://github.com/bigbaldy1128/DotNetDetour

1.爲什麼想作這個
說到hook你們都應該不陌生,就是改變函數的執行流程,讓本應該執行的函數跑到另外一個函數中執行,這是個頗有用也頗有趣的功能(例如獲取函數參數信息,改變函數執行流程,計算函數執行時間等等),殺軟中主防的原理就是hook,經過hook攔截函數獲取參數信息來判斷是不是危險行爲,但這類程序大可能是C++的,一直以來我都想實現能夠hook .net函數的庫,網上搜索了不少,但都不理想,因此想本身實現一個

2.實現原理
我採用的是inline hook的方式,由於我對.net虛擬機以及一些內部的結構並非很熟悉,而且有些東西的確找不到任何文檔,因此就採用原生代碼的inline hook的方式來實現。

首先說一下inline hook的基本原理,它是經過修改函數的前5字節指令爲jmp xxxxxxxx來實現的,例如一個C#方法:

用windbg調試查看方法信息:

查看已經jit了的原生代碼:

這裏的地址(0x008c0640)能夠經過MethodInfo.MethodHandle.GetFunctionPointer().ToPointer()方法獲取

到了這裏,咱們就知道了修改從push ebp開始的5個字節爲jmp跳轉指令,跳入咱們本身的函數就能夠達到hook的目的,但執行到咱們的函數後,若是咱們並非要攔截執行流程,那麼咱們最終是須要再調用原函數的,但原函數已經被修改了,這會想到的辦法就是恢復那修改的5字節指令,但這又會引起另外一個問題,就是當咱們恢復時,正好另外一個線程調用到這個函數,那麼程序將會崩潰,或者說漏掉一次函數調用,修改時暫停其餘線程並等待正跑在其中的CPU執行完這5字節再去恢復指令也許是個不錯的辦法,但感受並不容易實現,並且影響性能,因此我放棄了這種辦法

那麼如何才能調用修改前的函數呢,我首先想到是C中寫裸函數的方式,即本身用匯編拼出來一個原函數再執行:
原函數前5字節指令+jmp跳轉指令
但其實這也是不可行的,聰明的人已經發現,圖中所示的函數的前5字節並非一個完整的彙編指令,不一樣的函數,長度都不同,.net的函數並不像某些原生函數那樣,會預留mov edi,edi這樣的正好5字節的指令,我先想到的是複製函數的全部彙編指令生成新的函數,但這樣也會出問題,由於像E8,E9這樣的相對跳轉指令,若是指令地址變了,那麼跳轉的位置也就變了,程序就會崩潰,因此這也不可行。

到了這裏,我有些不耐煩了,畢竟我是要hook全部函數的,而不是某個固定的函數,而函數入口的指令又不相同,這可怎麼辦,難道我須要計算出大於等於5字節的最小完整彙編指令長度?

按照這個思路,最終找到了一個用C寫的反彙編庫(BlackBone),其中提供了相似的方法,我稍做了修改後試用了下,的確不錯,能夠準確求出彙編指令長度,例如

push ebp
mov ebp,esp
mov eax,dword ptr ds:[33F22ACh]

求出值是9,這樣我根據求出的值動態拼接一個函數出來便可,哈哈,到了這裏,感受實現的差很少了,但沒想到64位下又給了我當頭一棒,以前的原函數指令能夠寫成:

大於等於5字節的最小完整彙編指令+jmp跳轉指令便可構成咱們的原函數

但咱們知道,C#中要想執行彙編,是須要用Marshal.AllocHGlobal來分配非託管空間的,而這樣分配的地址與咱們要跳轉到的原函數的地址在64位下是超過2GB地址範圍的,通常的跳轉指令是沒法實現的,因此想到了用ret指令實現,而64位地址又不能直接push,因此最後寫出以下彙編:

push rax
mov rax,target_addr
push rax
mov rax,qword ptr ss:[rsp+8]
ret 8

因爲某些C#函數居然第一行就是修改rax寄存器的值,因此只能是先保存rax,推入堆棧後再恢復,這裏彙編操做就方便多了,以前實現另外一個東西,用到IL指令,但發現只有dup這種複製棧頂元素的指令,卻沒有獲取堆棧中某個非棧頂元素值的指令,因此說仍是彙編靈活啊,想怎麼寫就怎麼寫,啥都能實現。

最後就是這個原函數的調用過程了,由於是動態拼接的函數,因此想到的就是用Marshal.GetDelegateForFunctionPointer轉成委託來執行,後來發現不對,由於我雖然拼接的是彙編,而這個彙編是C#方法jit後的彙編,這個並非C方法編譯後的彙編,經過把非託管指針轉換爲委託的方式運行函數是會添加不少不須要的操做的,例如託管類型與非託管類型的轉換,但我拼接出的函數是不須要這些過程的,這個怎麼辦,看來只能用調用C#普通函數的方式調用,這個怎麼實現呢,其實很好辦,只需寫一個空殼函數,而後修改這個函數的方法表中的原生指令指針便可,具體方法以下:git

C# code
 
?
1
*(( ulong *)(( uint *)method.MethodHandle.Value.ToPointer() + 2)) = ( ulong )ptr;


method是空殼函數的MethodInfo,         ptr是動態拼接的原函數的地址

好,到了這裏就基本完成核心功能了,最很差處理的就是這個原函數調用,個人完整的64位原函數指令拼接就實現了,代碼不多,以下所示:github

C# code
 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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.類庫開發所用到的語言
以前我說,個人這個庫是徹底用C#實現的,但其中的確用到了一個C寫的反彙編庫,因而我用C#把那個庫重寫了一遍,說來也簡單,C的代碼粘過來,C#啓用unsafe代碼,改了10分鐘就行了,真心是很是方便,畢竟C#是支持指針和結構體的,並且基礎類型很是豐富,這裏得給C#點個贊!

4.具體使用
使用很是簡單,首先新建控制檯程序並添加一個類,繼承接口IMethodMonitor,Get是你本身的函數,Ori是原函數會在運行時動態生成,在Get中你能夠幹你想幹的任何事情網絡

C# code
 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public  class  CustomMonitor : IMethodMonitor  //自定義一個類並繼承IMethodMonitor接口
{
     [Monitor( "TargetNamespace" "TargetClass" )]  //你要hook的目標方法的名稱空間,類名
     public  string  Get()  //方法簽名要與目標方法一致
     {
         return  "B"  + Ori();
     }
 
     [MethodImpl(MethodImplOptions.NoInlining)]
     [Original]  //原函數標記
     public  string  Ori()  //方法簽名要與目標方法一致
     {
         return  null //這裏寫什麼無所謂,能編譯過便可
     }
}


而後定義目標函數,例如ide

C# code
 
?
1
2
3
4
public  string  Get()
  {
     return  "A" ;
  }


最後調用Monitor.Install()安裝監視器,例如:函數

C# code
 
?
1
2
3
Console.WrtieLine(Get());
Monitor.Install()
Console.WrtieLine(Get());


你會發現第一次調用Get輸出的值是"A",第二次是"BA"

固然這個庫只是hook,但hook通常都須要dll注入來配合,由於hook自身進程沒什麼意義,hook別人的進程纔有意義,我以後會發佈一個用於.net程序遠程注入的類庫,注入的是.net的dll哦,不是C++的

好了,講了這麼多,其實這個庫代碼量並不大,但主要是本身研究的一個成果,不少東西都是本身琢磨出來的,因此以爲這個過程頗有意思,也但願高手能指出改進方案,畢竟感受目前這種方法雖然實現了功能,可是並非很好,總以爲以hook .net虛擬機的方式來實現會更簡單一些,或者網絡上已經有了現成的解決方案我沒有找到,總之,拋磚引玉,但願你們能共同探討性能

來源ui

相關文章
相關標籤/搜索