微信4.0防撤回带提醒 (符号恢复+字符串解密)
前言
网上的防撤回都是搜字符串去Patch, 并没有去逆出撤回操作的真正逻辑, 且无法做到带提醒的效果.
本文将分三步去逆向撤回操作的逻辑:
- 符号恢复
- 字符串解密
- 函数逻辑逆向
并采用dll劫持的方式达到最终效果, 效果预览:
代码: 3f7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6q4c8f1g2q4K9r3g2^5i4K6u0r3f1X3g2$3L8$3E0W2d9r3!0G2K9H3`.`.
0. 环境
微信版本: 4.0.3.22
IDA Pro: 9.1
x64dbg: Mar 15 2025, 15:54:24
1. 符号恢复
对于一个大型软件来说, 一定会用到很多开源库, 因此可以恢复一部分符号.
且如果不做符号恢复, 很难猜测出上下文逻辑. 我个人是比较喜欢在逆向之前能恢复多少就恢复多少符号.
通过浏览字符串可以看到微信用到了一个叫mars的库:

谷歌一搜就能搜到: 054K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6f1k6h3&6U0k6h3&6@1i4K6u0r3L8h3q4J5M7H3`.`. 腾讯自己开发的微信官方的跨平台跨业务的终端基础组件
这个库包含了以下几个部分:
- comm: 可以独立使用的公共库, 包括 socket、线程、消息队列、协程等;
- xlog: 高可靠性高性能的运行期日志组件;
- SDT: 网络诊断组件;
- STN: 信令分发网络模块,也是 Mars 最主要的部分。
有日志模块就好说了, 因为通常会把函数信息通过日志模块输出出来.
1.1 编译mars
根据官方文档使用build_windows.py进行编译, 这个脚本用的是vs2019, 所以可以猜测微信本体也用的vs2019编译.
不过他这个脚本有一些小问题, 自己改改就能编译成功:
需要先设置$env:MSVC_TOOLS_PATH=""和$env:MSVC_TOOLS_PATH=""环境变量, 然后py .\build_windows.py --mars
生成静态库mars.lib
同时拿到vs2019的静态库: libcmt.lib + libcpmt.lib + libvcruntime.lib
1.2 恢复符号
本来是想使用bindiff进行符号恢复的, 但可能idb文件太大了, bindiff跑着跑着就崩溃了.
没办法, 就使用IDA官方的flair进行:
- 使用pcf.exe生成pat
- 使用sigmake.exe生成sig
然后在IDA里应用签名即可:

可以看到最重要的日志部分的符号被恢复了, 但显然输出的文本是动态解密的, 因此需要解密字符串.
2. 字符串解密
2.1 运行时解密模式
通过观察可以看到大体有两种解密逻辑:

