使用模拟器混淆前端代码

494 查看

前言

很多时候,我们都会觉得混淆脚本程序是件困难的事,效果远不及传统程序的混淆力度。毕竟,脚本的初衷就是简单易用。诸多先天不足的特征,使得混淆难以深入实施。

然而从理论上这似乎也说不通,只要是图灵完备的语言,解决问题的能力都是相同的。举个最简单的例子,网上有使用 JavaScript 实现的 x86 模拟器,我们抛开性能不说,单论功能,它和本地系统是一样的。因此使用传统工具混淆的程序,同样也是能在浏览器中运行的!

当然,这个代价不免有些太大。为了保护一段逻辑,还得加载一个庞大的模拟器和操作系统,显然是难以接受的。但是这个思路还是很有意义的 —— 将需要保护的代码逻辑,放入模拟器中执行。

事实上类似的方案也早已存在,例如大名鼎鼎的 VMProtect。在浏览器端同样也有应用的案例,例如 Google 曾经开发的 reCaptcha 验证系统,也用到了模拟器来保护重要逻辑。

如何将前端脚本程序,变成可被模拟器运行的指令?我们从最简单的案例开始讲解。

字节码

和传统的编译型程序不同,脚本程序始终是带语法的文本代码。如何将一段充满各种可读单词的代码,尽可能多得使用数字来描述?例如这段代码:

其中就有变量名 el、字符串 ‘script’、全局变量 document、属性 body 等可读单词。

对于变量名来说,普通的压缩工具就能很好处理,变成诸如 a、b、c 这样的短名字;但是字符串和属性,又该如何处理?

熟悉 JS 的都知道 obj.keyobj['key'] 是相等的。而且全局变量都是 window 下的属性。因此,我们可把全局变量和属性都变成字符串的形式:

这时,整个代码中除了 window 之外,都是字符串了。

既然我们的目标是将代码数字化,那就将数字以外的常量都提取出来,放到一个单独的数组里:

这样,就可以用 MEM[数字] 代替一切了:

看起来有些眼花缭乱了吧。不过这只是对常量进行替换,语法仍然存在,因此还是能推测出大致的逻辑。不少基于语法树的混淆工具,大多就到这一步。

下面我们进一步,将语法展开:

这时的每一步,都是一个基本操作。我们到了脚本层面最低级的形式。(可以试着粘到控制台,仍能正常运行~ 或者点击jsfiddle.net/qLtojr5z/ 演示)

由于失去了语法,因此需要一些临时变量来保存中间值,这里使用 A、X、Y、Z 四个变量来暂存。

观察上述代码,其中有大量相似操作,我们尝试用代号来进行替换。例如读取 MEM[i] 操作,使用 LDR(Load Reg)来描述: