[TOC]
前言
本文基于anhkgg大佬的文章《微信PC端技术研究(2)-拿下语音》原文链接:https://bbs.pediy.com/thread-249274.htm
anhkgg大佬的这篇文章找到了保存语音消息的接口,这里直接给出相关特征码,方便定位(我使用的微信版本依旧是2.6.8.52)
偏移为0x30E326,下面的特征码
67E3E319 C745 FC 0100000>mov dword ptr ss:[ebp-0x4],0x1
67E3E320 FF77 34 push dword ptr ds:[edi+0x34] ; 长度
67E3E323 FF77 30 push dword ptr ds:[edi+0x30] ; 内容
67E3E326 E8 85F07300 call WeChatWi.6857D3B0
67E3E32B 8D85 58FFFFFF lea eax,dword ptr ss:[ebp-0xA8]
67E3E331 50 push eax
67E3E332 E8 090E0000 call WeChatWi.67E3F140
C745 FC 01000000 FF77 ?? FF77 ?? E8 ???????? 8D85 ???????? 50 E8 ????????
基于保存语音的相关延伸
其实这个地方不单单有语音消息,还有图片消息,当我们发送一条图片消息时
[edi+0x30]的内容里面保存有这一次发送图片的相关数据,包括微信ID等一系列原始的数据。我们当然可以在这个地方写HOOK来保存图片,但是没有必要。因为这里的消息内容过多,处理起来相对会比较麻烦。
图片处理的相关流程
既然这个地方是最原始的消息内容,那么后面肯定会对消息进行相关处理。而且我们已经知道微信的接收的图片会用异或加密的方式保存到本地。那么我们不妨猜测一下图片相关的处理流程。
首先接收到原始的消息后,会对消息进行一系列的处理,其中就包括判断消息是否是图片。那么如果是图片则会取出图片数据,然后在内存中对图片进行加密。加密完成之后调用文件操作的API,写入加密后的图片到本地。
整个过程如图所示:
自动保存图片相关思路
既然了解图片处理的流程,而且已经有了接收图片消息的call,那么我们就可以在接收到图片消息之后,在CreateFileW创建图片之前,找到对图片进行加密的算法和函数,将未加密前的图片保存出来。
实战保存聊天图片
在OD中找到保存语音的call,发送图片消息让程序断下的同时,对CreateFileW进行下断。之后F9运行
此时文件路径为xlog,这个明显不符合我们的要求,继续F9运行
一直找到图片路径带有Image关键字时,在创建图片
此时我们点击K显示堆栈,找到第一层返回地址,右键显示调用
当微信运行到这里的时候,图片加密已经完成,我们要在这个函数之前找到图片的加密算法,其实就在上面一点点的位置,鼠标稍微往上翻一下就能看到
找到这个地方之后清除剩下的所有的断点,只保留这一个
对保存图片call的相关分析
再次发送一张图片,程序断下。这段代码首先用循环的方式对图片进行加密,循环的次数即ecx的值,也就是图片的大小。其中有两个数据比较重要。
我们先在内存中查看[ebp-0x14]的内容,想要知道这段数据是什么其实很简单。先下CreateFileW断点
当CreateFileW断点断下后,执行到返回,查看打开的文件句柄
此时打开的图片句柄为0xF80,此时再下WriteFile断点
WriteFile断下后可以看到句柄为F80,写入的缓冲区地址为39FE820,而[ebp-0x14]的地址正好也是39FE820。也就是说[ebp-0x14]这个位置保存的是加密后的图片数据
回到之前的断点,再次发送一张图片,查看[ebp-0x4]的数据
此时[ebp-0x4]中保存了接收的图片,而ecx保存了图片的大小
这里借用PCHunter查看->进程内存,将这一段数据dump下来
问题就在于这里只是一张大小为4KB的缩略图,回到OD,再次按F9运行
断点断下,但是此时ecx的值变成0x5A140
用同样的方法dump下内存,大小为360KB,这个就是我们需要的原图了。
也就是说这个地方会端下来两次,第一次是缩略图,第二次才是我们要的原图。
代码实现保存聊天图片
示例代码如下:
void HookSaveImages()
{
DWORD dwBaseAddress = (DWORD)GetModuleHandle(TEXT("WeChatWin.dll"));
//需要hook的地址
SaveImageAddress = dwBaseAddress + SaveImages;
//跳回的地址
SaveImageAddressBackAddress = SaveImageAddress + 5;
//组装跳转数据
BYTE jmpCode[5] = { 0 };
jmpCode[0] = 0xE9;
//计算偏移
*(DWORD*)& jmpCode[1] = (DWORD)FnSaveImages - SaveImageAddress - 5;
// 保存以前的属性用于还原
DWORD OldProtext = 0;
// 因为要往代码段写入数据,又因为代码段是不可写的,所以需要修改属性
VirtualProtect((LPVOID)SaveImageAddress, 5, PAGE_EXECUTE_READWRITE, &OldProtext);
//写入自己的代码
memcpy((void*)SaveImageAddress, jmpCode, 5);
// 执行完了操作之后需要进行还原
VirtualProtect((LPVOID)SaveImageAddress, 5, OldProtext, &OldProtext);
}
__declspec(naked) void FnSaveImages()
{
__asm
{
mov ebx, dword ptr ss : [ebp - 0x4];
mov ImageData, ebx;
mov ImageDataLen, ecx;
pushad;
pushfd;
}
//调用接收消息的函数
FnSaveImagesCore();
//恢复现场
__asm
{
popfd
popad
//跳回被HOOK指令的下一条指令
jmp SaveImageAddressBackAddress;
}
}
void FnSaveImagesCore()
{
//如果图片长度大于10KB则保存
if (ImageDataLen >= 10240)
{
//获取临时文件夹目录
char temppath[MAX_PATH] = { 0 };
GetTempPathA(MAX_PATH, temppath);
char imagedir[20] = { "WeChatRecordImages" };
//拼接目录
char WeChatExpressionsPath[MAX_PATH] = { 0 };
sprintf_s(WeChatExpressionsPath, "%s%s\\", temppath, imagedir);
//创建目录存放图片
CreateDir(WeChatExpressionsPath);
//保存图片
CreateFileWithCurrentTime(WeChatExpressionsPath, (char*)".jpg", ImageData, ImageDataLen);
}
}
实际效果