JS代码保护,有多种方式,如常规的JS混淆加密、如bytecode化、又或如虚拟机化。
这里简单探讨虚拟机JS保护。
一、原理
虚拟机保护的最终目标,是将JS代码转为opcode,或汇编语言式代码,在虚拟机中执行。
一般是保护重要的函数、算法、当然也可以保护更多更大段的代码。
更详细一些来说,汇编语言式代码,形态会类似:
push a
push b
push c
call fun
pop
这是古老的asm语法,没错,js代码可以转为此种形式,而且,可以更进一步,转为opcode,如上述asm代码,如果将push、pop等字符替换为数字的操作码,假设push为20,call为30,pop为40,形态可以变成:
20,1,20,20,3,30,4,40
如果我们的JS代码,变成了这样的数字,谁能理解它的代码逻辑和作用吗?
很显然,这样起到了对代码加密保护的作用。如果再与JShaman之类的混淆加密工具配合使用,JS代码的安全性将得到极大的提升。
二、开发一个JS虚拟机
一个简单的堆栈虚拟机,并不会十分复杂,用JS数组模拟堆栈,用数组的push方法模拟压栈,用数组索实现堆栈指针、指令指针、栈帧。
本例中,汇编指令,则实现一部分操作,如:
const I = {
CONST: 1,
ADD: 2,
PRINT: 3,
HALT: 4,
CALL: 5,
RETURN: 6,
LOAD: 7,
JUMP_IF_ZERO: 8,
JUMP_IF_NOT_ZERO: 9,
SUB: 10,
MUL: 11,
};
虚拟机的核心的部分,则是根据指令进行相应的堆栈操作,如:
//循环执行
switch (instruction) {
//常量
case I.CONST: {
//常量值
const op_value = code[ip++];
//存放到堆栈
stack[++sp] = op_value;
console.log("const",stack)
break;
}
case I.ADD: {
const op1 = stack[sp--];
const op2 = stack[sp--];
stack[++sp] = op1 + op2;
break;
}
//减法
case I.SUB: {
//减数
const op1 = stack[sp--];
//被减数,都放在堆栈里
const op2 = stack[sp--];
//相减的结果,放到堆栈
stack[++sp] = op2 - op1;
break;
}
case I.PRINT: {
const value = stack[sp--];
builtins.print(value);
break;
}
case I.HALT: {
return;
}
//函数调用
case I.CALL: {
//函数地址
const op1_address = code[ip++];
//参数个数
const op2_numberOfArguments = code[ip++];
console.log(".....",op1_address,op2_numberOfArguments)
//参数个数入栈
stack[++sp] = op2_numberOfArguments;
//旧栈帧入栈
stack[++sp] = fp;
//指令指针
stack[++sp] = ip;
//console.log("call",stack);return
//独立的栈帧,从当前堆栈指针处开始
fp = sp;
//指令指针变化,开始执行call函数
ip = op1_address;
break;
}
case I.RETURN: {
const returnValue = stack[sp--];
sp = fp;
ip = stack[sp--];
fp = stack[sp--];
const number_of_arguments = stack[sp--];
sp -= number_of_arguments;
stack[++sp] = returnValue;
break;
}
case I.LOAD: {
//补偿地址,ip指向指令地址,通过补偿值,获得函数调用前压入的参数
const op_offset = code[ip++];
const value = stack[fp + op_offset];
//console.log(value);return
stack[++sp] = value;
break;
}
case I.JUMP_IF_NOT_ZERO: {
const op_address = code[ip++];
const value = stack[sp--];
if (value !== 0) {
ip = op_address;
}
break;
}
default:
throw new Error(`Unknown instruction: ${instruction}.`);
}
三、实例
JS虚拟机已简单实现。然后,准备一段JS代码生成的opcode,如下:
1, 10, 5, 7, 1, 3, 4, 7, -3,1, 1, 10, 9, 17, 1, 1, 6, 7, -3, 7, -3, 1, 1, 10, 5, 7, 1, 11, 6
看起来仅仅是些数字,先看效果,在虚拟机中执行:
如上图,输出是一个数值。那么,这段opcode究竟是什么呢?
其实,它是这样一段JS源代码转化而来:
function factorial(n) {
if (n === 1) {
return 1;
}
return n * factorial(n - 1);
}
const result = factorial(10);
console.log(result);
将上述opcode转换一个形式,把数字替换为前面讲到过的汇编指令,会得到如下形式的类asm代码:
I.CONST,
10,
I.CALL,
/* factorial */ 7,
1,
I.PRINT,
I.HALT,
I.LOAD, // factorial start,7指向的即是这里
-3,
I.CONST,
1,
I.SUB,
I.JUMP_IF_NOT_ZERO,
17,
I.CONST,
1,
I.RETURN,
/* n */ I.LOAD,
-3,
/* factorial(n - 1) */ I.LOAD,
-3,
I.CONST,
1,
I.SUB,
I.CALL,
/* factorial */ 7,
1,
I.MUL,
I.RETURN, // factorial end
对照JS源码、虚拟机代码,仔细阅读,方能理解此段汇编代码的含意,相应的,也就可以理解opcode。
但如果未得到得虚拟机代码,或是虚拟机代码又被进行了加密,如:使用JShaman对虚拟机代码进行了混淆加密。那,想要理解opcode,则是万难。
最后,请再来欣赏这段优雅的JS代码:
1, 10, 5, 7, 1, 3, 4, 7, -3,1, 1, 10, 9, 17, 1, 1, 6, 7, -3, 7, -3, 1, 1, 10, 5, 7, 1, 11, 6
仅是一行,如果是大段大段的,或是夹杂在混淆加密保护过的JS代码中,酸爽。