压缩混淆工具是前端必用的工具之一,代码在上线之间需要经过压缩来减小体积,并且会做一些简单的混淆来防止源码直接泄漏。前端工程师可能每天都在用这种工具,可你有想过它的实现原理么?
代码的压缩和混淆都是对代码做转换,但是转换前后要保持语义一致,就是不能转完之后代码逻辑改变了。之所以能做这些转换是因为计算机执行代码并不需要换行、也不需要变量名多么易懂,那都是给人看的,可以简化掉,而且有的不会被执行到的代码也可以删掉。压缩和混淆就是分析代码中的这种代码,进行分析和转换,达到转换前后执行逻辑一致,但是代码体积更小、可读性更差的目的。
我们分别来实现一下压缩和混淆。
# 混淆
# 思路分析
混淆就是把代码变得难以阅读,让怀有恶意目的的人很难通过代码理清逻辑,但是不能改变执行的结果。要做等价转换。
这种转换包括两方面:
- 名字转换。变量名、函数名这些我们会注意命名要有含义,但是编译后的代码就不需要了,可以把各种 identifier 的 name 重命名为没有含义的 abcd,这个可以通过 path.scope.rename 的 api 来做到。
- 逻辑转换。if 的逻辑可以用 switch 来代替,for 的逻辑可以用 while 来代替,这都是等价的,把一种方式实现的代码转成另一种等价的形式就可以达到混淆的目的。做混淆工具主要是要找到这种等价的变化,而且后者一定要特别复杂难以分析,然后实现这种转换,就达到了混淆的目的。
这里我们只实现下名字的混淆。
目的是为了找出所有的声明,那就要遍历所有会生成作用域的节点,包括 FunctionDeclaration、BlockStatement 等,而这些节点又一个别名,叫 Scopable(所有的别名可以在这里查,详见第七节),然后对每一个声明(binding)都重命名为无意义的名字,并且更新所有引用这个声明的地方,这个逻辑在 path.scope.rename 已经实现了,直接调用这个 api 即可。
# 代码实现
依然先写好插件的结构:
const { declare } = require('@babel/helper-plugin-utils');
const mangle = declare((api, options, dirname) => {
api.assertVersion(7);
return {
pre(file) {
file.set('uid', 0);
},
visitor: {
Scopable: {
}
}
}
});
module.exports = mangle;
@前端进阶之旅: 代码已经复制到剪贴板
这里在 file 放了一个 uid 是为了获取唯一 id 的,后面会用到。
我们基于这个 uid 来获取唯一的名字,因为不能以数字开头,所以用 A-Z、a-z、$ 和 _ 这 54 个字符来生成。
根据传入的 num 来取对应下标的字符组成字符串:
