手把手教你给某讯滑块的JSVMP写反编译器(如宝宝辅食一样易懂)

admin 2026-01-29 01:17:02 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入剖析腾讯防水墙TDC.js的JSVMP保护机制,阐述虚拟机指令结构及变量装箱特性。作者提出基于AST符号执行的反编译方案,通过状态管理与分支探索,成功还原表达式、函数及控制流逻辑,实现从混淆字节码到源码的转换,为JS逆向分析提供了极具价值的实战经验与思路。 综合评分: 92 文章分类: 逆向分析,WEB安全,实战经验


cover_image

手把手教你给某讯滑块的JSVMP写反编译器 (如宝宝辅食一样易懂)

原创

吾爱pojie 吾爱pojie

吾爱破解论坛

2026年1月28日 16:34 北京

作者坛账号:Command

前言

文章中所有内容仅供学习交流使用,不用于其他任何目的,禁止用于商业和非法用途,否则由此产生的一切后果与作者本人无关。

本反编译器针对防水墙TDC.js的JSVMP, 先给大家上个图片看一下反编译后的效果 (恕我不能全放出来, 全部反编译完有2500多行, 太多了)

预备知识

  • JSVMP简单说就是将JS代码通过自己实现的编译器编译到字节码形式, 使用时通过自己实现的VM(虚拟机)运行字节码
  • JSVMP的代码里通常没有具体逻辑, 就是一个大数组然后不断跑循环, 一般的虚拟机都是基于栈/堆栈的
  • 一般来说, 目前逆向JSVMP都是通过插桩打日志然后硬看 (大概是吧)
  • 注意: TDC的指令会进行乱序处理, 每一次的指令数组会被打乱重排

虚拟机分析

解密字节码

直接把这个自执行函数和上面用到的的函数扣下来即可, 放到控制台就能得到指令字节码(一个大数组)

VM简单看一眼

 复制代码 隐藏代码
function__TENCENT_CHAOS_VM(PC, Codes, Stack) {
var Z = !Stack,
    ErrCB = [], // Try (异常时会从中获取设置)
    Stack = Stack || [[this], [{}]], // VM栈
    G = null; // 异常变量

for (
    var B = [func1, func2, func3...... /* 指令这会先不看 */]; ;
  )
    try {
      for (var A = !1; !A; ) A = B[Codes[PC++]](); // 从指令表中读取并执行, 返回True时停止执行(return或throw)
      if (G) throw G;
      return Z
        ? (Stack.pop(), Stack.slice(3 + __TENCENT_CHAOS_VM.v))
        : Stack.pop(); // 返回值, 一般执行完后直接从栈中pop
    } catch (X) { // 异常处理
      var o = ErrCB.pop(); // 从异常处理栈中获取 [跳转的位置, 栈高度, 异常变量存放位置(可选)] 并设置
      if (o === undefined) throw X; // 如果没有try catch处理该异常, 则继续抛
      ((G = X), // 保存异常变量
        (PC = o[0]),
        (Stack.length = o[1]),
        o[2] && (Stack[o[2]][0] = G));
    }
}

从这里我们可以看到, 一共有两个栈 (混合栈Stack, 异常栈ErrCB), 并且没有其他的寄存器, 堆等;

混合栈Stack在此既充当了作用域内存(存储变量), 又充当了操作数栈(用于计算)

VM指令分析

让函数数组更易读

 复制代码 隐藏代码
var D = [function() {w[w.length - 2] = w[w.length - 2] == w.pop()}
        ,,function() {w[w.length - 1] = E[w[w.length - 1]]},
        function() {w.push(w[j[I++]][0])}
        /*......*/ ]

原先的数组中会出现,,在数组中插入空白干扰, 且数组形式不方便知道当前指令的索引

不妨写代码使用Babel进行处理 (需提前NPM安装依赖)

 复制代码 隐藏代码
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
const t = require("@babel/types");

const code = ``; // 将函数数组代码放在这里

const ast = parser.parse(code, {
sourceType: "unambiguous",
});

traverse(ast, {
ArrayExpression(path) {
    const elements = path.node.elements;
    if (path.node.elements.length > 10) {
        // console.log(elements)
        const properties = elements.map((el, idx) =>{
            if (el !== null) {
                return t.objectProperty(
                    t.numericLiteral(idx),
                    el
                )
            } else {
                returnundefined
            }
            }
        ).filter((X) => X);
        const obj = t.objectExpression(properties);
        path.replaceWith(obj);
    }

  }
});

