上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
1234下一页
返回列表 发新帖

[逆向分析实战]三句话,让腾讯文档为我放下戒备

[复制链接]

8

主题

124

帖子

162

积分

注册会员

Rank: 2

积分
162

活跃会员热心会员突出贡献三好学生猫咪币纪念章中秋纪念章国庆纪念章

发表于 2021-10-7 10:49:42 | 显示全部楼层 | 阅读模式
本帖最后由 cxxjackie 于 2021-10-7 16:12 编辑

FBI WARNING
由于油猴的一些古怪特性造成预料之外的问题,请临时新建以下脚本来复现文章中的操作:
  1. // ==UserScript==
  2. // @name         教程专用测试脚本
  3. // @namespace    ...
  4. // @author       ...
  5. // @version      1.0
  6. // @include      https://docs.qq.com/doc/DQ1BMTk1BWlVWcndF
  7. // @grant        unsafeWindow
  8. // @run-at       document-start
  9. // ==/UserScript==
  10. document;
复制代码



其实大部分腾讯文档是没有限制的,只有作者设置了复制权限才会无法复制,本文将以这篇文档为例进行实战。腾讯文档的防复制手段与道客巴巴很相似,都是将文字绘制在canvas里(详情请看上文)。首先从事件监听入手,发现页面中存在copy事件,这可方便了不少(也可以从click事件或按键事件入手,但是事件很多分析起来要麻烦亿点)。copy事件逐个点进去看一眼,发现这样一段比较有趣的代码:
1.png
翻译一下就是判断然后执行a.onCopyByEvent(t),而a.onCopyByEvent就在这个函数的下面,继续往下看,可以大致看出这是复制函数的相关逻辑,开头用if判断了一下,我们在if这里打个断点,然后选一段文字ctrl c,成功断了下来。由于if条件内包含函数,这里要按F10继续(step over,调试图标第二个,逐行运行代码,但不进入子函数),结果if内的代码被跳过,说明条件不成立。观察if的条件,发现a.context.isCopyable()很可疑,可以把鼠标放上去跟进去看,也可以在控制台中输入a.context.isCopyable查看,函数内容很简单,就一句话:
  1. return this._docEnv.copyable;
复制代码
由于isCopyable是a.context的属性,所以这里的this指的是a.context,即a.context.isCopyable函数读取的是a.context._docEnv.copyable的值。控制台查看一下这个值,发现是false,把他改成true看看,取消断点,再次ctrl c复制,结果成功了。现在的问题是怎么得到这个copyable的路径,我们当然不可能在代码中直接写a.context._docEnv.copyable,因为这个a是打了断点以后获得的局部引用,在全局环境中是没有a这个东西的。这就要祭出我做的这个脚本了(自卖自夸),注入函数,然后在全局环境中搜索copyable这个属性:
2.png
只找到一条路径,要验证路径是否正确,可以再执行一次断点,然后控制台判断一下:
  1. a.context._docEnv === window['App']['CollabRoom']['collabRoomOptions']['notificationCenter']['_docEnv']
复制代码
结果为true,说明路径是正确的。这个判断的原理是copyable是前面那个对象的属性,我们只要证明两个引用属于同一个对象,那么改动其中一个必然会影响另一个。其实不是每个属性都能找到全局路径,这里算是比较取巧的做法,找不到的话就只能慢慢去跟代码逻辑了。接下来可以刷新页面,验证一下路径有没有变,控制台输入:
  1. window['App']['CollabRoom']['collabRoomOptions']['notificationCenter']['_docEnv']['copyable'] = true;
复制代码
ctrl c复制一下看看,发现成功了。至此我们已经可以写出一个脚本,用这一行代码来解决问题。接下来进行一些优化操作,当我们按ctrl c时,虽然能复制成功,但还是会弹出权限被限制的提示,点击调试工具右上角的3个点,选择search,在所有js中搜索提示文字:文档作者限制了(后面可以省略),找到一堆乱七八糟的东西,逐个点开看看,可以发现大部分都只是一堆字符串,只有public-doc-pc开头的这个文件比较可疑:
3.png
显然p.qO.getInstance().canCopy决定了是否出现提示,这里打个断点,ctrl c后断下来,控制台输入p.qO.getInstance().canCopy = true,继续运行,结果发现并没有效果。这种情况往往是因为该属性通过Object.defineProperty修改了getter造成的,即我们访问这个属性时,实际上访问的是别的东西。要验证这一点,可以断点后在控制台输入p.qO.getInstance(),展开这个对象看看,发现canCopy这个属性是灰色的,这就是通过getter访问的属性。怎么查看他的getter呢?有一种简单的方法:
4.png
与前面相似,可以得出读取的是p.qO.getInstance().privilege.canCopy的值(isPublish是undefined),这次我们输入p.qO.getInstance().privilege.canCopy = true看看,结果成功屏蔽了提示。接下来的流程就跟前面一样,$searchKey('canCopy'),找到2个路径,验证一下可以得出window['pad']['permissionCtrl']['privilege']['canCopy']是正确的,然后在脚本中加入这行代码就行了:
  1. window['pad']['permissionCtrl']['privilege']['canCopy'] = true;
