EasyHook(一)

前言

  在說C# Hook以前,咱們先來講說什麼是Hook技術。相信你們都接觸過外掛,不論是修改遊戲客戶端的也好,盜取密碼的也罷,它們都是如何實現的呢?html

  實際上,Windows平臺是基於事件驅動機制的,整個系統都是經過消息的傳遞來實現的。當進程有響應時(包括響應鼠標和鍵盤事件),則Windows會嚮應用程序發送一個消息給應用程序的消息隊列,應用程序進而從消息隊列中取出消息併發送給相應窗口進行處理。git

  而Hook則是Windows消息處理機制的一個平臺,應用程序能夠在上面設置子程以監視指定窗口的某種消息,並且所監視的窗口能夠是其餘進程所建立的。當消息到達後,在目標窗口處理函數以前處理它。鉤子機制容許應用程序截獲處理window消息或特定事件。github

  因此Hook就能夠實如今鍵盤/鼠標響應後,窗口處理消息以前,就對此消息進行處理,好比監聽鍵盤輸入,鼠標點擊座標等等。某些盜號木馬就是Hook了指定的進程,從而監聽鍵盤輸入了什麼內容,進而盜取帳戶密碼。windows

C# Hook

  咱們知道C#是運行在.NET平臺之上,並且是基於CLR動態運行的,因此只能操做封裝好的函數,且沒法直接操做內存數據。並且在C#經常使用的功能中,並未封裝Hook相關的類與方法,因此若是用C#實現Hook,必須採用調用WindowsAPI的方式進行實現。api

  WindowsAPI函數屬於非託管類型的函數,咱們在調用時必須遵循如下幾步:緩存

  一、查找包含調用函數的DLL,如User32.dll,Kernel32.dll等。併發

  二、將該DLL加載到內存中,並註明入口app

  三、將所需參數轉化爲C#存在的類型,如指針對應Intptr,句柄對應int類型等等dom

  四、調用函數函數

  咱們本篇須要使用的函數有如下幾個:

  SetWindowsHookEx     用於安裝鉤子

  UnhookWindowsHookEx   用於卸載鉤子

  CallNextHookEx      執行下一個鉤子

  詳細API介紹請參考MSDN官方聲明

  接下來在C#中須要首先聲明此API函數:

[DllImport("user32.dll",CharSet=CharSet.Auto,CallingConvention=CallingConvention.StdCall)]
public static extern int SetWindowsHookEx(int idHook, HookProc lpfn,IntPtr hInstance, int threadId);

[DllImport("user32.dll",CharSet=CharSet.Auto,CallingConvention=CallingConvention.StdCall)]
public static extern bool UnhookWindowsHookEx(int idHook);

[DllImport("user32.dll",CharSet=CharSet.Auto,CallingConvention=CallingConvention.StdCall)]
public static extern int CallNextHookEx(int idHook, int nCode,IntPtr wParam, IntPtr lParam);

 

聲明後便可實現調用,SetWindowsHookEx()把一個應用程序定義的鉤子子程安裝到鉤子鏈表中,SetWindowsHookEx函數老是在Hook鏈的開頭安裝Hook子程。當指定類型的Hook監視的事件發生時,系統就調用與這個Hook關聯的Hook鏈的開頭的Hook子程。每個Hook鏈中的Hook子程都決定是否把這個事件傳遞到下一個Hook子程。Hook子程傳遞事件到下一個Hook子程須要調用CallNextHookEx函數。 且鉤子使用完成後須要調用UnhookWindowsHookEx進行卸載,不然容易影響到其餘鉤子的執行,而且鉤子太多會影響目標進程的正常運行。

  關於實例詳細操做過程再也不贅述,請參考:http://blog.csdn.net/ensoo/article/details/2045101 及 https://www.cnblogs.com/ceoliujia/archive/2010/05/20/1740217.html

 

 

 

