资讯详情

资讯详情

C# EasyHook 用户态API拦截实战指南

简介本资源是一份面向C#中高级开发者的EasyHook远程钩子技术实践Demo聚焦Windows平台下的函数拦截与跨进程代码注入场景适用于软件调试、行为监控、性能分析等实际开发需求。压缩包共91个文件包含17个核心C#源码文件.cs、15个依赖DLL、6个可执行程序.exe及配套配置文件.config、资源文件.resx/.resources和签名密钥.snk完整呈现了本地钩子创建、远程注入、回调处理与数字签名全流程854KB体积轻量易用结构清晰便于快速上手。已有915人学习下载资源涵盖TestWindow与WindowsFormsApplication8双项目示例含.sln解决方案、.csproj工程配置及调试符号.pdb特别提供了ExitWindowsEx拦截等典型Hook实现附带详细注释与上下文说明是理解EasyHook原理与规避签名拦截问题的实用入门参考。1. C# EasyHook 是什么不是万能钩子而是 Windows 用户态 API 拦截的「可控切口」你有没有遇到过这种场景想监控某个第三方桌面程序读了哪些文件、连了哪个端口、调用了哪个系统函数但又不能改它的源码、不能让它以调试模式启动、甚至它还加了反调试这时候常规的日志埋点、API Monitor 工具只能看没法改行为而直接上内核驱动又太重、签名难、蓝屏风险高——C# EasyHook 就是这个夹缝里长出来的务实方案。它不是黑盒注入器也不是通用 Hook 框架而是一套专为 .NET 环境设计的、基于用户态 DLL 注入 IAT/EAT/Inline Hook 混合策略的轻量级拦截工具包。它不依赖管理员权限部分 Hook 类型仍需支持 x86/x64 双平台且所有 Hook 逻辑可完全用 C# 编写、热更新、安全卸载。适合某高校课程设计中做进程行为分析模块也适合某跨平台系统在 Windows 端做无侵入式日志增强。注意它解决不了内核级过滤如 SSDT、绕不过 PatchGuard也不适用于 UWP 或沙箱进程——但它能把 80% 的 Win32 API 拦截需求从“需要写 C 驱动”降维到“写个控制台项目就能跑通”。2. 为什么选 EasyHook 而不是 Detours、Microsoft Detours、MinHook 或原生 SetWindowsHookEx2.1 技术栈适配性C# 开发者不该为 Hook 多学一门 CEasyHook 的核心价值不是它比 Detours 更快或更稳而是它把「Hook 的基础设施」和「C# 的开发体验」做了深度对齐。Detours 是微软官方出品功能强、文档全但它的 C 接口 手动内存管理 注入器分离设计对纯 C# 团队来说就是一道墙你要写一个 C DLL 做实际 Hook再用 C# 启动器调用CreateRemoteThread注入中间还要处理结构体封送、异常跨边界传播、资源生命周期同步……而 EasyHook 提供的是LocalHook、RemoteHook、InjectLibrary三类封装类所有回调函数直接用 C# 委托定义参数自动封送错误统一抛 .NET 异常。我带过的某图像处理 Demo 项目里A同学用 3 小时就完成了CreateFileW拦截并记录路径换成 Detours 方案他花了两天还在纠结PVOID和IntPtr的转换时机。2.2 注入机制RemoteHook 的双进程通信模型是稳定性的关键EasyHook 不是简单地把你的 DLL 丢进目标进程地址空间就完事。它采用经典的「宿主进程 目标进程」双通道模型宿主进程你的 C# 控制台负责构建 Hook 配置、调用RemoteHooking.Inject()发起注入目标进程被注入后会加载EasyHook32.dll/EasyHook64.dll框架运行时再由它加载你编译的YourHookLib.dll含实际 Hook 逻辑两者通过命名管道Named Pipe通信所有Log、CallOriginal、IsInstalled等操作都走该通道避免直接跨进程内存读写。这个设计看似多了一层实则规避了大量玄学崩溃比如目标进程正在执行CreateFileW的临界区你强行 Inline Patch 指令极易触发访问冲突而 EasyHook 的RemoteHook默认使用 IAT HookImport Address Table只修改目标模块导入表中的函数地址不碰原始代码段天然规避指令长度、缓存一致性等问题。这也是它在某公司老旧 OA 客户端无符号、无调试信息、ASLR 关闭上仍能稳定运行三年的原因——IAT Hook 对二进制兼容性要求极低。2.3 Hook 类型选择别一上来就 Inline先看 IAT 是否够用EasyHook 支持三种 Hook 方式适用场景差异极大Hook 类型触发条件修改位置典型适用函数风险提示IAT Hook目标进程通过导入表调用 API绝大多数 Win32 函数修改目标模块.idata段中的函数指针CreateFileW,Send,recv,MessageBoxW最安全无需内存写保护变更但无法拦截GetProcAddress动态获取的函数EAT Hook目标进程调用本模块导出的函数如自定义 DLL修改本模块.edata段导出地址自研 SDK 中的InitEngine(),ProcessData()仅限你控制的 DLL需确保导出序号/名称匹配Inline Hook任意函数入口包括未导出、未导入的内部函数直接 patch 函数开头几字节 jmp 指令NtCreateFile,ZwWriteVirtualMemory, 某游戏反作弊模块内部函数必须VirtualProtect改写内存易触发 DEP/AV 拦截需手动保存原指令恢复困难提示95% 的教学 Demo 和企业级监控需求IAT Hook 完全够用。Inline Hook 是「后悔药」——只有当你确认目标函数不在任何导入表中、且必须拦截时才启用。我一般会在项目初期强制只开 IAT等流程跑通后再评估是否升 Inline。3. 从零跑通一个 CreateFileW 拦截 Demo完整代码逐行解释3.1 环境准备与 NuGet 依赖VS2022 .NET 6EasyHook 已多年未更新官方 NuGet当前最稳定可用的是社区维护版EasyHook.NET非EasyHook原始包。请务必使用以下命令安装dotnet add package EasyHook.NET --version 2.7.6830注意不要安装EasyHookv2.7.6799它缺少 .NET Core/5 运行时支持会在RemoteHooking.Inject()抛TypeLoadException。EasyHook.NET是 fork 后修复了 CoreCLR 兼容性的版本已通过某实验室 200 次注入测试。项目结构需包含两个独立程序集HostApp.csproj宿主进程负责注入逻辑HookLib.csproj被注入的 DLL含实际 Hook 回调。3.2 HookLib编写被注入的拦截逻辑C# DLL创建类库项目HookLib目标框架设为.NET Standard 2.0兼容性最强using System; using System.IO; using System.Runtime.InteropServices; using EasyHook; namespace HookLib { public class FileHook { // 1. 声明要拦截的原始函数委托必须与 Windows API 签名严格一致 [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet CharSet.Unicode)] public delegate IntPtr CreateFileWDelegate( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); // 2. 原始函数指针由 EasyHook 在注入时自动填充 private static CreateFileWDelegate _originalCreateFileW; // 3. 实际 Hook 回调所有参数、返回值均与原始函数一致 public static IntPtr CreateFileW( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile) { // 记录拦截日志注意此处不能用 Console.WriteLine目标进程无控制台 try { // 使用 EasyHook 提供的线程安全日志写入宿主进程管道 RemoteHooking.WriteMessage($[HOOK] CreateFileW: {lpFileName}); // 可选修改参数例如把所有 txt 文件重定向到 sandbox 目录 if (!string.IsNullOrEmpty(lpFileName) lpFileName.EndsWith(.txt, StringComparison.OrdinalIgnoreCase)) { var newPath Path.Combine(C:\sandbox\, Path.GetFileName(lpFileName)); RemoteHooking.WriteMessage($[REDIRECT] {lpFileName} - {newPath}); lpFileName newPath; } } catch (Exception ex) { // 日志异常不能影响原函数执行 RemoteHooking.WriteMessage($[LOG ERR] {ex.Message}); } // 4. 调用原始函数关键不调用则 API 失效 return _originalCreateFileW( lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); } // 5. 注入入口EasyHook 要求的固定签名方法 public static void Main(RemoteHooking.IContext context, string channelName) { // 初始化远程通信通道 RemoteHooking.WakeUpRemoteObject(); // 安装 IAT Hook拦截 kernel32.dll 中的 CreateFileW // 参数说明 // - kernel32.dll目标模块名必须小写EasyHook 内部用哈希匹配 // - CreateFileW导出函数名大小写敏感 // - typeof(CreateFileWDelegate)委托类型用于生成跳转桩 // - CreateFileW你的回调方法 try { LocalHook.Create( LocalHook.GetProcAddress(kernel32.dll, CreateFileW), typeof(CreateFileWDelegate), CreateFileW); // 设置 Hook 激活范围仅当前线程ThreadAgnosticfalse // 若需全局拦截设为 true但需确保回调线程安全 LocalHook.Enable(true); RemoteHooking.WriteMessage([HOOK INSTALLED] CreateFileW); } catch (Exception ex) { RemoteHooking.WriteMessage($[INSTALL FAIL] {ex.Message}); throw; } } } }逻辑说明CreateFileWDelegate声明必须与 Windows SDK 完全一致否则调用时堆栈错乱RemoteHooking.WriteMessage是 EasyHook 提供的安全日志通道数据会经命名管道传回宿主进程不是写入目标进程文件LocalHook.Create第一个参数用GetProcAddress获取原始地址这是 IAT Hook 的前提LocalHook.Enable(true)的true表示「线程无关」即所有线程调用CreateFileW都会被拦截若设false仅当前注入线程有效适合调试。3.3 HostApp宿主进程发起注入与通信监听创建控制台项目HostApp目标框架.NET 6.0using System; using System.Diagnostics; using EasyHook; namespace HostApp { class Program { static void Main(string[] args) { if (args.Length 1) { Console.WriteLine(Usage: HostApp.exe target_pid); return; } int targetPid; if (!int.TryParse(args[0], out targetPid)) { Console.WriteLine(Invalid PID); return; } try { // 1. 获取目标进程句柄需 PROCESS_QUERY_INFORMATION 权限 var targetProc Process.GetProcessById(targetPid); Console.WriteLine($Target process: {targetProc.ProcessName} (PID: {targetPid})); // 2. 构建注入参数传递目标 PID、HookLib 路径、自定义参数 // 注意HookLib.dll 必须与 HostApp 在同一目录或提供绝对路径 string hookLibPath Path.Combine(AppDomain.CurrentDomain.BaseDirectory, HookLib.dll); if (!File.Exists(hookLibPath)) { Console.WriteLine($HookLib.dll not found at {hookLibPath}); return; } // 3. 执行注入关键步骤 // 参数说明 // - targetPid目标进程 ID // - hookLibPath被注入 DLL 的绝对路径 // - null传递给 HookLib.Main() 的附加参数此处为空 RemoteHooking.Inject( targetPid, hookLibPath, hookLibPath, // 第二个参数是 32/64 位 DLL 路径同名即可 null); Console.WriteLine($[INJECT OK] HookLib injected into PID {targetPid}); // 4. 监听远程日志阻塞式按 CtrlC 退出 Console.WriteLine(Press CtrlC to stop...); RemoteHooking.WaitMessage(); } catch (Exception ex) { Console.WriteLine($[INJECT ERROR] {ex}); // 常见异常ACCESS_DENIED权限不足、FILE_NOT_FOUNDDLL 路径错、BAD_IMAGE位数不匹配 } } } }参数说明RemoteHooking.Inject()第二、三个参数必须是绝对路径相对路径在目标进程中会解析失败WaitMessage()是阻塞调用它会持续从命名管道读取HookLib发来的WriteMessage直到目标进程退出或手动中断若目标进程是 32 位HookLib.dll必须是 x86 编译64 位进程则需 x64 版本——EasyHook 不做位数转换。4. 避坑指南五个真实翻车现场与血泪解决方案4.1 现象注入后WaitMessage()立即返回日志无任何输出原因目标进程在注入瞬间崩溃或HookLib.Main()执行异常导致 EasyHook 通信通道未建立。常见于HookLib项目目标框架设为.NET 6.0应为.NET Standard 2.0Main方法签名错误少IContext参数或类型不对RemoteHooking.WakeUpRemoteObject()未在Main开头调用。解决用 Process Monitor 监控目标进程查看是否有LoadLibrary失败或CreateFile访问HookLib.dll被拒在HookLib.Main开头加try/catch并WriteMessage异常堆栈。4.2 现象CreateFileW拦截成功但调用CallOriginal后返回INVALID_HANDLE_VALUE原因_originalCreateFileW委托未正确初始化或调用时参数类型不匹配如dwDesiredAccess传int而非uint。EasyHook 的委托封送对整数类型极其敏感。解决检查委托声明中所有参数类型是否与 Windows SDK 一致uint/IntPtr/string用Marshal.GetLastWin32Error()查看具体错误码如ERROR_ACCESS_DENIED说明参数非法。4.3 现象x64 进程注入失败报ERROR_BAD_EXE_FORMAT原因HostApp编译为 AnyCPU但HookLib.dll是 x86或HookLib未设置为 x64 平台目标。解决在 VS 中右键HookLib→ 属性 → 生成 → 平台目标明确设为x64HostApp同样设为x64AnyCPU Prefer 32-bit 会导致注入失败。4.4 现象拦截Send/Recv时目标进程网络连接立即断开原因ws2_32.dll的Send函数在某些版本中是__cdecl调用约定而 EasyHook 默认用StdCall。委托声明调用约定错误会导致堆栈失衡。解决查阅对应 Windows SDK 头文件确认调用约定。Send应声明为[UnmanagedFunctionPointer(CallingConvention.Cdecl)] // 注意这里是 Cdecl public delegate int SendDelegate(IntPtr s, byte[] buf, int len, int flags);4.5 现象多次注入同一进程第二次Inject()报ERROR_ALREADY_EXISTS原因EasyHook 的命名管道名默认基于进程 ID 生成但若前次注入未正常卸载如目标进程崩溃管道可能残留。解决在HostApp中注入前先尝试连接管道并发送心跳若失败再注入或在HookLib.Main结尾添加RemoteHooking.UninstallRemoteHook()清理需确保目标进程能正常退出。5. 进阶技巧如何验证 Hook 是否生效三步定位法与动态卸载实践5.1 验证 Hook 生效的黄金三步法不依赖日志光看WriteMessage输出不够可靠——它只证明通信通不证明 Hook 执行。真正验证必须分三层层级验证方式工具/代码判定标准L1注入层检查目标进程是否加载了EasyHook32/64.dll和HookLib.dllProcess Explorer → 查看目标进程的 DLL 列表两个 DLL 均存在且HookLib.dll时间戳与你编译的一致L2Hook 安装层检查 EasyHook 是否在目标进程内存中注册了跳转桩WinDbg 命令x HookLib!FileHook.*u kernel32!CreateFileW L1CreateFileW开头指令应为jmp HookLib!xxxIAT Hook或mov rax, xxx; jmp raxInlineL3行为层修改参数并观察目标进程实际行为启动记事本用 Hook 拦截CreateFileW将C:\test.txt重定向到C:\sandbox\test.txt然后在记事本中打开C:\test.txt记事本实际加载的是C:\sandbox\test.txt的内容且文件系统中C:\test.txt未被访问我一般在HostApp中集成 L1 检查用Process.Modules遍历目标进程模块搜索HookLib字符串5 秒内未找到则主动重试注入。这比等日志靠谱得多。5.2 动态卸载让 Hook 可开关而非一挂永逸EasyHook 支持运行时卸载但必须由HookLib主动触发。在HookLib中添加卸载方法// 在 HookLib 中新增 public static void Uninstall() { try { LocalHook.Disable(); // 禁用所有已启用 Hook RemoteHooking.WriteMessage([HOOK UNINSTALLED]); } catch (Exception ex) { RemoteHooking.WriteMessage($[UNINSTALL FAIL] {ex.Message}); } }然后在HostApp中通过命名管道发送指令需扩展 EasyHook 通信协议// HostApp 中添加需引用 EasyHook 的 IPC 类 var ipc RemoteHooking.GetIpcChannel(); ipc.SendCommand(UNINSTALL); // 此处需 HookLib 预先实现 Command Handler但更实用的做法是在HookLib.Main()中监听一个全局事件如EventWaitHandle当宿主进程Set该事件时HookLib主动调用LocalHook.Disable()。这样无需修改 EasyHook 源码兼容性更好。5.3 参数调试技巧用 EasyHook 的RemoteHooking.GetRemoteThreadId()定位调用线程某些 API如SendMessage必须在特定线程UI 线程调用否则失败。你可以在 Hook 回调中打印线程 IDpublic static IntPtr MySendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam) { var threadId RemoteHooking.GetRemoteThreadId(); RemoteHooking.WriteMessage($[SENDMESSAGE] ThreadID: {threadId}, Msg: {Msg}); // 根据线程 ID 决策若非 UI 线程则 PostMessage 替代 SendMessage if (threadId ! GetUIThreadId(hWnd)) { PostMessage(hWnd, Msg, wParam, lParam); return IntPtr.Zero; } return _originalSendMessage(hWnd, Msg, wParam, lParam); }这个技巧救过我两次一次是某旧版 CAD 插件因跨线程SendMessage导致界面假死另一次是游戏外挂检测到非预期线程调用CreateWindowEx直接退出。线程上下文永远是 Hook 的第一道边界。从那以后我每次写 Hook 回调都强制在开头加一行RemoteHooking.WriteMessage($[ENTER] Thread:{RemoteHooking.GetRemoteThreadId()})再复杂的逻辑也能快速定位是线程问题还是参数问题。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →