基于栈的虚拟机反编译

Published: 2025年09月19日

In Reverse.

之前分析的全是基于寄存器的字节码,基于栈的C#/CPython/JVM反编译已经很成熟了,而实际还会遇到的栈式VM基本都是VMP混淆的代码,U1S1,这种代码通常是不需要反编译的,直接分析到每个handler后,写个反汇编器,再配合AI就能还原出90%了,再动态跟一遍就能百分百恢复,不过,就是想玩玩🐶~

栈 or 寄存器式虚拟机

最常见的就是基于栈和基于寄存器的虚拟机,现在说明一下他们各自的优缺点。

栈机

它的指令集特别简单,操作数隐含在栈上所以字节码非常紧凑,编译器实现起来也非常简单,只需要对AST做一次后序遍历即可,不需要考虑复杂的寄存器分配,缺点是不好直接做静态分析和优化,而且在栈在内存中没优化好会导致频繁的访存,当然了,只要技术好都不是问题,JVM就是栈机依然跑的飞快,还有.NET CLR...

寄存器机

相对于栈机,开发门槛更高(当然了部分场景可以基于LLVM这些成熟的东西搞),它的指令集更接近物理硬件,指令需要编码操作数(作为优化可隐含累加器)实现起来更复杂,字节码更长,不过优化和静态分析会好做很多,ART,V8,WASM等都是基于寄存器的...

原始VMP分析

静态分析

通过跟踪启动器可定位到该参数在bdms的X函数里生成的,该函数明显是一个while-switch形式的虚拟机解释器,使用了switch2if来混淆opcode的handler:

function X(t, e, r, n) {
    var o, i, u, c, a, s, f, l, p = -1, h = [], v = [];
    // var instructions, strictMode, exceptionTable, scopeChain, thisBinding, instructionPointer, executionState, stateValue, stackPointer = -1, operandStack = [], callStack = [];
    g(t, e, r, n);    // bytecode, thisValue, args, closureEnv
    do {    // 外层循环
        try {
            y()    // 内层while-switch执行每条指令
        } catch (t) {
            f = 3,l = t
        }
    } while (d());    // 处理状态
    return l;
    function g(t, e, r, n) {
        // ... 这个下面分析
        s = 0,    // 初始化PC指针
            f = 0,    // 初始化状态类型
            l = void 0 // 初始化状态值
    }
    function y() {
        for (; ; ) {
            var t = o[s++];
            if (t < 38)
                if (t < 19)
                    if (t < 9)
                        if (t < 4)
                            if (t < 2)
                                if (0 === t) {
                                    var e = h[p--];
                                    h[p] = h[p] === e
                                } else
                                    e = h[p--],
                                        h[p] = h[p] < e;
                            else if (2 === t) {
                                var r = h[p--]
                                    , n = (U = h[p--])[r]--;
                                h[++p] = n
                            } else {
                                var y = o[s++];
                                n = h[p--],
                                    Object.defineProperty(h[p], z[y], {
                                        value: n,
                                        writable: !0,
                                        configurable: !0,
                                        enumerable: !0
                                    })
                            }
            // ...
        }
    }
    function d() {
        var t = s, e = u;
        if (1 === f) {
            for (var r = e.length - 1; r >= 0; --r)
                if ((n = e[r])[0] < t && t <= n[3])
                    return t <= n[2] && n[2] !== n[3] ? s = n[2] : (s = l,
                        f = 0,
                        l = void 0),
                        !0;
            throw new SyntaxError("Illegal statement")
        }
        if (2 === f) {
            for (r = e.length - 1; r >= 0; --r)
                if ((n = e[r])[0] < t && t <= n[2] && n[2] !== n[3])
                    return s = n[2],
                        !0;
            return !!(g = v.pop()) && (h[++p] = l,o = g[0],
i = g[1],u = g[2],c = g[3],a = g[4],s = g[5],f = g[6],l = g[7],!0)
        }
        if (3 === f) {
            for (r = e.length - 1; r >= 0; --r) {
                var n;
                if ((n = e[r])[0] < t) {
                    if (t <= n[1] && n[1] !== n[2])
                        return s = n[1],h[++p] = l,f = 0,l = void 0,!0;
                    if (t <= n[2] && n[2] !== n[3])
                        return s = n[2],!0
                }
            }
            var g;
            if (g = v.pop())
                return o = g[0],i = g[1],u = g[2],c = g[3],a = g[4],s = g[5],d();
            throw l
        }
        return !0
    }
    function m(t, e) {
        var r = Object.create(null);
        return Object.defineProperty(r, t, {
            get: function() {
                if (globalThis[t])
                    return globalThis[t];
                throw new ReferenceError(t + " is not defined")
            },
            set: function(r) {
                if (e && !globalThis[t])
                    throw new ReferenceError(t + " is not defined");
                globalThis[t] = r
            }
        }),r
    }
}

