微信 PC 端技术研究-保存聊天语音
by anhkgg (公众号:汉客儿)
2019 年 1 月 31 日
微信 PC 开源项目地址:https://github.com/anhkgg/SuperWeChatPC
虽然一直知道 CE,也用过一段时间,但一直用不好,可能太笨。
最近又学习了某位大佬用 CE 的方法,大佬的一句话有点醍醐灌顶,然后有了新的感觉,然后开始尝试实践这篇文章。
自己总结一下 CE 用法的核心思路:通过各种技巧搜索找到内存中关键数据,然后结合动态调试找到操作数据的函数。
准备工具:Cheat Engine,OllyDbg,IDA。
官网:https://www.cheatengine.org/
看看来自百科的介绍:
Cheat Engine 是一款内存修改编辑工具 ,它允许你修改你的游戏或软件内存数据,以得到一些其他功能。它包括 16 进制编辑,反汇编程序,内存查找工具。与同类修改工具相比,它具有强大的反汇编功能,且自身附带了外挂制作工具,可以用它直接生成外挂。
在我看来,CE 做的最好的就是各种策略的内存搜索能力。
支持准确数据(整数、字符串、十六进制、浮点数、字节数组等等)搜索,针对目标数据明确效果显著,比如金币数。
支持数据范围的搜索,比如大于某个值,小于某个值等等。比如想找到没有显示数值的血量数据。
支持多组数据同时搜索,针对数据结构复杂的情况
支持搜索结果的多次过滤(图中框选的 Next Scan ),最终找到目标数据。比如血量未知时,通过加血、减血多次搜索最终找到血量地址。
说到底 CE 内存搜索的能力就是通过各种策略帮助你找到游戏中需要修改的数据(比如血量、分数、金币等等),然后通过内存修改能力(直接改血量)打破游戏平衡,外挂制作工具生成外挂,助你超神!
更多 CE 的高级应用可以访问:
https://blog.csdn.net/cgs_______/article/details/77799091
https://blog.csdn.net/zhaobisheng1/article/details/79259460
进入正题,本文是要拿到微信聊天的语音消息,然后 dump 保存下来。
要按以前我的思路,会通过网络通信找到接受消息的函数,然后找到语音数据,看起来很简单,但是有点难。
因为函数真的很多,网络消息也会受到很多干扰。
现在用 CE 了,应该怎么办呢?
关键数据肯定是语音消息了,但是怎么搜索呢,肯定搜语音内容不现实,所以转了弯,先看看文字消息,找到接受文字消息处理函数之后,猜测语音处理函数会相同或者在不同分支。
接着,如何搜索文字消息呢?已经收到的显示在聊天窗口的内容当然可以通过 CE 找到,但是没用啊,它和接受文字消息处理函数已经没关系了,流程已经处理完成了。
那么在测试中肯定知道发送的消息内容,通过 CE 来搜索可以吗?
额,我觉得不行,还没收到消息呢,内存中也没有这个文字消息,搜索不到(如果可以,请大佬指点一下)。
能想到的是,在接受到消息某一点通过调试器断下来,然后 CE 搜索,这样可以,但是这个断点找不到阿,放弃。
那怎么办呢?
看到左侧聊天列表中显示的最新一条消息,有了新的思路。
每次收到新消息后,都会在列表中显示最新消息内容(图中绿框指示位置、注意是 unicode 字符)。
那么,先用 CE ( First Scan )搜索当前搜到的消息内容,找到可能的内存地址。多次接受不同消息后,Next Scan 按钮搜索每次新的消息内容,最终确定聊天列表中显示的最新消息内容的内存地址。
多次刷选之后,留下两个地址,通过 CE 修改内容,在界面中查看是否改变,最终确认第二个地址就是我们的目标,暂把该地址记录为 MsgAddr。
关键数据地址已经找到,下面的工作复杂也不复杂,就看微信是如何实现的了。
猜测微信实现消息显示的流程是这样的:
recv 收到消息,组装完整包后,分发给消息处理函数
根据 wxid 找到要显示消息的列表项,如果不在已聊天消息列表,就新建一个项
在列表中显示消息,如果是表情显示[文字],语音显示为[语音],消息插入 wxid 对应消息队列,或者存入数据库
步骤 3 中肯定要写前面找到的 MsgAddr 内存,把最新消息显示到界面中,这个流程肯定在消息处理函数内部。
So,通过 OD 对 MsgAddr 下内存写入断点,回溯堆栈就可以找到消息处理函数。
具体操作如下:
OD 挂载 Wechat.exe 进程后,在左下角内存窗口处 Ctrl+G,输入找到的 MsgAddr ( 11A11F34 )回车,定位到该数据,然后再 HEX 数据处,右键弹出菜单,选择断点->内存写入。
断点设置完成后,测试发送文字消息,OD 断住,代码窗口显示的就是修改 MsgAddr 的代码位置,如上图 10CE412C 处。
Alt+K 查看当前堆栈。
调用堆栈
地址 堆栈 函数过程 / 参数 调用来自 结构
0012E068 106BD6F3 WeChatWi.10CE4110 WeChatWi.106BD6EE 0012E064 //wcsncpy
0012E088 106BD769 WeChatWi.106BD67E WeChatWi.106BD764 0012E084
0012E09C 1011DD8B WeChatWi.106BD753 WeChatWi.1011DD86 0012E098
0012E0EC 10206C67 包含 WeChatWi.1011DD8B WeChatWi.10206C64 0012E0E8
0012E600 1020E8F1 ? WeChatWi.10206460 WeChatWi.1020E8EC 0012E5FC //界面操作
看到这个调用栈是不是感觉好少,分析起来肯定简单。但,其实是 OD 显示的并不全,此时真的很想用 windbg。
在 OD 的右下角堆栈窗口,可以看到当前调用栈的参数和预览数据。F8 单步(或者 Alt+F8 执行到返回)逐步的回溯每层堆栈。关注 MsgAddr 的数据是如何生成的,也就是找到数据来源,然后找到消息处理函数。
跟踪过程不赘述(需要熟悉汇编知识),直到看到的最顶层的 WeChatWi.10206460 处,发现是把收到的消息内容显示到聊天列表处的一个界面功能函数。
那这里不是可以拿到消息了吗,是的,普通文字消息已经可以拿到,但是语音内容不行。
通过观察内存窗口的数据,整理 WeChatWi.10206460 处的关于消息参数的大致结构。
//聊天列表框信息
struct chat_list_msg {
DWORD unk;//
wstring wxid;//
//wchar_t* wxid;//4
//int len;//8
//int maxlen;//c
DWORD unk1;//10
DWORD unk2;//14
wstring name;
//wchar_t* name;//18 微信名
//int len;//1c
//int maxlen;//20
…
wstring msg; //
//wchar_t* msg;//3c
//int len;//
//int maxlen;
}
wstring msg 字段就是文字消息内容,而语音消息则是预览中看到的[语音]两字,并没有实际能够听到的语音数据,所以还得继续往前找。
继续往上回溯了 3 层左右,进入了 102DDC50,找到了语音消息的新信息。
struct msg_xx
{
char unk[0x40];//
wstring wxid1;//40
wstring wxid2;//4c
char unk1[0x10];//58
wstring msg;//68
char unk2[0x10];//74
;//84
}
在 wstring msg 处就是普通文字消息内容,而语音消息并不是我想象的就是直接语音的数据,而是...如下:
<msg><voicemsg endflag="1" cancelflag="0" forwardflag="0" voiceformat="4" voicelength="1176" length="1334" bufid="147445261304397871" clientmsgid="416261363964373964444633636200230013013119fdd53b1f494102" fromusername="wxid_xxxxxxxxx" /></msg>
真是一波三折,还不是语音的数据,而是关于语音信息的 xml,有语音的大小,来自谁,在语音缓冲区中的 id ( bufid )等等信息。
继续往前找呗,最后回溯到了所有消息处理的分发函数 10323FF0 中。这个函数处理逻辑很复杂,我并没有很快就找到如何生成语音消息的 xml,以及处理语音数据的函数。
一度卡住,重复分析了很多次。
后来又回神想到了逆向神器 IDA,xml 中数据如 voicemsg 肯定是模块中会在代码中用到,看看有没有有用的信息。
用 IDA 打开 Wechatwin.dll ,shift+F12 分析出所有字符串,Ctrl+F 找到关键字 voicemsg,看来有戏。
真的是柳暗花明又一村。
点击字符串跳到代码窗口,按下 x,跳到引用该数据的位置。
找到了解析语音 xml 数据和解码语音数据的关键函数。
f_parseVoiceXmlInfo_103148E0
text:103149DD 0E4 0F 84 74 02 00 00 jz loc_10314C57
.text:103149E3 0E4 68 D0 06 F0 10 push offset aVoicemsg ; "voicemsg"
.text:103149E8 0E8 8B CF mov ecx, edi
.text:103149EA 0E8 E8 31 28 3E 00 call f_xml_subnode_106F7220
.text:103149EF 0E4 85 C0 test eax, eax
.text:103149F1 0E4 0F 84 60 02 00 00 jz loc_10314C57
.text:103149F7 0E4 8D 70 2C lea esi, [eax+2Ch]
.text:103149FA 0E4 68 C4 06 F0 10 push offset aClientmsgid ; "clientmsgid"
.text:103149FF 0E8 8B CE mov ecx, esi
.text:10314A01 0E8 E8 CA 3A 3E 00 call f_xml_getvalue_106F84D0
函数 103148E0 解析 xml 拿到几个字段的内容,返回上层函数调用一个语音解码的函数进行处理,而这个解码函数就会直接操作语音数据。
(*(void (__thiscall **)(int *, _DWORD, _DWORD, int *, signed int))(*v7
+ 28))(//
v7,
*(_DWORD *)(voice_msg + 48), // 语音内容
*(_DWORD *)(voice_msg + 52), // 语音长度
v17,
v4);
函数 103148E0 回溯再看看,进入了分发函数 10323FF0 中,在一个循环中处理了多种流程,包括显示界面最新消息的流程和解码语音的流程。所以前面找的方向并没有问题,只是缺少认真分析数据和代码的耐心。
不过,目的都达到了,找到了数据处理函数,最后通过 hook 这个函数就能拿到语音数据。
另外可以看到语音数据中包含 SILK_V3 的字符,这种编码音频格式是 Skpye 曾经使用的一种编码方式,后来开源了。目前播放器并不能直接播放该编码音频文件,所以需要转码为 MP3 等格式。不过可喜的是已经有大佬完成了这个工作,并开源了工具 silk-v3-decoder。所以把代码拿来整合一下,就可以完整的实现实时 dump 语音聊天数据,转换为 mp3 进行保存,完美。
这是第一次比较成功的应用 CE,整个看来,确实省下来很多定位数据和函数的工作。
但 CE 并不是万能的,要找对方法,找对目标数据才可能成功,对于某些没有明显数据的功能,可能也是无能为力。
最终还是得提高对大型软件的逆向能力,总体实现思路的猜测以及调试验证。
最后,时间仓促,目前只是将保存语音的 demo 更新到到SuperWeChatPC项目中,后续会持续更新,欢迎关注。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.