
简介这份资源是面向C#开发者与调试爱好者的EasyHook远程钩子实战示例包聚焦运行时函数拦截与跨进程注入这一进阶主题适合已具备C#基础、希望掌握进程监控与行为修改技术的中级开发者参考。压缩包共91个文件、约854KB以17个cs源码、15个dll依赖库、6个exe可执行程序及若干config、pdb、resx等配置与资源文件为主并附sln解决方案与snk签名密钥便于直接打开工程对照调试。资源已有917人学习下载包含本地钩子创建、委托回调、ThreadACL设置及dll数字签名等关键环节的完整示例读者可据此理解钩子注入流程、排查签名导致的注入失败问题并在此基础上改造出符合自身需求的拦截逻辑。1. 从一次“改不动”的调试说起C# EasyHook 使用 Demo 能解决什么接手一个老工控上位机时最让人头疼的不是业务逻辑而是某个第三方 DLL 里写死的一段校验——日志打不出来参数改不了出了问题只能靠猜。想动它源码没有想绕开它调用链又埋得太深。这种时候进程内 Hook 就是那把手术刀而 C# EasyHook 使用 Demo 正是把“怎么下刀”这件事讲明白的一份可运行样例。它面向的是需要在 .NET 进程里拦截、观察甚至改写目标函数行为的开发者可能是做协议分析、做兼容层、做行为监控也可能只是想在别人的黑匣子里装一盏灯。这份 Demo 的价值不在于教你 Hook 的原理而在于把注入、托管回调、原始函数转发这条链路用最小可跑的代码串起来让你照着改就能落到自己的目标进程上。2. 架构与运行链路注入器、托管回调与原始函数转发EasyHook 的核心模型并不复杂但第一次接触时容易被“谁在谁的进程里跑”绕晕。它本质上是把一段托管代码注入到目标进程在目标进程内建立一个 Hook 通道当被拦截的函数被调用时控制权先交给你的托管回调回调里可以选择放行、修改参数、修改返回值或者调用原始函数。理解这条链路后面看代码才不会迷路。2.1 三个角色宿主进程、注入器、目标进程整个 Demo 里其实有三个角色。宿主进程是你启动的那个控制台或 WinForms 程序负责加载注入逻辑注入器是 EasyHook 提供的RemoteHooking相关 API负责把托管程序集送进目标进程目标进程是被 Hook 的那个进程可能是你自己写的一个测试程序也可能是任意一个正在运行的 .NET 或原生进程。常见做法是宿主进程先枚举或按 PID 找到目标进程然后调用注入接口把包含 Hook 逻辑的托管 DLL 路径和通道名传进去。目标进程被注入后会在内部加载这段托管代码执行你写的Run或OnInject入口在那里完成实际的 Hook 安装。这个“两段式”结构是理解 EasyHook 的关键——你的 Hook 代码不是跑在宿主里而是跑在目标进程里。// 宿主侧按进程名找到目标并注入 var target Process.GetProcessesByName(TargetApp).FirstOrDefault(); if (target null) { Console.WriteLine(目标进程未启动); return; } // 通道名用于宿主与目标进程内的 Hook 逻辑通信 string channelName hook_channel_ Guid.NewGuid().ToString(N); RemoteHooking.Inject( target.Id, // 目标进程 PID InjectionOptions.Default, // 注入选项默认即可 typeof(HookEntry).Assembly.Location, // 注入的托管程序集 typeof(HookEntry).Assembly.Location, // 32/64 位可分别指定 channelName // 传给目标进程内入口的参数 );这段代码里Inject的第一个参数是目标 PID第二个是注入选项后面两个分别是 32 位和 64 位场景下要加载的程序集路径最后一个参数会原样传到目标进程内的入口函数。参数说明看起来简单但实际踩坑最多的就是程序集路径和位数匹配——路径写错、位数不匹配注入会直接失败而且错误信息往往不够直观。2.2 目标进程内的入口Hook 安装发生在哪里注入成功后目标进程会加载你指定的程序集并调用约定的入口。EasyHook 的托管注入通常要求入口类实现特定接口入口方法里拿到通道名然后创建LocalHook实例。LocalHook是 EasyHook 里最常用的 Hook 类型它针对当前进程内的某个函数地址建立拦截。// 目标进程内注入后执行的入口 public void Run(RemoteHooking.IContext context, string channelName) { // 通过通道与宿主建立通信便于回传日志 _channel RemoteHooking.IpcCreateServerHookChannel( ref channelName, WellKnownObjectMode.SingleCall); // 定位目标函数这里以某个托管方法为例 var targetMethod typeof(TargetClass).GetMethod(DoWork); // 创建 LocalHook回调指向我们的处理函数 _hook LocalHook.Create( targetMethod.MethodHandle.GetFunctionPointer(), new DoWorkDelegate(DoWorkHandler), null); // 激活 Hook只拦截当前线程之外的所有调用 _hook.ThreadACL.SetExclusiveACL(new[] { 0 }); // 通知宿主 Hook 已就绪 _channel.OnReady(); }这里有几个关键点。LocalHook.Create的第一个参数是目标函数的地址托管方法用MethodHandle.GetFunctionPointer()拿原生函数则要用模块基址加偏移。第二个参数是委托签名必须和目标函数一致否则运行时会出问题。第三个参数是回调上下文一般传null即可。SetExclusiveACL用来控制哪些线程不被拦截传0表示当前线程放行避免自己的 Hook 逻辑递归触发。2.3 回调里的三岔路放行、改写、调用原始函数Hook 回调是整个 Demo 里最值得细看的部分。它决定了拦截之后到底做什么。EasyHook 的约定是回调里如果要调用原始函数必须通过HookRuntimeInfo拿到原始委托否则直接调用目标方法会再次进入 Hook形成递归。// Hook 回调拦截目标方法后的处理逻辑 private int DoWorkHandler(IntPtr thisPtr, int input) { try { // 记录入参回传给宿主 _channel.OnLog($DoWork 被调用input{input}); // 修改入参把小于 0 的值纠正为 0 if (input 0) { input 0; } // 调用原始函数拿到真实返回值 var original Marshal.GetDelegateForFunctionPointerDoWorkDelegate( _hook.OriginalDelegate); int result original(thisPtr, input); // 修改返回值在原始结果上加 1便于观察 _channel.OnLog($原始返回 {result}改写为 {result 1}); return result 1; } catch (Exception ex) { _channel.OnLog(Hook 回调异常 ex.Message); return -1; } }这段代码演示了回调里的三种典型操作记录、改参数、改返回值。_hook.OriginalDelegate是 EasyHook 提供的原始函数指针必须通过它来调用原逻辑。异常处理也不能省——Hook 回调里抛出的异常如果没被捕获可能导致目标进程行为异常排查起来非常痛苦。实际项目里我一般会把回调体包在 try/catch 里异常只记录不外抛保证目标进程至少能继续跑。3. 从零跑起来环境准备、注入与验证的完整步骤看懂链路之后下一步是把它跑起来。这份 Demo 的结构不复杂但涉及进程注入对环境和位数比较敏感。下面按“准备—编译—注入—验证”的顺序走一遍每一步都说明失败时该看什么。3.1 环境与项目结构先确认位数和框架版本EasyHook 对 .NET 版本和平台位数有要求。常见做法是宿主和目标进程保持相同的位数32 位对 32 位64 位对 64 位混用基本必失败。项目结构上Demo 通常分成两个工程一个是宿主控制台负责注入另一个是 Hook 逻辑的类库被注入到目标进程。目标进程可以是一个简单的测试程序也可以是你想观察的现成程序。组成作用注意点宿主工程查找目标进程、发起注入、接收日志位数要与目标一致Hook 类库在目标进程内安装 Hook、处理回调需被宿主引用并随注入加载目标进程被拦截的实际程序建议先用自己写的测试程序通信通道宿主与 Hook 逻辑之间回传数据通道名要唯一环境上先确认目标框架版本EasyHook 的托管注入对较新的 .NET 运行时支持有限常见做法是宿主和 Hook 类库都编译为与目标进程匹配的框架版本。如果目标进程是原生程序Hook 类库仍然可以是托管程序集但入口和回调签名要按原生调用约定来写。3.2 编译与注入一条命令背后的参数含义编译时最容易翻车的是平台目标。在项目属性里把平台目标显式设为 x86 或 x64不要用 AnyCPU否则注入时位数判断会出问题。编译完成后宿主程序需要能定位到 Hook 类库的路径常见做法是把两个工程的输出放到同一目录或者用相对路径拼出程序集位置。# 以命令行方式启动宿主传入目标进程名 HostApp.exe --target TargetApp --assembly .\HookLib.dll这条命令只是示意实际 Demo 里可能是硬编码或通过配置文件读取。参数含义很直接--target指定目标进程名--assembly指定要注入的托管程序集路径。失败时先看宿主输出的错误码再确认目标进程是否真的在运行、位数是否匹配、程序集路径是否存在。注入失败的错误信息有时只给一个 HRESULT这时候对照 EasyHook 的文档查一下通常能定位到是权限、位数还是路径问题。3.3 验证 Hook 是否生效日志、断点和返回值三路对照注入成功不等于 Hook 生效。验证时我一般三路对照宿主侧日志有没有收到回调记录、目标进程行为有没有变化、回调里的返回值改写有没有体现。最直接的办法是在目标方法里放一个明显的输入输出比如传入负数看是否被纠正为 0返回结果是否加了 1。// 目标进程里的测试方法用于验证 Hook 是否生效 public static int DoWork(int input) { Console.WriteLine($[Target] DoWork 收到 {input}); return input * 2; }如果 Hook 生效目标进程控制台里仍然会打印原始输入但返回值会变成input * 2 1同时宿主侧能看到回调日志。如果日志有但行为没变检查是不是调用了原始函数后没有使用改写后的返回值如果行为变了但日志没有检查通道是否建立成功。断点方面可以在 Hook 回调里下断点但要注意调试器附加的是宿主还是目标进程附加错了会看不到回调。提示第一次跑建议先用自己写的测试目标进程确认链路通了再换成真实目标。直接对生产进程下手出问题时很难判断是 Hook 逻辑的问题还是目标本身的问题。4. 核心代码拆解通道通信、委托签名与原始函数调用跑通之后真正决定这份 Demo 能不能改成自己项目的是几个核心细节宿主和 Hook 逻辑之间怎么通信、委托签名怎么对齐、原始函数怎么安全调用。这三块拆明白了改起来才有底。4.1 通道通信把目标进程里的日志送回宿主Hook 逻辑跑在目标进程里日志不能直接写宿主的控制台需要通过 EasyHook 的 IPC 通道回传。Demo 里通常定义一个通道接口宿主侧创建客户端目标进程侧创建服务端两边通过通道名配对。// 通道接口定义宿主与 Hook 逻辑之间的通信契约 public interface IHookChannel { void OnLog(string message); void OnReady(); } // 宿主侧创建通道客户端并订阅回调 var channel RemoteHooking.IpcCreateClientIHookChannel(channelName); channel.OnLog msg Console.WriteLine([Hook] msg);通道接口的方法不宜过多回传日志和状态就够了。参数说明上通道名必须在注入时传给目标进程两边一致才能配对。常见坑是通道名带特殊字符或重复使用导致连接失败我一般用 GUID 生成确保唯一。通道回调里不要做耗时操作否则会拖慢目标进程的调用链。4.2 委托签名和目标函数一字不差EasyHook 的 Hook 回调委托签名必须和目标函数完全一致包括参数类型、顺序、返回值甚至调用约定。托管方法相对好办用反射拿到方法信息后照着写原生函数则要按 Win32 的调用约定来常见的是StdCall或Cdecl。// 目标函数签名两个 int 参数返回 int [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int DoWorkDelegate(IntPtr thisPtr, int input);这里的thisPtr是实例方法的隐式 this 指针静态方法则没有这个参数。签名写错时表现往往是目标进程直接崩溃或返回值错乱而不是给出明确的类型错误。血泪经验是先用一个最简单的函数验证签名再逐步换成复杂签名别一上来就 Hook 带结构体参数的方法。4.3 原始函数调用为什么不能直接调目标方法回调里直接调用目标方法会再次触发 Hook形成递归轻则栈溢出重则目标进程卡死。EasyHook 通过OriginalDelegate暴露原始函数指针必须通过它来调用原逻辑。这个指针在 Hook 创建后可用调用方式和普通委托一致。// 通过 OriginalDelegate 调用原始函数避免递归 var original Marshal.GetDelegateForFunctionPointerDoWorkDelegate( _hook.OriginalDelegate); int result original(thisPtr, input);参数说明OriginalDelegate是IntPtr需要用Marshal.GetDelegateForFunctionPointer转成对应委托。转换时的泛型类型必须和 Hook 时用的委托一致否则调用会出错。如果原始函数本身会抛异常回调里要捕获避免异常穿透到目标进程的调用栈。5. 避坑与排查注入失败、递归崩溃与位数不匹配这份 Demo 跑通不难难的是换到真实目标上之后各种莫名其妙的问题。下面几条是我在实际项目里反复遇到的按“现象—原因—解决”记下来省得每次重新查。5.1 注入返回错误码目标进程毫无反应现象宿主调用注入接口后返回一个错误码目标进程没有任何变化也没有日志。原因通常是权限不足、目标进程位数不匹配或者注入的程序集路径不存在。解决先确认宿主以管理员权限运行再核对目标进程位数和 Hook 类库的平台目标是否一致最后检查程序集路径是否是绝对路径且文件存在。错误码可以对照 EasyHook 的枚举值查常见的是权限和位数两类。5.2 Hook 装上后目标进程直接崩溃现象注入成功但目标进程在第一次调用被 Hook 的函数时崩溃。原因多半是委托签名不匹配或者回调里调用了目标方法导致递归。解决先检查委托的参数类型、顺序、调用约定是否和目标函数完全一致再确认回调里调用原始函数用的是OriginalDelegate而不是直接调目标方法。如果签名里有结构体还要注意结构体布局和字符集。5.3 日志有回调记录但目标行为没变化现象宿主侧能看到回调日志说明 Hook 确实触发了但目标进程的实际行为没有改变。原因通常是回调里调用了原始函数后没有使用改写后的返回值或者改的是参数副本而不是实际参数。解决确认回调的返回值被正确返回参数修改在调用原始函数之前完成。对于引用类型参数修改字段和重新赋值是两回事要看清目标函数实际读的是哪个。5.4 调试时断点不命中或者命中在错误的进程现象在 Hook 回调里下断点调试器没有任何反应或者断在了一个不相关的进程里。原因调试器附加的是宿主进程而 Hook 回调跑在目标进程里。解决先启动宿主完成注入再用调试器附加到目标进程或者把 Hook 类库的调试符号一起部署到目标进程能加载的位置。附加错进程是新手最常见的翻车点确认 PID 再附加。5.5 多次注入后通道冲突或资源泄漏现象反复注入同一个目标进程出现通道连接失败或者目标进程句柄数持续上涨。原因通道名重复、Hook 没有正确释放、IPC 服务端没有关闭。解决每次注入用唯一通道名Hook 用完调用Dispose通道服务端在退出前关闭。长期运行的目标进程尤其要注意资源释放否则跑一段时间就会出问题。6. 二次开发改造把 Demo 变成可复用的 Hook 框架Demo 跑通只是起点真正要用起来得把它改造成能配置、能扩展、能验证的小框架。我一般从三个方向下手把目标函数和回调配置化、把通道通信抽象成通用日志通道、把验证流程固化成可重复的测试。6.1 配置化目标函数、回调与参数不再硬编码Demo 里目标函数和回调往往是写死的换一个函数就要改代码。改造思路是把“目标函数定位”和“回调处理”抽成配置项用配置文件描述目标模块、函数名或偏移、回调类型运行时根据配置动态创建 Hook。这样同一套宿主可以服务多个目标不用每次重新编译。// 配置化 Hook 描述目标与回调分离 public class HookSpec { public string ModuleName { get; set; } // 目标模块名 public string FunctionName { get; set; } // 函数名或导出名 public string HandlerType { get; set; } // 回调处理类型 public bool ModifyArgs { get; set; } // 是否改写参数 }参数说明ModuleName用于定位模块基址FunctionName用于取导出地址或方法信息HandlerType决定用哪个回调实现ModifyArgs控制是否改写参数。改造后新增一个 Hook 点只需要加一条配置和对应的处理类不用动注入主流程。6.2 通用日志通道把回传数据标准化通道接口如果每个项目都重新定义复用性很差。我一般把它标准化成“日志 事件 指标”三类方法日志走文本事件走结构化对象指标走数值。这样宿主侧可以统一做过滤、落盘和展示Hook 逻辑侧只负责调用不关心宿主怎么处理。方法用途数据形态Log记录调用与异常字符串Event上报结构化事件键值对Metric上报计数与耗时数值改造时注意通道方法不要设计得太重目标进程里的调用链对延迟敏感回传数据尽量轻量复杂处理放到宿主侧做。6.3 验证方法用测试目标进程固化回归每次改完 Hook 逻辑最怕的是“这次改好了下次又坏了”。我的习惯是保留一个专门的测试目标进程里面放几个签名各异的测试方法覆盖静态方法、实例方法、带结构体参数的方法。每次改造后跑一遍看日志、返回值和目标行为是否都符合预期。这个测试进程不参与生产只用于回归成本很低但能挡住大部分低级错误。// 测试目标覆盖多种签名便于回归验证 public static int Add(int a, int b) a b; public int Multiply(int a, int b) a * b; public static string Concat(string a, string b) a b;验证时重点看三件事Hook 是否触发、原始逻辑是否仍被执行、改写是否按预期生效。三件事都对了再换真实目标。从那以后我每次改 Hook 逻辑都强制先在这个测试目标上走一遍确认没有递归和签名问题再碰真实进程。希望这份拆解能帮到你少走几次注入失败的弯路。本文还有配套的精品资源点击获取