带自定义提示的防撤回
1.前言
最近太忙了,一直没有时间更新这个文章.今天做点好玩的东西
在第一篇分析获取登录二维码的数据中,我们分析了登录过程中二维码的显示逻辑,现在我们分析下怎么实现带自定义提示的消息防撤回,实现的效果图就是如下所示
可以看到这人一直在辱骂我,哈哈哈
PC微信核心模块:wechatwin.dll (2.6.7.57版本)
相关工具:
- windbg 或者 OD,x64dbg (动态调试工具)
- IDA (静态分析)
- vs2015(开发hook功能模块)
2.分析
大家知道ios/android/mac中都实现了防撤回,这些平台上的分析也是有相似之处,就是搜索"Revoke"的关键字进行分析,我们可以借鉴他们的思路,尝试从关键字"Revoke"入手,我们先脑中想一下整个撤回逻辑的实现
- 对方在一个终端上发起一个撤回消息,服务端收到这个通知,下发消息给我方的所有终端
- 我方所有的终端收到了服务端发来的撤回消息通知
- 我方所有的终端实现撤回消息逻辑
可以看出,如果我们要实现防撤回逻辑,需要在第二步的网络数据层或者第三步的撤回逻辑实现层做点手脚,我们先搜索下关键字"Revoke"吧
可以看到Revoke相当之多了, 初步可以认为上面标注的消息中可能存在我们需要查找的逻辑,通过调试确认,能发现 On RevokeMsg svrId : %d
这个就是服务端发来的撤回消息通知,看下下图中相关的逻辑
可以看出来服务端对于撤回消息的定义是10002,上图中还标注了sub_10252f80这个函数,下面解释下为什么要标注这个函数
看看上图中对sub_10483FE0的汇编调用
sub esp, 14h
mov ecx, esp;
push 0
push dword ptr [eax+4]
call sub_10483FE0
再看下sub_10483FE0函数的汇编
push ebp
mov ebp, esp
sub esp, 8
push esi
mov esi, ecx
mov ecx, [ebp+arg_0]
push edi
mov dword ptr [esi], 0
mov dword ptr [esi+4], 0
mov dword ptr [esi+8], 0
mov dword ptr [esi+0Ch], 0
mov dword ptr [esi+10h], 0
综合起来看,可以看出来
sub esp, 14h
mov ecx, esp;
mov esi, ecx
mov ecx, [ebp+arg_0]
push edi
mov dword ptr [esi], 0
mov dword ptr [esi+4], 0
mov dword ptr [esi+8], 0
mov dword ptr [esi+0Ch], 0
mov dword ptr [esi+10h], 0
其实就是有两个含义
而且从sub_10483FE0函数中可以发现这是一个MultiByteToWideChar的操作,可以推断下这个类其实有点类似Wstring,那么对于下面的汇编函数来说,此时ecx是wstring, dword ptr [eax+4]是string(UTF-8)
sub esp, 14h
mov ecx, esp;
push 0
push dword ptr [eax+4]
call sub_10483FE0
那将断点断在在call sub_10483FE0上, 先记录下此时的ecx的值,等执行完call sub_10483FE0,再dd刚才保存的ecx的值,下面用xxxxx打码了一些个人信息
0:000:x86> dd 00efcb20-14
00efcb0c 13f0e7f0 000000d6 00000100 00000000
00efcb1c 00000000 a6d44d71 00efee84 00efe484
00efcb2c 00000000 00000000 00000000 00000000
00efcb3c 00000000 00000000 00000000 00000000
00efcb4c 00000000 00000000 00000000 00000000
00efcb5c 00000000 00000000 00000000 00000000
00efcb6c 00000000 00000000 00000000 00000000
00efcb7c 00000000 00000000 00000000 00000000
0:000:x86> du 13f0e7f0
13f0e7f0 "<sysmsg type="revokemsg"><revoke"
13f0e830 "msg><session>wxid_xxxxxxxxxxxxx"
13f0e870 "</session><msgid>1675514177</msg"
13f0e8b0 "id><newmsgid>2468247753633590716"
13f0e8f0 "</newmsgid><replacemsg><![CDATA["
13f0e930 ""xxxxxxx" 撤回了一条消息]]></replacemsg"
13f0e970 "></revokemsg></sysmsg>"
可以看到拿到了服务端的消息数据,注意其中的 xxxx 撤回了一条消息 这个和微信界面显示是一致的,那么可以知道微信其实提取了replacemsg里面的消息显示给界面
ida中搜索"replacemsg"这个字符串,可以发现sub_10257570中是唯一处理replacemsg的函数,并且可以找到一条sub_10252F80 -> sub_10257570 的调用链条,现在可以知道,sub_10252F80这个函数其实就是第三步的撤回逻辑实现层,下图是sub_10252F80的逻辑,一步步的跟下去,可以知道sub_10253250这个函数是逻辑实现层
下图是sub_10253250函数中的逻辑,经过调试后能发现,如果v69为0的时候,界面仍然会显示出来 xxx撤回了一条消息 , 但是实际上这条消息并没有被撤回,所以我们的思路很简单,Patch这块的代码,让它一直走else里面的逻辑即可
3.深入挖掘
到第二步其实已经就可以了,但是其实还是有一个问题的,你会发现,上面在阻止别人撤回的同时也阻止了自己的撤回,更糟糕的是,当自己发起撤回的时候,界面会弹出来两次 你撤回了一条消息 ,一开始其实还好了,自己用这个插件的时候倒是能接受,后来把这个插件给好友使用后,被一直吐槽,说体验很差,还有就是希望加上自定义的撤回提醒,吐槽的久了以后,那就进一步研究下,当自己发起撤回的时候,就不拦截这个撤回了
注意看下sub_10253250函数中下图的代码段, 其中v46是个结构体,sub_1004E320初始化这个结构,sub_1025A390是对这个结构体的赋值,v50是结构体中的成员,可以看到当v50为10000的时候,sub_10253250是什么事情也不做就退出了,这个结构体值得我们的关注,它里面的成员对sub_10253250的逻辑有很大的影响,调试观察下这个结构体的数据吧
将断点断在sub_1025A390执行后,直接能打印出来此时v46中的值,然后先让别人撤回一条消息,再自己撤回一条消息,可以看到两次v46内容的差异性
为啥使用这两个数据的作为差异比较呢,当自己撤回的时候,微信还提供重新编辑的功能,但是当别人撤回的时候,微信就直接撤回了,也就是说,微信其实是保存了是自己撤回还是别人撤回这个变量作为区分的
再看下sub_10253250函数中下图的代码段
查看sub_102541D0 -> sub_102542A0 的调用,在sub_102542A0中可以看出当a1为1的时候,会提供撤回编辑的能力,这个a1是 (_DWORD )(v7 + 52) != 0 赋值的 v7就是v46,也就是上图中的差异的位置
至此就找到了判断是别人撤回还是自己撤回的标志,解决了第一个问题,那下面还要解决第二个问题,那就是撤回消息的定制
既然sub_102542A0函数中提供了撤回编辑的能力,并且可以在sub_102542A0中看到下面的函数片段
对应的汇编操作
可以看出来这个是字符串拼接,a5就是需要被拼接的字符串.调试看下这个数据
Breakpoint 1 hit
WeChatWin!IMVQQEngine::`default constructor closure'+0x1a7121:
69a844a1 85c0 test eax,eax
0:000:x86> du eax
0c073b08 ""xxx" 撤回了一条消息"
也就是说对a5进行修改就可以了,a5是sub_102542A0的参数,那么在sub_102541D0中看下a5的赋值
好了,只需要修改lpmem就行了.
4.工程实现
现在我们知道只需要hook住sub_102541D0,然后判断下撤回的来源
- 如果是别人撤回的,Patch掉sub_10253250的v69的逻辑,并且修改撤回的字符串消息为我们自定义消息
- 如果是自己的撤回,恢复sub_10253250的v69的逻辑
下图中SigPatternRevoke2_6_7_40是sub_102541D0函数头开始的特征码
下图是代{过}{滤}理函数的实现,注意修饰符__declspec(naked) 就是告诉编译器按照我们写的汇编代码生成代码,不要偷偷的再加入一些多余的汇编代码
解释下 上图 中间那段汇编push寄存器的来源, 首先是Push edx, 从下图中可以看到sub_102541D0中判断自己还是别人撤回的标志位是[exi+52],而esi来源于edx,故代{过}{滤}理函数需要的参数是edx寄存器
从上图中可以看到要修改的字符串是[ebp+8]的位置,而ebp又是
push ebp
mov ebp, esp
也就是说当断点断在 push ebp这个位置的时候,此时的[esp+4]就是字符串了, 而看下代码中的代{过}{滤}理函数
__asm
{
PUSHAD;
sub esp, 20;
}
其中 pushad会使得 esp-0x20(压了8个寄存器), 然后又 sub esp, 20(0x14), 所以代{过}{滤}理函数 esp + 0x14 + 0x20 + 4 的位置就是字符串了
剩下的RevokeMsg2_6_7_40函数就是比较简单了
- 需要注意下这个字符串使用的内存不是crt的堆,而是进程堆就可以了. 至于为啥这样,这里就当留个作业了,提示下sub_10484190函数内有答案
- PatchMemoryUCHAR中修改的就是sub_10253250的v69的逻辑
具体的工程实现就在上篇文章上增加而来,还是放到百度云盘了
网盘链接 提取码: 3964