
本系列文章所討論的 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 的核心與運作邏輯就能逐步拆解其混淆並還原出原始的程式邏輯。