JS VMP 原理详解:从虚拟机到代码保护

更新日期: 2026-04-22 阅读: 16 标签: 虚拟机

什么是VMP

VM即Virtual Machine,翻译为虚拟机。它的工作方式是:接受源代码编译生成的字节码,然后将字节码转换成对应平台CPU能够识别的机器码。

为什么要有VM,而不是直接把源代码编译成机器码?因为通过字节码加VM的运行方式,可以实现多平台运行,程序不再和平台强绑定。

JS VMP,指在JavaScript代码里,用软件手写一个虚拟的CPU,专门用来执行一套加密过的私有指令,从而保护核心逻辑。


创建VMP的原理

第一步:编译

将源代码编译成字节码。因为是自己的虚拟机,所以可以自定义每个操作对应的字节码,只要不产生冲突即可。

例如:

  • 0x12 代表加法运算

  • 0x55 代表取余

  • 0x99 代表减法运算

第二步:解释器/执行器

浏览器在获取字节码之后无法正常执行,因此需要虚拟机的CPU进行指令识别和指令执行。解释器和执行器一般同时出现在一个巨大的死循环中。解释器每拿到一条指令,就会通过switch来进行指令匹配,匹配到对应的case块之后执行case块内的语句。


常见VMP结构

// 此处 data 是源代码编译后得到的字节码
function virtual_machine(data) {
    var program_counter = 0; // 指令指针,记录当前执行到第几条指令
    var stack = []; // 栈,用来临时存放数据

    // 巨大的死循环,模拟机器持续运转
    while (true) {
        // 1. 取出一条指令
        var opcode = data[program_counter];
        program_counter++;

        // 2. 解释器解释指令
        switch (opcode) {
            case 0x01: // 加法
                var a = stack.pop();
                var b = stack.pop();
                stack.push(a + b);
                break;
            case 0x02: // 输出
                console.log(stack.pop());
                break;
            case 0x03: // 结束
                return;
            // 实际应用中这里会有成百上千个case
        }
    }
}


实战创建VMP

假设要完成一个加法操作 c = a + b,写出对应的指令集(栈模式写法):

PUSH 10
PUSH 20
ADD
POP ECX

栈模式对应的JS代码如下:

// 指令集定义:代表一系列待执行的操作
const instructionSequence = [
    { action: "LOAD", parameter: 2000 },
    { action: "LOAD", parameter: 210 },
    { action: "ADD" },
    { action: "RETURN" },
];

一、创建编译器

构建指令集:确定要完成的操作后,可以自定义每个指令对应的操作码。要实现加法一共有三个操作:LOAD、ADD、RETURN。定义如下:

// 映射表:将可读的指令动作映射为混淆后的操作码
const operationCodeMapping = {
    "LOAD": "_0x04de2",
    "ADD": "_0x04de3",
    "RETURN": "_0x04de4"
};

构建编译程序:将操作翻译为对应的操作码

/**
 * 编译器函数:将高层指令转换为虚拟机可识别的字节码字符串
 * @param {Array} instructions - 指令数组
 * @returns {string} - 格式化后的字节码
 */
function compileToBytecode(instructions) {
    const rawBytecodes = [];

    for (const instruction of instructions) {
        // 提取混淆后的指令名称
        const mappedCode = operationCodeMapping[instruction.action];
        rawBytecodes.push(mappedCode);

        // 如果存在参数,将其转换为ASCII字符编码
        if (instruction.parameter !== undefined) {
            // 将数字转化为对应的Unicode字符
            const charCode = String.fromCharCode(instruction.parameter);
            rawBytecodes.push(charCode);
            // 注意:此处默认参数只有一位
            // 如果参数是字符串类型,则需要先把字符串长度压入栈中,再处理字符串
        }
    }

    // 使用管道符|连接,形成最终的指令流
    return rawBytecodes.join("|");
}

二、编译源代码

完成编译器的操作后,编译源代码,最终得到:

_0x04de2|97|_0x04de2|98|_0x04de3|_0x04de4

三、创建解释器

如果提供上面这串莫名其妙的字符串作为输入,浏览器不知道要做什么,因此需要创建自己的解释器来解释输入,明确要执行哪些动作。

(1)初始化VM

初始化一个空栈用于运算,初始化操作码映射表用于将操作码翻译为指令:

function VirtualMachine() {
    // 数据栈:用来存放运算过程中的临时数据
    this.dataStack = [];
    // 指令映射表:将混淆后的字符串映射回逻辑动作
    this.operationCodeMapping = {
        "LOAD": "_0x04de2",
        "ADD": "_0x04de3",
        "RETURN": "_0x04de4"
    };
}

(2)定义核心运算方法

核心运算操作一般直接定义在VM的原型链上。此处相当于add被混淆成了_0x00af:

/**
 * 核心运算方法(原add)
 * 执行具体的数学加法逻辑
 */
VirtualMachine.prototype._0x00af = function(_0x00bf, _0x00cf) {
    return _0x00bf + _0x00cf;
};

(3)创建解释器

// 解释器和执行器
VirtualMachine.prototype.executer = function(rawOPCode) {
    let OPCodeSeq = rawOPCode.split("|");
    let pc = 0;
    while (pc < OPCodeSeq.length) {
        switch (OPCodeSeq[pc++]) {
            case this.operationCodeMapping.LOAD:
                this.dataStack.push(OPCodeSeq[pc++].charCodeAt());
                break;
            case this.operationCodeMapping.ADD:
                console.log(this.dataStack);
                this.dataStack.push(this._0x00af.apply(
                    this, [this.dataStack.pop(), this.dataStack.pop()]
                ));
                break;
            case this.operationCodeMapping.RETURN:
                return this.dataStack.pop();
        }
    }
};

四、调用并执行

const JSVMP = new VirtualMachine();
const res = JSVMP.executer("_0x04de2|ߐ|_0x04de2|Ò|_0x04de3|_0x04de4");
console.log(res);


总结

VMP的核心思想是将原始程序逻辑编译为自定义字节码,然后由虚拟机解释器来执行,而不是由宿主CPU或原生执行环境直接运行真实语义。

其本质结构通常包括:

  • 字节码

  • 虚拟CPU(包含程序计数器、数据栈或寄存器)

  • 指令分发机制

  • 状态驱动执行模型

解释器通过while循环不断读取opcode,根据调度机制(如switch或函数表)分发到对应的handler执行,从而将原本具有清晰结构的控制流(if、for、call等)转化为统一的状态机循环结构,实现控制流平坦化。

对于逆向分析来说,由于程序结构被抹平、语义被封装进自定义指令处理函数中,分析者难以通过控制流图还原真实逻辑,同时自动化分析、符号执行和模式匹配等工具也难以直接适配自定义指令集。

VMP的优势不仅在于混淆控制流,还在于隐藏指令语义、提高逆向成本、支持动态解密与环境绑定,并具备良好的跨平台封装能力。虽然理论上解释器存在就意味着语义可以被还原,但VMP的核心目标并非绝对不可破解,而是显著提高攻击成本,使破解变得复杂、耗时且难以规模化。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13668

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!