const output = generator(ast, {
minified: false,
comments: false,
}).code;

console.log(output);

经处理后即可得到

 复制代码 隐藏代码
var D = {
0: function () {
    w[w.length - 2] = w[w.length - 2] == w.pop();
  },
2: function () {
    w[w.length - 1] = E[w[w.length - 1]];
  },
3: function () {
    w.push(w[j[I++]][0]);
  }
/* ...... */
}

栈中变量存储&引用

个人认为此部分是这个VM比较难理解的地方, 理解这里之后基本上写反编译器就没啥问题了.

他难理解就难理解在, 变量不是直接以值的形式存储在栈的索引上, 而是被包裹在一个数组中(BOX), 就像这样

 复制代码 隐藏代码
Stack[index] = [ value ] // 实际值被包裹住, 应该是为了引用传递 (函数中会复制栈), 避免内部修改变量后无法从外部获取更新的值

以下是与变量&引用有关的指令(由于字节码会改变, 故不再展示字节码):

变量声明 (相当于var XXX;, 提前占位):

 复制代码 隐藏代码
function () {
    var Z = Codes[PC++]; // 操作数: 变量在栈上的索引
    Stack[Z] = Stack[Z] === undefined ? [] : Stack[Z];
}

创建变量引用 (注: 这里不会创建一个值为数字的变量):

 复制代码 隐藏代码
function () {
    // 引用栈上变量
    Stack.push([Codes[PC++]]); // 将变量索引包装后PUSH
}

根据引用赋值:

 复制代码 隐藏代码
function () {
    // 设置变量 (数字引用找变量, 然后复制)
    // Stack.length-2 是上文创建的变量引用, Stack.length-1 是要赋的值 (复制后并不会删除, 与AssignmentExpression行为一致)
    Stack[Stack[Stack.length - 2][0]][0] = Stack[Stack.length - 1];
}

变量读取:

 复制代码 隐藏代码
function () {
    // 读变量
    // 操作数: 变量在栈上的索引
    Stack.push(Stack[Codes[PC++]][0]);
},

结合函数定义(指令):

 复制代码 隐藏代码
function () {
    // ...
    Stack.push(functionQ() {
        var Z = W.slice(0); // 复制栈
        ((Z[0] = [this]),   // this变量
        (Z[1] = [arguments]), // arguments变量
        (Z[2] = [Q]));      // 函数自身 变量 (在大多数情况下会被直接覆盖占用)
        // ...
        return__TENCENT_CHAOS_VM(G, Codes, Z);
    });
},

不难得到如下栈布局:

(~~诶你说是哪个天才想到这么整的~~)

成员引用/访问 (MemberExpression)

(这里也挺抽象的, 第一次接触很容易就一脸懵)

当VM执行类似 obj.prop 的操作时, 它不会直接把 prop 的值压栈并读取, 而是把一个数组压栈(创建一个引用), 直接按MemberExpression就好, 此数组不会被存储为变量:

 复制代码 隐藏代码
[Object/* 目标对象 */, PropertyName/* 属性名称 */]

以下是与此有关的指令(由于字节码会改变, 故不再展示字节码):

创建成员引用:

 复制代码 隐藏代码
function () {
    // MemberExpression, 成员引用 [Object, Property]
    Stack.push([Stack.pop(), Stack.pop()].reverse());
}

读取变量然后创建成员引用:

 复制代码 隐藏代码
function () {
    // 栈布局: [var1, var2.., 变量引用([n]), 属性名]
    var Z = Stack.pop(); // 属性名
    // 下方Stack.pop() 获取变量引用,Stack[...] 从变量引用读取变量
    Stack.push([Stack[Stack.pop()][0], Z]); // 压入 成员引用[变量值, 属性名]
}

读成员引用:

 复制代码 隐藏代码
function () {
    var Z = Stack.pop(); // Z 是 [Object, Key]
    Stack.push(Z[0][Z[1]]); // 执行 Object[Key] 并将结果值压栈
}

赋值成员引用:

 复制代码 隐藏代码
