微信 PC 端技术研究-如何找到消息发送接口
by anhkgg (公众号:汉客儿)
2019 年 2 月 18 日
微信 PC 开源项目地址:https://github.com/anhkgg/SuperWeChatPC
准备工具:Cheat Engine,OllyDbg,IDA。
前一篇(微信 PC 端技术研究(2)-保存聊天语音)已经说过 CE 是什么,也应用 CE 研究了如何保存微信语音,这篇继续使用 CE 和 OD 来研究一下微信的消息发送接口。
思路大概是这样:在消息框中输入内容之后,通过 CE 找到内容地址,然后通过内存断点来找到发送该数据的相关代码,从而找到消息发送接口。
在输入框输入一个比较特别的文字内容(避免搜索时太多内存选项)后,使用 CE 搜索该内容地址。
由于已经知道确切的消息内容,很容易就能通过 CE 的 Exact value->String 来找到内容地址,修改内容多次筛选,最后留下两个结果(详细操作见上一篇文章)。
通过 CE 修改一下内存的内容,微信输入框中内容同步改变,说明这个内存地址就是输入框中内容地址,最终确认地址是2A1E1A8
。
接着在点击发送按钮之后,输入框内容会被清空,所以第一想法就是对内存地址下内存写入断点,可以找到发送过程中清空内容的代码。
打开 OD,挂载到 WeChat.exe 进程,在右下角数据窗口 Ctrl+G 输入2A1E1A8
,然后右键选择断点->内存写入断点。
F9 让 OD 跑起来,然后点击微信发送按钮,没想到意外发生了,输入框内容清空了,但是断点却没有触发。
怎么回事?断点弄错?地址找错?暂时没有答案。
用 CE 多次重复前面的操作,地址依然是这个地址,断点就是不触发。
通过 OD 查看到,在输入框清空后,2A1E1A8
的内容确实没有变化,和发送前一样,并且在重新输入新的内容之后,该内存内容同步更新。
所以结论就是输入框内容地址确实是2A1E1A8
,但是清空输入框并不是清空该内存内容,猜测编辑框可能通过控制字符串长短来控制显示的,清空输入框内容就是设置字符串长度为 0。
清空输入框没有进展了,那怎么办呢?
尝试去找了其他数据,比如发送按钮的发送(S)
,发送按钮的提示内容不能发送空白内容
等等,数据地址也可以很快找到,但是和我们的分析目标偏的太远了。
几番折腾后,作罢。
转念一想,清空不行,发送总的读取输入框中内容吧,那换成内存访问断点尝试一下。
依然是在右下角数据窗口 Ctrl+G 输入2A1E1A8
,然后右键选择断点->内存访问断点。
完成后回到微信界面,没想到直接断下了,我还没点发送按钮呢。根据以前的经验,下意识就觉得是界面刷新显示文字触发了断点,这可能会影响分析,根本没办法通过发送按钮来触发内存访问断点。
一般解决方法有:
放弃了一般的解决方法,我决定看看本次断点究竟干嘛了。
注意到断点的位置不是微信模块WeChatWin.dll
中,而是在msftedit.dll
,很少见的一个模块。根据目录可以看到是微软系统的一个模块,名字中的edit
也可以看出这应该是一个编辑框相关的模块。
可执行模块, 条目 20
基址=6F050000
大小=00094000 (606208.)
入口=6F05D53D msftedit.<ModuleEntryPoint>
名称=msftedit (系统)
文件版本=5.41.21.2510
路径=C:\Windows\System32\msftedit.dll
好像和我们的分析目标很贴近嘛,在 OD 中数据窗口右键断点->删除内存断点,然后按下 Alt+F9 回到用户模块领空,也就是跳过系统模块的代码,直接回到微信的模块代码中,省过对系统代码的分析。
看到返回到6E20CCC2
这个地址,上一行代码就是调用msftedit.dll
的函数,我们对其下一个断点,鼠标点击到6E20CCBF
这行代码,按下F2
下一个int 3
断点,然后F9
跳过本次分析。
OD 继续断下,此次直接断在了6E20CCBF
这个位置,可以看到call
调用了msftedit.6F05AD69
,这是个什么函数呢?
既然msftedit.dll
是微软模块,那么肯定是有符号的嘛,嘿嘿。
这里可以直接在 OD 中加载符号来分析,使用方法是:
1.在 WingDbg 目录下拷贝 dbgeng.dll ,dbghelp.dll ,srcsrv.dll ,symbolcheck.dll ,symsrv.dll ,symsrv.yes ,一共 6 个文件至 OD 目录下。
2.打开 OD,设置符号路径。调试--->选择符号路径。
3.设置 StrongOD 的插件选项。选择加载符号。
原文: https://blog.csdn.net/sr0ad/article/details/8253311
但是只支持本地符号,也就是得自己下载了模块对应符号到本地,OD 设置符号文件路径后,才能正常使用,有点麻烦。
我这时候一般就会使用 IDA 了,因为它会自己在线下载模块对应的符号,很方便。
用 IDA 打开msftedit.dll
,等待些许时间,IDA 下载符号,解析等等完成后,我们去找到msftedit.6F05AD69
对应的函数究竟是个什么东西。
但是这里msftedit.6F05AD69
的模块基址是6F050000
,而 IDA 解析使用的是默认基址0x6FCD0000
,要么修改 IDA 解析基址为6F050000
,等待 IDA 重新解析,要么通过偏移计算对应地址。
再解析等太久,直接计算吧,所以要安利我写的一个小工具(偏移计算工具),能够快速计算地址,具体使用见相关文章。
再 IDA 中按下 g,输入6fcdad69
,找到msftedit.6F05AD69
对应函数为CTxtEdit::OnTxInPlaceActivate
。
很明显通过名字OnTxInPlaceActivate
可以看出是编辑框中文字激活状态(显示)下就会触发该函数,这不是重点。
重点看CTxtEdit
,不言而喻,这就是msftedit.dll
中实现的编辑框的类。
如果写过MFC
相关代码,应该很快就能想到CTxtEdit
肯定还有其他读内容、写内容的函数,叫做GetXXX
或者SetXXX
。
在 IDA 的函数列表中翻看一下,果然很快就找到了CTxtEdit::GetTextEx
和CTxtEdit::SetText
。
但到底这两个函数是不是编辑框读写内容的函数呢,我们对这两个函数下断点试试,通过工具算到在 ID 调试中这两个函数的相应地址为6f068437
和6f056d37
。
在 OD 的底部命令窗口输入bp 6f068437
和bp 6f056d37
,删掉之前CTxtEdit::OnTxInPlaceActivate
的断点,然后F9
跑起来。
回到微信界面,这次能够正常显示了,点击发送按钮。OD 触发断点,断在了6f068437
也就是CTxtEdit::GetTextEx
上,很明显这是发送函数在读取输入框内容。
此时的调用堆栈是这样的:
调用堆栈
地址 堆栈 函数过程 / 参数 调用来自 结构
0026E280 6F06842D msftedit.6F068437 msftedit.6F068428 0026E3FC //CTxtEdit::GetTextEx
0026E400 6E20D239 包含 msftedit.6F06842D WeChatWi.6E20D233 0026E3FC
0026E43C 6DBD38EB 包含 WeChatWi.6E20D239 WeChatWi.6DBD38E8 0026E438 //TxtEdit_GetText
0026E5AC 6DC15B65 ? WeChatWi.6DBD3860 WeChatWi.6DC15B60 0026E5A8 //sendBtn_GetText
0026E60C 6DC15DEE WeChatWi.6DC15B10 WeChatWi.6DC15DE9 0026E608 //sendbtn_click
0026E618 6E20BFB8 WeChatWi.6E20BEF4 WeChatWi.6E20BFB3 0026E614
0026E62C 6E20362E WeChatWi.6E20BF90 WeChatWi.6E203629 0026E628
0026E6CC 6E203589 WeChatWi.6E2035A7 WeChatWi.6E203584 0026E6C8
0026E820 6DC53695 ? WeChatWi.6E20352E WeChatWi.6DC53690 0026E81C
在 OD 中回溯调用堆栈跟踪返回到WeChatWi.6E20D239
,看到右侧堆栈窗口已经获取到输入框中内容,证明前面的分析没有问题。
再次回溯两层到WeChatWi.6DC15B60
,可以看到堆栈中的参数依然是获取到的输入框内容。
[0026E5E4] = 0828C070
[0828C070 + 4] = 0828CAF0 => a12bcAAAAA
此时函数首地址是WeChatWi.6DC15B10
,进入到IDA
中对应函数100d5b10
(你要问我为什么此时进入 IDA 查看?我只好说其实这个步骤花费了很多时间,一边 OD 调试,一边 IDA 辅助确认等等,过程并没有这么顺利,篇幅原因省略),然后按下 x 回到上层函数,看到如下代码:
看到click
很明显可以看出这就是发送按钮的响应函数了(相关知识可以了解duilib
编程,微信界面是duilib
实现的)。
到目前找到了发送消息的函数,但还并不是消息发送接口,这还只是界面的操作函数,具体发送消息接口应该在该函数内部被调用。
先粗略地在 OD 中跟一遍WeChatWi.6DC15B10
的代码逻辑,函数很多,没法很快确认哪个函数是消息发送接口。
截取部分代码感受一下,大概 11 个函数。根据 OD 跟的逻辑大概是sendBtn_GetText_10093860
->sub_100DD340
->sub_100C50C0
->sub_10094100
->sub_100DD9D0
->sub_100C4450
->sub_10323DF0
->sub_100DE120
。
if ( sendBtn_GetText_10093860(a1->unk_560, (int)&savedregs, a2, a3, msg) <= 0 )// 这里是获取 msg
{ // x
//省略一大段逻辑
}
if ( msg[0] != msg[1] )
{ // x
//省略一大段逻辑
}
if ( sub_100DD340() )
{ // x
//省略一大段逻辑
sub_1047C070(&v34, v23);
sub_100DB8C0((int)a1_, v34, v35, (int)v36, v37, (int)v38, v39, v40, (int)v41, msg_);
}
if ( sub_100C50C0((_DWORD *)(a1_->unk_558 + 2528), (int)msg, (int)v43) )
{
sub_10094100((_DWORD *)a1_->unk_560);//
sub_100DD9D0(msg);
sub_100C4450((_DWORD *)(a1_->unk_558 + 2528), (_msg *)msg);//
v31 = sub_10323DF0();
sub_100DE120(v31, (int)a1_, (int)sub_100D6C40, 0, v40, (int)v41, msg_);// retn 18
v12 = 1;
}
else
{
//省略一大段逻辑
sub_10108D60(v30, *(&a1_->unk_558 + 1), v33, (int)v34, v35, v36, (int)v37, v38, v39, v40, v41);
}
通常通过调试每个函数的参数、返回结果等基本可以猜测到函数功能,然后来找到消息发送接口。
但这里我偷懒了,因为参数结构复杂,一时半会没法找到关键点,有点晕了。
所以我通过排除法来一一筛选函数,最多 11 次左右就能找到消息发送接口。举个例子,如果sub_100DD340
是消息发送接口,在我手工屏蔽其功能之后,消息肯定发不出去了,那么我就可以通过看到的结果(是否发送成功)来确认sub_100DD340
是不是要找到的消息发送接口。
具体屏蔽方法:
sub_100DD340
函数内部,找到函数结尾,找到 retn xx 类似代码sub_100DD340
函数开始修改汇编代码为 retn xx,双击输入 retn xx 即可这样sub_100DD340
函数直接在入口就返回了,功能没有了,也保证了函数调用时的栈平衡。
在确认sub_100DD340
并没有影响消息发送之后,通过右键撤销选择处修改恢复修改的内容。
如此重复筛选其他的函数,最终确认sub_100C4450
为发送消息函数。代码接口如下:
sub_100C4450((_DWORD *)(a1_->unk_558 + 2528), (_msg *)msg);//
msg
是发送内容,a1_->unk_558 + 2528)
是当前聊天窗口的好友信息,包括wxid
和名字之类的信息。
但作为接口依然不够简洁,需要构造好友信息,比较复杂,所以继续深入sub_100C4450
内部,看看是否能够找到最简单的接口,比如:
sendmsg(wxid, msg); //传入发给谁,发什么即可
sub_100C4450
内部依然很复杂,使用和前面同样的方式,先大致跟一遍执行流程,然后通过排除法逐个筛选。
if ( !sub_100C43D0(msg_.buf, msg_.len, msg_.maxlen, wxid_) )// 是不是全是特殊字符\r\n\t 等,是返回 1,不是返回 0
{
sub_1007D390();
msg_packet = sub_102DA4A0((int)wxid, (int)&v67, msg__, &unk, 1);// 数据打包,发送
sub_100494E0(msg_packet_, (size_t)msg_packet);//
sub_1004B550(&v67); //
v11 = sub_102478D0();
v12 = sub_10402C10((int)v11);
v89 = (void **)v13;
if ( sub_10402C10((int)msg_packet_) != v12 || v14 != v89 )
{
if ( sub_100C6770(this_) ) //
{
sub_1004BBF0((int *)&msgpacket);//
sub_10056940((int *)&msgpacket, (size_t)msg_packet_);//
sub_100C56D0(this___, (size_t)&msgpacket, 1);
sub_10081210((LPVOID *)&msgpacket);
v16 = sub_100C0EC0();
sub_10247250((int **)v16, (int)path);
}
}
}
if ( (signed int)(msg->msgend - (unsigned int)msg->msg) / 0x24 != 1 )
v9 = sub_10323DF0();
sub_10324E70(v9, msg_.len, msg_.maxlen, (int)wxid_, (int)path);
sub_100ADA10(&msg___);
这一次筛选屏蔽的方法换一种,直接在某个函数执行完成之后,通过jmp
跳到sub_100C4450
结尾,如果某次消息发送成功,最后执行的函数就是我们要的接口。
很幸运,这次在第三个函数就找到了消息发送函数sub_102DA4A0
,看看它的参数:
sub_102DA4A0((int)wxid, (int)&v67, msg__, &unk, 1);
sub_102DA4A0@<eax>(int wxid@<edx>, int a2@<ecx>, wxstring *msg, _DWORD *a4, int a5)
下图是调试中看到的数据,确认接口没有问题。至于其他两个参数,经过分析是用于接收输出的,没有实际作用,在此不赘述。
如此分析消息发送接口的工作完成,找到了和预期基本一致的接口函数。
篇幅好像有点长了,最后做一下此次分析的总结:
此次分析中 CE 找到地址是第一步非常关键的点,直接就进入了函数调用堆栈内部,对此次分析作用非常明显。
再就是在发送消息响应函数内部,逐个分析找到消息发送接口函数中,通过修改指令来屏蔽函数功能来确认函数功能,比每个函数去分析参数猜测确认功能来的更快,效果更明显。
调试工具非常重要,动( OD )静态( IDA )分析结合能够提高分析速度。
OD 适合分析函数参数、解析数据结构、确认函数功能,IDA 适合分析函数逻辑、整体函数结构、代码框架等等,各有优势。
最后,再次安利一下开源项目https://github.com/anhkgg/SuperWeChatPC,此次分析的发送消息接口也会在后续合入到项目中,欢迎star
和PR
。
相关文章:
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.