复制代码
最后是解锁右键的复制,可以看到右键菜单中的复制是灰色的,一个简单的思路是监听节点插入(即监听右键菜单出现),获取元素后修改css,把复制这一项改成黑色,然后给他加一个click事件,模拟ctrl c按键即可。但是我们前面既然都能通过修改全局属性来达到目的,这里能不能也这么做呢?答案是可以的。
我们先定位到这个复制元素,查看他的click事件,可以看到一大堆,其实这些事件大部分是重复的,只是绑定在不同节点上,逐个点进去看一眼,可以找到这段代码:
5.png
从代码上可以推测出,t.props.disabled判断元素是否禁用,从而决定后续代码是否执行。虽然可以通过修改disabled的值来验证我们的想法,但由于这个事件绑定的节点非常多,每次t都不一样,调试起来比较困难。我们可以试着$searchKey('disabled')看看,找到6个结果,除了两个jquery的明显不对,其他4个是非常明显的React路径(实际上不止4个,因为我脚本设置了最大搜索深度,而React路径通常非常长),一堆return、child、stateNode,以及最关键的__reactInternalInstance这个字段。为了验证我们的想法,可以打个断点,鼠标放到t上看看,可以看到context、props、state、updater等属性,这就是典型的React对象,说明这些元素是用React库构建的。这里要用到一个chrome插件:React Developer Tools(链接就不提供了,请自行搜索下载),可以调试React,安装后会在调试工具处出现一个React标签。定位到右键菜单的元素后切换至React标签,可以看到这些东西:
6.png
注意这里我选中了copy组件,在右边的Props中出现了disabled: true这一项,前面还有个勾,去掉勾试试,disabled变成了false,右键菜单中的复制变成了黑色!点击一下,成功复制到了文字(如果你刷新过页面,别忘了前面的2行代码)。这个disabled要在脚本中修改是比较复杂的,首先copy不是页面中实际存在的节点,但他的上一级组件是(可以看到ul标签,className属性),在ul组件上右键选择"Find the DOM node"会跳到页面中的节点,此时切换到控制台,输入$0.__reactInternalInstance,后面还有一串像乱码一样的东西会自动补齐。在控制台中$0是我们当前选中的节点(这也是一个调试小技巧),__reactInternalInstance是React注入的,后面那一串东西是标识符,每次刷新页面都会变。展开这个对象,由于我们要找的copy是他的子组件,要从child里找,展开child后可以看到key是cut,这是第一个组件,我们要找的copy是第二个,路径就是child.sibling(sibling代表下一个),展开sibling后看到key是copy,他的props位于stateNode中,所以完整路径就是$0.__reactInternalInstance$标识符.child.sibling.stateNode.props.disabled(我知道这有点绕,但React就这尿性)。我们姑且用一个sn来存储stateNode,将disabled修改为false:
  1. const sn = $0.__reactInternalInstance$标识符.child.sibling.stateNode;
  2. sn.props.disabled = false;
复制代码
此时点击复制已经有效果了,但复制还是灰色的,这是因为组件没有更新,再加一行代码即可:
  1. sn.updater.enqueueForceUpdate(sn);