function () {
    // 从引用设置Property, 不会pop (与AssignmentExpression相符)
    var Z = Stack[Stack.length - 2]; // 获取栈顶下方的引用 [Object, Key]
    Z[0][Z[1]] = Stack[Stack.length - 1]; // 将栈顶的值赋给 Object[Key]
}

函数调用:

 复制代码 隐藏代码
function () {
    // 调用函数, 但是MemberExpression -> CallExpression
    var Z = Codes[PC++], // 参数个数
        l = Z ? Stack.slice(-Z) : [], // 获取参数
        Z = ((Stack.length -= Z), Stack.pop()); // 弹出成员引用

    // Z[0] 是 Object, Z[1] 是 Key
    Stack.push(Z[0][Z[1]].apply(Z[0], l)); // 以Z[0]为this调用Z[0][Z[1]](l...)
}

链式访问:

 复制代码 隐藏代码
function () {
    // 读MemberExpression并再MemberExpression (例 a.b .c)
    var Z = Stack.pop(), // 新的属性 (c)
        l = Stack.pop(); // 原引用 [a, b]
    // l[0][l[1]] 读a.b
    // 压入新的引用 [a.b(值), c]
    Stack.push([l[0][l[1]], Z]);
}

OK, 对虚拟机的分析到这里就结束了, 接下来直接开干!

反编译器编写

思路

使用AST表示所有值:不再PUSH具体的字面量, 而是使用 AST 节点 (如 {type: 'Literal', value: 1}),

符号执行:当虚拟机执行具体的操作时, 不计算结果, 而是生成与操作相对应的AST并正常放入栈中

(上面那俩是不是重复了, 说白了就是全套一层AST)

分支探索: 遇到有条件跳转时, 复制现场环境并递归探索另一条分支

使用标记: 对于被使用过的AST, 将它们从列表中隐藏, 用于区分中间产物Expression与结果ExpressionStatement   (我知道你看不懂这个, 你往下看就知道了)

警告, 下文假设你足够了解Javascript AST

状态管理

在遇到分支/进入函数时, 需要保存现场, 并复制一份(用于递归调用, 走入另一条分支); 还需要留存AST, 用于最后生成代码

这时就要引入一个状态类

 复制代码 隐藏代码
classVMState {
    // 位置, 栈, 异常栈, 变量数
    constructor(PC, Stack, ErrCB, VARCount = 0) {
        this.PC = PC
        this.Stack = Stack
        this.ErrCB = ErrCB
        this.VARCount = VARCount
        this.ASTs = [] // 存储AST (未被当做值引用的Expression和Statement, 这样可以分行)
    }

    // 复制一份
    Copy() {
        returnnewVMState(this.PC, [...this.Stack], [...this.ErrCB], this.VARCount)
    }

    // 获取临时变量名
    GetVARName() {
        return'v' + this.VARCount++
    }
}

基础架构

符号执行, 启动!

 复制代码 隐藏代码
classSymbolic {
    // 传字节码
    constructor(Codes) {
        this.Codes = Codes
        this.Visited = newSet() // 存储已经走过的PC, 防止重复执行
    }

    // Run
    Run() {
        letAST = this.ProcessBlock(newVMState(0, [[{type: 'ThisExpression'}], [{type: 'ObjectExpression'}]], []))
        // console.log(JSON.stringify(AST))
        console.log(escodegen.generate({type: 'Program', body: AST}))
    }

    // 一开始我是想分块然后做队列的, 结果好像也没分成... 直接递归了
    ProcessBlock(State) {
        let i = State.PC
        letStack = State.Stack
        letCodes = this.Codes
        var Z;
        var l;
        letExpr;
        letLastI = 0// 存储上一个PC
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Outside:&nbsp;while&nbsp;(i <&nbsp;this.Codes.length) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(this.Visited.has(i)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(LastI&nbsp;> i)&nbsp;State.ASTs.push({type:&nbsp;'Identifier',&nbsp;name:&nbsp;'GOTO_'&nbsp;+ i});&nbsp;// 向上跳转的我没处理
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 但是如果是向下跳转的可以不管, 因为另一条分支应该已经走到了
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;this.Visited.add(i)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;LastI&nbsp;= i
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;switch&nbsp;(this.Codes[i++]) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 指令处理
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 我知道你最想要的是这部分, 但是你先别想
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;default:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;thrownewError('UNKNOWN '&nbsp;+&nbsp;Codes[i-1])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log('UNKNOWN ',Codes[i-1])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 我下面会解释这里
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnState.ASTs.filter(X&nbsp;=>&nbsp;!X.used).map(X&nbsp;=>&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(X.type.endsWith('Statement') || X.type.endsWith('Declaration')) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;X
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;{type:&nbsp;'ExpressionStatement',&nbsp;expression: X}
&nbsp; &nbsp; &nbsp; &nbsp; })
&nbsp; &nbsp; }
}

解释一下最后return那里: 简而言之, 我将所有可能是最终语句的AST节点添加到State.ASTs中(节点与栈中是同一个Object), 如果执行过程中使用了此节点, 就将其标为used, 最后返回所有未被使用的节点, 他们就是每一行的根节点. 更形象的说:

&nbsp;复制代码&nbsp;隐藏代码
ThisIsAVar&nbsp;=&nbsp;ThisIsAFun()&nbsp;// 先向栈中PUSH CallExpression, 再PUSH AssignementExpression
// 由于并没有办法区分该二者是否是单独的语句, 他会被反编译成这样:
ThisIsAFun()
ThisIsAVar&nbsp;=&nbsp;ThisIsAFun()
// 而当加入used标记后, 栈中的CallExpression在赋值被使用后会被标记为used, 然后会从State.ASTs中被清理掉, 而AssignementExpression未被使用, 因此可以确认AssignementExpression是单独的一个语句

接下来的都是示例, 其他的你们留作课后练习, 自己写去!!!

表达式还原

注: 引入 used 标记. 因为栈中的节点可能是计算中间产物 (比如没人单独算加法然后不用他的值), 只有那些从未被使用的节点, 才是最终的“语句”

&nbsp;复制代码&nbsp;隐藏代码
switch&nbsp;(this.Codes[i++]) {
&nbsp; &nbsp;&nbsp;/* ... */
&nbsp; &nbsp;&nbsp;// 这些指令数字会变, 你们根据自身情况填充
&nbsp; &nbsp;&nbsp;caseOPCodes.ADD:&nbsp;caseOPCodes.SUB:&nbsp;caseOPCodes.MULT:&nbsp;caseOPCodes.DIV:
&nbsp; &nbsp;&nbsp;caseOPCodes.MOD:&nbsp;caseOPCodes.AND:&nbsp;caseOPCodes.OR:&nbsp;caseOPCodes.XOR:&nbsp;caseOPCodes.LSH:
&nbsp; &nbsp;&nbsp;caseOPCodes.RSH:&nbsp;caseOPCodes.URSH:&nbsp;caseOPCodes.SEQUAL:
&nbsp; &nbsp;&nbsp;caseOPCodes.EQUAL:&nbsp;caseOPCodes.GE:&nbsp;caseOPCodes.GEQ:
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;let&nbsp;right =&nbsp;Stack.pop();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;let&nbsp;left =&nbsp;Stack.pop();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 将栈中取下来的节点标记为已使用
&nbsp; &nbsp; &nbsp; &nbsp; left.used&nbsp;=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; right.used&nbsp;=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;opMap = {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [OPCodes.ADD] :'+', [OPCodes.SUB]:'-', [OPCodes.MULT]:'*', [OPCodes.DIV]:&nbsp;'/', [OPCodes.MOD]:&nbsp;'%',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [OPCodes.AND]:&nbsp;'&', [OPCodes.OR]:'|', [OPCodes.XOR]:&nbsp;'^', [OPCodes.LSH]:'<<', [OPCodes.RSH]:'>>', [OPCodes.URSH]:'>>>',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [OPCodes.SEQUAL]:&nbsp;'===', [OPCodes.EQUAL]:'==', [OPCodes.GE]:'>', [OPCodes.GEQ]:'>='
&nbsp; &nbsp; &nbsp; &nbsp; };
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;node = {type:&nbsp;'BinaryExpression', left, right,&nbsp;operator: opMap[Codes[i-1]]};
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Stack.push(node);&nbsp;// 没有人会直接把比较结果扔在这不管, 所以不需要State.ASTs.push(Expr), Expr变量的作用是为了保证node一致性
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;break
}

变量与变量引用还原

