前言
在说 C# Hook 之前, 我们先来说说什么是 Hook 技术相信大家都接触过外挂, 不管是修改游戏客户端的也好, 盗取密码的也罢, 它们都是如何实现的呢?
实际上, Windows 平台是基于事件驱动机制的, 整个系统都是通过消息的传递来实现的当进程有响应时 (包括响应鼠标和键盘事件), 则 Windows 会向应用程序发送一个消息给应用程序的消息队列, 应用程序进而从消息队列中取出消息并发送给相应窗口进行处理
而 Hook 则是 Windows 消息处理机制的一个平台, 应用程序可以在上面设置子程以监视指定窗口的某种消息, 而且所监视的窗口可以是其他进程所创建的当消息到达后, 在目标窗口处理函数之前处理它钩子机制允许应用程序截获处理 window 消息或特定事件
所以 Hook 就可以实现在键盘 / 鼠标响应后, 窗口处理消息之前, 就对此消息进行处理, 比如监听键盘输入, 鼠标点击坐标等等某些盗号木马就是 Hook 了指定的进程, 从而监听键盘输入了什么内容, 进而盗取账户密码
C# Hook
我们知道 C# 是运行在. NET 平台之上, 而且是基于 CLR 动态运行的, 所以只能操作封装好的函数, 且无法直接操作内存数据而且在 C# 常用的功能中, 并未封装 Hook 相关的类与方法, 所以如果用 C# 实现 Hook, 必须采用调用 WindowsAPI 的方式进行实现
WindowsAPI 函数属于非托管类型的函数, 我们在调用时必须遵循以下几步:
1 查找包含调用函数的 DLL, 如 User32.dll,Kernel32.dll 等
2 将该 DLL 加载到内存中, 并注明入口
3 将所需参数转化为 C# 存在的类型, 如指针对应 Intptr, 句柄对应 int 类型等等
4 调用函数
我们本篇需要使用的函数有以下几个:
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 程序用于获取目标进程, 并对目标进程进行注入, 相关步骤如下:
1 根据进程 ID 获取相关进程, 并判断是否为 64 位;
2 将所需 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
3 注入目标进程, 此处需使用 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 的方法:
1 先引入 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 的内容和标题, 分别在前缀加上了 "已注入 -" 的标记
2 完成定义之后我们就需要对函数进行 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
来源: https://www.cnblogs.com/wackysoft/p/8544365.html