复制代码
由于每次右键菜单都会重新生成,调试起来比较麻烦。要在脚本中实现,具体步骤就是监听节点插入,获取右键菜单(即$0),然后遍历这个元素的属性,找到__reactInternalInstance开头的属性(用正则或者startsWith),之后的代码就跟上面一样了。嗯,听上去不比自己绑一个click事件简单多少(这其实是在讲怎么处理React),好吧,让我们回归初心。我们已经知道这个组件的key是"copy",那么在设置disabled的地方修改判断逻辑不就行了吗?
在所有文件中搜索copy,找到一大堆,这是当然的,页面中的copy事件也在搜索范围内,还有其他一些奇奇怪怪的copy,分析起来很麻烦。换个思路,切换到React观察一下,可以看到右键菜单中还有一个“插入链接”,他的key是"insert_link",这个应该不常见,搜索一下看看,只有2个结果(注意带:formatted的是我们点击了格式化后产生的副本),点开第一个按ctrl f,点一下右边的.*,搜索copy.*disabled(这是用正则搜索的意思,搜索所有文件也可以正则,但是会很慢),没有结果,搜索第二个,找到了这样一个有趣的东西:
7.png
这里显然是设置disabled的地方,不过代码经过混淆,读起来比较费劲,我可以稍微整理一下,让他看起来可读性好一点:
  1. la([
  2.     (0, ci.v)("cut", "shouldDisabled"),
  3.     (0, sa.x)("cut", "shouldDisabled"),
  4.     da(0, (0, ji.M)()),
  5.     pa("design:type", Function),
  6.     pa("design:paramtypes", ["function" == typeof (t = void 0 !== Fe.z && Fe.z) ? t : Object]),
  7.     pa("design:returntype", void 0)
  8. ], e.prototype, "cannotCut", null),
  9. la([
  10.     (0, ci.v)("copy", "shouldDisabled"),
  11.     (0, sa.x)("copy", "shouldDisabled"),
  12.     da(0, (0, ji.M)()),
  13.     pa("design:type", Function),
  14.     pa("design:paramtypes", ["function" == typeof (n = void 0 !== Fe.z && Fe.z) ? n : Object]),
  15.     pa("design:returntype", void 0)
  16. ], e.prototype, "cannotCopy", null),
复制代码

对于这种混淆过的代码,不必去纠结每一步的含义,可以从前后文推敲。比较上下两段代码的差异,区别在于最后的"cannotCut"和"cannotCopy",猜测这就是设置disabled的关键。这次我们搜索cannotCopy,只找到一个文件(就是这段代码所在的文件),点开找到cannotCopy的定义处,看到有一个notCopyable函数,这里我们可以打个断点,右键一下试试能不能断下来,成功断下来后跟进这个notCopyable函数看看:
8.png
可以看出e.permission.isCopyable是关键属性,在return前打个断点,运行到这后鼠标放上去,看到是false,控制台改成true试试,结果复制还是灰的,点击也没效果。这并不是说我们的方向错了,还记得前面说过的getter吗?鼠标放到e.permission上看看,发现isCopyable属性是灰的,控制台输入:
  1. e.permission.__lookupGetter__('isCopyable')
复制代码
这次的函数不太直观,可以点一下跟进去看看:
9.png
这里的代码就是判断是否为编辑状态(isEditable)或复制是否被禁用(view_forbid_copy_print),isEditable其实跟isCopyable一样是个判断函数,我们去跟这个有点绕,直接从后面入手。还是在return前打个断点断下来,看一下e.view_forbid_copy_print是1,所以等于1是true,加上前面的!,返回的结果就是false。我们在控制台把e.view_forbid_copy_print改成0试试(这次记得看一眼e确定view_forbid_copy_print属性不是灰的),复制变成了黑色!接下来取消断点,多划几个地方试试,每次右键菜单的复制都是黑色的,而且点击也都能生效,说明改对了地方。接下来就是传统艺能了,搜索view_forbid_copy_print的全局路径,最终确定为window['App']['CollabRoom']['collabRoomOptions']['clientVars']['advPolicy']['view_forbid_copy_print']。至此,给我们的脚本加上第3行,就算完美收工了:
  1. window['App']['CollabRoom']['collabRoomOptions']['clientVars']['advPolicy']['view_forbid_copy_print'] = 0;
复制代码

结语
虽然本文的废话洋洋洒洒写了一大篇,但最后的代码只有3行,用简单的代码解决复杂的需求,这正是编程的艺术啊~


已有1人评分油猫币 理由
陈公子的话 + 4 ggnb!

查看全部评分 总评分:油猫币 +4 

8

主题

124

帖子

162

积分

注册会员

Rank: 2

积分
162

活跃会员热心会员突出贡献三好学生猫咪币纪念章中秋纪念章国庆纪念章