&nbsp;复制代码&nbsp;隐藏代码
switch&nbsp;(this.Codes[i++]) {
&nbsp; &nbsp;&nbsp;/* ... */
&nbsp; &nbsp;&nbsp;caseOPCodes.VAR:
&nbsp; &nbsp; &nbsp; &nbsp; Z =&nbsp;Codes[i++];
&nbsp; &nbsp; &nbsp; &nbsp; l = {type:&nbsp;'VariableDeclaration',&nbsp;declarations: [{type:&nbsp;'VariableDeclarator',&nbsp;id: {type:&nbsp;'Identifier',&nbsp;name:&nbsp;State.GetVARName()},&nbsp;init:&nbsp;null}],&nbsp;kind:&nbsp;'var'};
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Stack[Z] ===&nbsp;undefined&nbsp;?&nbsp;State.ASTs.push(l):&nbsp;0;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Stack[Z] =&nbsp;Stack[Z] ===&nbsp;undefined&nbsp;? l.declarations[0].id:&nbsp;Stack[Z];
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp;&nbsp;caseOPCodes.VAR_FROM_INTREF:
&nbsp; &nbsp; &nbsp; &nbsp; l =&nbsp;Stack.pop()[0] &nbsp;// 引用Index
&nbsp; &nbsp; &nbsp; &nbsp; Z =&nbsp;Stack[l] || {type:&nbsp;'Identifier',&nbsp;name:&nbsp;'UNDEF_'&nbsp;+ l}&nbsp;// 不知道为啥, 有时候会爆
&nbsp; &nbsp; &nbsp; &nbsp; Z.used&nbsp;=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Stack.push(Z)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp;&nbsp;caseOPCodes.MAKE_INTREF:&nbsp;// 变量引用, 这里不需要当AST处理 (同样其他pop也是)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Stack.push([Codes[i++]]);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
}

函数还原

