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

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

[复制链接]
  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 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 

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

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

    使用道具 举报

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5084

    回帖

    5958

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5958

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

    发表于 2021-10-7 12:57:08 | 显示全部楼层
    大佬copy事件是在哪里找的
    我试了一下没看到...
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

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


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

    使用道具 举报

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5084

    回帖

    5958

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5958

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

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

    cxxjackie 发表于 2021-10-7 13:29

    在最顶层的html上就有啊,应该随便哪个元素都能找到的。

    好像开什么插件了

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

    图片.png

    这种情况正常么

    图片.png

    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

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

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

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

    使用道具 举报

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5084

    回帖

    5958

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5958

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

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

    全关了...始终搞不出来
    等我晚上换个电脑试一下
    邪了
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

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

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

    使用道具 举报

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5084

    回帖

    5958

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5958

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

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

    具体是document-start的时候操作了什么
    大佬你的那个监听器是因为脚本误触搞出来了的
    感觉好奇怪啊...
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

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

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

    使用道具 举报

    发表回复

    本版积分规则

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