关于我用 Rust 实现了百度文库”Plus 版“的那些事

6,502 阅读6分钟

一个契机

最近,有位同学写的百度文库文本复制油猴脚本又又又失效了,借此机会我重新分析了一下百度文库防文本复制的策略,看看百度如何低成本的实现这一需求。

由于恶意脚本持续更新,已出了续集专门讲解如何反制最新的恶意脚本。Rust+wasm 对抗"X度文库"恶意脚本(续集)

看看百度文库的防文本复制策略

1. 看看文本渲染方式

image-20220323141046219.png

打开百度文库,随便选择一个文档,通过审查元素可以看到,每个文档都被渲染成了 canvas。由于 canvas 开放了直接绘制文本的接口 fillText1,所以想要将文本信息直接绘制在 canvas 上是非常轻松的一件事情。而 canvas 中绘制的文本都是无法被选取的图像,因此从根本上解决了文本可复制的问题。

题外话:上个版本的百度文库用了双重画布,通过原始信息反推回画布上渲染的文本信息,在上层画布中监听鼠标事件,模拟出文本选中和复制的效果,不过这个版本为了防止油猴脚本取巧而取消了(猜测)。

2. 看看数据源

那么,绘制 canvas 所用的数据源从哪里来,总不可能是服务端直出吧,如果是服务端直出的话那大概率是图片或 svg,这样即增加了服务器成本又降低了用户体验(清晰度与体积)。进而,我们通过控制台 network 面板可以定位到一些可疑的数据包。 image-20220323144506851.png

这些冗长的 Json 文档便是 canvas 所需的绘制信息,我们只分析其中的关键字段。其中,字段cunicode编码的文本,字段p为文本的宽高与位置信息,剩余字段可能与文本样式与文档格式相关。由此,通过解析服务器请求得到的 Json 数据可以获得完整的 canvas 绘制信息。

3. 大致得到整体流程

基于前两步的分析,可以大致猜出百度文库实现防文本复制的前端整体流程,这些流程都可以在前端 js 文件中一一找到对应,这里不讨论其具体实现细节。 1.png

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 用户可以享受直接复制文本的功能。

2.png

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。最终我们实现了三类视图,它们分别基于标签 canvasimagediv

其中,canvas 调用了绘图指令,image调用了canavas.to_data_url()方法5div则是使用绝对定位来完成布局。具体实现的源码参见附录,此处不再讨论具体实现细节。

4. 效果展示

image-20220323162854815.png

浅谈我理解的前端安全

在前端想要实现绝对的安全是不可能的,因为所有运行所需的数据和信息都已经下载到前端了,想要逆向它只是时间问题。但是,我们可以通过增加较小的反逆向成本来大幅提高逆向的成本,从而打破它们之间的平衡。

这让我想起了,在逆向一个 Android 应用时,由 Java 虚拟机生成的字节码可以通过工具轻松逆向为 Smali 这种可读性较强的语言,并且通过插桩等形式动态地调试代码。为了提高应用的安全性,开发者将关键函数都封装在 So 库中,此时 hacker 可以通过附加等形式动态调试由 ARM 指令集组成的汇编代码,此时代码的可读性已经非常差了,逆向成本大大增加。如果再将通过一些混淆加壳等形式对 So 库进行加密保护,那么将进一步提高逆向门槛。

经过我的体验,我认为仅从安全层面考虑,Java 和 so 库的关系就如 js 和 wasm 的关系(虽然 wasm 的诞生之初可能不是为了前端安全做准备),并且目前我还没有找到动态调试 wasm 的方法,如果针对 wasm 这一个格式还能进行混淆加壳等操作,那么其安全性会得到进一步保障。

附录

  1. 具体实现源码 github.com/ascodelife/…

参考文章

  1. canvas fillText 接口 - developer.mozilla.org/zh-CN/docs/…
  2. WebAssembly 介绍 - developer.mozilla.org/zh-CN/docs/…
  3. RUST 是 JavaScript 基建的未来 - juejin.cn/post/703099…
  4. wasm-bindgen 文档 - rustwasm.github.io/wasm-bindge…
  5. canvas toDataURL 函数 - developer.mozilla.org/zh-CN/docs/…