iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Security

走進資安現場: JavaScript資安逆向工程超實戰系列 第 14

Day 14 JSVMP 實戰觀察:逐步還原 switch-case VM 混淆程式

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250914/201697756edsme1cDG.jpg

本系列文章所討論的 JavaScript 資安與逆向工程技術,旨在分享知識、探討防禦之道,並促進技術交流。
所有內容僅供學術研究與學習,請勿用於任何非法或不道德的行為。
讀者應對自己的行為負完全責任。尊重法律與道德規範是所有技術人員應共同遵守的準則。

本文同步發佈:https://nicklabs.cc/jsvmp-switch-case-vm-restore

當開發者採用 JSVMP 進行程式保護時,程式碼會被轉換為一系列「虛擬指令」。

這些指令不再直接表達原始邏輯,而是交由自製的虛擬機(VM)在執行時解譯。

現在我們就來探討如何在實戰中解析 switch-case VM 這種模式。

JSVMP混淆程式碼

我們將以一個簡單的加法程式作為範例,觀察它如何被混淆成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;
        }
    }
})();

JSVMP 還原流程

辨識與分析 VM 結構

從程式碼中可以看到 _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 區塊,將混淆的指令名稱與其對應的實際操作進行對照。

case _0x5b6c('0x12d'):

_0x5b6c('0x12d') 透過前面的解密函式得到實際內容為 '__op_load_const'。

_0x1a84f3.push(_0x12128e[_0x393c0d++])。

從指令陣列中取出下一個值並將其推入堆疊陣列中。

case _0x5b6c('0x12e'):

_0x5b6c('0x12e') 透過前面的解密函式得到實際內容為 '__op_add'。

var _0x1f727c = _0x1a84f3.pop();
var _0x5e97ee = _0x1a84f3.pop();
_0x1a84f3.push(_0x5e97ee + _0x1f727c);

從_0x1a84f3 堆疊陣列透過 .pop() 取出兩個需要相加的資料。

然後將相加結果 push 回堆疊陣列中。

case _0x5b6c('0x12f'):

var _0x3d4389 = _0x1a84f3.pop();

_0x5b6c('0x12f') 透過前面的解密函式得到實際內容為 '__op_print'。

_0x5b6c('0x12a') 透過前面的解密函式得到實際內容為 'log'。

_0x5b6c('0x12b') 透過前面的解密函式得到實際內容為 '結果是:'。

console.log('結果是:' + _0x3d4389)。

這是一個輸出結果的指令。

追蹤指令執行流程

從上面的分析已經知道執行的邏輯及每一個switch case相對應處理的邏輯,接下來就手動追蹤虛擬機的執行流程:

指令 __op_load_const, 5

執行 __op_load_const 將 5 推入堆疊。

此時堆疊內容為 [5]。

指令 __op_load_const, 10

執行 __op_load_const 將 10 推入堆疊。

此時堆疊內容為 [5, 10]。

指令 __op_add

執行 __op_add,取出 10 和 5,計算 5 + 10,將結果 15 推回堆疊。

此時堆疊內容為 [15]。

指令 __op_print

執行 __op_print,從堆疊取出 15,並輸出 "結果是:15"。

此時堆疊內容為 []。

還原成可讀的程式碼

將這些步驟組合成一個可讀的程式碼,最終得到:

var a = 5;
var b = 10;
var result = a + b;
console.log("結果是:" + result);

透過這個簡單範例可以看到 JSVMP 混淆的運作模式,將原始程式轉換為虛擬指令,並透過自製 VM 步步解譯。

藉由分析指令集、指令指標、堆疊,以及 switch-case 分發器,我們能夠一步步還原出程式的原貌。

不過這個案例是簡單的加法運算但在實際逆向工程中,JSVMP 的複雜度會更高。

只要掌握 VM 的核心與運作邏輯就能逐步拆解其混淆並還原出原始的程式邏輯。


上一篇
Day 13 JSVMP 混淆: JavaScript 虛擬機的原理
下一篇
Day 15 Node.js 如何生成 AES、DES 加解密
系列文
走進資安現場: JavaScript資安逆向工程超實戰15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言