天天看點

JS加密?用虛拟機opcode保護JS源碼

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
           

看起來僅僅是些數字,先看效果,在虛拟機中執行:

JS加密?用虛拟機opcode保護JS源碼

如上圖,輸出是一個數值。那麼,這段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代碼中,酸爽。