EasyHook

 

  C#自己調用WindowsAPI進行Hook功能受到很大的限制,而C++則不受此限制,所以就有一些聰明的人想到了聰明的方法:使用C++將基本操做封裝成庫,由C#進行調用,由此誕生了偉大的EasyHook,它不只使用方便,並且開源免費,還支持64位版本。

 

  接下來咱們一塊兒使用C#操做EasyHook來實現一個Demo,完成對MessageBox的改寫。

 

  首先咱們創建一個WinForm項目程序,並添加一個類庫ClassLibrary1,再從官網https://easyhook.github.io/或Nuget獲取到dll後引用到咱們的項目中,注意:32位和64位版本都須要引用,創建項目如圖所示:

 

 

 其中WinForm程序用於獲取目標進程,並對目標進程進行注入,相關步驟以下:

  一、根據進程ID獲取相關進程,並判斷是否爲64位;

  二、將所需DLL註冊到GAC(全局程序集緩存),註冊到GAC的目的是須要在目標進程中調用EasyHook及咱們所編寫的DLL;

 

 

private bool RegGACAssembly()
 {
     var dllName = "EasyHook.dll";
     var dllPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dllName);
     if (!RuntimeEnvironment.FromGlobalAccessCache(Assembly.LoadFrom(dllPath)))
     {
         new System.EnterpriseServices.Internal.Publish().GacInstall(dllPath);
         Thread.Sleep(100);
     }
   dllName = "ClassLibrary1.dll";
     dllPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dllName);
     new System.EnterpriseServices.Internal.Publish().GacRemove(dllPath);
     if (!RuntimeEnvironment.FromGlobalAccessCache(Assembly.LoadFrom(dllPath)))
     {
           new System.EnterpriseServices.Internal.Publish().GacInstall(dllPath);
           Thread.Sleep(100);
     }
     return true;
  }

 

 

 

此處須要注意,要將本身編寫的類庫DLL加入GAC,須要對DLL進行強簽名操做,操做方法請參考:https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/how-to-sign-an-assembly-with-a-strong-name

  三、注入目標進程,此處需使用EasyHook的RemoteHooking.Inject()方法進行注入:

 

 

private static bool InstallHookInternal(int processId)
{
  try
  {
     var parameter = new HookParameter
     {
         Msg = "已經成功注入目標進程",
         HostProcessId = RemoteHooking.GetCurrentProcessId()
      };
    RemoteHooking.Inject(
                    processId,
                    InjectionOptions.Default,
                    typeof(HookParameter).Assembly.Location,
                    typeof(HookParameter).Assembly.Location,
                    string.Empty,
                    parameter
                );
            }
            catch (Exception ex)
            {
                Debug.Print(ex.ToString());
                return false;
            }
            return true;
}

 

 HookParameter類爲定義在ClassLibrary1中的一個類,包含消息與進程ID:

[Serializable]
    public class HookParameter
    {
        public string Msg { get; set; }
        public int HostProcessId { get; set; }
    }

 

  到這一步咱們就完成了對主窗體代碼的編寫,如今咱們開始編寫注入DLL的方法:

  一、先引入MessageBox相關的WindowsAPI:

 

 

 

