JS代码保护,有多种方式,如常规的JS混淆加密、如bytecode化、又或如虚拟机化。这里简单探讨虚拟机JS保护。一、原理虚拟机保护的最终目标,是将JS代码转为opcode,或汇编语言式代码,在虚拟机中执行。
一般是保护重要的函数、算法、当然也可以保护更多更大段的代码。
更详细一些来说,汇编语言式代码,形态会类似:pushapushbpushccallfunpop这是古老的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方法模拟压栈,用数组索实现堆栈指针、指令指针、栈帧。本例中,汇编指令,则实现一部分操作,如:
constI={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){//常量caseI.CONST:{//常量值constop_value=code[ip++];//存放到堆栈stack[++sp]=op_value;console.log("const",stack)break;}caseI.ADD:{constop1=stack[sp--];constop2=stack[sp--];stack[++sp]=op1+op2;break;}//减法caseI.SUB:{//减数constop1=stack[sp--];//被减数,都放在堆栈里constop2=stack[sp--];//相减的结果,放到堆栈stack[++sp]=op2-op1;break;}caseI.PRINT:{constvalue=stack[sp--];builtins.print(value);break;}caseI.HALT:{return;}//函数调用caseI.CALL:{//函数地址constop1_address=code[ip++];//参数个数constop2_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;}caseI.RETURN:{constreturnValue=stack[sp--];sp=fp;ip=stack[sp--];fp=stack[sp--];constnumber_of_arguments=stack[sp--];sp-=number_of_arguments;stack[++sp]=returnValue;break;}caseI.LOAD:{//补偿地址,ip指向指令地址,通过补偿值,获得函数调用前压入的参数constop_offset=code[ip++];constvalue=stack[fp+op_offset];//console.log(value);returnstack[++sp]=value;break;}caseI.JUMP_IF_NOT_ZERO:{constop_address=code[ip++];constvalue=stack[sp--];if(value!==0){ip=op_address;}break;}default:thrownewError(`Unknowninstruction:{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源代码转化而来:
functionfactorial(n){if(n===1){return1;}returnn*factorial(n-1);}constresult=factorial(10);console.log(result);将上述opcode转换一个形式,把数字替换为前面讲到过的汇编指令,会得到如下形式的类asm代码:
I.CONST,10,I.CALL,/*factorial*/7,1,I.PRINT,I.HALT,I.LOAD,//factorialstart,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,//factorialend对照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代码中,酸爽。