使用bytebode保护代码

2/28/2021 JavaScript

# 背景

electron 的业务源码经过 electron-builder 打包之后存在 app.asar 中,但是可以使用 asar (opens new window) 解压出来:

asar e app.asar ./app-dist
1

如果是比较重要的逻辑,如加解密。会导致安全隐患。目前比较的普遍的做法是对 js 代码进行混淆,这里推荐:

javascript-obfuscator (opens new window) 在线混淆地址 (opens new window)

即使是这样,还是有风险。

# vm

当 V8 编译 JavaScript 代码的时候,解析器会将其转成抽象语法树,然后进一步生成字节码。Node.js 的 vm 内置模块,在创建 vm.Script (opens new window) 的实例的时候,将 produceCachedData 设置为 ture,就能得到字节码,比如:

const vm = require("vm");
const ORIGIN = 'console.log("Hello world");'; // 源代码
const script = new vm.Script(ORIGIN, {
  produceCachedData: true,
});
const bytecodeBuffer = script.cachedData; // 字节码
1
2
3
4
5
6

相反的有了字节码,也能运行:

const anotherScript = new vm.Script(" ".repeat(ORIGIN.length), {
  cachedData: bytecodeBuffer,
});
anotherScript.runInThisContext(); // 'Hello world'
1
2
3
4

创建 vm.Script 实例时,V8 会检查字节码(cachedData)是否与源代码(第一个参数传入的代码)匹配,所以第一个参数不能省略。其次,这个检查非常简单,它只会对比代码长度是否一致,所以只要使用与源代码长度相同的空格,就可以“欺骗”这个检查。参考详细文章 (opens new window)

# bytenode (opens new window)

基于上述的原理,就能很好的保护我们的源码了。大致的实现:

"use strict";

const bytenode = require("bytenode");
const fs = require("fs");
const v8 = require("v8");
v8.setFlagsFromString("--no-lazy");

require("./compiled-file.jsc"); // 这里是编译之后的字节码文件
1
2
3
4
5
6
7
8

当然,因为是在 electron 中。渲染进程只要开启了 nodeIntegration,也能执行上述的逻辑

但是有几个问题得注意:

  1. bytenode 和 node 的版本相关,即生成字节码的 node 环境和执行字节码的 node 环境需要一致。这里推荐用项目中的 electron 命令代替 node,去生成字节码
  2. 箭头函数会导致编译之后的字节码在渲染进程中执行奔溃
  3. 缺少 sourcemap 的 debug 能力。持续关注issue (opens new window)
Last Updated: 2/21/2022, 11:35:56 AM