#region MessageBoxW

        [DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode)]
        public static extern IntPtr MessageBoxW(int hWnd, string text, string caption, uint type);

        [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]
        delegate IntPtr DMessageBoxW(int hWnd, string text, string caption, uint type);

        static IntPtr MessageBoxW_Hooked(int hWnd, string text, string caption, uint type)
        {
            return MessageBoxW(hWnd, "已注入-" + text, "已注入-" + caption, type);
        }

        #endregion
        
        #region MessageBoxA

        [DllImport("user32.dll", EntryPoint = "MessageBoxA", CharSet = CharSet.Ansi)]
        public static extern IntPtr MessageBoxA(int hWnd, string text, string caption, uint type);

        [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
        delegate IntPtr DMessageBoxA(int hWnd, string text, string caption, uint type);

        static IntPtr MessageBoxA_Hooked(int hWnd, string text, string caption, uint type)
        {
            return MessageBoxA(hWnd, "已注入-" + text, "已注入-" + caption, type);
        }

        #endregion

 

 

 

 

其中MessageBoxA與MessageBoxW是微軟用於區分不一樣操做系統中的編碼類型,早期的Windows並不屬於真正的32位操做系統,執行的API函數屬於ANSI類型,而從Windows2000開始,屬於Unicode類型,Windows在實際操做中,調用的MessageBox會自動根據平臺區分使用前者仍是後者,咱們在這裏就須要把兩者都包含其中。

  而DMessageBoxA與DMessageBoxW屬於IntPtr類型的委託,用於咱們在Hook函數以後傳入咱們須要修改的方法,此處咱們改變了MessageBox的內容和標題,分別在前綴加上了"已注入-"的標記。

  二、完成定義以後咱們就須要對函數進行Hook,此處使用LocalHook.GetProcAddress("user32.dll", "MessageBoxW")函數,經過指定的DLL與函數名,獲取函數在實際內存中的地址,獲取到以後,傳入LocalHook.Create()方法,用於建立本地鉤子:

 

 

public void Run(
            RemoteHooking.IContext context,
            string channelName
            , HookParameter parameter
            )
        {
            try
            {
                MessageBoxWHook = LocalHook.Create(
                    LocalHook.GetProcAddress("user32.dll", "MessageBoxW"),
                    new DMessageBoxW(MessageBoxW_Hooked),
                    this);
                MessageBoxWHook.ThreadACL.SetExclusiveACL(new int[1]);

                MessageBoxAHook = LocalHook.Create(
                    LocalHook.GetProcAddress("user32.dll", "MessageBoxA"),
                    new DMessageBoxW(MessageBoxA_Hooked),
                    this);
                MessageBoxAHook.ThreadACL.SetExclusiveACL(new int[1]);     
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                return;
            }

            try
            {
                while (true)
                {
                    Thread.Sleep(10);
                }
            }
            catch
            {

            }
        }

 

 

 

 

 

 

其中MessageBoxWHook與MessageBoxAHook均爲LocalHook類型的變量,MessageBoxAHook.ThreadACL.SetExclusiveACL(new int[1]); 這句代碼用於將本地鉤子加入當前線程中執行。

  運行以後咱們來查看Hook的效果,先打開一個測試窗體,彈出MessageBox,這時候MessageBox沒有標題,且內容是正常的:

 

 

 

 

 接着咱們對目標進程進行注入,獲取進程ID後點擊注入,提示已經成功注入目標進程:

 

 

 此時點擊目標進程MessageBox,能夠發現已經Hook成功,並改變了內容和標題:

 

 

 

 至此,C#調用EasyHook對目標進程Hook已經實現。

後記

  從此次實踐中咱們能夠感覺到,C#對程序進行Hook是徹底可行的,雖然不能直接操做內存和地址,可是咱們能夠經過操做WindowsAPI與使用EasyHook的方式完成,尤爲是後者,大大減小了代碼數量與使用難度。

  可是EasyHook目前中文資料很是少,我在使用的過程當中也遇到了很大困難,Hook其餘函數的方法也未能徹底實現,但願可以集思廣益,與你們共同思考交流!

  本人剛研究Hook時間不久,文中不免出現紕漏,懇請各位評論指正。

    源代碼已經上傳至百度網盤:連接: https://pan.baidu.com/s/1wyin9Ezn6AwFQlQxMenQeg 密碼: dv9b

 

 

 EasyHook函數四步走

  • 找函數
  • 匹配委託
  • 編寫hook函數
  • 注入hook(這裏包含了裝載hook和卸載hook

首先第一步驟,windows下的不少api,都是已經寫好了的,咱們程序中寫的不少函數都是依靠他們做爲底層來完成的,咱們直接拿來用就行,就咱們的程序而言,就是哪一個動態連接庫(這裏寫了一堆一堆的函數),裏面的哪一個函數,能夠作什麼。因此第一步,咱們須要知道這個函數在哪裏,其實也就是easyHook須要知道,他要hook哪一個函數的地址。

第二步,匹配委託,簡單的理解就是,函數指針(再通俗點就是咱們編寫函數的地址),要替換函數,要知道替換函數的地址,還要知道咱們本身寫的函數地址才能實現注入,注意這裏寫委託必定要和欲hook的函數參數和返回值同樣,不然,就會出現大問題,我知道你不想搞一個大事情,因此,老老實實按照原型寫委託吧。

第三步,編寫咱們本身的函數就是上文中說到的sayHelloHook,咱們本身實現函數要作哪些事情,最後別忘了必定要和原函數,參數,返回值對應。

第四步,最輕鬆的一步,由easyHook替咱們完成,他會本身實現函數的替換,注入啥的。咱們只須要聲明調用就ok

 

參考

C# Hook原理及EasyHook簡易教程

[.Net] C#使用easyHook筆記一

Hook原理

相關文章
相關標籤/搜索