
简介这份资源是面向C#开发者、调试与逆向分析学习者的EasyHook实战示例包聚焦运行时远程函数拦截与DLL注入这一进阶主题。包内共91个文件以17个cs源码、15个dll依赖库、6个exe可执行程序、3个csproj工程文件及2个sln解决方案为主另含pdb调试符号、config配置、resx资源与snk签名文件压缩包约854KB结构完整可直接编译运行。示例演示了通过LocalHook.Create拦截user32.dll中ExitWindowsEx方法、设置ThreadACL独占ACL并安装钩子的完整流程同时覆盖NuGet安装配置、本地与远程方法定义、回调委托获取以及DLL数字签名等关键环节帮助读者理解钩子注入的签名要求与安全机制。目前已有912人学习下载适合希望掌握跨进程拦截、性能监控与行为修改技术的中高级开发者参考实践。1. C# easyHook 使用 demo从零跑通第一个 API Hook线上环境有个老系统某个第三方 DLL 每次调用CreateFileW都会把路径写死成C:\ProgramData\...运维那边又不敢动原文件。这种时候最省事的做法不是反编译重写而是用 easyHook 在运行时把CreateFileW拦下来把路径参数改掉再放行。easyHook 是 .NET 生态里做 API Hook 比较成熟的一个库托管侧写起来比纯 C 的 Detours 舒服很多适合做 C# 上位机、桌面工具、自动化测试这类场景的运行时拦截。这篇笔记围绕「C# easyHook 使用 demo」这个主题把托管 Hook 的完整链路走一遍从环境准备、目标函数选择、Hook 代码怎么写、参数怎么改、到实际跑起来会翻车的几个点。读者最好有 C# 基础知道 P/Invoke 和委托是什么剩下的跟着步骤走就行。demo 本身不复杂难的是把「为什么这么写」和「哪里会炸」讲清楚这也是我踩过坑之后最想补上的部分。2. easyHook 的托管 Hook 原理与选型理由2.1 为什么是 easyHook 而不是 Detours 或 MinHook做 API Hook 的库不少Detours 是微软官方的MinHook 轻量easyHook 的定位在中间它同时支持托管和非托管 Hook托管侧用 C# 就能写不需要单独编译一个 C 注入 DLL。对于 C# 上位机、WinForm/WPF 工具这类项目团队里往往没有专门的 C 人手easyHook 能省掉一整条工具链。它的核心机制是在目标进程里注入一个托管运行时宿主然后通过LocalHook或RemoteHook把目标函数的入口改成跳转指令跳到我们自己的委托上。托管侧写 Hook 的时候easyHook 帮你处理了 ABI 对齐、栈平衡这些脏活你只要保证委托签名和原函数一致就行。选型上我一般这么判断如果只是 Hook 自己进程内的 Win32 API用LocalHook就够了不需要注入调试也方便如果要 Hook 别的进程才需要RemoteHook加注入器。demo 阶段强烈建议先在本进程内跑通把签名和参数搞对了再考虑跨进程。2.2 环境准备与依赖安装先建一个 .NET Framework 4.7.2 或 .NET 6 的控制台项目。easyHook 对 .NET Core/.NET 5 的支持一直有点玄学稳妥起见 demo 用 .NET Framework 4.7.2NuGet 上直接装EasyHook包即可。# 在项目目录下用 dotnet CLI 装包.NET Framework 项目用 VS 的包管理器控制台也行 dotnet add package EasyHook装完之后确认两件事一是EasyHook.dll和EasyHook32.dll/EasyHook64.dll都被复制到输出目录二是项目平台目标要和系统位数对上。64 位系统上跑 AnyCPU 有时候会加载错 native dll直接指定 x64 最省心。提示easyHook 的 native dll 必须和宿主进程位数一致32 位进程加载 64 位 dll 会直接抛BadImageFormatException这个报错信息很不直观第一次遇到容易懵。2.3 目标函数的选择从 CreateFileW 说起demo 选CreateFileW作为目标原因是它签名清晰、调用频繁、参数好观察。它的原型是HANDLE CreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile );对应到 C# 的 P/Invoke 声明参数类型要一一对上LPCWSTR用[MarshalAs(UnmanagedType.LPWStr)] stringHANDLE用IntPtrDWORD用uint。这里有个容易翻车的点lpFileName如果声明成string而不是IntPtr在 Hook 里想改路径就得重新分配非托管内存麻烦很多。demo 里我一般直接声明成IntPtr需要读的时候用Marshal.PtrToStringUni需要改的时候用Marshal.StringToHGlobalUni。3. 写一个能跑的 LocalHook demo3.1 声明原函数与 Hook 委托先写 P/Invoke 声明和委托类型。委托签名必须和原函数完全一致返回值、参数个数、参数类型一个都不能差否则 Hook 上去之后栈会乱表现是进程直接崩或者返回值莫名其妙。using System; using System.Runtime.InteropServices; internal static class NativeMethods { // 原函数声明用于调用原始逻辑 [DllImport(kernel32.dll, CharSet CharSet.Unicode, SetLastError true)] public static extern IntPtr CreateFileW( IntPtr lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); // Hook 委托签名必须和上面完全一致 [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet CharSet.Unicode, SetLastError true)] public delegate IntPtr CreateFileWDelegate( IntPtr lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); }UnmanagedFunctionPointer里的CallingConvention.StdCall是关键Win32 API 默认就是 StdCall写错了 Hook 调用时参数会错位。SetLastError true也要带上不然Marshal.GetLastWin32Error拿不到真实错误码。3.2 用 LocalHook.Create 挂上 Hook接下来创建 Hook 实例把目标函数地址和我们的委托关联起来。LocalHook.Create的第一个参数是目标函数地址用GetProcAddress拿第二个参数是 Hook 委托第三个参数是回调上下文demo 里传null就行。using EasyHook; public class FileHook { private LocalHook _hook; private NativeMethods.CreateFileWDelegate _original; private NativeMethods.CreateFileWDelegate _hookDelegate; public void Install() { // 拿到 kernel32 里 CreateFileW 的真实地址 IntPtr kernel32 NativeMethods.GetModuleHandle(kernel32.dll); IntPtr target NativeMethods.GetProcAddress(kernel32, CreateFileW); // Hook 委托指向我们自己的实现 _hookDelegate HookImpl; // 创建 LocalHook第三个参数是回调上下文这里不需要 _hook LocalHook.Create(target, _hookDelegate, null); // 只 Hook 当前线程避免影响其他线程造成不可预期行为 _hook.ThreadACL.SetExclusiveACL(new int[] { 0 }); // 保存原始函数委托用于放行 _original (NativeMethods.CreateFileWDelegate)Marshal.GetDelegateForFunctionPointer( target, typeof(NativeMethods.CreateFileWDelegate)); } private IntPtr HookImpl( IntPtr lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile) { string path Marshal.PtrToStringUni(lpFileName) ?? string.Empty; // 把写死的路径替换成我们想要的路径 if (path.StartsWith(C:\ProgramData\, StringComparison.OrdinalIgnoreCase)) { string newPath path.Replace(C:\ProgramData\, D:\Redirected\); IntPtr newPtr Marshal.StringToHGlobalUni(newPath); try { return _original(newPtr, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); } finally { Marshal.FreeHGlobal(newPtr); } } // 不匹配的路径原样放行 return _original(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); } }几个参数说明一下。ThreadACL.SetExclusiveACL(new int[] { 0 })里的0是线程 ID表示只 Hook 主线程这样调试的时候不会因为其他线程也走 Hook 逻辑而干扰判断。Marshal.GetDelegateForFunctionPointer把原始地址转成委托这是放行的关键不调用它就没法走回原逻辑。Marshal.StringToHGlobalUni分配的非托管内存一定要在finally里释放否则每次 Hook 都漏一块内存跑久了进程内存会涨得很难看。这是血泪经验第一次写的时候忘了释放跑了一晚上内存涨到 2G。3.3 触发 Hook 并验证效果装好 Hook 之后随便调一次CreateFileW就能看到效果。demo 里直接调File.OpenRead触发因为 .NET 内部最终会走到CreateFileW。class Program { static void Main(string[] args) { var hook new FileHook(); hook.Install(); // 触发一次 CreateFileW路径会被重定向 try { using (var fs File.OpenRead(C:\ProgramData\test.txt)) { Console.WriteLine(打开成功长度 fs.Length); } } catch (Exception ex) { Console.WriteLine(打开失败 ex.Message); } Console.ReadLine(); } }验证的时候在HookImpl里加一句Console.WriteLine(path)能看到实际传入的路径。如果路径被成功替换说明 Hook 生效了。注意File.OpenRead内部可能走的是CreateFileW的宽字符版本如果发现 Hook 没触发先确认目标进程是不是真的调了这个 API可以用 Process Monitor 抓一下调用栈。4. easyHook 实战避坑与常见问题排查4.1 Hook 装上了但没触发现象代码没报错LocalHook.Create也返回了实例但HookImpl里的断点就是不进。原因通常有三个一是目标函数地址拿错了比如GetProcAddress返回IntPtr.Zero但没检查二是ThreadACL设置的范围不对当前线程不在 Hook 列表里三是目标进程调用的其实是另一个 API比如CreateFileA而不是CreateFileW。解决先检查GetProcAddress的返回值为零就说明模块名或函数名写错了。然后把SetExclusiveACL换成SetInclusiveACL试试或者干脆不设 ACL 让所有线程都 Hook。最后用 Process Monitor 确认目标进程实际调用的 API 名称。4.2 进程崩溃或返回值异常现象Hook 装上之后进程直接崩或者函数返回值变成一堆乱码。原因委托签名和原函数不一致最常见的是CallingConvention写成了Cdecl或者参数类型对不上比如把uint写成了int。栈不平衡的时候返回地址会被读错进程就崩了。解决把委托签名和 P/Invoke 声明逐字对比确保参数个数、类型、顺序、调用约定完全一致。SetLastError也要和原函数声明保持一致。如果还是崩先用一个参数最简单的 API 比如GetTickCount练手确认 Hook 机制本身没问题再换复杂函数。4.3 内存泄漏与句柄泄漏现象跑一段时间后进程内存持续上涨或者句柄数不断增加。原因在 Hook 里分配了非托管内存但没释放比如Marshal.StringToHGlobalUni之后忘了FreeHGlobal或者 Hook 里打开了文件/句柄但没关闭。解决所有非托管分配都用try/finally包起来确保释放。Hook 逻辑里尽量不做重资源操作如果必须做用using或者显式释放。可以用性能监视器观察进程的Private Bytes和Handle Count跑个几万次调用看曲线是否平稳。4.4 多线程环境下的竞态现象单线程跑得好好的一上多线程就偶发崩溃或者 Hook 失效。原因LocalHook的 ACL 是按线程管理的如果多个线程同时进出 Hook而 Hook 逻辑里有共享状态没加锁就会出问题。另外_original委托在多线程下调用本身是安全的但如果 Hook 逻辑里改了共享变量就要小心。解决Hook 逻辑尽量做成无状态的只做参数转换和放行。如果必须有状态用lock或者Interlocked保护。ACL 设置上如果业务允许直接 Hook 所有线程反而更简单避免线程漏 Hook 的问题。4.5 目标进程是 64 位但加载了 32 位 dll现象LocalHook.Create抛BadImageFormatException或者直接找不到 native dll。原因easyHook 的 native dll 分 32 位和 64 位宿主进程位数和 dll 位数不匹配就加载失败。解决在项目属性里把平台目标设成和宿主进程一致x64 进程就用 x64不要用 AnyCPU。输出目录里确认EasyHook32.dll和EasyHook64.dll都在easyHook 会根据进程位数自动选但前提是两个都在。5. 进阶用 RemoteHook 跨进程拦截与验证技巧LocalHook只能 Hook 自己进程实际工作中更常见的是要 Hook 别人的进程比如那个写死路径的第三方 DLL 跑在独立进程里。这时候要用RemoteHook配合RemoteHooking.Inject把托管宿主注入目标进程。跨进程 Hook 的代码结构和 LocalHook 差不多区别在于 Hook 逻辑要写在一个继承自EasyHook.IEntryPoint的类里注入的时候通过RemoteHooking.Inject传参。注入器这边大概长这样// 注入器把 Hook 宿主注入目标进程 int targetPid 1234; // 目标进程 ID RemoteHooking.Inject( targetPid, InjectionOptions.Default, typeof(FileHookEntryPoint).Assembly.Location, // 32 位宿主 dll typeof(FileHookEntryPoint).Assembly.Location, // 64 位宿主 dll null, // 通道名不需要就传 null null // 传给宿主的参数 );InjectionOptions.Default是默认注入选项如果目标进程有反注入保护可能需要换InjectionOptions.NoService之类的选项但那些属于对抗范畴demo 阶段用默认的就行。注入成功之后目标进程里会加载你的宿主 dllRun方法被调用在里面装 Hook 即可。验证跨进程 Hook 有没有生效最直接的办法是在 Hook 逻辑里往一个日志文件写记录然后看目标进程运行的时候日志有没有新增。也可以用DebugView这类工具看OutputDebugString的输出但要注意目标进程的调试输出权限。一个我常用的技巧在 Hook 里记录调用次数和参数摘要跑一段时间后统计哪些路径被拦截得最多。这样既能验证 Hook 生效又能顺便摸清目标进程的行为模式。如果发现某个路径频繁被拦截但替换后目标进程行为异常说明那个路径可能有特殊语义不能无脑替换。跨进程 Hook 的坑比 LocalHook 多得多比如目标进程位数、权限、反注入、托管运行时版本冲突等等。我的习惯是先用 LocalHook 在本进程把逻辑跑通确认签名和参数处理没问题再原样搬到 RemoteHook 的宿主里。这样出问题的时候能快速定位是 Hook 逻辑的问题还是注入环节的问题。另外注入之前一定要确认目标进程的位数32 位注入器只能注入 32 位进程64 位同理搞错了会直接失败。最后说个习惯每次写 Hook 之前先用 Process Monitor 把目标进程的 API 调用抓一遍确认要 Hook 的函数确实被调用了、调用频率大概多少、参数长什么样。这一步花五分钟能省掉后面半小时的瞎猜。希望帮到你。本文还有配套的精品资源点击获取