最近,有位同学写的百度文库文本复制油猴脚本又又又失效了,借此机会我重新分析了一下百度文库防文本复制的策略,看看百度如何低成本的实现这一需求。
由于恶意脚本持续更新,已出了续集专门讲解如何反制最新的恶意脚本。
Rust+wasm 对抗"X度文库"恶意脚本(续集)
看看百度文库的防文本复制策略
1. 看看文本渲染方式
打开百度文库,随便选择一个文档,通过审查元素可以看到,每个文档都被渲染成了 canvas。由于 canvas 开放了直接绘制文本的接口 fillText
1
,所以想要将文本信息直接绘制在 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/…
WebAssembly
4620
MeloAlright
Vue.js
WebAssembly
1828
鼠子的前端CodeLife
JavaScript
Promise
1633
鼠子的前端CodeLife
JavaScript
1380
鼠子的前端CodeLife
React.js