简单逆向分析即可得出各函数作用: 1.X: vm入口,所有虚拟函数从这里被执行 2.g: 设置vm上下文信息,如PC,作用域链,推入参数等 3.y: while-switch handler,字节码解释执行的主逻辑 4.d: 结果处理,如处理异常,函数返回到上层等 5.m: 创建全局对象的代理 主要看g函数,这是个栈机,它只有70来条指令且很多根本没被执行,所以只需要trace一遍把执行过的指令分析了即可,先说一个非常有利于分析的,字符串常量池,它在一个全局的变量z里,直接在任意点下断即可输出:

} else if (7 === t) {
    y = o[s++];    // 获取指令操作数
    var w = z[y];    // 读常量池
    globalThis[w] || (globalThis[w] = void 0) // 写值

接着有个关键的概念,作用域链,它有点像x86用的操作数栈,里面会存储函数参数,本地变量,以及" 链接"上层作用域:

// g 作用域链初始化
function g(t, e, r, n) {
    var p = Math.min(r.length, t[1])    // 实参和形参取最小值
        , h = {};
    Object.defineProperty(h, "length", {
        value: r.length,
        writable: !0,
        enumerable: !1,
        configurable: !0
    }),o = t[0],i = t[2],u = t[3],
        c = [n, h];    // 作用域链 c[0]=n=parent c c[1]=arguments
    for (var v = 0; v < p; ++v)
        c.push(r[v]);    // 将放入c c[1]=arg0 c[2]=arg1 ...
    if (i)
        for (a = e,
                 v = 0; v < r.length; ++v)
            h[v] = r[v];    // 再将所有参数全部放入c[1]
    // .....
}
// y 本地变量读 t = opcode = 46
else {
    for (x = o[s++],    // scope level
             y = o[s++],    // scope index
             d = c; x > 0; )    
        d = d[0],    // 向上查找到指定作用域
            --x;
    n = d[y],    // 在作用域内访问指定变量
        h[++p] = n
}

接着就可以分析另一个关键指令,函数调用,它会判断被调用者是虚拟机函数还是普通函数,如果是虚拟机函数,它会使用自己实现的调用栈来模拟函数调用:

        else if (36 === t) {
            var k = o[s++];    // 指令里获取参数个数
            p -= k;    
            var L = h.slice(p + 1, p + k + 1)    // 从操作数栈获取参数
                , T = h[p--]        // 获取被调函数
                , C = h[p--];    // this值
            if ("function" != typeof T)
                return f = 3,
                    void (l = new TypeError(typeof T + " is not a function"));
            var I = B.get(T);    // B为函数注册表
            if (I)        // 1. 为虚拟函数
                v.push([o, i, u, c, a, s, f, l]),    // 将当前上下文压栈
                    g(I[0], C, L, I[1]); // 重新初始化vm 上下文
            else { // 普通函数,直接调用
                var q = T.apply(C, L);
                h[++p] = q
            }

如果是虚拟函数,在函数调用处是不会处理返回值的,相反,在被调函数的最后函数的最后会执行ret指令,此时会将结果存入状态值里,再由d将结果压入栈中:

// y t = opcode = 15 
else if (t < 16) {
    if (14 !== t)
        return f = 2, // 状态f为2
            void (l = h[p--]); // 状态值l为栈顶

// d f = execState = 2
if (2 === f) {    
    for (r = e.length - 1; r >= 0; --r)
        if ((n = e[r])[0] < t && t <= n[2] && n[2] !== n[3])
            return s = n[2],
                !0;
    return !!(g = v.pop()) && (h[++p] = l,  // 将结果压栈
        o = g[0],// 恢复调用者上下文 
        i = g[1],
        u = g[2],
        c = g[3],
        a = g[4],
        s = g[5],
        f = g[6],
        l = g[7],
        !0)
}

现在再继续看看虚拟化函数是怎么被注册的,这含三个部位:

// W 是直接从外部直接调用虚拟化函数,其中n是特殊的scope信息
function W(t, e, r, n) {
    return z.length || function(t) {
        var e = function(t) {
            for (var e = atob(t), r = 0, n = 4; n < 8; ++n)
                r += e.charCodeAt(n);
            return {
                d: T(Uint8Array.from(e.slice(8), _, r % 256)),
                i: 0
            }
        }(t);
        z.length = 0,
            Y.length = 0,
            B.clear();
        for (var r = J(e), n = 0; n < r; ++n)
            z.push(Q(e));
        var o = J(e);
        for (n = 0; n < o; ++n) {
            for (var i = J(e), u = Boolean(J(e)), c = new Array, a = J(e), s = 0; s < a; ++s)
                c.push([J(e), J(e), J(e), J(e)]);
            for (var f = new Array, l = J(e), p = 0; p < l; ++p)
                f.push(J(e));
            Y.push([f, i, u, c])    // 将每个虚拟函数的字节码,参数个数,是否为严格模式,异常处理表等信息存放于Y
        }
    }("UEsCAJ..."),    // 解码字符串,初始化虚拟化函数信息
        X(Y[t], e, r, n)    // 调用序号为t的函数
}


// y函数内部也可注册虚拟化函数,此时的作用域链就是当前的 (t = opcode = 18)
t < 17 ? (n = h[p--],
    h[p] /= n) : 17 === t ? (n = h[p],
    h[++p] = n) : (n = K(o[s++], c),    // 将指定编号函数和作用域链注册到B
    h[++p] = n);

// K 如下,返回的n是索引,用来在函数调用时重新找到注册的函数及闭包信息
function K(t, e) {
    var r = Y[t];
    Z.has(t) && B.delete(Z.get(t));
    var n = function() {
        return X(r, this, arguments, e)
    };
    return Z.set(t, n),
        B.set(n, [r, e]),
        n
}

外界其实都是通过W函数进入虚拟机的,例如:

W(156, void 0, arguments, {    // 这是个特殊的作用域链,它不再链接上层了,格式也不符合前面提到的格式(scope[0]=parentscope scope[1]=arguments ...)!
                get 0() {return I},
                set 0(t) {I = t},
                get 1() {return q},
                set 1(t) {q = t},
                get 2() {return F},
                set 2(t) {F = t},
                // ..
            })

现在就可以直接硬干了

动态分析技巧

1.将if-else if改为switch-case形式,所有替换直接用chrome的source code替换功能即可

2.它模拟了函数调用,我们调试器的step over会失效,多个函数调用容易搞晕,简单改改就能直接把它改成系统的调用机制:

// ---- orgin
v.push([o, i, u, c, a, s, f, l]),
    g(I[0], C, L, I[1]);
// ---- modify
var q = X(I[0], C, L, I[1]);;
h[++p] = q

3.这个虚拟机和外部的所有联络都依赖于global变量,hook它就能看到它和外界的所有联络

4.可以在while第一行trace所有的opcode oprand与栈 本地变量的状态,到时候直接数据分析会十分简单

反汇编

分析y可知每个字节码指令是不定长的,除了一字节操作码,可能会存在0~2字节的立即数操作数,按这个直接编写:

        # 格式: opcode: (mnemonic, operand_count, description, original_code_line)
        self.opcodes = {
            0: ("STRICT_EQ", 0, "严格相等比较 ===",
                "operandStack[stackPointer] = operandStack[stackPointer] === right;"),

            1: ("LT", 0, "小于比较 <",
                "operandStack[stackPointer] = operandStack[stackPointer] < right;"),

            2: ("POST_DEC", 0, "后缀递减 obj[prop]--",
                "var value = (U = operandStack[stackPointer--])[prop]--;"),

            3: ("DEFINE_PROP", 1, "定义属性",
                "Object.defineProperty(operandStack[stackPointer], z[stringIndex], {...});"),

            4: ("JMP", 1, "跳转指令",
                "executionState = 1; stateValue = instructionPointer + offset;"),

            5: ("UNARY_PLUS", 0, "一元加号 +",
                "operandStack[stackPointer] = +operandStack[stackPointer];"),
            # ...

然后利用每个函数定义时的参数,即可再解析scope,执行效果为:

0000  28 CA        GET_GLOBAL 202        # 常量: "performance"
0002  11           DUP                 
0003  47 AD        GET_PROP_STR 173      # 常量: "now"
0005  24 00        CALL 0                # 参数数量: 0
0007  33 00 05     SET_SCOPE_VAR 0, 5    # local_var0
000A  39           LOAD_UNDEFINED2     
000B  2E 02 0D     GET_SCOPE_VAR 2, 13   # 上层作用域[2][13]
000E  24 00        CALL 0                # 参数数量: 0
0010  4B           POP                 
0011  2E 00 02     GET_SCOPE_VAR 0, 2    # arg_0
0014  33 00 06     SET_SCOPE_VAR 0, 6    # local_var1
0017  2E 00 03     GET_SCOPE_VAR 0, 3    # arg_1
001A  33 00 07     SET_SCOPE_VAR 0, 7    # local_var2
001D  2E 00 03     GET_SCOPE_VAR 0, 3    # arg_1
0020  0D           TYPEOF              
0021  15 08        LOAD_STRING 8         # 常量: "string"
0023  36           STRICT_NE           
0024  46 13        JMP_TRUE_KEEP 19      # 跳转到: L1
0026  2E 00 04     GET_SCOPE_VAR 0, 4    # arg_2
0029  23 0E        TERNARY_COND 14     
002B  2E 00 04     GET_SCOPE_VAR 0, 4    # arg_2
002E  11           DUP                 
002F  47 CD        GET_PROP_STR 205      # 常量: "indexOf"
0031  15 CE        LOAD_STRING 206       # 常量: "multipart/form-data"
0033  24 01        CALL 1                # 参数数量: 1
0035  48 01        LOAD_NUMBER 1       
0037  18           UNARY_MINUS         
0038  1B           NE                  

L1:
0039  1F 05        JMP_FALSE 5           # 跳转到: L2
003B  15 24        LOAD_STRING 36        # 常量: ""
003D  33 00 07     SET_SCOPE_VAR 0, 7    # local_var2

L2:
0040  28 B0        GET_GLOBAL 176        # 常量: "navigator"
0042  47 CF        GET_PROP_STR 207      # 常量: "userAgent"
0044  33 00 08     SET_SCOPE_VAR 0, 8    # local_var3
0047  2E 00 08     GET_SCOPE_VAR 0, 8    # local_var3
004A  11           DUP                 
004B  47 CD        GET_PROP_STR 205      # 常量: "indexOf"
004D  15 D0        LOAD_STRING 208       # 常量: "baiduboxapp"
004F  24 01        CALL 1                # 参数数量: 1
0051  48 00        LOAD_NUMBER 0       
0053  40           GE                  
0054  1F 13        JMP_FALSE 19          # 跳转到: L3
0056  2E 00 08     GET_SCOPE_VAR 0, 8    # local_var3
0059  11           DUP                 
005A  47 D1        GET_PROP_STR 209      # 常量: "replace"
005C  28 13        GET_GLOBAL 19         # 常量: "RegExp"
005E  15 D2        LOAD_STRING 210       # 常量: "\\s(EasyBrowser)?[Ww]ebCore=0x[a-z0-9]{9}$"
0060  4A 01        NEW 1                 # 参数数量: 1
0062  15 24        LOAD_STRING 36        # 常量: ""
0064  24 02        CALL 2                # 参数数量: 2
0066  33 00 08     SET_SCOPE_VAR 0, 8    # local_var3

L3:
0069  2E 00 08     GET_SCOPE_VAR 0, 8    # local_var3
006C  11           DUP                 
006D  47 CD        GET_PROP_STR 205      # 常量: "indexOf"
006F  15 D3        LOAD_STRING 211       # 常量: "AlipayClient"
0071  24 01        CALL 1                # 参数数量: 1
0073  48 00        LOAD_NUMBER 0       
0075  40           GE                  
0076  1F 13        JMP_FALSE 19          # 跳转到: L4
0078  2E 00 08     GET_SCOPE_VAR 0, 8    # local_var3
007B  11           DUP                 
007C  47 D1        GET_PROP_STR 209      # 常量: "replace"
007E  28 13        GET_GLOBAL 19         # 常量: "RegExp"
0080  15 D4        LOAD_STRING 212       # 常量: "\\sChannelId\\(\\d+\\)"
0082  4A 01        NEW 1                 # 参数数量: 1
0084  15 24        LOAD_STRING 36        # 常量: ""
0086  24 02        CALL 2                # 参数数量: 2
0088  33 00 08     SET_SCOPE_VAR 0, 8    # local_var3

L4:
008B  39           LOAD_UNDEFINED2     
008C  2E 02 0E     GET_SCOPE_VAR 2, 14   # 上层作用域[2][14]
008F  48 01        LOAD_NUMBER 1       
0091  48 00        LOAD_NUMBER 0       
0093  48 08        LOAD_NUMBER 8       
0095  2E 00 06     GET_SCOPE_VAR 0, 6    # local_var1
0098  2E 00 07     GET_SCOPE_VAR 0, 7    # local_var2
009B  2E 00 08     GET_SCOPE_VAR 0, 8    # local_var3
009E  2E 02 13     GET_SCOPE_VAR 2, 19   # 上层作用域[2][19]
00A1  47 89        GET_PROP_STR 137      # 常量: "pageId"
00A3  2E 02 13     GET_SCOPE_VAR 2, 19   # 上层作用域[2][19]
00A6  47 88        GET_PROP_STR 136      # 常量: "aid"
00A8  15 86        LOAD_STRING 134       # 常量: "1.0.1.20"
00AA  24 09        CALL 9                # 参数数量: 9
00AC  33 00 09     SET_SCOPE_VAR 0, 9    # local_var4
00AF  28 CA        GET_GLOBAL 202        # 常量: "performance"
00B1  11           DUP                 
00B2  47 AD        GET_PROP_STR 173      # 常量: "now"
00B4  24 00        CALL 0                # 参数数量: 0
00B6  2E 00 05     GET_SCOPE_VAR 0, 5    # local_var0
00B9  19           SUB                 
00BA  33 00 0A     SET_SCOPE_VAR 0, 10   # local_var5
00BD  39           LOAD_UNDEFINED2     
00BE  2E 02 12     GET_SCOPE_VAR 2, 18   # 上层作用域[2][18]
00C1  2E 00 0A     GET_SCOPE_VAR 0, 10   # local_var5
00C4  24 01        CALL 1                # 参数数量: 1
00C6  4B           POP                 
00C7  2E 00 09     GET_SCOPE_VAR 0, 9    # local_var4
00CA  0F           RETURN              

要将做反编译,粗暴的办法是直接将每个字节码生成AST的节点,用一个虚拟栈去压AST Node代替实际值,通过这构造AST再恢复源码,但这样没有做控制流和数据流分析,很难恢复出高级的易读的结构,最好的办法是将基于栈的指令集转换为基于寄存器的,之后就很容易做各种分析了,由它转AST再转源码会好很多。

字节码转PandaBC

待续...

参考