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

谷歌一搜就能搜到: 404K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6f1k6h3&6U0k6h3&6@1i4K6u0r3L8h3q4J5M7H3`.`. 腾讯自己开发的微信官方的跨平台跨业务的终端基础组件
这个库包含了以下几个部分:
有日志模块就好说了, 因为通常会把函数信息通过日志模块输出出来.
根据官方文档使用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
本来是想使用bindiff进行符号恢复的, 但可能idb文件太大了, bindiff跑着跑着就崩溃了.
没办法, 就使用IDA官方的flair进行:
然后在IDA里应用签名即可:

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

共同点为:
具体脚本代码在github上, 写的比较乱
我采用的方式是模式匹配, 获取到全部需要的指令后, 进行模拟执行原解密逻辑, 然后把解密出来的字符串Patch到放置解密字符串的全局变量处即可.
这种方法的缺点就是模式匹配不一定能正确的匹配到解密逻辑的汇编指令, 因为可能会有编译优化导致两块或多块解密逻辑共用一些指令.
但优点就是简单, 构思简单写着也简单:
为了应对编译优化的情况, 这个脚本还写了一个dec select功能, 即由用户手动选择涉及到的汇编指令, 然后模拟执行再Patch:
通过你的逆向经验可以找到关键函数在sub_182973360处, 然后使用脚本解密字符串, 可以猜出大部分的逻辑.
这里就不再赘述了, 大体逻辑是这样的:
关键逻辑在于v188 = GetMessageBySvrId_181141130(v221, (__int64)&_RCX, (v6 + 200), final_srvid, 0);:
当拿到要撤回的这条消息的SrvID时, 会先1.删除这条消息, 然后2.添加撤回提醒到数据库.
当拿不到要撤回的这条消息的SrvID时, 会直接插入一条撤回提醒到数据库中.
因此想要达到防撤回且带提醒目的则有两种思路:
但实际测试一下可以发现逻辑1是行不通的, 因为执行AddMessageToDBbyWxID时, 使用的SrvID还是原消息的SrvID, 会冲突导致插入失败.
所以无论如果都要去修改SrvID.
经过再次逆向, 确定这两个防撤回思路都可以, 具体思路如下:
最简单的方式即在函数开头就修改SrvID, 让其走'origin msg not found'分支, 这样就会直接在消息最末尾插入一条'撤回提醒'.
但这样有个问题, 就是如果对方发了几条后再撤回的, 那么并不是在撤回的那条那里添加的撤回提醒, 还是在最后面, 这样就不知道对方具体撤回的是第几条了, 比如, 对方发了 1 2 3, 撤回了2, 那效果是1 2 3 '对方撤回了如上消息', 还是在末尾插入的.
此思路即Nop掉call DeleteMessage函数的地方, 不删除要撤回的消息, 然后继续插入撤回提醒.
但实际测试下来发现, nop掉delmsg后, 当调用AddMessageToDBbyWxID时还是插入不成功, 而且函数内部并没有走到插入失败的分支.
那么就需要再次逆向AddMessageToDBbyWxID函数, 这个函数最内层调用的是:
CoAddMessageToDB这个函数, 而这个函数的在'origin msg not found'分支被调用时最后一个参数是1.
那么根据你的逆向经验, 合理猜测, 最后这个参数是一个bool, 控制着是否可以新增local_id.
即为false时只能插入到原消息的位置, 相当于替换了原local_id, 为true则可以新增local_id.
实际动调修改一下发现确实是这样, 那么此思路即:
这种方式解决了思路1的问题, 即对方即使是撤回的前面的消息也会在之前消息的位置插入撤回提醒.
通过静态动态分析可以知道, int64 v6 = _RTDynamicCast( *(arg3 + 472), 0, &off_188151B10,&off_1881E1FB0, 0); //dynamic_cast<> 处拿到的内存是关键内存.
该内存的结构如下:
因此只要在执行CoReplaceOriginMessageByRevoke中的_RTDynamicCast之前或之后, 修改掉srvid处的数据即可.
关键内存即CoAddMessageToDB父函数的第三个参数r8:
这里的revoke_msg和思路1内存处的revoke_msg不一样, 思路1处就是撤回提醒字符串, 这里是经过构造的xml sysmsg字符串.
具体代码逻辑在github上
使用DLL劫持的方式, 发现ilnk2.dll这个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要和第一次的一样, 要不然会插入两条消息撤回提醒.
我选择的Hook点在这里:

这三条指令恰好12个字节, 可以构造一个mov rax, *; + jmp rax;
这样操作完后有个小问题是, 思路1是撤回提醒不会立刻显示, 思路2是撤回的消息不会立刻显示, 都需要点击其他聊天框再点回来刷新一下才会显示, 但也无伤大雅吧.
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;
}
}
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;
}
}
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 2025-4-30 10:01
被0xEEEE编辑
,原因: 新增逆向思路