钉钉消息防撤回功能研究与实现-可查看历史消息[文件/图文/管理员/链接 撤回拦截]
Posted SwBack
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了钉钉消息防撤回功能研究与实现-可查看历史消息[文件/图文/管理员/链接 撤回拦截]相关的知识,希望对你有一定的参考价值。
研究背景
由于在某个大学进行上课的时候,遇到的某个老师,总是习惯发过的消息,到第二天的时候撤回,我们用聊天工具的其中一个原因,不就是因为可以随时去查看发过的消息吗,,而这位老师的操作,也让包括我在内的很多人感到痛不欲生。
想一想,当自己想要去看下聊天记录的时候,发现消息都被撤回,每天那么多的消息,难道我们还需要每一条都复制下来到本地吗?
以上是本次技术研究的初衷,并未涉及到任何敏感信息的获取,逆向,文中所有逆向操作,均未实际进行。
前置知识
废话不多说了,简单介绍下在本次实验中需要的基础知识,javascript
, 逆向
逻辑思考能力
实际都是废话,因为有很多过程都已经被很多大佬总结出来了。这里就不去做太多的赘述。
实操过程
打开安装目录
首先我们打开钉钉的安装目录。右键钉钉图标,打开文件所在位置。 依次进入main
- current
文件夹下面
复制关键文件
另外如果有人的钉钉安装目录下存在current_new 文件,是 因为钉钉更新的原因. 需要确定钉钉究竟使用的哪个对应的文件。
找到web_content.pak
或者web_content.dat
我这里是web_content.dat文件。
该文件是可以解压的,我们可以通过解压工具解压,然后通过搜索关键字 撤回了一条消息
来快速定位到关键的JS 代码。
这里我将这个文件复制出来到桌面, 千万不要直接修改该文件,即使非要如此,也一定要有备份。
你解压的时候发现存在密码,**密码长度是9位,由数字和小写字母组成。**不要尝试去暴力破解… 下图是暴力破解的时间。。
这里有两种获取密码的方式
第一种破解密码方式
由于这个文件钉钉需要不断的去读取这个文件,所以钉钉的程序中肯定有该密码或者说文件的加密方式。
那么我们可以通过逆向工具调试钉钉的主程序即DingtalkLauncher.exe
常用的工具Ollydbg
IDA
这里就不做赘述了,这种方式,会的人不用我这里写,不会的人,写了照着做也整不出来。直接跳到第二种方式即可。
第二种方式
上面在说第一种方式的时候,说了可以通过逆向去破解钉钉的加密方式,同时也可以获取密码的格式.这里借助其他大佬的一段话。。
————————————————————————————————————————————————
ollydbg拉起DingTalk.exe后,
**方法一、**在CreateFileW下断点,F9运行多次后,当出现web_content.dat时,向上回溯搜索字符串 web_content.dat 的引用,可以找到相关代码
**方法二、**待模块MainFrame.dll加载后,在内存窗口模块MainFrame.dll中Ctrl+B查找 web_content.dat,找到后记录下字符串地址,然后到CPU窗口查找地址常量即可快速定位到相关代码。
6371973C 6A 09 push 0x9
6371973E 6A 03 push 0x3
63719740 8D4424 4C lea eax,dword ptr ss:[esp+0x4C]
63719744 50 push eax
63719745 8D8C24 98000000 lea ecx,dword ptr ss:[esp+0x98]
6371974C E8 EFE88AFE call MainFram.61FC8040 ; get_key
这里密码key长度是9,具体算法是 md5(version)[2:12],其中version可以从DingTask.exe的属性中拼接而来
以上内容 来源于 原文链接
————————————————————————————————————————————————
查看钉钉版本号
我这里的版本是7.0.11-Release.3139102
md5加密
可以到在线的md5加密网站 md5加密
我的版本号是 7.0.11-Release.3139102
将版本号进行md5加密,并截取[2,12] 共9位
tip: 索引从0开始
这里我截取的就是 77b8d48f8
解压文件,静态分析
我这里是借助VSCODE 进行搜索,结果如下,
可以看到变量的命名,这里我们继续搜索命名函数。
钉钉的 群主或管理员撤回消息, XX 撤回消息. 撤回的图文消息, 单独文字消息都是调用不同的方法,所以如果想要防撤回所有消息格式,需要多次修改.
这里我就演示一种。
复制 pc_im_recalled_a_message
,继续搜索。
我们锁定文件名最像的那个,
打开之后,代码非常乱,应该是通过vite build 直接构建的,
我们美化一下,
分析-修改-保存
可以通过JSON.stringify
进行打印,然后进行分析.
# 文本消息
"atCustomRoleIds": ,
"atOpenIds": ,
"contentType": 1,
"textContent":
"templateData": [ ],
"templateId": "",
"text": "具体内容"
所以撤回的文本消息就是 i.baseMessage.content.textContent.text
"atCustomRoleIds": ,
"atOpenIds": ,
"attachments": [
"extension":
"imageUrl": "https://cdn2.jianshu.io/assets/web/recommend-author-03cc8798d5cc3f986e49cbcb2eb63079.png",
"picUrl": "@lQDPNDgdkcl3OmDMyMzIsME6THfi_fNeBBZJkRtAdQA",
"rule": "spiderbot",
"source_from": "1",
"source_url": "https://www.jianshu.com/recommendations/users",
"text": "在简书,仍有数百万创作者在坚持产出优质创作,有数千万读者在用心交流创作;众多精彩创作,只在简书看得到",
"title": "推荐作者 - 简书",
"url": "https://www.jianshu.com/recommendations/users"
,
"isPreload": false,
"size": "0",
"type": 16,
"url": "https://www.jianshu.com/recommendations/users"
],
"contentType": 102
所以撤回链接的变量 应该是: i.baseMessage.content.attachments[0].url
简单分析之后,我们在关键的位置,添加拦截代码。
如果想要实现每种消息的防撤回,那么应该添加判断该消息的种类. 如下:
## 文件必须下载过,本地才会存在
try
if(i.baseMessage.content.contentType == 1)
s = " 撤回的消息为:" + i.baseMessage.content.textContent.text;
else if(i.baseMessage.content.contentType == 102)
s = " 撤回的链接为:" + i.baseMessage.content.attachments[0].url;
else if(i.baseMessage.content.contentType == 2)
s = " 撤回消息类型: 图片,暂时无法锁定所在路径";
// s = " 撤回消息类型: 图片 :" + JSON.stringify(i.baseMessage);
else if(i.baseMessage.content.contentType == 3100)
s = " 撤回图文消息为:" + i.baseMessage.content.attachments[0].extension.desc;
else if(i.baseMessage.content.contentType == 501)
s = " 撤回文件名为:" + i.baseMessage.content.attachments[0].extension.f_name + "请在本地搜索[需要保存过]";
else
s = " 未识别出撤回的信息类型";
//s = JSON.stringify(i.baseMessage);
catch (e)
s = " 读取撤回消息失败。"
;
效果如下:
关于图片,经过我不断测试,发现只要消息阅读过,即使没有特意保存到本地,钉钉也会将其下载到你的电脑本地,
保存地址是 C:\\Users\\用户名\\AppData\\Roaming\\DingTalk\\584039362_v2\\resource_cache\\XX\\*.png
可以根据修改时间手动查看图片。
C:\\Users\\用户名\\AppData\\Roaming\\DingTalk\\584039362_v2\\resource_cache\\\\584039362_v2\\ImageFiles\\
中是缩略图
最终修改文件为 chatbox-index.05b147d5.js
修改前关键代码
if(i.baseMessage.shieldStatus===X.s6.YES)let e;e=a?"dt_message_shield_tip_message_file":"dt_message_shield_tip_message",t=g.i18next.t(e)else if(s)let e;e=a?"dt_message_recall_yourecallmessage_file":"pc_conv_list_you_recall_last_message",t=g.i18next.t(e)elselet e;e=a?"dt_message_recall_message_file_format":"pc_im_recalled_a_message";let s=g.i18next.t(e);t=u.createElement("span",null,n," ",s)return u.createElement("div",className:"msg-recall-hint","data-msg-id":i.baseMessage.messageId,t,this.renderReEdit())
修改后内容: 又增加了管理员防撤回
if(i.baseMessage.shieldStatus===X.s6.YES)let e;e=a?"dt_message_shield_tip_message_file":"dt_message_shield_tip_message",t=g.i18next.t(e);tryif(i.baseMessage.content.contentType==1)t=" 管理员撤回的消息为:"+i.baseMessage.content.textContent.textelse if(i.baseMessage.content.contentType==102)t=" 管理员撤回的链接为:"+i.baseMessage.content.attachments[0].urlelse if(i.baseMessage.content.contentType==2)t=" 管理员撤回消息类型: 图片,暂时无法锁定所在路径"else if(i.baseMessage.content.contentType==3100)t=" 管理员撤回图文消息为:"+i.baseMessage.content.attachments[0].extension.descelse if(i.baseMessage.content.contentType==501)t=" 管理员撤回文件名为:"+i.baseMessage.content.attachments[0].extension.f_name+"请在本地搜索[需要保存过]"elset=" 未识别出管理员撤回的信息类型"catch(e)t=" 读取管理员撤回消息失败。"else if(s)let e;e=a?"dt_message_recall_yourecallmessage_file":"pc_conv_list_you_recall_last_message",t=g.i18next.t(e)elselet e;e=a?"dt_message_recall_message_file_format":"pc_im_recalled_a_message";let s=g.i18next.t(e);tryif(i.baseMessage.content.contentType==1)s=" 撤回的消息为:"+i.baseMessage.content.textContent.textelse if(i.baseMessage.content.contentType==102)s=" 撤回的链接为:"+i.baseMessage.content.attachments[0].urlelse if(i.baseMessage.content.contentType==2)s=" 撤回消息类型: 图片,暂时无法锁定所在路径"else if(i.baseMessage.content.contentType==3100)s=" 撤回图文消息为:"+i.baseMessage.content.attachments[0].extension.descelse if(i.baseMessage.content.contentType==501)s=" 撤回文件名为:"+i.baseMessage.content.attachments[0].extension.f_name+"请在本地搜索[需要保存过]"elses=" 未识别出撤回的信息类型"catch(e)s=" 读取撤回消息失败。";t=u.createElement("span",null,n," [作者:SwBack] ",s)
下面的贴图是旧版, 后进行了改进
这里有一点需要注意,不要原样照抄,注意撤回的消息后面跟着的i
在你本地是不是这个,还有 将撤回消息赋值给 s 是因为后面 createElement 调用的是s.
然后保存内容. 进行压缩,然后将内容替换web_content.dat。
重启钉钉,如果没有意外的话.你应该会遇到这样子的情况。
这里我怀疑是因为JS美化的问题,所以我记住修改的位置之后,如下操作,未发现问题.不去美化代码.直接修改原生的JS。
然后去替换文件
效果
修改之后,重启钉钉,就会发现以前撤回的消息,大多数都可以看到了,
只要你的电脑上不是第一次安装钉钉,那么即使是很久以前的消息,也会显示出来。
实验总结
这整个过程,在不断的遇到问题,解决问题的过程就是一个学习的过程.同时也会让自己感觉很有成就感, 这里面需要的技术含量不高,因为主要借助了其他大佬的思路。
另外网上也有很多别人开发的类似工具,插件之类的Q,原理基本上都是这样.并且,如果仔细研究的话,还可以去做到很多事情,比如,让撤回的消息,以卡片的形式展现出来。
拦截已读 状态的发送. 等等。
最主要的还是安全性.可自定义性。 嗯~ 有些有想法的朋友 给我讲可以在撤回消息前给添加前缀. 比如 [**] XXX 撤回了一条消息
,不得不说,这一届的网友,是知道怎么加前缀的。
用Python写微信防撤回脚本,锁定那些被撤回的消息,就是撤回了也可以看到
如果好友短时间发送多条消息然后撤回会难以判断究竟撤回的是哪条信息,只能靠猜。后来我觉得“猜”这个事情特别不Pythonic,研究一段时间后找到了解决方案,不得不惊叹ItChat真的好强大。
之前解决方案
大概是这样:短时间内同一位好友发送了多条消息,当他随便撤回一条消息时,我们不能确定他到底撤回的到底是哪一条消息。只能猜他可能是撤回了最近的一条消息,然后将其他消息贴出来作为备选。代码如下:
target_msg_pattern = '"" 撤回了一条消息'.format(sender_name)
if content == target_msg_pattern:
return_msg = '【】撤回了一条消息:\\n'.format(sender_name)
if len(log[sender_name].items()) == 0:
return_msg = '缓存信息列表为空!'
else:
return_msg += log[sender_name].items()[-1][-1] + '\\n'
if len(log[sender_name].items()) > 1:
msgs = [msg for timestamp, msg in log[sender_name].items()[:-1]]
return_msg += '也有可能是下列信息中的某一条:\\n' + '\\n'.join(msgs)
实际效果是这样:
我这个强迫症简直受不了这么不确定的说法。
分析msg信息
要想确定撤回了哪一条信息,就必须先熟悉普通msg和撤回的msg里面都有哪些信息,他们的相同点和不同点。下面就来看看这两种情况下msg都是怎么样的,不需要仔细的看每一行,后面会作具体分析。
先是用机器人“小帮帮”发送过来的信息得到的msg信息:
'MsgId': '2018511155698964390',
'FromUserName': '@**********f511363f8200853d724137bb31236a7ea81e5183cc06cb4ec978e3',
'ToUserName': '@**********c2e61fdb47b5c241553a2f',
'MsgType': 1,
'Content': 'msg里面到底有什么?',
'Status': 3,
'ImgStatus': 1,
'CreateTime': 1578069291,
'VoiceLength': 0,
'PlayLength': 0,
'FileName': '',
'FileSize': '',
'MediaId': '',
'Url': '',
'AppMsgType': 0,
'StatusNotifyCode': 0,
'StatusNotifyUserName': '',
'RecommendInfo':
'UserName': '',
'NickName': '',
'QQNum': 0,
'Province': '',
'City': '',
'Content': '',
'Signature': '',
'Alias': '',
'Scene': 0,
'VerifyFlag': 0,
'AttrStatus': 0,
'Sex': 0,
'Ticket': '',
'OpCode': 0
,
'ForwardFlag': 0,
'AppInfo':
'AppID': '',
'Type': 0
,
'HasProductId': 0,
'Ticket': '',
'ImgHeight': 0,
'ImgWidth': 0,
'SubMsgType': 0,
'NewMsgId': 2018511155698964390,
'OriContent': '',
'EncryFileName': '',
'User': < User:
'MemberList': < ContactList: [] > ,
'Uin': 0,
'UserName': '@**********f511363f8200853d724137bb31236a7ea81e5183cc06cb4ec978e3',
'NickName': '小帮帮',
'HeadImgUrl': '/cgi-bin/mmwebwx-bin/webwxgeticon?seq=699837854&username=@**********f511363f8200853d724137bb31236a7ea81e5183cc06cb4ec978e3&skey=@crypt_****c00c_92668c8ba7d285c221a85e**********',
'ContactFlag': 2049,
'MemberCount': 0,
'RemarkName': '小帮帮',
'HideInputBarFlag': 0,
'Sex': 2,
'Signature': '',
'VerifyFlag': 0,
'OwnerUin': 0,
'PYInitial': 'XBB',
'PYQuanPin': 'xiaobangbang',
'RemarkPYInitial': 'XBB',
'RemarkPYQuanPin': 'xiaobangbang',
'StarFriend': 0,
'AppAccountFlag': 0,
'Statues': 0,
'AttrStatus': 33658937,
'Province': '浙江',
'City': '台州',
'Alias': '',
'SnsFlag': 17,
'UniFriend': 0,
'DisplayName': '',
'ChatRoomId': 0,
'KeyWord': '',
'EncryChatRoomId': '',
'IsOwner': 0
> ,
'Type': 'Text',
'Text': 'msg里面到底有什么?'
下面是机器人撤回刚才的信息得到的msg信息:
'MsgId': '4056955577161654067',
'FromUserName': '@**********f511363f8200853d724137bb31236a7ea81e5183cc06cb4ec978e3',
'ToUserName': '@**********c2e61fdb47b5c241553a2f',
'MsgType': 10002,
'Content': '<sysmsg type="revokemsg"><revokemsg><session>wxid_4gngrr04aqjn21</session><oldmsgid>1123721956</oldmsgid><msgid>2018511155698964390</msgid><replacemsg><![CDATA["小帮帮" 撤回了一条消息]]></replacemsg></revokemsg></sysmsg>',
'Status': 4,
'ImgStatus': 1,
'CreateTime': 1578069381,
'VoiceLength': 0,
'PlayLength': 0,
'FileName': '',
'FileSize': '',
'MediaId': '',
'Url': '',
'AppMsgType': 0,
'StatusNotifyCode': 0,
'StatusNotifyUserName': '',
'RecommendInfo':
'UserName': '',
'NickName': '',
'QQNum': 0,
'Province': '',
'City': '',
'Content': '',
'Signature': '',
'Alias': '',
'Scene': 0,
'VerifyFlag': 0,
'AttrStatus': 0,
'Sex': 0,
'Ticket': '',
'OpCode': 0
,
'ForwardFlag': 0,
'AppInfo':
'AppID': '',
'Type': 0
,
'HasProductId': 0,
'Ticket': '',
'ImgHeight': 0,
'ImgWidth': 0,
'SubMsgType': 0,
'NewMsgId': 4056955577161654067,
'OriContent': '',
'EncryFileName': '',
'User': < User:
'MemberList': < ContactList: [] > ,
'Uin': 0,
'UserName': '@**********f511363f8200853d724137bb31236a7ea81e5183cc06cb4ec978e3',
'NickName': '小帮帮',
'HeadImgUrl': '/cgi-bin/mmwebwx-bin/webwxgeticon?seq=699837854&username=@**********f511363f8200853d724137bb31236a7ea81e5183cc06cb4ec978e3&skey=@crypt_****c00c_92668c8ba7d285c221a85e**********',
'ContactFlag': 2049,
'MemberCount': 0,
'RemarkName': '小帮帮',
'HideInputBarFlag': 0,
'Sex': 2,
'Signature': '',
'VerifyFlag': 0,
'OwnerUin': 0,
'PYInitial': 'XBB',
'PYQuanPin': 'xiaobangbang',
'RemarkPYInitial': 'XBB',
'RemarkPYQuanPin': 'xiaobangbang',
'StarFriend': 0,
'AppAccountFlag': 0,
'Statues': 0,
'AttrStatus': 33658937,
'Province': '浙江',
'City': '台州',
'Alias': '',
'SnsFlag': 17,
'UniFriend': 0,
'DisplayName': '',
'ChatRoomId': 0,
'KeyWord': '',
'EncryChatRoomId': '',
'IsOwner': 0
> ,
'Type': 'Note',
'Text': '"小帮帮" 撤回了一条消息'
得到了两种类型的msg,下面是对比(高亮的部分是不同处,省略了部分相同内容。可以点击放大查看大图):
现在来分析几条关键信息:
- MsgId(与下面的NewMsgId同)
消息编号。这个很好理解,每条消息都是通过一个独一无二的编号来与其他消息区分,所以这两条消息的编号不同很正常。如果我们能拿到好友撤回消息的编号,也就能锁定这条消息了。 - MsgType(与下面的Type同)
消息类型。如下图,左边是普通的对话消息,右边类似于系统提示消息。是不是可以根据这条信息来判断是不是有好友撤回了消息?
- Content
消息内容,注意与下面的Text区分,这两种消息类型在内容上最大的区别可能就在这里了。
来看一下撤回消息的Content是怎么样的(为了便于查看,已经经过格式化)
<sysmsg type="revokemsg">
<revokemsg>
<session>wxid_4gngrr04aqjn21</session>
<oldmsgid>1123721956</oldmsgid>
<msgid>2018511155698964390</msgid>
<replacemsg><![CDATA["小帮帮" 撤回了一条消息]]></replacemsg>
</revokemsg>
</sysmsg>
一眼就能发现关键点:撤回的那条消息属于系统消息(sysmsg
),类型是撤回消息(revokemsg
),对应的消息编号是2018511155698964390
。
细心的读者已经发现,这个消息编号正好就是左边那条消息的编号。
通过这个推理,猜测Content
字段是系统内部传输的内容,而Text字段则是用户看到的内容。
确定消息类型
根据上述分析,有三个地方帮助确定收到的某条信息是否是撤回的消息:
- MsgType
是1就是普通消息,是10002则可能为撤回消息。
- Content
如果Content里有包含type="revokemsg"
则可能为撤回消息,否则不是撤回消息。
- Type
是Text就是普通消息,是Note则可能为撤回消息。
精确起见,消息还要同时满足上面三种情况才可认定为撤回消息。
锁定撤回的消息
由于要锁定撤回消息必须要MsgId
才能确定,所以在存储临时消息时需要加上这一字段。
log[sender_name][cur_timestamp] = msg['MsgId'] + '|||' + content
为了简化数据复杂度,我通过分隔符|||
直接把MsgId
加在前面。
于是,锁定并发送撤回消息的代码就时这样:
content = str(msg['Text'])
revoke_info = msg['Content']
print(', 发来消息: '.format(formatted_timestamp, sender_name, content))
target_msg_pattern = '"" 撤回了一条消息'.format(sender_name)
if target_msg_pattern == content and msg['Type'] == 'Note' and str(msg['MsgType']) == '10002' and 'type="revokemsg"' in revoke_info:
return_msg = ''
return_msg_head = ',【】撤回了一条消息:\\n'.format(formatted_timestamp, sender_name)
revoke_msg_id = revoke_info.split('<msgid>')[-1].split('</msgid>')[0]
for _, value in log[sender_name].items():
if value.split('|||')[0] == revoke_msg_id:
return_msg = value.split('|||')[1]
if return_msg == '':
return_msg = '缓存信息列表为空!'
return_msg = return_msg_head + return_msg
print(return_msg)
itchat.send_msg(return_msg, 'filehelper')
测试一下,为便于查看,将撤回提醒直接发给机器人“小帮帮”
一个完美的微信防撤回脚本大功告成!
结语
Python有很多好用好玩的库,可以慢慢发掘。本期我们利用ItChat
库编写了一个微信防撤回脚本。其实ItChat
功能远远不止这些,它还可以处理微信群消息以及各种其他类型的消息,我们讲到的只是九牛一毛,更多的还要大家自己去探索。
最后,感谢您的阅读。您的每个点赞、留言、分享都是对我们最大的鼓励,笔芯~
如有疑问,欢迎在评论区一起讨论!
以上是关于钉钉消息防撤回功能研究与实现-可查看历史消息[文件/图文/管理员/链接 撤回拦截]的主要内容,如果未能解决你的问题,请参考以下文章