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代碼中,酸爽。