一个契机
最近,有位同学写的百度文库文本复制油猴脚本又又又失效了,借此机会我重新分析了一下百度文库防文本复制的策略,看看百度如何低成本的实现这一需求。
由于恶意脚本持续更新,已出了续集专门讲解如何反制最新的恶意脚本。Rust+wasm 对抗"X度文库"恶意脚本(续集)
看看百度文库的防文本复制策略
1. 看看文本渲染方式
打开百度文库,随便选择一个文档,通过审查元素可以看到,每个文档都被渲染成了 canvas。由于 canvas 开放了直接绘制文本的接口 fillText1,所以想要将文本信息直接绘制在 canvas 上是非常轻松的一件事情。而 canvas 中绘制的文本都是无法被选取的图像,因此从根本上解决了文本可复制的问题。
题外话:上个版本的百度文库用了双重画布,通过原始信息反推回画布上渲染的文本信息,在上层画布中监听鼠标事件,模拟出文本选中和复制的效果,不过这个版本为了防止油猴脚本取巧而取消了(猜测)。
2. 看看数据源
那么,绘制 canvas 所用的数据源从哪里来,总不可能是服务端直出吧,如果是服务端直出的话那大概率是图片或 svg,这样即增加了服务器成本又降低了用户体验(清晰度与体积)。进而,我们通过控制台 network 面板可以定位到一些可疑的数据包。
这些冗长的 Json 文档便是 canvas 所需的绘制信息,我们只分析其中的关键字段。其中,字段c
为unicode
编码的文本,字段p
为文本的宽高与位置信息,剩余字段可能与文本样式与文档格式相关。由此,通过解析服务器请求得到的 Json 数据可以获得完整的 canvas 绘制信息。
3. 大致得到整体流程
基于前两步的分析,可以大致猜出百度文库实现防文本复制的前端整体流程,这些流程都可以在前端 js 文件中一一找到对应,这里不讨论其具体实现细节。
4. 存在的问题
由于 Json 数据是完全明文的,所以恶意脚本或插件完全可以自己通过解析 Json 数据来得到完整的文档数据,这个成本是比较低的。另一方面,如果想要对 Json 数据加密后再传输,那么若解密函数使用 js 编写,即使对 js 脚本做混淆加密,破解其解密函数也是一件成本很低的工作。
实现一个百度文库 "Plus 版"
基于上述分析可以总结我们的需求为,想要在实现防文本复制的同时,对原始数据进行加密,并且提高关键解密函数的破解成本。
1. 技术选型
使用 js 编写的脚本文件,无论通过何等混淆加密最后都将是一个可下断调试的脚本文件,因此想要定位到关键函数是比较轻松的一件事情,并且通过动态调试也能很快的跟出解密流程,所以我们不能把解密函数和解密后的明文数据存放在 js 文件和堆栈中。
为此,我们选择了以 WebAssembly 技术2 (以下简称 wasm)为主体的解决方案,由于 wasm 是一种二进制的格式,可读性差,且无法通过下断的方式进行调试,因此想要通过静态分析还原出关键函数是一件成本较高的事情。 编写 wasm 的方式有很多种,受文章《RUST 是 JavaScript 基建的未来》3的鼓舞,我选择了使用 Rust 作为 wasm 的开发语言,且 Rust 已经有了针对 wasm 的较为完整的工具链4 。
2. 整体流程
下面,我们给出了基于 wasm 改造后的整体流程,将关键步骤下放到 wasm 层中进行,包括 dom 的相关操作。特别地,我们考虑对用户权限进行划分,VIP 用户可以享受直接复制文本的功能。
3. 关键技术点分析
首先,我们参照百度文库定义一下原始信息,其中 cipher
为密文,position
为定位信息,font_style
为文本样式。
const info = [
{
cipher: "\u{1d}\u{14}\u{14}",
position: { x: 30, y: 30 },
font_style: { size: 16 },
},
{
cipher: "\u{19}\u{1a}\t",
position: { x: 50, y: 50 },
font_style: { size: 30 },
},
];
复制代码
定义 js 层与 wasm 层的接口为 encrypt_canvas
,传入渲染所需加密信息和用户身份令牌。
encrypt_canvas({ render_info: info, user_token: "1234567" });
复制代码
在 wasm 层,我们通过调用解密函数来得到明文信息,并通过 web-sys 来操作 dom。最终我们实现了三类视图,它们分别基于标签 canvas
、image
和 div
。
其中,canvas
调用了绘图指令,image
调用了canavas.to_data_url()
方法5,div
则是使用绝对定位来完成布局。具体实现的源码参见附录,此处不再讨论具体实现细节。
4. 效果展示
浅谈我理解的前端安全
在前端想要实现绝对的安全是不可能的,因为所有运行所需的数据和信息都已经下载到前端了,想要逆向它只是时间问题。但是,我们可以通过增加较小的反逆向成本来大幅提高逆向的成本,从而打破它们之间的平衡。
这让我想起了,在逆向一个 Android 应用时,由 Java 虚拟机生成的字节码可以通过工具轻松逆向为 Smali 这种可读性较强的语言,并且通过插桩等形式动态地调试代码。为了提高应用的安全性,开发者将关键函数都封装在 So 库中,此时 hacker 可以通过附加等形式动态调试由 ARM 指令集组成的汇编代码,此时代码的可读性已经非常差了,逆向成本大大增加。如果再将通过一些混淆加壳等形式对 So 库进行加密保护,那么将进一步提高逆向门槛。
经过我的体验,我认为仅从安全层面考虑,Java 和 so 库的关系就如 js 和 wasm 的关系(虽然 wasm 的诞生之初可能不是为了前端安全做准备),并且目前我还没有找到动态调试 wasm 的方法,如果针对 wasm 这一个格式还能进行混淆加壳等操作,那么其安全性会得到进一步保障。
附录
- 具体实现源码 github.com/ascodelife/…
参考文章
- canvas fillText 接口 - developer.mozilla.org/zh-CN/docs/…
- WebAssembly 介绍 - developer.mozilla.org/zh-CN/docs/…
- RUST 是 JavaScript 基建的未来 - juejin.cn/post/703099…
- wasm-bindgen 文档 - rustwasm.github.io/wasm-bindge…
- canvas toDataURL 函数 - developer.mozilla.org/zh-CN/docs/…