简介:这份资源是一套面向Windows平台逆向分析入门与进阶学习者的实战源码,聚焦进程内存读取与用户信息解析场景,界面层采用Qt搭建,底层进程操作与内存访问则使用Windows原生方法实现,适合具备一定C++基础、希望理解反编译与内存结构分析流程的开发者参考。压缩包共6个文件,以cpp源码与h头文件为核心,配合pro工程文件、ui界面描述文件及user配置,整体仅6KB,结构精简,便于快速导入Qt环境阅读与调试。目前已有1518人学习下载,说明该方向具备一定关注度。通过阅读源码,读者可以了解如何借助Windows接口定位目标进程、读取指定内存区域,并结合Qt界面完成数据展示,从而掌握逆向分析中进程枚举、内存读写与结构解析的基本思路,为后续深入学习反编译与内存结构分析打下实践基础。
1. 从一份 Qt 源码说起:Windows 下读取微信用户信息的逆向路径
很多人第一次接触 Windows 逆向,都是从「读一个进程的内存」开始的。这份 WeChatUserInfo 源码包,做的事情很具体:用 Qt 搭一个界面,通过 Windows 原生 API 去枚举进程、打开目标进程、读取内存,把微信进程里跟当前登录用户相关的信息捞出来显示在窗口上。它不是一个完整的商业级工具,而是一份能跑通「进程枚举 → 句柄获取 → 内存读取 → 数据解析」这条链路的教学型源码,适合想入门 Windows 逆向分析、又不想一上来就啃汇编和调试器的开发者。
源码结构很干净:mainwindow.cpp负责界面逻辑和按钮响应,mainwindow.h声明槽函数和成员变量,main.cpp是 Qt 应用入口,mainwindow.ui是 Qt Designer 拖出来的界面文件,.pro是 qmake 工程配置。换句话说,它把「界面」和「底层读取」这两件事拆得很清楚,你改界面不影响读取逻辑,改读取逻辑也不动 UI。接下来我会按「这份源码怎么用 → 关键 API 怎么调 → 参数怎么设 → 哪里容易翻车」的顺序,把它拆成能照着复现的步骤。
2. 环境搭建与工程结构:把 Qt 和 Windows API 接起来
2.1 为什么选 Qt 而不是 MFC 或纯 Win32
这份源码用 Qt 做界面,不是因为它比 MFC 高级,而是因为 Qt 的信号槽机制让「点击按钮 → 触发读取 → 刷新界面」这条链路写起来更直观。mainwindow.ui里拖一个按钮和一个文本显示区,在mainwindow.cpp里用connect把按钮的clicked信号绑到自定义槽函数上,槽函数里再去调 Windows API。整个过程不需要手动处理消息循环,也不用管窗口重绘。
另一个现实原因是跨版本兼容。Qt 的 qmake 工程在不同 Windows 版本上迁移成本低,你换一台机器,只要 Qt 版本对得上,.pro文件基本不用改。而 MFC 工程一旦换了 VS 版本,经常要重新配包含目录和库目录,对新手不友好。
常见做法是装 Qt 5.12 或 Qt 5.15 的 MinGW 版本,因为这两个版本在 Windows 上编译这类调用 Win32 API 的工程最稳。如果你装的是 Qt 6,QApplication的头文件路径变了,.pro里要加greaterThan(QT_MAJOR_VERSION, 4): QT += widgets,否则编译会报找不到QApplication。
2.2 工程文件逐个拆:每个文件负责什么
先把源码包解压,你会看到这几个文件:
| 文件名 | 作用 | 改动频率 |
|---|---|---|
WeChatUserInfo.pro | qmake 工程配置,声明模块和源文件 | 低 |
main.cpp | 程序入口,创建QApplication和主窗口 | 极低 |
mainwindow.h | 主窗口类声明,含槽函数和成员变量 | 中 |
mainwindow.cpp | 界面逻辑 + Windows API 调用核心 | 高 |
mainwindow.ui | Qt Designer 界面布局 | 中 |
WeChatUserInfo.pro.user | Qt Creator 本地配置,不要提交 | 不碰 |
.pro文件里最关键的是QT += core gui和greaterThan(QT_MAJOR_VERSION, 4): QT += widgets,以及LIBS += -lpsapi或-luser32这类链接选项。读取进程内存要用到OpenProcess、ReadProcessMemory,这些在kernel32.dll里,Qt 的 MinGW 默认能链到,但如果你用 MSVC 编译器,可能要在.pro里显式加LIBS += -lkernel32。
mainwindow.h里一般会声明一个槽函数,比如void onReadButtonClicked();,以及一个QTextEdit *outputArea;用来显示结果。mainwindow.cpp里则是connect(ui->readButton, &QPushButton::clicked, this, &MainWindow::onReadButtonClicked);这样的绑定。
2.3 编译前必须确认的三件事
第一,Qt 版本和编译器要匹配。如果你下载的是 MinGW 版 Qt,就用 Qt Creator 里对应的 MinGW 套件;如果用的是 MSVC 版 Qt,就要装对应版本的 Visual Studio Build Tools。混用会报fatal: cannot mix incompatible qt library这类错误,本质是 Qt 库和编译器 ABI 不一致。
第二,.pro.user文件不要跨机器拷贝。它记录了本机的 Qt 路径和构建套件,换机器后 Qt Creator 会提示重新配置,直接删掉让它重新生成即可。
第三,确认目标进程的位数。如果你的 Qt 编译出来是 32 位程序,去读 64 位微信进程的内存,OpenProcess会失败,GetLastError返回 5(拒绝访问)。解决办法是编译 64 位 Qt 程序,或者用 64 位 Qt 套件重新构建。
# 在 Qt Creator 的构建目录下,可以用命令行确认生成的 exe 位数 dumpbin /headers WeChatUserInfo.exe | findstr machine # 32 位输出 x86,64 位输出 x64这段命令用来验证你编译出来的程序是 32 位还是 64 位。dumpbin是 Visual Studio 自带的工具,如果你没装 VS,可以用file命令在 Git Bash 里看,或者直接在任务管理器里看进程有没有带(32 位)标记。位数不匹配是新手最容易翻车的地方,现象是OpenProcess返回 NULL,但代码逻辑看起来完全正确。
3. 进程枚举与内存读取:Windows API 的调用顺序和参数含义
3.1 枚举进程:从 CreateToolhelp32Snapshot 到 Process32Next
读取微信用户信息的第一步是找到微信进程。Windows 下枚举进程最常用的方式是CreateToolhelp32Snapshot配合Process32First和Process32Next。这套 API 在tlhelp32.h里,属于kernel32.dll。
#include <windows.h> #include <tlhelp32.h> #include <QString> #include <QDebug> DWORD FindWeChatProcessId() { DWORD pid = 0; // 创建系统进程快照,TH32CS_SNAPPROCESS 表示只枚举进程 HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot == INVALID_HANDLE_VALUE) { qDebug() << "CreateToolhelp32Snapshot failed:" << GetLastError(); return 0; } PROCESSENTRY32 pe32; pe32.dwSize = sizeof(PROCESSENTRY32); // 必须设置结构体大小,否则 Process32First 失败 if (Process32First(snapshot, &pe32)) { do { // 微信进程名通常是 WeChat.exe,旧版可能是 WeChatApp.exe if (_wcsicmp(pe32.szExeFile, L"WeChat.exe") == 0) { pid = pe32.th32ProcessID; break; } } while (Process32Next(snapshot, &pe32)); } CloseHandle(snapshot); // 快照句柄必须关闭,否则句柄泄漏 return pid; }这段代码的逻辑是:先创建快照,拿到一个包含所有进程信息的句柄;然后用Process32First取第一条,Process32Next往后遍历;每次比较szExeFile是否等于WeChat.exe。找到就记录th32ProcessID并跳出循环。
参数上要注意pe32.dwSize = sizeof(PROCESSENTRY32)这行不能省。Process32First内部会检查这个字段,如果大小不对,直接返回 FALSE,GetLastError返回 24(ERROR_BAD_LENGTH)。这是血泪经验,很多人抄代码时漏了这行,排查半天以为是权限问题。
_wcsicmp是宽字符不区分大小写比较,因为szExeFile是WCHAR数组。如果你用strcmp去比,编译能过但逻辑永远不相等,因为宽字符和窄字符的内存布局不同。
3.2 打开进程:OpenProcess 的权限参数怎么选
拿到 PID 之后,下一步是OpenProcess获取进程句柄。读取内存需要PROCESS_VM_READ权限,如果还要查询模块信息,再加PROCESS_QUERY_INFORMATION。
HANDLE OpenTargetProcess(DWORD pid) { // PROCESS_VM_READ 允许读取进程内存 // PROCESS_QUERY_INFORMATION 允许查询进程信息(如模块列表) HANDLE hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid); if (hProcess == NULL) { DWORD err = GetLastError(); qDebug() << "OpenProcess failed, error code:" << err; // 5 = 拒绝访问,通常是权限不足或位数不匹配 // 87 = 参数错误,通常是 PID 无效 } return hProcess; }OpenProcess的第二个参数bInheritHandle设为 FALSE,表示子进程不继承这个句柄。第三个参数就是 PID。
最常见的失败是错误码 5。原因通常有两个:一是你的程序没有足够权限,微信以更高权限运行;二是位数不匹配,32 位程序打不开 64 位进程。解决办法是以管理员身份运行你的 Qt 程序,或者用 64 位套件重新编译。
另一个坑是PROCESS_QUERY_INFORMATION在部分 Windows 版本上需要SeDebugPrivilege权限。如果你只是读内存,可以只请求PROCESS_VM_READ,减少失败概率。但如果你后续要遍历模块找基址,就必须加PROCESS_QUERY_INFORMATION。
3.3 读取内存:ReadProcessMemory 的地址和缓冲区
ReadProcessMemory是核心中的核心。它的原型是:
BOOL ReadProcessMemory( HANDLE hProcess, // OpenProcess 返回的句柄 LPCVOID lpBaseAddress, // 目标进程中的起始地址 LPVOID lpBuffer, // 你自己的缓冲区 SIZE_T nSize, // 要读多少字节 SIZE_T *lpNumberOfBytesRead // 实际读了多少 );在微信用户信息这个场景里,你通常不是直接读一个固定地址,而是先通过模块基址加偏移的方式定位。比如微信的WeChatWin.dll加载到某个基址,用户信息结构体在基址加某个偏移的位置。
bool ReadMemory(HANDLE hProcess, uintptr_t address, void *buffer, size_t size) { SIZE_T bytesRead = 0; BOOL ok = ReadProcessMemory(hProcess, (LPCVOID)address, buffer, size, &bytesRead); if (!ok) { qDebug() << "ReadProcessMemory failed at address:" << Qt::hex << address << "error:" << GetLastError(); return false; } if (bytesRead != size) { qDebug() << "Partial read, expected:" << size << "got:" << bytesRead; return false; } return true; }参数说明:address是目标进程虚拟地址空间里的地址,不是物理地址;buffer是你自己进程里的缓冲区,大小至少size字节;bytesRead返回实际读取的字节数,正常情况下等于size,如果目标地址跨了不可读页,可能小于size。
常见错误码 299(ERROR_PARTIAL_COPY)通常表示地址无效或权限不够。如果你读的是一个动态分配的字符串指针,要先读指针值,再拿指针值去读字符串内容,不能一步到位。
3.4 从内存字节到用户信息:解析偏移和字符串
读出来的是一段原始字节,要变成可读的用户信息,需要按结构体布局解析。微信的用户信息通常包含昵称、微信号、手机号等字段,这些字段在内存里可能是std::string或std::wstring的布局。
struct UserInfo { char nickname[64]; char wxid[64]; char phone[32]; }; // 假设已经通过基址+偏移拿到了 userInfoAddr UserInfo info; if (ReadMemory(hProcess, userInfoAddr, &info, sizeof(UserInfo))) { QString nickname = QString::fromLocal8Bit(info.nickname); QString wxid = QString::fromLocal8Bit(info.wxid); ui->outputArea->append("昵称: " + nickname); ui->outputArea->append("微信号: " + wxid); }这里用fromLocal8Bit是因为微信在中文 Windows 上存的可能是 GBK 编码的窄字符串。如果实际是 UTF-8,就要用fromUtf8;如果是宽字符wchar_t,用fromWCharArray。编码判断错了,显示出来就是乱码,这是第二个高频翻车点。
偏移的获取方式,常见做法是用 Cheat Engine 或 x64dbg 附加到微信进程,搜索已知的微信号字符串,找到地址后反推基址和偏移。这个过程需要反复验证,因为微信版本更新后偏移会变。源码里如果写死了偏移,换一个微信版本就可能读不到。
4. 避坑与排查:读不到数据时先查这五件事
4.1 现象:OpenProcess 返回 NULL,错误码 5
原因:权限不足或位数不匹配。微信可能以管理员权限运行,而你的 Qt 程序是普通权限;或者你的程序是 32 位,微信是 64 位。
解决:右键 Qt Creator 或编译出的 exe,选择「以管理员身份运行」。如果还不行,检查 Qt 套件是不是 64 位,用dumpbin /headers确认 exe 位数,和微信进程位数对齐。
4.2 现象:ReadProcessMemory 返回 FALSE,错误码 299
原因:目标地址无效,或者地址所在的内存页不可读。常见于偏移写错、基址没算对、或者目标模块还没加载。
解决:先用VirtualQueryEx查询目标地址的内存属性,确认Protect字段包含PAGE_READABLE相关标志。如果地址是动态分配的,确认指针链每一级都读对了。可以在每一步读完后打印地址和值,逐级比对。
4.3 现象:读出来的字符串是乱码
原因:编码判断错误。微信可能存 GBK、UTF-8 或 UTF-16,不同版本不一样。
解决:先读原始字节,用十六进制打印出来看特征。GBK 中文两个字节高位都在 0x80 以上,UTF-8 中文通常是三个字节E4-BF-A1这种模式,UTF-16 会有明显的00间隔。根据特征选fromLocal8Bit、fromUtf8或fromWCharArray。
4.4 现象:编译报 fatal: cannot mix incompatible qt library
原因:Qt 库版本和编译器 ABI 不匹配,或者系统里有多套 Qt 环境变量冲突。
解决:在 Qt Creator 里检查「构建套件」是否选对了 Qt 版本和编译器。如果命令行编译,确认PATH里只有一套 Qt 的 bin 目录。删掉构建目录重新 qmake 一次。
4.5 现象:程序能跑但读不到微信进程
原因:微信进程名不是WeChat.exe。新版微信可能是WeChatApp.exe,或者有多个微信进程。
解决:在FindWeChatProcessId里把进程名打印出来,看看实际叫什么。也可以枚举所有进程,用qDebug()输出szExeFile,确认目标名称后再改比较逻辑。
5. 进阶技巧:用模块基址加偏移替代硬编码地址
硬编码地址在微信每次更新后都会失效,这是这份源码最大的局限。更稳的做法是动态获取模块基址,再加固定偏移。GetModuleInformation配合EnumProcessModules可以拿到WeChatWin.dll的基址。
#include <psapi.h> uintptr_t GetModuleBase(DWORD pid, const wchar_t *moduleName) { HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); if (!hProcess) return 0; HMODULE modules[1024]; DWORD needed = 0; if (!EnumProcessModules(hProcess, modules, sizeof(modules), &needed)) { CloseHandle(hProcess); return 0; } uintptr_t base = 0; int count = needed / sizeof(HMODULE); for (int i = 0; i < count; i++) { wchar_t name[MAX_PATH] = {0}; if (GetModuleBaseNameW(hProcess, modules[i], name, MAX_PATH)) { if (_wcsicmp(name, moduleName) == 0) { base = (uintptr_t)modules[i]; break; } } } CloseHandle(hProcess); return base; }拿到基址后,用户信息地址就是base + offset。偏移的获取需要用调试器附加微信,搜索已知字符串,找到地址后减去基址。这个过程我一般会做三次验证:重启微信、切换账号、重启电脑,确认偏移在多次运行中稳定,再写进代码。
另一个技巧是用VirtualQueryEx遍历内存区域,找可读且类型为MEM_PRIVATE的块,在里面搜索特征字符串。这种方式比固定偏移更抗版本更新,但性能差一些,适合做辅助定位。
验证读取结果是否正确,最直接的办法是拿你已知的微信号去比对。如果你当前登录的微信号是test_wxid_123,读出来的wxid字段应该完全一致。如果不一致,先检查偏移,再检查编码,最后检查是不是读到了缓存或旧数据。
从那以后我每次调这类内存读取,都会先写一个最小验证:只读一个已知字符串,打印十六进制和解析结果,确认链路通了再往上叠业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取