C# Hook原理及EasyHook简易教程

前言

  在说C# Hook以前,咱们先来讲说什么是Hook技术。相信你们都接触过外挂,不论是修改游戏客户端的也好,盗取密码的也罢,它们都是如何实现的呢?html

  实际上,Windows平台是基于事件驱动机制的,整个系统都是经过消息的传递来实现的。当进程有响应时(包括响应鼠标和键盘事件),则Windows会向应用程序发送一个消息给应用程序的消息队列,应用程序进而从消息队列中取出消息并发送给相应窗口进行处理。git

  而Hook则是Windows消息处理机制的一个平台,应用程序能够在上面设置子程以监视指定窗口的某种消息,并且所监视的窗口能够是其余进程所建立的。当消息到达后,在目标窗口处理函数以前处理它。钩子机制容许应用程序截获处理window消息或特定事件。github

  因此Hook就能够实如今键盘/鼠标响应后,窗口处理消息以前,就对此消息进行处理,好比监听键盘输入,鼠标点击坐标等等。某些盗号木马就是Hook了指定的进程,从而监听键盘输入了什么内容,进而盗取帐户密码。缓存

C# Hook

  咱们知道C#是运行在.NET平台之上,并且是基于CLR动态运行的,因此只能操做封装好的函数,且没法直接操做内存数据。并且在C#经常使用的功能中,并未封装Hook相关的类与方法,因此若是用C#实现Hook,必须采用调用WindowsAPI的方式进行实现。并发

  WindowsAPI函数属于非托管类型的函数,咱们在调用时必须遵循如下几步:app

  一、查找包含调用函数的DLL,如User32.dll,Kernel32.dll等。dom

  二、将该DLL加载到内存中,并注明入口函数

  三、将所需参数转化为C#存在的类型,如指针对应Intptr,句柄对应int类型等等测试

  四、调用函数ui

  咱们本篇须要使用的函数有如下几个:

  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

相关文章
相关标签/搜索