PC微信hook学习笔记(三)—— 获取登录二维码
本篇笔记学习教程:获取微信二维码基址,手动实现hook
本篇笔记参考博客:PC微信逆向:使用HOOK拦截二维码
1. 二维码图片基址
1.1 PNG文件在内存中的表现形式
微信的登录二维码是png格式,png图片在OD中能看到,就是png图片在内存中的表现形式。PNG文件头格式解析,这篇文章就介绍了png图片在内存中的数据表现。
在OD中,如果能看到某个数据块的前三行是这种样子的话,就说明这块内存存储了一个png图片。
1.2 用CE查找变量所在地址
- 打开微信,打开CE
- 在CE中,查询【未知的初始值】,此时,因为还没有获取登录二维码,所以存储二维码的变量尚未初始化。
- (1)微信点击【切换账号】,CE选择【变动的数值】,点击【再次扫描】。(2)手机扫一扫【登录二维码】,不要点登录,CE点击【再次扫描】
- 重复第3步,每次都滚动到最底下查看绿色基址。当绿色地址只剩下几个的时候,添加到下方窗口里,观察这几个地址的变化。
1.3 用OD在地址附近打断点
- 打开OD,附加进程,输入命令:【dd 776DC348】。注意,这个地址,是CE中刚才加入观察的数值长度为5个的,如
15990
。 - 选中地址【776DC348】右击,添加断点,添加断点后微信点击【切换账号】。这一步有可能造成OD卡死,多试几次,或换一个地址试试,直到出现下图中的二维码还未刷新出来的场景(就算出现这个空白,地址也可能是错的,参见第6步)。
3. 在右下角窗口中,查找类似“返回到WeChatWi.0x地址 来自 WeChatWi.0x地址
”的条目,选中右击【反汇编窗口中跟随】,并在左上角窗口中,选中在语句【lea ecx,dword ptr ss:[ebp+0xC]
】接着的【call】语句,按F2加断点。找差不多三四个就可以了。
4. 【删除内存断点】,并点击小三角运行程序。让这次的二维码扫描掉,再来显示一次二维码,使得OD进入函数断点。
5. 进入某个断点后,输入命令:dc [ecx]
查询数据。能出现下图所示数据的话,恭喜你找到了二维码图片。
6. 若是OD卡死,从第5步再来几次;若是没找到图片,或者输入dc [ecx]命令后是一串别的乱码,说明一开始用来打断点的地址不对,那么,不要着急,从第1步再来几次。(淡定如君子其实慌如疯狗…特喵的我找了近2小时!)
1.4 打印图片
OD打印数据的命令:dm 数据的地址,数据的长度,"文件名称"
。
dm 04D7DD80,EA2,"w_qrcode.png"
ret
- 将上面的命令,修改成自己的地址,保存到txt文件中。
- 在OD左上角的窗口中任意位置右击→选择【运行脚本】→选择刚才保存的命令文本。
- 保存的图片在微信的安装目录里。
- 验证图片的话,就删掉刚才保存的图片,重新显示出一张二维码,扫描OD保存的那张,观察微信是否有变化。
1.5 计算二维码图片基址
二维码图片基址:(call语句的地址)53C3ACE7 - WeChatWin.dll = 1F ACE7
WeChatWin.dll 的地址,可以在OD中点击【e-显示模块窗口】快速查找。
2. 二维码内容基址
2.1 获取二维码图片中包含的文本内容
首先,关于二维码生成原理,大家可看下这位大佬的博客:二维码原理详解。
而识别二维码,就是将二进制编码的图片,按编码规则解码出来并解析所包含的内容(可能是字符串、网址等)。
怎么找出微信登录二维码中所包含的内容呢?
- 打开微信→切换账号→截图二维码→保存为图片
- 打开草料-二维码解码器→上传刚才的二维码截图,可得到解码结果:
http://weixin.qq.com/x/4_QBLO7V2dAAUmd28I54
- 很显然,
http://weixin.qq.com/
是一个服务器地址。在浏览器里访问这个地址,首页显示pc版微信的下载;后面的x/4_QBLO7V2dAAUmd28I54
,应该是接口和参数,只不过做了一些处理。
每一次点击切换账号,二维码都会变化,那么,上面的4_QBLO7V2dAAUmd28I54
也是每次都不一样,我们需要找到的,就是存储这个变量的基址。
2.2 找到存放二维码内容的基址
- 打开CE→打开微信进程→搜索二维码解析出的字符串,如
4_QBLO7V2dAAUmd28I54
;需要注意的是,微信的二维码隔一段时间就会刷新,所以要以最新的二维码截图解析。 - 等待几秒钟,刚才扫描出的地址列表会变化,这时,切换到微信窗口,等到新的二维码刷新出来,会看到CE里有一个地址里的当前值,与搜索的字符串长度相等;此时,将刷新出来的二维码内容解析出来,正好与CE里变化的字符串相同。那么先记住这个地址,如下图的
072FBFB0
- 打开OD→附加微信→在右下角命令框中输入:
dd 072FBFB0
- 选中
072FBFB0
并右击→断点→内存写入,并点击小三角运行。 - 点击OD操作栏的【t】→在窗口内右击→“Resume All Threads”→点击【c】,切换回主窗口
6.等到OD页面出现【暂停】时,说明断点起效,二维码刷新了!点击右下角第一行,则左下角的地址栏会显示对应地址信息。
这里有几个地址信息:(1)WeChatWi.796CA008
(2)796CA010
- 用CE验证下刚才找到的地址:(1)手动添加地址→WeChatWin.dll→添加后右击,以十六进制显示。(2)手动添加地址→
796CA010
→添加后右击,以十六进制显示(3)用计算器计算基址偏移量:796CA010 - WeChatWin.dll地址 = 13AA010
这样就得到存放二维码内容的基址:WeChatWin.dll + 0x13AA010
3. 编写程序显示二维码
首先,找到需要hook的call,在OD左上角窗口中右击,选择【转到】→【表达式】,输入二维码图片基址偏移量: 1FACE7
。
3.1 hook思路
在call语句前面,有个E8 747F2A00
,E8
是call的机器码,747F2A00
是函数地址相对于当前地址的偏移量。
- 第一步——截住信鸽
首先修改内存,让call语句执行的程序整个变成执行我们自己的函数。将原来的E8 747F2A00
,替换成E9 新的地址
,E9
是jmp的机器码;新的地址 = 要跳转的地址 - hook的地址 - 5
;减5是因为E9 新的地址
占5个字节,要跳转的地址就是自定义函数的地址。 - 第二步——copy书信
需要用一个变量pic,存储ecx里的png图片数据,并显示到自己的窗口中; - 第三步——让信鸽按原来的方向继续飞
用jmp命令
跳回到原来call语句的下一句,让程序继续执行。下一句的地址偏移为:78D9ACEC - WeChatWin.dll = 1FACEC
3.2 代码
- 打开VS2017→新建【动态链接库DLL】
- 在【资源文件】下右击→添加→资源→Dialog,并建立如下图的布局,对布局元素修改ID以便引用。
- 新建
hook.cpp
和hook.c
。 - 选中项目,右击,选择【重新生成】。
hook.cpp
#include "pch.h"
#include "resource.h"
#include "hook.h"
#include "stdio.h"
#include "atlimage.h"
// 一条命令的字节长度
#define HOOK_LEN 5
// 原来的call编码
BYTE oldCallCode[HOOK_LEN] = { 0 };
// 弹框句柄
HWND wDlg = 0;
// 保存二维码并显示
void showImg(DWORD qrcodeEcx) {
// 获取图片长度
DWORD picLen = qrcodeEcx + 0x4;
// 图片数据
char picData[0xFFF] = { 0 };
size_t copyLen = (size_t)*((LPVOID *)picLen);
memcpy(picData, *((LPVOID *)qrcodeEcx), copyLen); // 拷贝图片
//将文件写到本地
HANDLE hFile = CreateFileA("E:\\wechat_qrcode.png", GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == NULL)
{
MessageBox(NULL, L"创建图片文件失败", L"错误", 0);
return;
}
DWORD dwRead = 0;
if (WriteFile(hFile, picData, copyLen, &dwRead, NULL) == 0)
{
MessageBox(NULL, L"写入图片文件失败", L"错误", 0);
return;
}
CloseHandle(hFile);
// 读取图片,显示到弹框中
CImage cimg;
CRect crect;
HWND picHw = GetDlgItem(wDlg, QRCODE);
GetClientRect(picHw, &crect);
cimg.Load(L"E:\\wechat_qrcode.png");
cimg.Draw(GetDC(picHw), crect);
}
// 需要返回地址
DWORD returnAddr = 0;
// 声明备份的8个寄存器
DWORD pEax = 0;
DWORD pEcx = 0;
DWORD pEdx = 0;
DWORD pEbx = 0;
DWORD pEbp = 0;
DWORD pEsp = 0;
DWORD pEsi = 0;
DWORD pEdi = 0;
// 声明裸函数,这样编译器就不会做其他多余的操作,不会修改堆栈
void __declspec(naked) showPic() {
// 备份寄存器
__asm{
mov pEax, eax
mov pEcx, ecx
mov pEdx, edx
mov pEbx, ebx
mov pEbp, ebp
mov pEsp, esp
mov pEsi, esi
mov pEdi, edi
}
// 图片数据在ecx里
showImg(pEcx);
// 返回地址 1FACEC
returnAddr = (DWORD)GetModuleHandle(L"WeChatWin.dll") + 0x1FACEC;
// 恢复寄存器,并跳回到原来的执行语句地址
__asm {
mov eax,pEax
mov ecx,pEcx
mov edx,pEdx
mov ebx,pEbx
mov ebp,pEbp
mov esp,pEsp
mov esi,pEsi
mov edi,pEdi
jmp returnAddr
}
}
// 开始hook
void startHook(DWORD imgAddr,LPVOID funAddr, HWND hwndDlg) {
// ----------------------- 1.获取基址 ----------------------
// 模块基址
DWORD weChatWinAddr = (DWORD)GetModuleHandle(L"WeChatWin.dll");
// 二维码图片地址
DWORD imgHook = weChatWinAddr + imgAddr;
// ----------------------- 2.组装E9数据 ----------------------
// 二进制byte
BYTE jmpCode[HOOK_LEN] = { 0 };
// 头部E9
jmpCode[0] = 0xE9;
// 计算公式:要跳转的地址 - hook的地址 - 5
// 要跳转的地址就是自己函数的地址
*(DWORD *)&jmpCode[1] = (DWORD)funAddr - imgHook - HOOK_LEN;
// ----------------------- 3.备份原来的call地址,用于卸载 ----------------------
// 获取进程句柄
HANDLE wechatH = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId());
if (ReadProcessMemory(wechatH, (LPCVOID)imgHook, oldCallCode, HOOK_LEN, NULL) == 0) {
MessageBox(NULL, L"读取内存失败", L"错误", 0);
return;
}
// ----------------------- 4.写入E9数据 ----------------------
if (WriteProcessMemory(wechatH, (LPVOID)imgHook, jmpCode, HOOK_LEN, NULL) == 0) {
MessageBox(NULL, L"内存写入失败", L"错误", 0);
return;
}
// 赋值弹框句柄,用于显示图片
wDlg = hwndDlg;
// 二维码链接
char qrcodeUrl[0x300] = { 0 };
DWORD qrcodeUrlAddr = weChatWinAddr + 0x13AA010;
// 读取指针里的数据
sprintf_s(qrcodeUrl, "%s%s", "http://weixin.qq.com/x/", *((DWORD *)qrcodeUrlAddr));
MessageBoxA(NULL, qrcodeUrl, "二维码链接", NULL);
}
// 卸载hook
void unHook(DWORD imgAddr) {
// 模块基址
DWORD weChatWinAddr = (DWORD)GetModuleHandle(L"WeChatWin.dll");
// 二维码图片地址
DWORD imgHook = weChatWinAddr + imgAddr;
// 写入备份
HANDLE wechatH = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId());
if (WriteProcessMemory(wechatH, (LPVOID)imgHook, oldCallCode, HOOK_LEN, NULL) == 0) {
MessageBox(NULL, L"卸载hook失败", L"错误", 0);
return;
}
}
hook.h
#pragma once
#include "pch.h"
void startHook(DWORD imgAddr, LPVOID funAddr, HWND hwndDlg);
void showPic();
void unHook(DWORD imgAddr);
dllmain.cpp
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "hook.h"
#include "windows.h"
#include "resource.h"
INT_PTR CALLBACK DialogProc(
HWND hwndDlg,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
DialogBox(hModule, MAKEINTRESOURCE(MAIN), NULL, &DialogProc);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
INT_PTR CALLBACK DialogProc(
HWND hwndDlg,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
char urlText[0x100] = { 0 };
switch (uMsg)
{
case WM_INITDIALOG:
break;
case WM_COMMAND:
if (wParam == START_HOOK) {
// 开始hook
startHook(0x1FACE7, showPic, hwndDlg);
SetDlgItemText(hwndDlg, HOOK_STATUS, L"已hook");
}
if (wParam == UNHOOK) {
// 卸载hook
unHook(0x1FACE7);
SetDlgItemText(hwndDlg, HOOK_STATUS, L"已卸载hook");
}
break;
case WM_CLOSE:
EndDialog(hwndDlg, 0);
break;
default:
break;
}
return false;
}
OD注入
在OD左上角窗口中右击,选择【StrongOD】→【InjectDll】→【Remote Thead】 ,然后选择生成的dll。
选中注入的那一行,按回车,再按回车,可以看到代码的汇编编码,按回退键回到原来的页面。按F8单步执行。
代码的执行结果如下图,在hook成功时,call语句是调用的DLL1;在卸载hook成功时,call语句就恢复成原来的WeChatWi函数。
但是,不知道为啥,我这边没有实现修改成E9的jmp命令。