前言
很多时候,我们都会觉得混淆脚本程序是件困难的事,效果远不及传统程序的混淆力度。毕竟,脚本的初衷就是简单易用。诸多先天不足的特征,使得混淆难以深入实施。
然而从理论上这似乎也说不通,只要是图灵完备的语言,解决问题的能力都是相同的。举个最简单的例子,网上有使用 JavaScript 实现的 x86 模拟器,我们抛开性能不说,单论功能,它和本地系统是一样的。因此使用传统工具混淆的程序,同样也是能在浏览器中运行的!
当然,这个代价不免有些太大。为了保护一段逻辑,还得加载一个庞大的模拟器和操作系统,显然是难以接受的。但是这个思路还是很有意义的 —— 将需要保护的代码逻辑,放入模拟器中执行。
事实上类似的方案也早已存在,例如大名鼎鼎的 VMProtect。在浏览器端同样也有应用的案例,例如 Google 曾经开发的 reCaptcha 验证系统,也用到了模拟器来保护重要逻辑。
如何将前端脚本程序,变成可被模拟器运行的指令?我们从最简单的案例开始讲解。
字节码
和传统的编译型程序不同,脚本程序始终是带语法的文本代码。如何将一段充满各种可读单词的代码,尽可能多得使用数字来描述?例如这段代码:
1 2 3 |
var el = document.createElement('script'); el.text = 'alert(123)'; document.body.appendChild(el); |
其中就有变量名 el、字符串 ‘script’、全局变量 document、属性 body 等可读单词。
对于变量名来说,普通的压缩工具就能很好处理,变成诸如 a、b、c 这样的短名字;但是字符串和属性,又该如何处理?
熟悉 JS 的都知道 obj.key
和 obj['key']
是相等的。而且全局变量都是 window 下的属性。因此,我们可把全局变量和属性都变成字符串的形式:
1 2 3 |
var el = window['document']['createElement']('script'); el['text'] = 'alert(123)'; window['document']['body']['appendChild'](el); |
这时,整个代码中除了 window 之外,都是字符串了。
既然我们的目标是将代码数字化,那就将数字以外的常量都提取出来,放到一个单独的数组里:
1 2 3 4 |
var MEM = [ window, 'document', 'createElement', 'script', 'text', 'alert(123)', 'body', 'appendChild' ]; |
这样,就可以用 MEM[数字]
代替一切了:
1 2 3 |
var el = MEM[0][ MEM[1] ][ MEM[2] ]( MEM[3] ); el[ MEM[4] ] = MEM[5]; MEM[0][ MEM[1] ][ MEM[6] ][ MEM[7] ](el); |
看起来有些眼花缭乱了吧。不过这只是对常量进行替换,语法仍然存在,因此还是能推测出大致的逻辑。不少基于语法树的混淆工具,大多就到这一步。
下面我们进一步,将语法展开:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var A, X, Y, Z A = MEM[0] // window X = MEM[1] // 'document' X = A[X] // X = window['document'] A = MEM[2] // 'createElement' Y = MEM[3] // 'script' A = X[A](Y) // A = document['createElement']('script') Y = MEM[4] // 'text' Z = MEM[5] // 'alert(123)' A[Y] = Z // A['text'] = 'alert(123)' Y = MEM[6] // 'body' X = X[Y] // X = document['body'] Y = MEM[7] // 'appendChild' X[Y](A) // body['appendChild'](A) |
这时的每一步,都是一个基本操作。我们到了脚本层面最低级的形式。(可以试着粘到控制台,仍能正常运行~ 或者点击jsfiddle.net/qLtojr5z/ 演示)
由于失去了语法,因此需要一些临时变量来保存中间值,这里使用 A、X、Y、Z 四个变量来暂存。
观察上述代码,其中有大量相似操作,我们尝试用代号来进行替换。例如读取 MEM[i] 操作,使用 LDR(Load Reg)来描述:
1 |
r = MEM؊这似乎也说不通,只要是图灵完备的语言,解决问题的能力都是相同的。举个最简单的例子,网上有使用 JavaScript 实现的 x86 模拟器,我们抛开性能不说,单论功能,它和本地系统是一样的。因此使用传统工具混淆的程序,同样也是能在浏览器中运行的!
当然,这个代价不免有些太大。为了保护一段逻辑,还得加载一个庞大的模拟器和操作系统,显然是难以接受的。但是这个思路还是很有意义的 —— 将需要保护的代码逻辑,放入模拟器中执行。 事实上类似的方案也早已存在,例如大名鼎鼎的 VMProtect。在浏览器端同样也有应用的案例,例如 Google 曾经开发的 reCaptcha 验证系统,也用到了模拟器来保护重要逻辑。 如何将前端脚本程序,变成可被模拟器运行的指令?我们从最简单的案例开始讲解。 字节码和传统的编译型程序不同,脚本程序始终是带语法的文本代码。如何将一段充满各种可读单词的代码,尽可能多得使用数字来描述?例如这段代码:
其中就有变量名 el、字符串 ‘script’、全局变量 document、属性 body 等可读单词。 对于变量名来说,普通的压缩工具就能很好处理,变成诸如 a、b、c 这样的短名字;但是字符串和属性,又该如何处理? 熟悉 JS 的都知道
这时,整个代码中除了 window 之外,都是字符串了。 既然我们的目标是将代码数字化,那就将数字以外的常量都提取出来,放到一个单独的数组里:
这样,就可以用
看起来有些眼花缭乱了吧。不过这只是对常量进行替换,语法仍然存在,因此还是能推测出大致的逻辑。不少基于语法树的混淆工具,大多就到这一步。 下面我们进一步,将语法展开:
这时的每一步,都是一个基本操作。我们到了脚本层面最低级的形式。(可以试着粘到控制台,仍能正常运行~ 或者点击jsfiddle.net/qLtojr5z/ 演示) 由于失去了语法,因此需要一些临时变量来保存中间值,这里使用 A、X、Y、Z 四个变量来暂存。 观察上述代码,其中有大量相似操作,我们尝试用代号来进行替换。例如读取 MEM[i] 操作,使用 LDR(Load Reg)来描述: |