&nbsp;复制代码&nbsp;隐藏代码
switch&nbsp;(this.Codes[i++]) {
&nbsp; &nbsp;&nbsp;caseOPCodes.FUNCTION:
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(var&nbsp;G =&nbsp;Codes[i++], W = [], Z =&nbsp;Codes[i++], l =&nbsp;Codes[i++], B = [], A =&nbsp;0; A < Z; A++) W[Codes[i++]] =&nbsp;Stack[Codes[i++]];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(A =&nbsp;0; A < l; A++) B[A] =&nbsp;Codes[i++];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;Z = W.slice(0);&nbsp;// Stack
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 如果你眼睛好的话应该能看出来上面三行我是直接从VMP复制过来的
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;nm =&nbsp;State.GetVARName()&nbsp;// 函数名
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Z[0] = {type:&nbsp;'ThisExpression'},
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Z[1] = {type:&nbsp;'Identifier',&nbsp;name:&nbsp;'arguments'},
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Z[2] = {type:&nbsp;'Identifier',&nbsp;name: nm};
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;co =&nbsp;0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(var&nbsp;l =&nbsp;0; l < B.length; l++)&nbsp;// 原先这里还有< arguments.length, 我这里默认按照最多参数处理
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;0&nbsp;< B[l] && (Z[B[l]] = {type:&nbsp;'Identifier',&nbsp;name:&nbsp;'a'&nbsp;+ ++co});&nbsp;// 这个改了一下
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;varS1&nbsp;=&nbsp;State.Copy()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;S1.Stack&nbsp;= Z
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;S1.PC&nbsp;= G

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;cnmb = []&nbsp;/* 嗯对求我当时的精神状态 */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(var&nbsp;awa =&nbsp;0; awa < co; awa++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cnmb.push({type:&nbsp;'Identifier',&nbsp;name:&nbsp;'a'&nbsp;+ (awa+1)})
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!this.Visited.has(i)) {&nbsp;// 考虑边界情况
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;this.Visited.add(i)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;State.ASTs.push({type:&nbsp;'FunctionDeclaration',&nbsp;id: {type:&nbsp;'Identifier',&nbsp;name: nm},&nbsp;body: {type:&nbsp;'BlockStatement',&nbsp;body:&nbsp;this.ProcessBlock(S1)&nbsp;/* 递归 */},&nbsp;params: cnmb});
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;this.Visited.delete(i)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;State.ASTs.push({type:&nbsp;'FunctionDeclaration',&nbsp;id: {type:&nbsp;'Identifier',&nbsp;name: nm},&nbsp;body: {type:&nbsp;'BlockStatement',&nbsp;body:&nbsp;this.ProcessBlock(S1)},&nbsp;params: cnmb});
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Stack.push({type:&nbsp;'Identifier',&nbsp;name: nm})&nbsp;// 函数实际作为Identifier PUSH
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
}

控制流还原

终于到了万众瞩目的控制流部分!

我在这里假定所有的WhileStatement(While循环)被编译出来都是(事实上我没考虑Break, Continue, 还有欠缺):

&nbsp;复制代码&nbsp;隐藏代码
Loop:
; ... 条件代码 ...
JZ&nbsp;&nbsp;LoopEnd(不管是JZ还是JNZ吧)
; ... 循环中的代码 ...
JMP&nbsp;Loop
LoopEnd:
......

代码如下:

&nbsp;复制代码&nbsp;隐藏代码
switch&nbsp;(this.Codes[i++]) {
&nbsp; &nbsp;&nbsp;caseOPCodes.JZ:&nbsp;// 嘶, 其实我也不知道是JZ还是JNZ, 反正是个有条件跳转就是了
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;varS1&nbsp;=&nbsp;State.Copy()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;S1.PC&nbsp;=&nbsp;Codes[i++]
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Stack[Stack.length&nbsp;-&nbsp;1].used&nbsp;=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Expr&nbsp;= {type:&nbsp;'IfStatement',&nbsp;test:&nbsp;Stack[Stack.length&nbsp;-&nbsp;1],&nbsp;consequent: {type:&nbsp;'BlockStatement',&nbsp;body:&nbsp;this.ProcessBlock(S1)},&nbsp;alternate:&nbsp;null}&nbsp;// 满足条件跳走
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;State.ASTs.push(Expr)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp;&nbsp;caseOPCodes.JMP:
&nbsp; &nbsp; &nbsp; &nbsp; Z = i
&nbsp; &nbsp; &nbsp; &nbsp; i =&nbsp;Codes[i++];
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(i < Z &&&nbsp;this.Visited.has(i) &&&nbsp;State.ASTs.some(X&nbsp;=>&nbsp;X.type&nbsp;===&nbsp;'IfStatement')) {&nbsp;// JMP上跳为循环
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Expr&nbsp;=&nbsp;State.ASTs.pop()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;varExprs&nbsp;= [Expr]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;(Expr.type&nbsp;!==&nbsp;'IfStatement') {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Expr&nbsp;=&nbsp;State.ASTs.pop()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Exprs.push(Expr)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Expr&nbsp;=&nbsp;Exprs.pop()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;State.ASTs.push({type:&nbsp;'WhileStatement',&nbsp;test:&nbsp;Expr.test,&nbsp;body: {type:&nbsp;'BlockStatement',&nbsp;body:&nbsp;Exprs.reverse()}})&nbsp;// 直接处理为WhileStatement
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;State.ASTs&nbsp;=&nbsp;State.ASTs.concat(Expr.consequent.body)
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
}

碎碎念

2026, 又是新的一年, 新能力GET! 没想到我不仅能写JSVMP, 我还能写反编译器, 嘿嘿!

~~话说, 以我目前的能力, 能上班吗 ( (~~

马上就是高中阶段第一个寒假, 难得能抽出时间 🙂 🙂

-官方论坛

www.52pojie.cn

👆👆👆

公众号设置“星标”,不会错过新的消息通知

开放注册、精华文章和周边活动等公告


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:吾爱破解论坛 吾爱pojie 吾爱pojie《手把手教你给某讯滑块的JSVMP写反编译器 (如宝宝辅食一样易懂)》

网安牛马能有多挑剔? 网络安全文章

网安牛马能有多挑剔?

文章总结: 文档仅包含标题论网安牛马能有多挑剔及图片占位符,缺乏正文内容。文章疑似为吐槽网络安全从业者工作状态的娱乐或随笔,但因内容缺失,无法提炼出具体观点、技
PE文件静态注入 网络安全文章

PE文件静态注入

文章总结: 本文介绍PE文件静态注入技术,通过修改输入表结构实现特定DLL的加载。文章详细解析了输入表及相关结构体原理,并以HelloWorld.exe为例,演
评论:0   参与:  0