ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Qt 逆向实战:读取微信 PC 端用户信息的内存分析

Qt 逆向实战:读取微信 PC 端用户信息的内存分析 简介这份资源是一套面向Windows平台逆向分析学习者的实战源码聚焦于通过进程内存读取方式获取微信用户信息适合具备一定C基础、希望了解Windows进程操作与内存结构解析的开发者参考。界面层采用Qt搭建进程枚举与内存读取均调用Windows原生API实现标签涉及逆向分析、反编译与内存结构等方向。压缩包内共6个文件包含2个cpp源文件、1个h头文件、1个ui界面文件、1个pro工程文件及1个user配置整体约6KB体量轻巧但结构完整便于快速导入Qt Creator编译调试。目前已有1518人学习下载说明该案例在逆向入门群体中具有一定参考价值。读者可从中了解Qt与Windows API混合编程的组织方式、进程内存读取的基本流程以及用户信息结构定位思路适合作为逆向分析练手项目或课程设计参考也可在此基础上扩展更多进程操作与内存解析功能。1. 从一条进程内存里的昵称说起Qt 读微信用户信息到底在做什么微信 PC 端登录后你的昵称、微信号、手机号、头像路径这些字段就静静躺在 WeChat.exe 进程的私有内存里。用 Qt 写一个 Windows 逆向分析工具去读它们本质是三件事的串联找到承载数据的模块基址按结构体偏移把字段抠出来再用 Qt 的界面把结果呈现给操作者。它解决的是「不依赖微信自身接口、不修改任何文件就能拿到当前登录账号基础信息」的问题常见于多开管理、客服辅助、账号资产盘点这类内部工具场景。适合有 C 基础、懂一点 Win32 内存 API、又想用 Qt 快速搭界面的工程师。读完你能自己定位偏移、写出可复现的读取代码也知道哪些字段能读、哪些碰不得。2. 定位与读取从模块基址到结构体字段的完整链路2.1 为什么选 Qt 而不是 MFC 或纯 Win32做 Windows 逆向分析工具界面层其实是最不该消耗精力的部分。MFC 那套消息映射和资源编辑器写个小工具还行一旦要加表格、搜索框、实时刷新代码量立刻失控。纯 Win32 更原始光一个 ListView 的虚拟列表就能写掉半天。Qt 的优势在于信号槽天然适合「后台线程读内存、主线程刷界面」这种模式QTableWidget 和 QSortFilterProxyModel 拿来就能做字段展示和过滤跨版本编译也稳定。我一般会选 Qt 5.15.2 MSVC2019 64 位这套组合原因是微信 PC 端主流版本是 64 位进程用 32 位 Qt 去 OpenProcess 会直接失败。这里有个血泪经验很多人装完 Qt 默认用的是 MinGW 套件编译出来的 exe 去读 64 位微信进程OpenProcess 返回 NULL查半天以为是权限问题其实是位数不匹配。所以第一件事就是确认套件是 msvc2019_64。选型上还有一点不要用 Qt 的 QProcess 去调外部工具读内存那样每次读取都要起进程延迟高且容易被安全软件盯上。直接在 Qt 进程内调 Win32 API用 QThread 包一层才是正路。2.2 用 Qt 搭一个能跑的最小读取框架先建一个 Qt Widgets Application工程文件里加上必要的库。核心代码分三块进程查找、内存读取、字段解析。下面是最小可运行版本。// wechatreader.h #ifndef WECHATREADER_H #define WECHATREADER_H #include QObject #include QString #include windows.h #include tlhelp32.h struct WeChatUserInfo { QString nickname; // 昵称 QString wxid; // 微信号 QString phone; // 手机号 QString avatarPath; // 头像本地路径 }; class WeChatReader : public QObject { Q_OBJECT public: explicit WeChatReader(QObject *parent nullptr); bool attach(); // 附加到微信进程 WeChatUserInfo readUserInfo(); // 读取用户信息 bool isAttached() const { return m_hProcess ! nullptr; } private: DWORD findWeChatPid(); // 遍历进程找 WeChat.exe bool readMemory(DWORD64 addr, void *buf, SIZE_T size); DWORD64 findModuleBase(DWORD pid, const wchar_t *moduleName); HANDLE m_hProcess nullptr; DWORD64 m_moduleBase 0; }; #endif // WECHATREADER_H// wechatreader.cpp #include wechatreader.h #include QDebug WeChatReader::WeChatReader(QObject *parent) : QObject(parent) {} DWORD WeChatReader::findWeChatPid() { HANDLE snap CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap INVALID_HANDLE_VALUE) return 0; PROCESSENTRY32W pe; pe.dwSize sizeof(pe); DWORD pid 0; if (Process32FirstW(snap, pe)) { do { if (_wcsicmp(pe.szExeFile, LWeChat.exe) 0) { pid pe.th32ProcessID; break; } } while (Process32NextW(snap, pe)); } CloseHandle(snap); return pid; } DWORD64 WeChatReader::findModuleBase(DWORD pid, const wchar_t *moduleName) { HANDLE snap CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); if (snap INVALID_HANDLE_VALUE) return 0; MODULEENTRY32W me; me.dwSize sizeof(me); DWORD64 base 0; if (Module32FirstW(snap, me)) { do { if (_wcsicmp(me.szModule, moduleName) 0) { base reinterpret_castDWORD64(me.modBaseAddr); break; } } while (Module32NextW(snap, me)); } CloseHandle(snap); return base; } bool WeChatReader::attach() { DWORD pid findWeChatPid(); if (pid 0) { qWarning() WeChat.exe not running; return false; } // 需要管理员权限才能拿到完整读权限 m_hProcess OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid); if (!m_hProcess) { qWarning() OpenProcess failed, error GetLastError(); return false; } m_moduleBase findModuleBase(pid, LWeChatWin.dll); if (m_moduleBase 0) { qWarning() WeChatWin.dll not found; CloseHandle(m_hProcess); m_hProcess nullptr; return false; } return true; } bool WeChatReader::readMemory(DWORD64 addr, void *buf, SIZE_T size) { SIZE_T bytesRead 0; BOOL ok ReadProcessMemory(m_hProcess, reinterpret_castLPCVOID(addr), buf, size, bytesRead); return ok bytesRead size; }这段代码的逻辑链条是findWeChatPid用进程快照遍历所有进程按名字匹配 WeChat.exefindModuleBase用模块快照拿到 WeChatWin.dll 的加载基址因为用户信息相关的全局对象指针通常存在这个模块的数据段里attach把进程句柄和模块基址都准备好readMemory是后续所有字段读取的底座。参数上要注意几个点。OpenProcess的权限只要PROCESS_VM_READ和PROCESS_QUERY_INFORMATION不要贪多去要PROCESS_ALL_ACCESS权限越大越容易被拦截。ReadProcessMemory的返回值必须同时检查ok和bytesRead size因为跨页边界读取时可能只读回一部分只判断ok会拿到脏数据。模块名写WeChatWin.dll是因为微信把核心逻辑都放在这个 DLL 里主 exe 只是个壳。2.3 偏移怎么找从基址到字段的三层跳转拿到模块基址只是起点。微信用户信息不是直接放在模块基址加固定偏移处而是「模块基址 一级偏移 → 指针 → 二级偏移 → 指针 → 字段偏移」这种多层结构。常见做法是用 Cheat Engine 或 x64dbg 先搜昵称字符串找到引用它的代码再反推指针链。我一般会这样操作先用 CE 附加微信进程搜当前登录账号的昵称得到若干地址然后重启微信重新登录再搜一次取两次都命中的地址那就是稳定的全局指针路径终点。接着用「找出是什么访问了这个地址」功能往上追两层指针记录每一层的偏移。最终得到类似[[moduleBase 0x1A2B3C0] 0x8] 0x10这样的链。WeChatUserInfo WeChatReader::readUserInfo() { WeChatUserInfo info; if (!isAttached()) return info; // 三层指针链偏移随版本变化需自行用调试器确认 const DWORD64 off1 0x1A2B3C0; const DWORD64 off2 0x8; const DWORD64 off3 0x10; DWORD64 p1 0, p2 0, p3 0; if (!readMemory(m_moduleBase off1, p1, sizeof(p1))) return info; if (!readMemory(p1 off2, p2, sizeof(p2))) return info; if (!readMemory(p2 off3, p3, sizeof(p3))) return info; // 读昵称QString 在内存里是 UTF-16先读长度再读内容 // 这里假设结构体里昵称是 std::wstring 布局指针 长度 容量 DWORD64 strPtr 0; int strLen 0; if (!readMemory(p3 0x20, strPtr, sizeof(strPtr))) return info; if (!readMemory(p3 0x28, strLen, sizeof(strLen))) return info; if (strLen 0 strLen 256) { std::vectorwchar_t buf(strLen 1, 0); if (readMemory(strPtr, buf.data(), strLen * sizeof(wchar_t))) { info.nickname QString::fromWCharArray(buf.data(), strLen); } } return info; }这段的关键在于理解字符串在内存里的布局。微信内部大量使用std::wstring它的结构是「堆上字符缓冲区指针 当前长度 容量」三个字段各占 8 字节64 位下。所以读昵称要分两步先读指针和长度再按长度去读实际字符。直接按固定长度读会读到相邻字段出现乱码或截断。偏移值0x1A2B3C0、0x8、0x10、0x20、0x28都是示意每个微信版本都会变。这就是为什么这类工具必须做版本适配——没有一劳永逸的偏移。我一般会把偏移做成配置文件启动时按微信版本号加载对应的一套避免每次更新都重新编译。3. 避坑与排查读不到、读出错、读一半的典型现场3.1 OpenProcess 返回 NULL 的五种原因现象是attach()里OpenProcess失败GetLastError返回 5拒绝访问。原因通常有五种一是没用管理员权限运行微信进程有保护二是 Qt 套件位数和微信进程位数不匹配32 位读 64 位必然失败三是微信版本带了反调试进程句柄被保护四是目标进程已经退出PID 是快照里的旧值五是被安全软件拦截了句柄请求。解决办法按顺序排查先确认任务管理器里 WeChat.exe 的位数再确认 Qt 编译套件一致然后以管理员身份运行如果还不行用 Process Explorer 看进程的 Protection 级别有些版本会开 PPL。PPL 进程普通OpenProcess拿不到需要走内核驱动那就超出用户态工具的范围了不建议继续。3.2 读到的昵称是乱码或半截现象是昵称显示成方块或者只显示前两个字。原因基本是字符串布局判断错了。有的版本用QString而不是std::wstringQString的布局是「指向 QArrayData 的指针」长度和字符数据在另一个结构里偏移完全不同。还有的版本字符串是 UTF-8 的std::string按宽字符读自然乱码。解决方法是先用调试器看内存原始字节。如果看到的是61 00 62 00这种隔字节有 0 的是 UTF-16如果是61 62 63连续的是 UTF-8。确认编码后再决定用fromWCharArray还是fromUtf8。长度字段也要验证读出来的长度如果大于 256基本可以判定偏移错了。3.3 指针链在重启后失效现象是昨天还能读今天微信一更新就全读不到了。原因是微信每次版本更新都会重新编译全局对象的地址和结构体布局都会变。这不是 bug是这类工具的固有成本。解决办法是建立版本适配机制把偏移抽成 JSON 或 INI按微信版本号索引启动时先读微信版本再加载对应偏移读不到就提示「当前版本未适配」。我一般还会在代码里加一层校验——读到昵称后检查长度和字符范围明显不合理就判定偏移失效而不是把脏数据展示给用户。3.4 读取频率过高导致微信卡顿现象是工具开着的时候微信明显变卡甚至无响应。原因是ReadProcessMemory会挂起目标进程的线程高频读取等于频繁打断微信运行。有人为了做实时刷新100 毫秒读一次微信不卡才怪。解决办法是把读取频率降到 1 秒以上并且只读变化概率低的字段。昵称、微信号这种登录后基本不变的读一次缓存起来就行没必要轮询。真要实时也应该用ReadProcessMemory批量读一整块结构体而不是一个字段一次调用。3.5 杀软误报与合规边界现象是编译出来的 exe 一运行就被删。原因是「读其他进程内存」这个行为本身就是杀软的重点监控项不管你的用途是什么。解决办法是加白名单或者做数字签名但更根本的是想清楚合规边界只读当前登录账号自己的信息、不落盘、不上传、不碰聊天记录这是底线。一旦涉及批量读取他人账号或者导出聊天内容性质就变了不在技术讨论范围内。4. 进阶把偏移适配做成可维护的工程习惯写到能读昵称只是第一步真正决定这个工具能不能长期用的是偏移维护。我踩过最深的坑就是硬编码偏移微信一更新整个工具报废还得重新翻一遍调试器。后来改成配置驱动情况好很多。具体做法是建一个offsets.json按版本号组织{ 3.9.5.81: { module: WeChatWin.dll, userInfoChain: [42123072, 8, 16], nicknameOffset: 32, wxidOffset: 48, phoneOffset: 64 }, 3.9.7.15: { module: WeChatWin.dll, userInfoChain: [42123520, 8, 16], nicknameOffset: 32, wxidOffset: 48, phoneOffset: 64 } }加载时先通过文件版本 API 拿到微信版本号再查表。查不到就明确告诉用户「未适配」而不是拿旧偏移硬读。这个习惯的价值在于适配新版本只需要用调试器确认几个数字改 JSON 就行不用动代码、不用重新编译。验证偏移是否正确我一般用三步法。第一步读出来的昵称和微信界面上显示的一致第二步微信号字段符合微信 ID 的字符规则字母开头、6 到 20 位第三步手机号字段要么为空要么是 11 位数字。三个都过才认为这套偏移可用。任何一步不对就回到调试器重新确认。还有一个技巧把读取逻辑和界面彻底解耦。WeChatReader只负责返回结构体界面层用QTimer定时调用并刷新QTableWidget。这样换界面框架、加导出功能、做多账号切换都不用碰底层读取代码。我现在的习惯是任何涉及内存偏移的代码第一版就写成配置驱动加校验哪怕当时只有一个版本要支持——因为后悔药永远比适配成本贵。希望帮到你。本文还有配套的精品资源点击获取
返回列表