本系列文章所討論的 JavaScript 資安與逆向工程技術,旨在分享知識、探討防禦之道,並促進技術交流。
所有內容僅供學術研究與學習,請勿用於任何非法或不道德的行為。
讀者應對自己的行為負完全責任。尊重法律與道德規範是所有技術人員應共同遵守的準則。
本文同步發佈:https://nicklabs.cc/jsvmp-switch-case-vm-restore
當開發者採用 JSVMP 進行程式保護時,程式碼會被轉換為一系列「虛擬指令」。
這些指令不再直接表達原始邏輯,而是交由自製的虛擬機(VM)在執行時解譯。
現在我們就來探討如何在實戰中解析 switch-case VM 這種模式。
我們將以一個簡單的加法程式作為範例,觀察它如何被混淆成JSVMP,然後一步步將其還原。
function calculate() {
var a = 5;
var b = 10;
var result = a + b;
console.log("結果是:" + result);
}
calculate();
假設你得到以下這段混淆後的 JavaScript 程式碼:
var _0x53d2 = [
'log',
'結果是:',
'342358wWzYvP',
'__op_load_const',
'__op_add',
'__op_print',
'call',
'prototype',
'1523456oPtQvB'
];
var _0x5b6c = function(_0x4d21) {
_0x4d21 = _0x4d21 - 0x12a;
var _0x54d2 = _0x53d2[_0x4d21];
return _0x54d2;
};
(function() {
var _0x12128e = [
_0x5b6c('0x12d'), 5,
_0x5b6c('0x12d'), 10,
_0x5b6c('0x12e'),
_0x5b6c('0x12f'), _0x5b6c('0x12b')
];
var _0x393c0d = 0;
var _0x1a84f3 = [];
while (true) {
var _0x58249a = _0x12128e[_0x393c0d++];
switch (_0x58249a) {
case _0x5b6c('0x12d'): // __op_load_const
_0x1a84f3.push(_0x12128e[_0x393c0d++]);
break;
case _0x5b6c('0x12e'): // __op_add
var _0x1f727c = _0x1a84f3.pop();
var _0x5e97ee = _0x1a84f3.pop();
_0x1a84f3.push(_0x5e97ee + _0x1f727c);
break;
case _0x5b6c('0x12f'): // __op_print
var _0x3d4389 = _0x1a84f3.pop();
console[_0x5b6c('0x12a')](_0x5b6c('0x12b') + _0x3d4389);
return;
default:
// ... 其他情況
break;
}
}
})();
從程式碼中可以看到 _0x12128e 陣列變數包含了類似指令的字串以及資料。
_0x5b6c('0x12d') => __op_load_const
_0x5b6c('0x12e') => __op_add
_0x5b6c('0x12e') => __op_print
_0x5b6c('0x12b') => 結果是:
上面是指令
5, 10
上面是資料
這些就是虛擬機要執行的指令集及資料。
_0x393c0d 變數被初始化為 0,並且在 _0x12128e[_0x393c0d++] 中遞增。
這就是用來追蹤執行進度的指令指標。
_0x1a84f3 陣列透過 .push() 和 .pop() 操作就是典型的堆疊結構,用於儲存運算過程中的臨時變數。
while (true) 迴圈和內部的 switch (_0x58249a) 就是整個虛擬機的主迴圈和分發器。
分析 switch 語句中的每個 case 區塊,將混淆的指令名稱與其對應的實際操作進行對照。
_0x5b6c('0x12d') 透過前面的解密函式得到實際內容為 '__op_load_const'。
_0x1a84f3.push(_0x12128e[_0x393c0d++])。
從指令陣列中取出下一個值並將其推入堆疊陣列中。
_0x5b6c('0x12e') 透過前面的解密函式得到實際內容為 '__op_add'。
var _0x1f727c = _0x1a84f3.pop();
var _0x5e97ee = _0x1a84f3.pop();
_0x1a84f3.push(_0x5e97ee + _0x1f727c);
從_0x1a84f3 堆疊陣列透過 .pop() 取出兩個需要相加的資料。
然後將相加結果 push 回堆疊陣列中。
var _0x3d4389 = _0x1a84f3.pop();
_0x5b6c('0x12f') 透過前面的解密函式得到實際內容為 '__op_print'。
_0x5b6c('0x12a') 透過前面的解密函式得到實際內容為 'log'。
_0x5b6c('0x12b') 透過前面的解密函式得到實際內容為 '結果是:'。
console.log('結果是:' + _0x3d4389)。
這是一個輸出結果的指令。
從上面的分析已經知道執行的邏輯及每一個switch case相對應處理的邏輯,接下來就手動追蹤虛擬機的執行流程:
執行 __op_load_const 將 5 推入堆疊。
此時堆疊內容為 [5]。
執行 __op_load_const 將 10 推入堆疊。
此時堆疊內容為 [5, 10]。
執行 __op_add,取出 10 和 5,計算 5 + 10,將結果 15 推回堆疊。
此時堆疊內容為 [15]。
執行 __op_print,從堆疊取出 15,並輸出 "結果是:15"。
此時堆疊內容為 []。
將這些步驟組合成一個可讀的程式碼,最終得到:
var a = 5;
var b = 10;
var result = a + b;
console.log("結果是:" + result);
透過這個簡單範例可以看到 JSVMP 混淆的運作模式,將原始程式轉換為虛擬指令,並透過自製 VM 步步解譯。
藉由分析指令集、指令指標、堆疊,以及 switch-case 分發器,我們能夠一步步還原出程式的原貌。
不過這個案例是簡單的加法運算但在實際逆向工程中,JSVMP 的複雜度會更高。
只要掌握 VM 的核心與運作邏輯就能逐步拆解其混淆並還原出原始的程式邏輯。