简介:这是一款基于Hook机制实现的微信机器人项目,面向具备一定Python基础、希望快速搭建微信自动化工具的开发者与爱好者。项目历经一年迭代,功能更贴近日常使用场景,支持AI对话(GPT、星火、千帆)、关键词拉人进群、自动群发、入群欢迎等,并会不定期维护,有代码能力者可自行二次开发。资源包共76个文件,以34个png截图、12个py源码、9个pyc编译文件为主,另含xml配置、yaml参数文件及少量jpg、gif素材,整体约4.93MB,目录按Bot_Server、Db_Server、Api_Server、Push_Server、Config等模块划分,结构清晰,便于理解消息接收、分发与推送的完整链路。目前已有846人学习下载。借助该资源,读者可获取可运行的机器人源码、配置示例与模块化目录参考,快速上手微信Hook开发,并在此基础上扩展AI接入与群管理能力。
1. 基于HOOK机制的微信机器人:为什么它比模拟点击更稳,也更难写
做微信自动化的人,大多经历过一个阶段:用模拟点击、找图定位、坐标偏移去操作界面。窗口一挪、分辨率一变、版本一更新,脚本当场翻车。基于HOOK机制的微信机器人走的是另一条路——不去“看”界面,而是直接拦截微信进程内部的函数调用,在消息到达界面之前就把它截下来,在发送动作发生之前就把它改掉。它解决的核心问题是稳定性与实时性:消息不用轮询、不靠截图识别,响应从秒级压到毫秒级,界面被遮挡、最小化都不影响。
这套方案适合谁?适合已经会一门语言、懂基本进程与内存概念、想把手里的自动化从“玩具”做成“工具”的开发者。它不适合零基础直接上手,因为HOOK涉及注入、地址偏移、调用约定这些硬骨头。但只要你愿意啃,它带来的可控性是模拟点击给不了的。下面我按“先立住原理、再动手复现、最后讲坑”的顺序,把这条路讲清楚。
2. HOOK机制到底拦了什么:从消息接收到发送的完整链路
2.1 微信PC端消息流的三个关键节点
要HOOK,先得知道拦哪里。微信PC端处理一条消息,大致经过三个节点:网络层收到数据包并解密、业务层把数据组装成消息对象、UI层把消息渲染到会话列表。模拟点击是在UI层之后做文章,所以它慢且脆。HOOK的甜点区在业务层——消息对象已经成型、但还没进UI的那一刻。
常见做法是盯住接收消息的回调函数。这个函数通常有一个参数指向消息结构体,结构体里含消息类型、发送者、内容、时间戳等字段。你在这个回调里插一脚,就能拿到原始消息。发送侧同理,找到发送函数,在它真正把数据交给网络层之前改参数,就能实现自动回复、内容替换。
这里有个反直觉的点:不是所有消息都走同一个回调。文本、图片、文件、系统消息可能分属不同处理分支。新手常犯的错是只HOOK了文本回调,然后疑惑为什么图片消息收不到。正确姿势是先定位消息分发的主函数,再顺着分支往下找。
2.2 注入方式选型:远程线程与APC的取舍
HOOK的前提是把你的代码送进微信进程。常见注入方式有两种:远程线程注入和APC注入。远程线程注入用CreateRemoteThread在目标进程里起一个线程执行LoadLibrary,加载你的DLL。它通用、好调试,但容易被安全软件盯上,因为“在别人进程里创建线程”这个行为本身就很显眼。
APC注入把回调排进目标进程某个线程的APC队列,等该线程进入可警告等待状态时执行。它相对隐蔽,但依赖目标线程的状态,时机不好控制,调试也麻烦。我一般先用远程线程把逻辑跑通,确认HOOK点找对了,再考虑换APC做优化。选型不是越隐蔽越好,而是先能跑、再求稳。
// 远程线程注入的最小骨架(仅示意流程) #include <windows.h> BOOL InjectDll(DWORD pid, const char* dllPath) { HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (!hProc) return FALSE; // 1. 在目标进程申请内存,写入DLL路径 SIZE_T len = strlen(dllPath) + 1; LPVOID remoteMem = VirtualAllocEx(hProc, NULL, len, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProc, remoteMem, dllPath, len, NULL); // 2. 拿到LoadLibraryA地址(同系统下模块基址一致) LPTHREAD_START_ROUTINE loadAddr = (LPTHREAD_START_ROUTINE)GetProcAddress( GetModuleHandleA("kernel32.dll"), "LoadLibraryA"); // 3. 创建远程线程执行加载 HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, loadAddr, remoteMem, 0, NULL); WaitForSingleObject(hThread, INFINITE); // 4. 清理 VirtualFreeEx(hProc, remoteMem, 0, MEM_RELEASE); CloseHandle(hThread); CloseHandle(hProc); return TRUE; }逻辑说明:OpenProcess拿句柄,VirtualAllocEx在目标进程开内存,WriteProcessMemory写路径,CreateRemoteThread让目标进程自己调LoadLibraryA加载你的DLL。参数上,PROCESS_ALL_ACCESS权限最大但最招摇,实际可降到PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE。WaitForSingleObject的INFINITE是为了确保加载完成再清理,否则可能内存释放了DLL还没读完路径。
2.3 找函数地址:特征码定位比硬编码偏移靠谱
微信每次更新,函数地址都会变。硬编码偏移的脚本活不过一个版本。常见做法是用特征码(signature)扫描:取目标函数开头一段字节序列,在模块内存里搜这段特征,命中位置就是函数地址。特征码要选那些不随编译优化大幅变动的指令序列,通常取函数序言加几条稳定指令。
// 特征码扫描示意:在模块范围内找匹配字节 uintptr_t ScanSignature(uintptr_t base, size_t size, const char* pattern, const char* mask) { for (uintptr_t i = 0; i < size; i++) { bool found = true; for (size_t j = 0; pattern[j]; j++) { // mask中'x'表示精确匹配,'?'表示通配 if (mask[j] == 'x' && *(char*)(base + i + j) != pattern[j]) { found = false; break; } } if (found) return base + i; } return 0; }逻辑说明:pattern是特征字节,mask标记哪些字节必须精确匹配、哪些可以通配。通配符用来吸收那些因编译或版本变化而不同的字节。参数上,base和size来自模块信息(用GetModuleInformation拿),pattern和mask需要你从目标版本里手动提取。提取方法是用调试器打开微信,找到目标函数,复制开头十几到几十字节,把易变部分标成?。
注意:特征码越短越容易误匹配,越长越容易因版本更新失效。经验值是12到24字节,且尽量包含call或jmp这类结构指令。
3. 从零跑通一个最小HOOK:接收消息并打印
3.1 环境准备与调试器附加
你需要一台Windows机器、Visual Studio(或MinGW)、一个调试器(x64dbg或WinDbg)、以及目标微信PC端。先把微信登录好,用调试器附加到微信进程。附加后,在符号里找模块基址,记下来。这一步的目的是让你能实时看内存、下断点、验证特征码。
调试器附加有个坑:微信可能有反调试检测。如果附加后微信卡死或退出,先检查是不是触发了检测。常见规避手段是附加时暂停所有线程、或者用插件隐藏调试器。但我不建议一上来就对抗,先把调试器附加到自己的测试进程上练手,确认HOOK逻辑没问题,再上真实目标。
3.2 定位接收回调并下断点验证
在调试器里,搜索字符串是定位回调的捷径。比如搜消息内容里出现的某个关键词,找到引用它的代码,往上追调用栈,往往能追到消息处理函数。另一种方法是从网络接收函数往下追,但链路长。我一般先用字符串交叉引用快速定位,再用调用栈确认。
找到疑似回调后,下断点,然后用另一个账号给目标发一条消息。如果断点命中,看栈上和寄存器里的参数,确认哪个寄存器指向消息结构体。x64下前四个参数走RCX、RDX、R8、R9,x86走栈。确认后,把函数地址和结构体偏移记下来。
3.3 写一个打印消息的HOOK
确认回调地址后,用Detours或MinHook这类库做inline hook。下面用MinHook示意。
#include "MinHook.h" // 原始函数指针,类型要和目标函数一致 typedef void (*RecvMsg_t)(void* msgObj); RecvMsg_t OriginalRecvMsg = nullptr; // 我们的替换函数 void HookedRecvMsg(void* msgObj) { // 从消息结构体里按偏移取内容 // 偏移需根据实际版本调整 char* content = *(char**)((char*)msgObj + 0x30); if (content) { printf("[recv] %s\n", content); } // 调用原始函数,保证微信正常逻辑 OriginalRecvMsg(msgObj); } void InstallHook(uintptr_t targetAddr) { MH_Initialize(); MH_CreateHook((LPVOID)targetAddr, (LPVOID)HookedRecvMsg, (LPVOID*)&OriginalRecvMsg); MH_EnableHook((LPVOID)targetAddr); }逻辑说明:MH_CreateHook把目标地址的指令改成跳转到HookedRecvMsg,同时把原始指令保存到OriginalRecvMsg。HookedRecvMsg里先做自己的事(打印),再调原始函数,保证不破坏微信原有流程。参数上,msgObj是消息对象指针,0x30是内容字段的偏移,这个值因版本而异,必须自己用调试器确认。targetAddr来自特征码扫描结果。
注意:偏移写错不会报错,只会打印出乱码或崩溃。确认偏移的方法是在调试器里看msgObj指向的内存,逐字段比对。
4. 发送消息与自动回复:把HOOK用起来
4.1 定位发送函数与参数构造
发送比接收难,因为你要构造合法的参数。发送函数通常接收会话ID、消息内容、消息类型等。定位方法和接收类似:搜发送相关字符串,追调用栈。找到后,看它参数怎么传。常见做法是HOOK发送函数,在它执行前替换内容参数,实现自动回复。
但更稳的做法是不HOOK发送,而是直接调用发送函数。你拿到函数地址后,自己构造参数调它。这样不依赖微信内部的触发时机,可控性更高。代价是参数构造要对,会话ID、消息类型这些字段错一个就发不出去。
4.2 自动回复的最小闭环
把接收HOOK和发送调用接起来,就是一个自动回复机器人。接收HOOK拿到消息内容和发送者,判断是否命中规则,命中就调发送函数回一条。
void HookedRecvMsg(void* msgObj) { char* content = *(char**)((char*)msgObj + 0x30); char* sender = *(char**)((char*)msgObj + 0x40); if (content && strcmp(content, "ping") == 0) { // 调用发送函数,参数按实际签名调整 SendMsg(sender, "pong", 1); } OriginalRecvMsg(msgObj); }逻辑说明:content和sender按偏移取出,判断内容后调SendMsg。SendMsg是你封装好的发送函数,内部调微信的发送地址。参数上,sender是会话标识,1代表文本类型。这里的关键是sender的格式——它可能是内部ID而非昵称,需要你从消息结构体里正确取。
4.3 参数表:几个必须确认的偏移
| 字段 | 常见偏移 | 说明 |
|---|---|---|
| 消息内容 | 0x30 | 字符串指针,因版本变动 |
| 发送者ID | 0x40 | 会话标识,非昵称 |
| 消息类型 | 0x18 | 1文本、3图片等 |
| 时间戳 | 0x50 | Unix时间 |
这张表只是起点,不是标准答案。每个版本都要重新确认。确认方法:在调试器里看消息对象内存,对照已知消息逐字段验证。
5. 避坑与排查:那些让我熬夜的翻车现场
5.1 注入后微信闪退
现象:注入成功,但微信几秒内崩溃。原因通常是HOOK点选错,或者替换函数破坏了栈平衡。x86下cdecl和stdcall的栈清理责任不同,写错就崩。解决:先用调试器单步跟替换函数,确认返回时栈指针和进入时一致。另一个原因是特征码误匹配,HOOK到了无关函数。
5.2 消息能收不能发
现象:接收正常,发送无反应。原因多是发送函数参数构造错误,尤其是会话ID格式。解决:在调试器里观察微信自己发消息时参数长什么样,照抄。别自己猜格式。
5.3 版本更新后全部失效
现象:微信一升级,特征码扫不到,偏移全乱。原因:编译变化导致指令序列和结构体布局改变。解决:把特征码和偏移做成配置,更新时只改配置不改代码。同时准备一套自动化提取脚本,减少手工。
5.4 多线程下的竞态
现象:偶发崩溃或消息错乱。原因:接收回调可能在多线程被调,你的全局状态没加锁。解决:共享数据加临界区,或者用线程局部存储。别假设回调是单线程的。
5.5 被安全软件拦截
现象:注入被拦,DLL加载失败。原因:注入行为触发防护。解决:换注入方式、降低权限、或者在自己的测试环境先跑通逻辑。这里不展开对抗细节,重点是先确认逻辑正确,再考虑环境适配。
6. 进阶:把HOOK机器人做成可维护的工具
走到这里,你已经能跑通最小闭环。但要做成能长期用的工具,还得解决几个问题。第一是配置化:把特征码、偏移、规则都抽到配置文件,更新时只改配置。第二是日志:每次HOOK命中、每次发送都记日志,出问题能回溯。第三是降级:HOOK失败时自动退回模拟点击,保证基本可用。
验证HOOK是否生效,我习惯用一个自检函数:启动时扫描特征码,命中就打印地址,扫不到就告警。这样每次微信更新,第一时间知道哪里断了。
# 配置化示例:把偏移和特征码放JSON import json config = json.load(open("hook_config.json")) recv_sig = config["recv_signature"] content_offset = config["offsets"]["content"] # 启动时校验,扫不到就告警 if not scan(recv_sig): print("[warn] signature not found, check version")逻辑说明:配置和代码分离,更新只改JSON。scan是特征码扫描函数,返回地址或0。参数上,recv_signature是十六进制字符串,offsets是各字段偏移。这样维护成本大幅下降。
最后说个我自己的教训:别在没确认偏移的情况下批量发消息。我曾经因为偏移错了一位,把消息内容字段读成了会话ID,结果给一堆人发了乱码。从那以后,我坚持先在调试器里逐字段验证,再写业务逻辑。这个习惯帮我省了无数次后悔药。希望帮到你。
本文还有配套的精品资源,点击获取