发表于 2021-10-7 12:53:51 | 显示全部楼层
在shouldDisabled这段代码的分析中,由于截图和整理代码是我在不同时间进行的,文件混淆发生了变化(可能你读到这篇文章的时候又变了),所以出现上下不一致的情况,不过不影响理解,我就不改了。
回复

使用道具 举报

159

主题

1105

帖子

618

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
618
发表于 2021-10-7 12:57:08 | 显示全部楼层
大佬copy事件是在哪里找的
我试了一下没看到...
混的人。
回复

使用道具 举报

8

主题

124

帖子

162

积分

注册会员

Rank: 2

积分
162

活跃会员热心会员突出贡献三好学生猫咪币纪念章中秋纪念章国庆纪念章

发表于 2021-10-7 13:29:18 | 显示全部楼层
本帖最后由 cxxjackie 于 2021-10-7 13:32 编辑
李恒道 发表于 2021-10-7 12:57
大佬copy事件是在哪里找的
我试了一下没看到...
在最顶层的html上就有啊,应该随便哪个元素都能找到的。
无标题.png


是不是装了什么插件把copy干掉了,可能有些通用的解除复制限制的插件或脚本会这么做。
回复

使用道具 举报

159

主题

1105

帖子

618

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
618
发表于 2021-10-7 13:48:23 | 显示全部楼层

[quote][size=2][color=#999999]cxxjackie 发表于 2021-10-7 13:29[/color][/size] 在最顶层的html上就有啊,应该随便哪个元素都能找到的。

[/quote]

好像开什么插件了

换个找到了,但是还是跟大佬的不太一样

图片.png

这种情况正常么

图片.png

混的人。
回复

使用道具 举报

8

主题

124

帖子

162

积分

注册会员

Rank: 2

积分
162

活跃会员热心会员突出贡献三好学生猫咪币纪念章中秋纪念章国庆纪念章

发表于 2021-10-7 14:02:02 | 显示全部楼层
李恒道 发表于 2021-10-7 13:48
[md]好像开什么插件了

换个找到了,但是还是跟大佬的不太一样

不太正常,函数应该在feature-pc开头的文件里,你把插件都关了试试?
回复

使用道具 举报

159

主题

1105

帖子

618

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
618
发表于 2021-10-7 14:12:55 | 显示全部楼层
cxxjackie 发表于 2021-10-7 14:02
不太正常,函数应该在feature-pc开头的文件里,你把插件都关了试试?

全关了...始终搞不出来
等我晚上换个电脑试一下
邪了
混的人。
回复

使用道具 举报

8

主题

124

帖子

162

积分

注册会员

Rank: 2

积分
162

活跃会员热心会员突出贡献三好学生猫咪币纪念章中秋纪念章国庆纪念章

发表于 2021-10-7 14:41:03 | 显示全部楼层
李恒道 发表于 2021-10-7 14:12
全关了...始终搞不出来
等我晚上换个电脑试一下
邪了

研究了一下,是我的一个脚本引起的,原因似乎是在document-start阶段读取了一下document,结果就出现了这个非常奇怪的现象,这难道是油猴的某些“特性”?有点难以理解,我再折腾一下,你可以先用这种方法试试。
回复

使用道具 举报

159

主题

1105

帖子

618

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
618
发表于 2021-10-7 14:58:49 | 显示全部楼层
cxxjackie 发表于 2021-10-7 14:41
研究了一下,是我的一个脚本引起的,原因似乎是在document-start阶段读取了一下document,结果就出现了这 ...

具体是document-start的时候操作了什么
大佬你的那个监听器是因为脚本误触搞出来了的
感觉好奇怪啊...
混的人。
回复

使用道具 举报

8

主题

124

帖子

162

积分

注册会员

Rank: 2

积分
162

活跃会员热心会员突出贡献三好学生猫咪币纪念章中秋纪念章国庆纪念章

发表于 2021-10-7 15:16:33 | 显示全部楼层
李恒道 发表于 2021-10-7 14:58
具体是document-start的时候操作了什么
大佬你的那个监听器是因为脚本误触搞出来了的
感觉好奇怪啊... ...

console.log(document)就可以触发,我debugger跟了一下油猴的代码,他在读取document的时候会执行一些自己的逻辑,劫持addEventListener等等,可能是document-start阶段使这些逻辑被过早的执行,从而导致了这个有点奇怪的问题出现。
回复

使用道具 举报

发表回复

本版积分规则

快速回复 返回顶部 返回列表