简介一套面向C#开发者的微信PC端消息监听与自动回复源码资源包主要解决PC版微信不具备现成自动化接口的痛点适合研究桌面端Hook钩子、消息截获与应答注入的工程师。资源来自第三方博客付费分享原作者称代码可运行但实际监听效果有限现公开供读者自行研究不保证开箱即用更适合作为调试与二次开发基座。压缩包为rar格式共364个文件、约46.85MB类型以96个dll依赖库、65个xml配置、22个cs源码为主同时包含24个pdb调试符号、17个config应用配置、11个nupkg包引用及少量exe可执行文件整体结构完整可清晰看到项目依赖、通信模块和日志配置。目前已有4072人学习/下载。对于想了解微信PC端HOOK机制、Supersocket网络通信或log4net日志集成的读者该资源提供了完整工程样例可辅助梳理消息监听、规则匹配、自动回复调度的实现路径减少从零搭建的时间成本。1. C# PC 微信消息监听自动回复这个项目到底干了什么写代码写烦了的时候你有没有冒出过一个念头如果电脑微信能把我那些重复的回复自动发出去我是不是就能专心写 bug 了。这就是 C# PC 版微信消息监听自动回复项目最直接的动机通过 HOOK 微信 PC 客户端内部函数把收到的消息实时捞出来再根据预设规则自动回复。它不是简单的按键模拟而是直接调用微信进程内的接收和发送逻辑所以响应速度是毫秒级而且能做到「看起来像真人手动回」的效果。这个项目适合两类人。一类是做账号自动化测试的开发者需要批量验证「收到某个关键词后系统该给什么反馈」微信只是它的消息通道。另一类是研究 Windows 进程注入和 API Hook 的开发者正好拿微信当练手对象C# 负责界面和规则C 负责和微信进程打交道边界特别清晰。如果你完全没接触过 Hook读完后至少能分清「找到函数地址」和「改函数执行流」这俩环节到底差在哪。2. 技术选型与总体架构三条路线和一条完整消息链路2.1 为什么最终选了 HOOK 而不是模拟键鼠做微信 PC 消息监听常走的路线有三条各有各的坑我整理对比一下。路线实时性维护成本风险开发难度键鼠模拟 屏幕 OCR秒级有延时界面一变就得改坐标低低监测微信数据目录下的本地数据库文件接近实时要处理 SQLite 锁和增量读取中中HOOK 微信进程内部函数毫秒级微信一升级偏移就变有账号风控风险高键鼠模拟最容易被系统判定为「非人类操作」坐标位置一改就翻车。监测本地数据库能拿到完整历史消息但拿不到「正在被窗口弹出」这个实时事件而且想回复还得回到模拟键鼠那套等于两头都要写。HOOK 方案上手难度最高但收益也最明显消息回调里就能直接拿到来源 ID 和内容回复时调用微信自己的发送函数链路最短速度最快。我一般会直接选 HOOK理由很朴素这个项目是「监听 自动回复」两个动作都在微信进程内完成与其在外面绕圈不如直接进进程里面拿数据、发消息。2.2 总体架构C# 主控 C Hook DLL 双进程协作整个项目拆成两个进程分工很清晰。一是 C# 写的主控程序我用的 WPFWinForms 也一样负责配置管理、规则引擎、日志展示、命名管道服务端二是 C 写的 HookDLL负责特征码定位、Inline Hook、解析消息结构体、调用微信发送函数以及命名管道客户端。C# 主控和 C DLL 之间通过命名管道通信消息体用一个简单结构体封装。关键点是C# 不能直接进微信进程所有危险操作全部下沉到 C DLL 里。消息链路大概是这样的微信收到新消息 → HookDLL 里的回调函数被触发 → 从消息结构体里取出来源ID、消息内容、消息类型、是否群消息 → 拼成结构体写入命名管道 → C# 主控收到事件 → 规则引擎匹配关键词/正则 → 命中后调用 HookDLL 导出的发送函数 → 微信发送消息接口被调用 → 消息发出目录结构我参考的是最常见的跨语言工程布局WeChatHooker/ ├── WeChatHooker.csproj # C# 主控工程WPF │ ├── MainWindow.xaml # 界面开关、日志、配置区 │ ├── PipeServer.cs # 命名管道服务端收 DLL 上报的消息 │ ├── RuleEngine.cs # 关键词/正则匹配规则引擎 │ └── ConfigHelper.cs # config.json 读写 ├── HookDll/ # C 注入用 DLL 工程 │ ├── dllmain.cpp # DllMain 里做 Hook 安装/卸载 │ ├── scanner.cpp # 特征码扫描模块 │ ├── hook.cpp # Inline Hook 安装与回调 │ ├── pipe.cpp # 命名管道客户端 │ └── send.cpp # 调用微信发送函数的封装 ├── Injector/ # C# 注入器也可合并进主控 │ ├── Injector.cs # OpenProcess VirtualAllocEx CreateRemoteThread │ └── ProcessHelper.cs # 按进程名找微信 PID、判断位数 └── config.json # 回复规则与黑白名单为什么敢让 C# 和 C 这么拆因为 DLL 注入这个行为只能由系统 API 触发C# 的 P/Invoke 完全够用但 Hook 回调里的代码必须保持最精简不能做任何可能阻塞的调用所以回调里只做「取字段 推管道」规则判断全部留给 C# 主控进程。2.3 消息结构体与配置约定HookDLL 回调里微信传进来的参数是一个指向内部消息结构体的指针。这个结构体不同版本都不一样我不会把它定义成固定结构而是用「基址 偏移」的方式读取每个偏移量都在代码里写清楚参照的版本。真正的代码长这样// dllmain.cpp 中收到消息结构体指针后的解析逻辑参考某版本的微信 3.x struct WxRawMsg { // 这里不是完整结构体只是按偏移取值 BYTE data[0x300]; }; void OnRecvMsg(WxRawMsg* msg) { // 偏移量都是实测出来的随微信版本变化 wchar_t* wxid *(wchar_t**)(msg-data 0x28); // 发信人/群 ID wchar_t* content *(wchar_t**)(msg-data 0x50); // 消息内容 int msgType *(int*)(msg-data 0x30); // 1文本 3图片 49文件等 int isGroup *(int*)(msg-data 0x38); // 是否群消息 MsgInfo info; info.fromWxid wxid; info.content content; info.type msgType; info.isGroup isGroup; PipePush(info); // 推进命名管道不阻塞微信自身流程 }偏移值 0x28、0x50、0x30 这类数字在二次开发时是你最需要关注的参数不同版本的微信同一个字段的位置能差出几十个字节。我会把偏移定义成宏或常量集中放在一个头文件里方便某次微信更新后统一调整。C# 主控那边的规则配置用一个 JSON 文件维护这样改回复语不用重新编译{ myWxid: wxid_xxxx, enable: true, defaultReply: 收到在忙稍后回复你。, rules: [ { pattern: (你好|在吗|在), reply: [自动回复] 你好我正在忙留言看到后马上回。, matchType: regex }, { pattern: 报价|价格|多少钱, reply: [自动回复] 价格表已经发你邮箱了查收一下。, matchType: contains } ], blacklist: [wxid_zhangshan, filehelper], groupWhiteList: [工作群, 项目群] }myWxid用来在规则引擎里过滤自己发的消息否则你自动回复出去的消息也会被 Hook 重新捞回来形成死循环。blacklist是硬过滤比规则优先。groupWhiteList允许你只在指定群里自动回复避免在工作大群里发自动回复造成事故。3. 核心实现特征码定位、进程注入与 Inline Hook3.1 特征码扫描定位微信接收消息函数微信每次启动模块基址可能因为 ASLR 而变化但函数机器码是固定的。所以第一步不是找地址而是找「这一串字节在不在内存里」。这个扫描逻辑写在 scanner.cpp 里BYTE pattern[] { 0x48, 0x8B, 0xC4, 0x48, 0x89, 0x58, 0x20, 0x57 }; // mov rax,rsp; mov [rax-0x60],rbx; push rdi BYTE mask[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; // 全匹配 uintptr_t ScanPattern(HANDLE hProcess, uintptr_t startAddr, size_t length, BYTE* pattern, BYTE* mask, size_t patternSize) { BYTE* buffer new BYTE[length]; SIZE_T bytesRead 0; ReadProcessMemory(hProcess, (LPCVOID)startAddr, buffer, length, bytesRead); for (size_t i 0; i bytesRead - patternSize; i) { bool match true; for (size_t j 0; j patternSize; j) { if (mask[j] 0xFF buffer[i j] ! pattern[j]) { match false; break; } } if (match) { delete[] buffer; return startAddr i; } } delete[] buffer; return 0; }这段代码的逻辑是先从微信进程某个内存区域读出一块连续字节然后在里面逐字节比对。mask数组是匹配掩码0xFF表示这一字节必须严格相等0x00表示这一字节忽略这样做是因为有些指令里有立即数不同版本那几位会变但特征结构不变。扫描范围一般取微信主模块的.text段用 VirtualQueryEx 遍历内存区域时过滤掉MEM_IMAGE且Protect包含可执行属性的页。这里有个常见的坑ReadProcessMemory 一次读太多会失败我一般习惯一次读 64KB配合 VirtualQueryEx 循环扫。特征码数组本身用 CECheat Engine或 IDA 从不同版本的微信里提取如果你自己要从头找建议先找一个「字符串引用」下断点消息内容的内存会经过接收函数顺着引用往上就是函数头。3.2 进程注入把 HookDLL 送进微信进程找到接收函数地址以后还需要让 HookDLL 在微信进程里跑起来。最经典的方式是 CreateRemoteThread 加 LoadLibraryC# 注入器代码[DllImport(kernel32.dll, SetLastError true)] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid); [DllImport(kernel32.dll, SetLastErrortrue)] static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint allocType, uint protect); [DllImport(kernel32.dll, SetLastErrortrue)] static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] buffer, uint size, out IntPtr bytesWritten); [DllImport(kernel32.dll, SetLastErrortrue)] static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttr, uint stackSize, IntPtr startAddr, IntPtr param, uint flags, out uint threadId); public bool Inject(uint pid, string dllPath) { IntPtr hProcess OpenProcess(0x1F0FFF, false, pid); // PROCESS_ALL_ACCESS IntPtr dllPathAddr VirtualAllocEx(hProcess, IntPtr.Zero, (uint)(dllPath.Length 1) * 2, 0x3000 /*MEM_COMMIT*/, 0x04 /*PAGE_READWRITE*/); byte[] pathBytes Encoding.Unicode.GetBytes(dllPath); WriteProcessMemory(hProcess, dllPathAddr, pathBytes, (uint)pathBytes.Length, out _); IntPtr loadLibAddr GetProcAddress(GetModuleHandle(kernel32.dll), LoadLibraryW); CreateRemoteThread(hProcess, IntPtr.Zero, 0, loadLibAddr, dllPathAddr, 0, out _); return true; }这段代码做的事情很直白在微信进程里申请一块内存把 DLL 的全路径字符串写进去然后远程线程以 LoadLibraryW 为入口点参数就是我们写的路径字符串。LoadLibraryW 会触发 DLL 的 DllMain我们在 DllMain 的DLL_PROCESS_ATTACH分支里做 Hook 安装。一个容易忽略的参数是OpenProcess的权限位0x1F0FFF是 PROCESS_ALL_ACCESS一般调试工具都这么开。如果目标进程是管理员权限启动的那么注入器本身也需要以管理员权限运行否则 OpenProcess 会报拒绝访问这是排查注入失败时首先要确认的一件事。3.3 Inline Hook接管函数执行流HookDLL 被加载后接下来要改接收函数的入口字节让它先跳到我自己的回调处理完再跳回去。这就是 Inline Hook也叫垫片。64 位下我用的跳转方式是FF 25 00 00 00 00加 8 字节目标地址一共 12 字节typedef struct _HookEntry { BYTE originalBytes[12]; // 保存原函数前 12 字节 BYTE jmpCode[12]; // FF 25 00 00 00 00 目标地址 BYTE* targetFunc; // 被 Hook 的函数入口 } HookEntry; bool InstallInlineHook(BYTE* targetFunc, BYTE* hookFunc, HookEntry* entry) { entry-targetFunc targetFunc; memcpy(entry-originalBytes, targetFunc, 12); // 写跳转指令FF 25 00 00 00 00 8字节hookFunc地址 entry-jmpCode[0] 0xFF; entry-jmpCode[1] 0x25; for (int i 2; i 6; i) entry-jmpCode[i] 0x00; *(uint64_t*)(entry-jmpCode 6) (uint64_t)hookFunc; DWORD oldProtect; VirtualProtect(targetFunc, 12, PAGE_EXECUTE_READWRITE, oldProtect); memcpy(targetFunc, entry-jmpCode, 12); VirtualProtect(targetFunc, 12, oldProtect, oldProtect); return true; }里面有个关键点跳转指令FF 25 00 00 00 00是「间接绝对跳转」处理器会把RIP 0位置处的 8 字节作为目标地址所以指令后面的 8 个字节放的必须是 hookFunc 的实际内存地址。VirtualProtect把函数头所在页改成可写不然直接 memcpy 会触发访问保护异常。被覆盖的 12 字节原指令必须在回调函数里用memcpy拼回去再手动调用targetFunc这就是所谓的「原始指令恢复」。如果原函数开头不是 12 字节对齐的指令边界或者本身就是一个jmp那你保存和恢复的字节可能截断一条指令微信收到消息时大概率直接崩。这是 HOOK 项目最常见、也最让人抓狂的翻车点。回调函数里要注意的是不能再调用会阻塞的东西也不能再触发同一接收函数否则会递归爆炸。我习惯的做法是标准回调原函数把原始参数透传过去LRESULT HookRecv(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { OnRecvMsg((WxRawMsg*)lp); // 解析并推管道 // 恢复原始字节后继续执行原函数 return ((RecvMsgFunc)originalRecvFunc)(hWnd, msg, wp, lp); }Hook 卸载时要先把 12 字节原始指令拷贝回去再 FreeLibrary顺序反了也会崩。3.4 规则引擎与发送回包让回复真正发出去C# 那边收到管道消息后先判断来源是不是我自己然后开始跑规则引擎public string MatchReply(MsgInfo msg) { if (blacklist.Contains(msg.FromWxid)) return null; if (msg.IsGroup !groupWhiteList.Contains(msg.RoomName)) return null; foreach (var rule in rules) { if (rule.MatchType regex Regex.IsMatch(msg.Content, rule.Pattern)) return rule.Reply; if (rule.MatchType contains msg.Content.Contains(rule.Pattern)) return rule.Reply; } return defaultReply; // 没有命中规则时的兜底回复 }参数说明msg.FromWxid可能是个人 ID也可能是群 ID群消息时微信传进来的是群 ID同时会有 RoomName 字段所以群白名单判断用的是RoomName。规则的匹配顺序是从上到下如果你配置了「你好」和「你好啊」两个规则把更具体的放在前面否则短关键词会先命中。回复动作通过 P/Invoke 调 HookDLL 的导出函数完成[DllImport(HookDll.dll, CharSet CharSet.Unicode)] static extern bool SendByHook(string wxid, string text); SendByHook(wxid_target, 收到自动回复);C 侧的实现对应地调用微信的发送函数地址extern C __declspec(dllexport) BOOL __stdcall SendByHook(wchar_t* wxid, wchar_t* text) { typedef int(*WxSendFunc)(void*, wchar_t*, wchar_t*); // 微信内部发送函数 WxSendFunc sendFunc (WxSendFunc)sendFuncAddr; // 特征码扫描得到 MyWxSession* session GetWxSession(); // 微信进程的当前会话对象 return sendFunc(session, wxid, text) 1; }这一步的本质是「借用微信自己的手来打字」所以发出的消息不会像模拟键鼠那样被输入法干扰也不受聊天窗口是否聚焦的影响这是 HOOK 方案最大的价值点。4. 避坑与排查版本偏移、注入崩溃与账号安全4.1 微信秒退Inline Hook 恢复不完整现象注入之后微信收到第一条消息直接闪退或者注入瞬间就崩。 原因90% 是保存原始指令时长度不够或者跨了指令边界。原函数入口的 12 字节里如果有一条指令是不完整的跳转进去之后执行流直接走到无效内存。剩下的 10% 是回调函数里解引用空指针比如消息结构体偏移取错了从非法内存地址读了 4 个字节就崩。 解决写一个VerifyOriginalBytes工具函数Hook 安装前把 12 字节 print 出来跟 IDA 里的字节比对一遍确认指令边界完整。另外回调里对每个指针取值之前先做IsBadReadPtr检查宁可读不到也别直接崩。4.2 收到乱码或空消息偏移量不对现象Hook 成功了管道里能收到事件但 content 字段是乱的或者长度对不上。 原因消息结构体里字符串大多数情况下是wchar_t*指针我见过有版本直接用宽字符数组内联存放偏移和长度都要跟着变。引用 0x28 偏移取出来的是一个地址但这个地址指向的可能是 UTF-16 也可能是 UTF-8得看微信编译时的字符集设置。 解决不要猜偏移用 CE 打开微信进程随便找一个已知联系人给这个号发一条内容独特的消息然后在内存里搜这条消息的 Unicode 字符串搜到后往前追引用最终定位到消息结构体的准确偏移。把这个偏移写进一个offsets.h头文件并注明版本号每次微信升级后重新测一遍。4.3 命名管道连不上启动顺序和重试机制导致的玄学现象C# 主控先启动了注入也成功了但日志里一直没消息进来管道客户端连不上。 原因HookDLL 是在微信进程里被动加载的DLL 里写连接管道的时候如果 C# 主控还没开始 Listen客户端会直接失败返回。这是我这个项目里最典型的时序玄学你得在 DLL 里做重试而不是只在主控里做。 解决管道的服务端先BeginWaitForConnectionDLL 里的连接线程做 10 次重试每次间隔 200ms。同时 C# 侧在收到连接请求后立即读数据不要在界面上做耗时的控件刷新阻塞了管道线程。4.4 自动回复死循环把自己回复的消息又当成新消息现象用两个微信号互发测试A 号收到自动回复后B 号的 Hook 又把这句自动回复当成新消息两边互相回个没完。 原因没有在规则引擎入口过滤掉自己发出的消息。注意这里不是过滤 A 号而是过滤 B 号自己当时登录的微信号。 解决主控启动时把这个微信号的 wxid 注入到配置里MatchReply里第一行判断msg.FromWxid myWxid直接返回 null。另外在 HookDLL 里也可以加一层当回调里的消息方向是「发出」时直接跳过两种过滤叠加更保险。4.5 账号风控与安全边界现象连续高频自动回复后微信号被限制登录或触发安全提示。 原因虽然调的是微信内部函数但发送频率和人工操作明显不一样风控系统能识别出这类自动化行为特征。 解决没必要硬扛风控这是平台规则问题。我的习惯是只在测试小号上跑自动回复重要账号一律不碰同一账号每条消息之间至少加 1 到 2 秒随机延迟替代直接回复日志里只保留消息类型和关键词命中情况不落原始内容降低数据本身的风险。项目的代码不要包含任何「监听他人设备」的设计只处理自己设备自己账号收到的消息。5. 二次开发把监听工具改成通用消息中间件拿到这个项目后「自动回复」只是第一个也是最简单的一个落点。真正有意思的玩法是把消息出口做成可插拔的让微信变成你个人自动化系统的输入源。我建议按下面三步去改造。第一步把管道协议从「文本回复」改成「统一消息事件」。现在管道里传的是 MsgInfo里面只有来源 ID、内容、类型。改造方向是给每个消息加一个EventType字段区分INCOME_MSG、FRIEND_REQUEST、TRANSFER_MSG、SYSTEM_NOTIFY。这样规则引擎就不再只是「匹配关键词返回文本」而是「收到事件 → 走处理器链」。比如收到转账事件可以先记日志再决定回什么收到好友申请自动走一遍审批逻辑。第二步把回复策略从代码里抽到配置层之外允许注册外部 Webhook。我给项目加过一个很简单的扩展规则命中后除了本地回复还可以 POST 一个 JSON 到自建服务由那个服务决定最终要不要回、回什么。这样回复策略可以做到免重新编译热更新。你不需要内置什么框架用HttpClient就够了规则里加一个webhook字段即可。第三步给版本偏移加适配层。微信一升级偏移表就废了这是这个项目最大的维护成本。我后面会把所有偏移和一个特征码表一起维护到offsets.json并给每个版本做标记HookDLL 启动时先读这个文件的版本号匹配不上就直接拒绝 Hook 而不是猜测偏移避免冒险操作导致的崩溃。配合这个机制你还可以写一个小工具版本更新后自动抓一次特征码并生成新的大量文件。另外验证自动回复逻辑有没有写对我建议加一个「试运行模式」在这个模式下规则引擎照常跑但发送函数被替换成日志输出。先在群里发一条测试消息看日志里是否出现了「命中规则xx预期回复yy」确认无误后再切到真实发送。这个模式在调规则时结合前面提到的高频卡顿问题能省很大一部分的时间。这个项目里我栽过最多的跟头就是改完代码以为没事直接在真微信号上开跑结果发错了人。从那以后我每次改完都要强制走一遍「三个一」试运行模式跑一遍模拟消息、确认管道日志里没有异常、先拿小号做一次真实收发。每次都这样过一遍之后我才敢把工具交出去。这套流程也顺带帮我发现了好几处偏移写错的问题省下的沟通成本远远大于做试运行模式那点时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?