共同点为:
- 可以先定位到cmp *, 0; 之后可能穿插着几条被优化提前的汇编指令
- 然后jnz addr; xor reg, reg;之后为两个lea;
- 第一种模式为末尾一个cmp reg, value; jnz addr; mov *, 1; 其中reg为上面xor的
- 第二种模式为中间一个cmp reg, value; jz addr; 最后末尾为一个jmp. 且mov *, 1;指令在jz跳转到的addr处
2.2 字符串解密脚本
具体脚本代码在github上, 写的比较乱
我采用的方式是模式匹配, 获取到全部需要的指令后, 进行模拟执行原解密逻辑, 然后把解密出来的字符串Patch到放置解密字符串的全局变量处即可.
这种方法的缺点就是模式匹配不一定能正确的匹配到解密逻辑的汇编指令, 因为可能会有编译优化导致两块或多块解密逻辑共用一些指令.
但优点就是简单, 构思简单写着也简单:
为了应对编译优化的情况, 这个脚本还写了一个dec select功能, 即由用户手动选择涉及到的汇编指令, 然后模拟执行再Patch:
3. 逻辑逆向
3.1 关键函数逆向
通过你的逆向经验可以找到关键函数在sub_182973360处, 然后使用脚本解密字符串, 可以猜出大部分的逻辑.
这里就不再赘述了, 大体逻辑是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | BYTE * CoReplaceOriginMessageByRevoke_182973360(int64 arg1, _BYTE * arg2, __int64 arg3)
{
CheckIsReuestRevokingMessage_182976AE0(arg1, arg3);
int64 v6 = _RTDynamicCast( * (arg3 + 472 ), 0 , &off_188151B10,&off_1881E1FB0, 0 ); / / dynamic_cast<>
if (!v6) {
XLogger::DoTypeSafeFormat( "sys_extinfo is nullptr" );
return ;
}
if (CheckIsProcessingRevokeNewXml_1829770A0(arg1, v6 + 200 , * (v6 + 160 )))
return ;
v19 = sub_18295DC80(_RCX.m128i_i64[ 0 ], * (v6 + 160 ));
if (v19)
{
XLogger::DoTypeSafeFormat( "message:%_, pat revoke msg , no need to show" );
return ;
}
GetMessageBySvrIdOnRecent_181155750(?, &v180, (v6 + 200 ), * (v6 + 160 )); / / 获取消息类型
if ( v180.m128i_i64[ 1 ] = = 10000 ) / / 系统消息(撤回、加入群聊、群管理、群语音通话等)
{
XLogger::DoTypeSafeFormat( "message:%_, alerdy is system message" );
return ;
}
if (v180.m128i_i64[ 1 ] = = 0x3E00000031 ) / / 拍一拍消息
{
XLogger::DoTypeSafeFormat( "revoke message:%_, is pat message" );
DeleteMessage_18114F590(?, &v180, 1 ); / / True
return ;
}
ConstructRevokeMsg_181A09E20(v6, &?); / / 构造revoke的sysmsg的xml格式
final_srvid = GetFileFinalSvrid_181175CF0(?, * (v6 + 160 )); / / srvid
if (final_srvid = = 0 )
{
bool add_revoke_flag = false; / / 是否将消息成功加入到数据中
v188 = GetMessageBySvrId_181141130(v221, (__int64)&_RCX, (v6 + 200 ), final_srvid, 0 );
if (v188) {
DeleteMessage_18114F590(args[ 0 ], (__int64)&v180, 0 ); / / 删除原消息
add_revoke_flag = AddMessageToDBbyWxID_181198500( * &v219[ 0 ], &_RCX, &args_); / / 把revoke消息添加到数据库中
/ / 即首先删除srvid为...的消息, 再插入一条srvid为...的撤回消息, 两条消息的srvid相同
}
else {
add_revoke_flag = sub_181198460(v221, (__int64)&_RCX, (__int64)&args_); / / 插入revoke msg到数据库中
XLogger::DoTypeSafeFormat( "origin msg not found, just insert placeholder sysmsg, session_name:%_,serverId:%_" )
}
if (!add_revoke_flag) {
XLogger::DoTypeSafeFormat( "add system message to db failed" );
}
}
else
{
XLogger::DoTypeSafeFormat( "old svrid:%_ can't get msg, will try new svrid:%_" );
return ;
}
}
|
关键逻辑在于v188 = GetMessageBySvrId_181141130(v221, (__int64)&_RCX, (v6 + 200), final_srvid, 0);:
当拿到要撤回的这条消息的SrvID时, 会先1.删除这条消息, 然后2.添加撤回提醒到数据库.
当拿不到要撤回的这条消息的SrvID时, 会直接插入一条撤回提醒到数据库中.
因此想要达到防撤回且带提醒目的则有两种思路:
- 直接Nop掉DeleteMessage函数, 让其只执行插入撤回提醒到数据库的操作.
- 在内存中修改SrvID让其走else分支, 即origin msg not found那里.
但实际测试一下可以发现逻辑1是行不通的, 因为执行AddMessageToDBbyWxID时, 使用的SrvID还是原消息的SrvID, 会冲突导致插入失败.
所以无论如果都要去修改SrvID.
3.2 关键内存逆向
通过静态动态分析可以知道, int64 v6 = _RTDynamicCast( *(arg3 + 472), 0, &off_188151B10,&off_1881E1FB0, 0); //dynamic_cast<> 处拿到的内存是关键内存.
该内存的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | / / v6是关键内存 其保存着撤回所需的信息
/ / class v6
/ / {
/ / + 00 vtable
/ / + 08 unk1
/ / + ...
/ / + A0 srvid: int64 / / + 160
/ / + A8 revoke_msg: std::string / / + 168
/ / + C8 wxid: std::string / / + 200
/ / }
/ / 其中
/ / std::string | size:( 0x20 )
/ / {
/ / + 00 data_ptr: const char[ 16 ]
/ / + 10 size: int64
/ / + 18 capability: int64
/ / }
|
因此只要在执行CoReplaceOriginMessageByRevoke中的_RTDynamicCast之前或之后, 修改掉srvid处的数据即可.
4. 劫持 + HOOK思路
具体代码逻辑在github上
使用DLL劫持的方式, 发现ilnk2.dll这个dll的导出函数比较少, 使用以下方式直接转发:
1 2 | / / 劫持ilink2.dll - > ilink2Org.dll
|
然后在dll加载的时候进行Hook, 执行修改内存的逻辑.
我选择的Hook点在这里:

即执行完CheckIsReuestRevokingMessage函数之后, 此时[rdi + 1D8]即是需要的内存.
后两条指令共15个字节, 且不涉及重定位操作, 所以HOOK逻辑是把这些指令改为mov rax, HijackLogicWarpper; + jmp rax;(12个字节)
然后执行完HijackLogic后, jmp 中转区; 在这块内存里执行原先两条汇编指令 + jmp next_insn;
即|jmp hijack| -> |hijack_logic + jmp transfer_zone| -> |org_logic + jmp org_next_insn| -> |...|
还有一个小细节需要注意的是, CoReplaceOriginMessageByRevoke会被执行两次, 第二次修改的SrvID要和第一次的一样, 要不然会插入两条消息撤回提醒.
结语
这样操作完后有个小问题是, 撤回提醒不会立刻显示, 需要点击其他聊天框再点回来刷新一下才会显示, 但也无伤大雅吧.
最后于 8小时前
被0xEEEE编辑
,原因: 标题修改