JSVMP 反编译

2026-04-01

将 VMP 中的字节码,重新构建成 AST 节点,实现对 JSVMP 代码进行还原

参考内容 BV1bFXPBWEgzopen in new window BV1vz4y1v7Wmopen in new window

预处理

  • 基础准备: 使用 AST 去除 OB 混淆、编码混淆等基础混淆内容

  • 补环境(复杂代码需要此步骤): 保证代码顺利运行,方便后续VMP指令还原时的调试与打印

  • 常量还原: 代码中读取的常量数组内的常量、与其他确定的常量(var glb = this) 转换成 AST 节点(this 转换成节点 types.ThisExpression

  • 定位虚拟机与关键参数: 明确 索引(PC),堆栈,作用域数组 等变量

  • 保持整体架构不变,仅指令解析时需要手动处理(switch外的整体内容保持不变,指令中保护的代码才是关键的代码)

  • 手动处理指令的准备

    const types = require('@babel/types')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const generator = require('@babel/generator').default
    const { scope } = require('@babel/traverse/lib/cache.js')
    
  • 定义一些初始化值

    // 函数计数器,还原函数时函数名使用
    let functionCounter = 1
    // 函数返回值计数器
    let funcRetValCounter = 1
    // 函数声明数组 - 当遇到函数声明时存入该数组
    const funcSaver = []  
    // 函数作用域名,还原函数用于存储与判断当前作用域。该数组存放还原节点
    const programScopeList = []  // 全局作用域 - 可用于得到还原后的代码
    programScopeList.name = 'programScopList'
    programScopeList.parentScope = null   // 父作用域,父为全局作用域时设置为 null
    
  • 复制一个栈传入VM。执行到分支时,栈会选择一个分支执行并修改。通过读取原始栈,可将没有走的分支逻辑还原出来

    stack = Array.from(<原始栈>)
    
  • 生成还原代码: 代码最后一行进行还原代码的输出

    fs.writeFileSync('vmp-reverse-output.js', generator(types.program(programScopeList)).code)
    

虚拟机分析

  • 定位虚拟机函数。分析 作用域、栈、SP、PC 等内容的标识符
  • 修改虚拟机函数。增加参数,以使虚拟机使用额外传入的作用域、栈、SP
    function xxx(..., scopeListm stackArray, stackPosition) {
        // 修改 作用域、栈、SP的初始化
        _0xXXX = stackArray ? stackArray : []  // 正常初始值是空数组,修改成优先接收传入内容
        _0xYYY = stackPosition ? stackPostion : 0
    }
    
  • 在switch判断指令前,增加数组记录已处理的 case 编号。每处理一个,在数组内记录
    const caseHandledArray = []
    if (!caseHandledArray.includes(_0xXXX))) {
        console.log("指令未处理:", _0xXXX)
        process.exit()
    }
    switch (_0xXXX) {
        ....
    }
    

分支还原

不同分支在虚拟机中的表现不同,根据具体情况而定。此处结合行号查看

  • ifelse

    // if前的其他语句
    if (test) {
        pc = 4  // 这里可以省略
        // 满足条件的语句
    }  else {
        pc = 7
    }
    // if 后的其他语句
    
  • if + else

    // if前的其他语句
    if (test) {
        // 满足条件的语句
        pc = 9
    }  else {
        // 不满足条件的语句
        pc = 4  // 这里可以直接 9 也可以到 4 后,再修改成 9
    }
    // if 后的其他语句
    
  • while 循环

    pc = 3
    // 循环体
    if (test) {  // 循环条件判断
        pc = 2
    } else {
        pc = 8
    }
    

CASE处理入门

所用代码open in new window,其中 src.jsopen in new window 是VMP后的代码,pmvsjyc.jsopen in new window 是VMP反编译代码

运行代码,查看待解析 case 编号,同时分析 case 的功能。如果 case 在构造语句,则需手动使用 AST 还原出对应的语句

非语句CASE

入常量入栈、变量名入栈等操作。方便后续构造语句涉及到常量时,可从栈中获取

  • 通过源代码在待解析的 case 设置断点,将 case 中执行的代码放回 case。保证涉及到常量都已被处理为 AST 节点

    case 1:
        a8 = duei.length
        // 循环未执行 - 循环用于声明函数
        for (a1 = 0; a1 < a8; a1++) {
            a7 = duei.pop()
            if (a7 === "cbbiyhh.online") {
                break
            ...
        break
    
    // 解析代码
    case 1:
        a8 = duei.length
        break
    
  • 将常量压入堆栈供后续的真实代码获取

    case 23:
        // 此处 cbbb 是虚拟机函数的入参。而向该函数参数传入的变量,其值为 this,需确保 this 已被转换为节点
        all = cbbb
        duei.push(cbbb)
        break
    // 外部没进行转换,则此时需要进行转换
    cywindow = types.identifier("window")  // cywindow 就是外部传入的 cbbb
    

语句构造CASE

经过断点调试分析, case 是语句时: 构建语句,存放时分析该语句的内容,根据内容考虑两种情况

  • case中生成的语句是一个功能完整语句,如console.log(a)。该语句已还原完成,放入预先声明的,用于存放还原节点的 AST 中 resultAst.body.push(exp)
  • case中生成的语句是别的语句的子语句,如 console.log(a()) 中的 a()。该语句是完整语句的一部分,放回堆栈,从而让最终语句从堆栈中读取到该子语句节点,并组装出完成语句节点后方可放入还原树中

即“子语句(表达式)入栈,完整语句(语句)入树”。可观察原 case 中,操作完成后是否进行压栈操作,判断该操作是否为子语句

完整语句

  • 声明变量并赋予初始值
    case 22:
        a1 = shuz[start++]
        a2 = duei.pop()  // a2 是栈中取出的全局对象
        a3 = duei.pop()  // a3 是栈中取出的常量节点
        a2[constantPool[a1]] = a3  // 全局对象的属性是一个常量节点,即在全局中创建变量语句
        break
    
    // 解析代码
    case 22:
        a1 = shuz[start++]
        a2 = duei.pop()
        a3 = duei.pop()
        
        // 构建语句
        const exp = types.assignmentExpression("=",
            types.memberExpression(a2, types.identifier(constantPool[a1])),
            a3
        );
        // 放入树中
        resultAst.body.push(exp)
        break
    

子语句

子语句由于需要参与完整语句的构建,往往会将值放回栈内供后续内容读取,case 中存在计算后的压栈操作

  • 读取属性: 该case中读取了一个对象的属性,并将对应值压回栈内。说明后续内容需要读取这个属性,极有可能是一个子语句
    case 181:
        a1 = duei.pop();  // 取出内容
        a2 = duei.pop();  
    
        // 获取一个对象的属性
        try {
            a1 = a2[a1]
        } catch (e) {
            a1 = window[a1]
        }
    
        all = a2
        duei.push(a1)  // 属性值压回栈
        break
    
    // 解析后
    case 181:
        a1 = duei.pop();
        a2 = duei.pop();
    
        try {
            a1 = types.memberExpression(a2,a1,true)
        } catch (e) {
            a1 = types.memberExpression(types.identifier("window"),a1,true)
        }
    
        all = a2
        // 获取对象属性(memberExpression),通常不放入还原树,而是放入堆中正常执行
        // 保持压栈操作
        duei.push(a1)
        break
    
  • 计算: 该 case 中从栈内获取了内容,并计算,随后将结果放回栈中供后续内容读取
    case 20:
        a1 = duei.pop()
        a2 = duei.pop()
        a1 = a2 + a1;
        duei.push(a1)
        break
    
    // 解析代码
    case 20:
        a1 = duei.pop()
        a2 = duei.pop()
        a1 = types.binaryExpression('+', a2, a1)
        // 计算通常不放入还原树,而是放入堆栈正常执行
        duei.push(a1)
        break
    
  • 函数调用: 需要分析 case 逻辑。这里的逻辑是进行函数调用,并将结果返回
    case 150:
        a1 = shuz[start++]
        a3 = duei.pop()
        args = []
        for (a2 = 0; a2 < a1; a2++) {
            args.splice(0, 0, duei.pop())
        }
    
        if (a3 == window.setTimeout) {
            a4 = setTimeout(...args)
        } else if (a3 == window.atob) {
            a4 = atob(...args)
        } else if (a3 == window.clearInterval) {
    
            a4 = clearInterval(...args)
        } else if (a3 == window.setInterval) {
    
            a4 = setInterval(...args)
        } else if (a3 == window.RegExp) {
            a4 = RegExp(...args)
        } else if (a3 == window.alert) {
            a4 = alert(...args)
        } else {
            a4 = a3.apply(all, args)  // 根据调试,只执行了 apply, 且 all 是全局。原版在此处已经执行了该函数
    
        }
    
        duei.push(a4)  // 将函数执行结果压栈
        break
    
    // 解析代码
    case 150:
        a1 = shuz[start++]
        a3 = duei.pop()
        args = []
        for (a2 = 0; a2 < a1; a2++) {
            args.splice(0, 0, duei.pop())
        }
        // 由于调试时,整个 vmp 过程中此 case 只执行了 apply。此处移除其他情况判断
        // 由于 all 是全局,相当于直接调用一个全局函数,直接使用 callExpression
        
        // 方案 1
        a4 = types.callExpression(a3, args)  // 构建函数调用节点
        duei.push(a4)  // 直接将语句压栈,若该语句不是子语句,在结束虚拟机或 pop 时(该语句结束了生命周期),将其放入还原树
        // 优点: 使代码更简洁(当函数调用作为子语句时),缺点: 执行时机可能异常(当该函数调用就是一个没有返回值的语句,后续没有读取该返回值,则该语句一直在栈内)
    
        // 方案 2 (标准做法)
        // 构造标识符接收函数执行返回值的语句,将该语句放入还原树
        // 将返回值标识符压栈
        // 优点:保证了执行顺序完全与原逻辑一致,缺点:代码冗长
        break
    

处理完成

在 while 循环结束时(或者 default 的退出分支),把堆栈里剩下的没有被消耗掉的节点当作独立语句放入树中:

case 200:
    return
default:
    return "-90_cbb"

// 解析代码
case 200: // 原版 VM 结束指令
        // 【关键操作】:VM 要退出了,把堆栈里残留的节点(比如 console.log)全部当作独立语句排队放入树中
        while (duei.length > 0) {
            let leftNode = duei.shift(); // 从栈底往栈顶拿,保证执行顺序
            result.body.push(types.expressionStatement(leftNode));
        }
        console.log("解析遇到 case 200,正常结束!");
        return;

default:
    // 处理原版的 default 异常情况
    if (s_cbb === undefined) {
            console.log("指令数组越界结束");
            return;
    }
    console.log("指令没有解析:", s_cbb);
    return;

复杂CASE处理

在真实代码中,存在着构建函数,流程控制等内容。这些内容的处理方式往往较为复杂

代码块还原思路

在还原代码时,我们需要还原函数、流程控制等内容,这些内容往往存在嵌套关系。如函数的函数体是代码块,代码块中存在流程控制(循环,条件判断),而循环体或分支也是代码块。

因此还原时需要递归进行还原。

  • 作用域: 每个代码块都有其内部作用域与父作用域,需要进行判断
    • 函数若是全局函数,其作用域是函数,父作用域是全局
    • 函数内的流程控制块,其作用域是块,父作用域是所在函数
    • ...

还原时,读取要是代码块,则将当前代码块的字节码、作用域 与 堆栈情况 递归调用虚拟机函数,进行还原。使用返回值接收还原的代码块,从而可用于还原父层代码块

因此虚拟机函数需要改造成可以接收传入 堆栈、作用域

作用域数组用于接收当前代码块已经生成并需要存放的 AST 节点,如之前提及的

  • 函数调用。创建变量接收调用结果,将该变量标识符压栈,但变量接收调用表达式的语句需要生成并存放在结果数组中
  • 创建变量
  • 读取对象属性。创建变量接收结果,将该变量标识符压栈
  • ... 只要这个操作具有“副作用(Side Effects)”,就必须构造临时变量放入 ScopeList。 在反编译中,没有副作用的纯计算操作(如 1 + 1、a === b、读取 obj.name)可以直接把 AST 节点压栈,等待后续组合。 但如果操作会改变程序的执行状态,如果直接压栈可能会导致执行时机错乱或重复执行,这些情况必须立即执行并用变量接收(放入 ScopeList):
  • 函数调用 (CallExpression):比如 console.log() 或修改了全局变量的函数。
  • 赋值操作 (AssignmentExpression):比如 a = 2 或 obj.b = 3。
  • 对象实例化 (NewExpression):比如 new Date(),每次 new 都是新对象,万一被 VM 复制了栈顶,会导致 new 两次。
  • 自增/自减 (UpdateExpression):比如 i++。
  • etc.

虚拟机分析

阅读源码,分析变量含义 虚拟机外共两层

  • 最外层

    // _0x1f8d7a 字节码字符串,包含常量池,指令。函数起始地址、长度等。后续切分并 parseInt 从而得到 int 数组,进行 case 循环
    // _0x4ec77a 函数起始地址。VMP声明函数时使用
    // _0x3596f9 / 2  函数长度 / 函数的指令个数 - 每个指令2字节
    // []  函数参数数组。VMP声明函数时使用
    // _0x4ede15 外部作用域(全局作用域)数组: 全局变量
    // _0xb2668e 传入 undefined 且没有使用,忽略此参数
    return _0x31ad27(_0x1f8d7a, _0x4ec77a, _0x3596f9 / 2, [], _0x4ede15, _0xb2668e)  // 进入虚拟机外层
    
  • 调用虚拟机层 - 虚拟机外层

    funcion _0x31ad27(_0x541033, _0x534897, _0x12161e, _0x1e583c, _0xaf8777, _0x40f090, _0x19443e) {
        // 入参说明
        // _0x541033 字节码字符串
        // _0x534897 函数起始地址
        // _0x1e583c 函数参数数组。VMP声明函数时使用
        // _0x12161e 函数长度 / 函数的指令个数
        // _0xaf8777 外部作用域(全局作用域)数组: 全局变量
        
        // _0x31bb79 赋值为 window
        _0x31bb79 = this
        
        // 声明一个对象
        var _0x5f1d55 = {}
        // 获取外部作用域长度
        _0x31ff41 = _0x5f1d55['d'] = _0xaf8777 ? _0xaf8777['d'] + 1 : 0 
        // 外部作用域放入该对象 - 作用域数组转换成作用域对象
        for (__0x5f1d55['$' + _0x31ff41] = __0x5f1d55, _0x219876 = 0; _0x219876 < _0x31ff41; _0x219876++) {
            _0x5f1d55[_0x13a1af = '$' + _0x219876] = _0xaf8777[_0x13a1af]
        }
    
        // 参数赋值 VMP声明函数时使用,函数内遍历参数列表获取参数值
        for (_0x219876 = 0, _0x31ff41 = _0x5f1d55['length'] = _0x1e583c['length']; _0x31ff41; _0x219876++) {
            _0x5f1d55[_0x219876] = _0x1e583c[_0x219876]
        }
    
        // 对象构造完成 - 存储外部作用域和参数列表
    
        // 调用虚拟机函数 
        return _0x19443e && _0x509192[_0x534897],
            _0x509192[_0x534897], 
            // 传入参数: 字节码字符串、 函数起始地址、 函数长度 / 函数的指令个数、存储作用域与参数的对象,全局对象
            _0x1233dd(_0x541033, _0x534897, _0x12161e, 0, _0x5f1d55, _0x31bb79, null)[1]
    }
    
  • 虚拟机函数

    function _0x1233dd(_0x30754b, _0x1019bd, _0x3064f2, _0x26e9ab, _0x5d7e17, _0x2c01e5, _0x469ec1, _0x1e438f) {
        // 入参说明
        // _0x30754b 字节码字符串
        // _0x1019bd 函数起始地址
        // _0x3064f2 函数长度 / 函数的指令个数
        // _0x5d7e17 存储作用域与参数的对象
        // _0x2c01e5 全局对象
    
        // 虚拟机初始化部分代码
        // _0x2c01e5: 全局对象 window 此变量为空时再次赋值为 this
        _0x2c01e5 = this  //  注意 nodejs 中的 this 指向全局时与浏览器不同,需要处理成 identifier('window')
        // _0x795c5c: 堆栈 - 接收外部处理时复制的堆栈 `stack`
        _0x795c5c = []
        // _0x17e91c: SP 栈指针 / 栈的长度
        _0x17e91c = 0
        // _0x415185: PC / 指令索引(当前指令在字符串的索引) - 接收外部传入函数起始地址作为初始值,循环读取一个指令时,自 +2
        _0x415185 += 2
        // _0x7ea922: 函数的结束地址(起始地址 + 2 * 指令个数 ,每个指令 2 字节) 
        _0x7ea922 = _0x415185 + 2 * _0x3064f2
    
        // 循环获取每个指令码 - 读取字符串两个字符,作为 16 进制字符串,转换成整形,得到当前指令的 case 编号
        _0x4ac770 = parseInt('' + _0x30754b[_0x415185] + _0x30754b[_0x415185+1], 16)
    
        ...
    }
    

代码输出

移除原本的调用虚拟机函数处

// 移除此处的调用并返回虚拟机函数部分 
// return _0x19443e && _0x509192[_0x534897],
// 

自己编写额外的调用虚拟机函数,传入存放节点空数组,进行输出。覆盖原本的虚拟机调用

var result = _0x31ad27(_0x1f8d7a, _0x4ec77a, _0x3596f9 / 2, [], _0x4ede15, _0xb2668e, undefined, undefined, programScopeList)
return result

数值压栈

将数值压栈。还原时需要使压栈的内容都是 AST 节点

  • 字节码整型压栈: 字节码内指定字符的整型(这里以读取 2 字节的字节码转换的值)压入堆栈

    • 源代码
      // 预定义函数
      // 字符串指定位置开始,连续读取 2 字节转换成有符号整型
      _0x262766 = function (_0x24c906, _0x2b76de) {
          // 字符串指定位置开始,连续读取 2 字节转换成整型
          var _0x4dfc08 = parseInt('' + _0x24c906[_0x2b76de] + _0x24c906[_0x2b76de + 1], 16);
          // 把 0~255 的无符号数,转换成 -128~127 的有符号数(8位补码)。模拟 C 语言的 int8_t,等效为 (x << 24) >> 24
          return _0x4dfc08 = _0x4dfc08 > 127 ? -256 + _0x4dfc08 : _0x4dfc08;
      }
      
      // case
      // 压栈 (SP+1)                   字节码中读取的整数             
      _0x795c5c[++_0x17e91c] = _0x262766(_0x30754b, _0x415185),
      // 指令索引 / PC _0x415185 前进2个字节(跳过已处理的字节码 - 2 字节的整型)
      _0x415185 += 2;
      break;
      
    • 还原: 将待压栈的内容转换为 AST 节点再压栈
      _0x795c5c[++_0x17e91c] = types.valueToNode(_0x262766(_0x30754b, _0x415185))
      _0x415185 += 2
      break
      
  • 固定常量压栈

    • 源代码: 将 true 压栈
      _0x797c7c[++_0x17e91c] = !0;
      break;
      
      _0x797c7c[++_0x17e91c] = void 0;
      break
      
    • 还原
      _0x797c7c[++_0x17e91c] = types.valueToNode(!0)
      break
      
      _0x797c7c[++_0x17e91c] = types.unaryExpression('void', types.valueToNode(0))
      break
      
  • 全局对象压栈

    • 源代码
      _0x797c7c[++_0x17e91c] = _0x210b4b
      break
      
    • 还原: _0x210b4b 在预定义处赋值为全局 this,预处理阶段已经处理成 ast 节点。此处无需额外操作
  • 内置函数调用压栈

    • 源代码
      //栈内取值
      _0xfd8a61 = _0x797c7c[_0x17e91c--],
      // 将该值的 typeof 结果压栈
      _0x797c7c[_0x17e91c] = typeof _0xfd8a61;
      break;
      
    • 还原
      _0xfd8a61 = _0x797c7c[_0x17e91c--]
      // 将 typeof 语句构造成 AST 节点并压栈
      _0x797c7c[_0x17e91c] = types.unaryExpression('typeof', _0xfd8a61)
      
  • 栈内数据二元计算后压栈

    • 源代码
      // 弹出一个数据
      _0xfd8a61 = _0x797c7c[_0x17e91c--],
      
      // 栈顶元素与之前数据计算结果压栈(操作数1 被 计算结果覆盖,实现操作数1的出栈与结果入栈)
      _0x797c7c[_0x17e91c] = _0x797c7c[_0x17e91c] == _0xfd8a61;
      break;
      
    • 还原
      _0xfd8a61 = _0x797c7c[_0x17e91c--],
      // 构造计算表达式
      _0x797c7c[_0x17e91c] = types.binaryExpression('==', _0x797c7c[_0x17e91c], _0xfd8a61)
      break
      
  • 取栈中对象的属性值压栈: 堆栈中取出对象,字节码中取出属性,访问对应值,进行压栈

    • 源代码
      _0x425731 = _0x2ec1e9(_0x30754b, _0x415185);
      _0x415185 += 4;
      _0x795c5c[_0x17e91c] = _0x795c5c[_0x17e91c][_0x425731];
      break;
      
    • 还原: 栈中对象已经是 AST 节点,取出来的属性也是 AST 节点,直接压栈,无需额外操作
  • 取栈中对象的属性值压栈: 栈中获取对象,栈中获取属性名,访问对应值,进行压栈

    • 源代码
    • 还原: 构建一个标识符接收取出的内容,将该标识符压栈
      _0x17e91c -= 1  // SP 下移
      // 构建一个接收对象属性值的标识符名称
      var getFromObjectName = `${scopeList.name}_getFromObject${functionCounter++}`
      // 构建赋值表达式语句放入当前作用域的语句数组
      scopeList.push(types.expressionStatement(types.assignmentExpression(
          '=',
          types.identifier(getFromObjectName),
          types.memberExpression(memberExpression(_0x795c5c[_0x17e91c], _0x795c5c[_0x17e91c + 1], true))
      )))
      // 标识符入栈用于后续语句访问该属性值
      _0x795c5c[_0x17e91c] = types.identifier(getFromObjectName)
      
  • 取局部变量压栈: 从存放函数作用域内变量的对象中取值并压栈

    • 源代码
      // 预定义函数
      // 字符串指定位置开始,连续读取 4 字节转换成整型
      _0x2ec1e9 = function (_0x345827, _0xb025b6) {
          return parseInt((('' + _0x345827[_0xb025b6]) + _0x345827[_0xb025b6 + 1] + _0x345827[_0xb025b6 + 2]) + _0x345827[_0xb025b6 + 3], 16);}
      
      // case
      // 通过函数 _0x2ec1e9,解析 4 位字节码的数字 _0x425731 作为索引值
      _0x425731 = _0x2ec1e9(_0x30754b, _0x415185),
      // 指令索引 / PC _0x415185 前进4个字节(跳过已处理的字节码: 4 字节的整型索引值)
      _0x415185 += 4,
      // 获取 局部变量对象  _0x5d7e17 的指定索引 _0x425731 位置的值 _0xfd8a61
      _0xfd8a61 = _0x5d7e17[_0x425731],  // _0x5d7e17 - 虚拟机外层传入的函数作用域与参数的对象
      // 将获取到的局部变量值压栈
      _0x795c5c[++_0x17e91c] = _0xfd8a61;
      break;
      
      // case
      // 通过函数 _0x4d989d,解析 2 位字节码的数字 _0x425731 作为索引值
      _0x425731 = _0x425731(_0x30754b, _0x415185),
      // 指令索引 / PC _0x415185 前进4个字节(跳过已处理的字节码: 2 字节的整型索引值)
      _0x415185 += 2,
      // 获取 局部变量对象  _0x5d7e17 的指定 key 的内容压栈(根据虚拟机外层,$ 开头的 key 是函数的外层作用域变量)
      _0x795c5c[++_0x17e91c] = _0x5d7e17['$' + _0x425731];
      
    • 还原: 放入局部变量数组内的值也会被预先处理成 AST 节点。因此取出并压栈的内容也会是 AST 节点,无需额外操作
  • 取全局变量压栈: 从常量池或预定义的内容中取值。此处示例是从预定义的数据表取值

    • 源代码
      // 预定义函数
      // 字符串指定位置开始,连续读取 4 字节转换成整型
      _0x2ec1e9 = function (_0x345827, _0xb025b6) {
      return parseInt((('' + _0x345827[_0xb025b6]) + _0x345827[_0xb025b6 + 1] + _0x345827[_0xb025b6 + 2]) + _0x345827[_0xb025b6 + 3], 16);}
      
      // case
      // 通过函数 _0x2ec1e9,解析 4 位字节码的数字 _0x425731 作为索引值
      // 遍历预定义数据表 _0x390b16['q'][_0x425731] 中的一个范围
      // 将数据表中的每个字符与 _0x482e39 进行异或后转换为字符
      // 将这些字符连接成一个字符串 _0xfd8a61
      // 将解密后的字符串压入堆栈 _0x795c5c,实现代码 _0x795c5c[++_0x17e91c]
      // 指令索引 / PC _0x415185 前进 4 个字节(跳过已处理的字节码 - 4 字节的整型索引值)
      for (
          _0x425731 = _0x2ec1e9(_0x30754b, _0x415185), _0xfd8a61 = '', _0x16b085 = _0x390b16['q'][_0x425731][0]; 
          _0x16b085 < _0x390b16['q'][_0x425731][1]; 
          _0x16b085++
      )
          _0xfd8a61 += String['fromCharCode'](_0x3191ea ^ _0x390b16['p'][_0x16b085]);
      _0x795c5c[++_0x17e91c] = _0xfd8a61, _0x415185 += 4;
      break;
      
    • 还原: 将取出的内容转换成 AST 节点即可
      for (_0x425731 = _0x2ec1e9(_0x30754b, _0x415185), _0xfd8a61 = '', _0x16b085 = _0x390b16['q'][_0x425731][0]; _0x16b085 < _0x390b16['q'][_0x425731][1]; _0x16b085++) _0xfd8a61 += String['fromCharCode'](_0x3191ea ^ _0x390b16['p'][_0x16b085]);
      _0x795c5c[++_0x17e91c] = types.valueToNode(_0xfd8a61), _0x415185 += 4;
      break;
      
  • 将栈顶多个元素组成一个数组压栈: 字节码内指定索引开始到栈顶的栈内元素组成一个数组,压入堆栈

    • 源码
      // 预定义函数
      // 字符串指定位置开始,连续读取 4 字节转换成整型
      _0x2ec1e9 = function (_0x345827, _0xb025b6) {
          return parseInt((('' + _0x345827[_0xb025b6]) + _0x345827[_0xb025b6 + 1] + _0x345827[_0xb025b6 + 2]) + _0x345827[_0xb025b6 + 3], 16);}
      
      // case
      // 字节码中取整型 - 作为需要压栈的数组长度
      _0x2e5381 = _0x2ec1e9(_0x30754b, _0x415185),
      // 跳过已处理的字节码
      _0x415185 += 4, 
      // 计算 slice 索引(左闭右开,末尾索引要 +1)
      _0x491a16 = _0x17e91c + 1,
      // slice复制 指定索引(字节码中获取的整型)到栈顶的数组
      // _0x17e91c(SP) 通过数组长度(即 SP) - 待取长度,计算出入栈数组的在栈中开始位置的索引作为 SP
      // _0x795c5c[SP] 存放 slice 得到的数组
      _0x795c5c[_0x17e91c -= _0x2e5381 - 1] = _0x2e5381 ? _0x795c5c['slice'](_0x17e91c, _0x491a16) : [];
      break;
      
    • 还原
      const elementCount = _0x2ec1e9(_0x30754b, _0x415185)
      _0x415185 += 4
      const arrayEndIndex = _0x17e91c + 1
      _0x17e91c -= elementCount - 1
      // 检查是否是空数组
      if (elementCount > 0) {
          const arrayElements = _0x795c5c.slice(_0x17e91c, arrayEndIndex)
          
          // 构造一个数组变量
          var arrayName = `${scopeList.name}_array${functionCounter++}`
          var arrayIdentifier = types.identifier(arrayName)
          arrayIdentifier['length'] = arrayElements.length
          // 将该变量的声明放入作用域
          scopeList.push(types.expressionStatement(types.assignmentExpression('=', arrayIdentifier, 
          types.arrayExpression(arrayElements))))
          // 将该变量压栈
          _0x795c5c[_0x17e91c] = arrayIdentifier
          
          // 或者不构建数组变量,而是将数组节点直接压栈
          // _0x795c5c[_0x17e91c] = types.arrayExpression(arrayElements)
      } else {
          // 构造一个数组变量。将该变量压栈
          var arrayName = `${scopeList.name}_array${functionCounter++}`
          var arrayIdentifier = types.identifier(arrayName)
          arrayIdentifier['length'] = 0
          scopeList.push(types.expressionStatement(types.assignmentExpression('=', arrayIdentifier, 
          types.arrayExpression([]))))
      
          // 或者不构建数组变量,而是将数组节点直接压栈
          // _0x795c5c[_0x17e91c] = types.arrayExpression([])
      }
      break
      
  • 构建新对象压栈: new 一个新对象,并将该对象压栈

    • 源代码
      // 字节码中读取一个整数,表示需要从栈中弹出元素的个数,作为 new 对象是的参数个数
      _0x425731 = _0x4d989d(_0x30754b, _0x415185), 
      _0x415185 += 2, 
      // 压栈操作,计算 SP 指针,进行压栈    判断参数个数是否为 0
      _0x795c5c[_0x17e91c -= _0x425731] = 0 === _0x425731 ? 
          // 为 0, new 对象没有参数
          new _0x795c5c[_0x17e91c]() : 
          // 不为 0 ,new 一个对象,参数是栈中取出的元素
          _0x111479(_0x795c5c[_0x17e91c], _0x493880(_0x795c5c["slice"](_0x17e91c + 1, _0x17e91c + _0x425731 + 1)));
      break;
      
    • 还原
      // 字节码中读取一个整数
      _0x425731 = _0x4d989d(_0x30754b, _0x415185)
      _0x415185 += 2
      // 堆栈弹出该整数的元素个数,作为新建对象的参数
      _0x17e91c -= _0x425731
      // 弹出的元素
      if (_0x425731 === 0) {
          // 构建一个标识符接收 new 出来的对象,将此标识符压栈
          var newObjectName = `newObj_${functionCounter++}`
          scopeList.push(types.expressionStatement(types.assignmentExpression(
              '=', 
              types.identifier(newObjectName),
              types.newExpression(_0x795c5c[_0x17e91c])
          )))
          _0x795c5c[_0x17e91c] = types.identifier(newObjectName[_0x17e91c], [])
          
          // 或者直接将构建出来的对象压栈
          // _0x795c5c[_0x17e91c] = types.newExpression(_0x795c5c[_0x17e91c], [])
      } else {
          // 构建一个标识符接收 new 出来的对象,将此标识符压栈
          var newObjectName = `newObj_${functionCounter++}`
          scopeList.push(types.expressionStatement(types.assignmentExpression(
              '=', 
              types.identifier(newObjectName),
              types.newExpression(
                  _0x795c5c[_0x17e91c],
                  // 参数数组
                  _0x493880(_0x795c5c["slice"](_0x17e91c + 1, _0x17e91c + _0x425731 + 1)))
          )))
          _0x795c5c[_0x17e91c] = types.identifier(newObjectName)
          
          // 或者直接将构建出来的对象压栈
          // _0x795c5c[_0x17e91c] = types.newExpression(
          //     _0x795c5c[_0x17e91c],
          //     _0x493880(_0x795c5c["slice"](_0x17e91c + 1, _0x17e91c + _0x425731 + 1))
          // )
      }
      break
      

堆栈取值

  • 读取函数作用域与参数(函数作用域内的局部变量): 堆栈内取出的值,存入函数作用域与参数的对象(局部变量)的指定位置
    • 源代码
      // 预定义函数
      // 字符串指定位置开始,连续读取 4 字节转换成整型
      _0x2ec1e9 = function (_0x345827, _0xb025b6) {
      return parseInt((('' + _0x345827[_0xb025b6]) + _0x345827[_0xb025b6 + 1] + _0x345827[_0xb025b6 + 2]) + _0x345827[_0xb025b6 + 3], 16);}
      
      // case
      // 通过函数 _0x2ec1e9,解析字节码 _0x30754b 内的数字 _0x425731 作为索引值 - 字节码转换成一个索引 _0x425731
      _0x425731 = _0x2ec1e9(_0x30754b, _0x415185);
      // 指令索引 / PC _0x415185 前进4个字节(跳过已处理的字节码 - 4 字节的整型索引值)
      _0x415185 += 4;
      // 从堆栈获取值 _0xfd8a61
      _0xfd8a61 = _0x795c5c[_0x17e91c--];
      // 将该值存入局部变量数组  _0x5d7e17 的指定索引 _0x425731 内
      _0x5d7e17[_0x425731] = _0xfd8a61;  // _0x5d7e17 - 虚拟机外层传入的函数作用域与参数的对象
      break
      
    • 还原: 在压栈时,已经使压栈的内容是 AST 节点。此处从栈内取到的值已经是 AST 节点,最终放入局部变量数据的也是 AST 节点。因此无需额外处理

对象属性

  • 对象属性赋值: 字节码中取出值,将堆栈中的对象属性赋值为这个值
    • 源代码
      // 预定义函数
      // 字符串指定位置开始,连续读取 4 字节转换成整型
      _0x2ec1e9 = function (_0x345827, _0xb025b6) {
          return parseInt((('' + _0x345827[_0xb025b6]) + _0x345827[_0xb025b6 + 1] + _0x345827[_0xb025b6 + 2]) + _0x345827[_0xb025b6 + 3], 16);}
      
      // case
      for (
          // 堆栈取出一个值                       从字节码取出一个整型
          _0xfd8a61 = _0x795c5c[_0x17e91c--], _0x425731 = _0x2ec1e9(_0x30754b, _0x415185), _0x58ef42 = '', _0x16b085 = _0x390b16['q'][_0x425731][0]; 
          _0x16b085 < _0x390b16['q'][_0x425731][1]; 
          _0x16b085++) 
          _0x58ef42 += String["fromCharCode"](_0x46ff72 ^ _0x390b16['p'][_0x16b085]);
      // 跳过字节码中已处理的 4 个字节,  栈顶元素的 _0x58ef42 属性赋值为  _0xfd8a61
      _0x415185 += 4, _0x795c5c[_0x17e91c--][_0x58ef42] = _0xfd8a61;
      break;
      
    • 还原: 仅将对象赋值语句处理为对应的 AST 节点,其余不进行变动
      for(...)...
      // 跳过已处理的 4 个字节
      _0x415185 += 4
      // 判断堆栈取出的值是否是一个节点  -  也可能是局部作用域,局部变量数组
      if (_0x795c5c[_0x17e91c].type) {
          // 构建赋值语句 放入作用域
          scopeList.push(
              types.expressionStatement(types.assignmentExpression('=', types.memberExpression(_0x795c5c[_0x17e91c], types.valueToNode(_0x58ef42), true), _0xfd8a61))
          )
      } else {
          // 保持原本的运行逻辑 局部变量数组重构
          _0x795c5c[_0x17e91c--][_0x58ef42] = _0xfd8a61
      }
      _0x17e91c--
      break
      
  • 对象属性赋值: 将堆栈中的对象设置从堆栈中获取的属性名和对应值(三者均来自堆栈)
    • 源代码
      // SP下移两位(弹出两个元素) , _0x27df96 为第二个弹出的元素是属性名
      var _0x27df96 = _0x795c5c[(_0x17e91c -= 2) + 1];
      // 堆栈栈顶元素 _0x795c5c[_0x17e91c] 赋值属性名 _0x27df96 值为弹出的第一个元素
      _0xfd8a61 = _0x795c5c[_0x17e91c][_0x27df96] = _0x795c5c[_0x17e91c + 2];
      // 特殊情况处理,如果属性名是 5742 的额外处理
      5742 === _0x27df96 && (_0xfd8a61 = _0x795c5c[_0x17e91c][_0x27df96 - 1] = !_0x795c5c[_0x17e91c + 2]), _0x17e91c--;
      break;
      
    • 还原
      _0x17e91c -= 2
      const propertyName = _0x795c5c[_0x17e91c + 1]
      const targetObject = _0x795c5c[_0x17e91c]
      const propertyValue = _0x795c5c[_0x17e91c + 2]
      scopeList.push(types.expressionStatement(types.assignmentExpression(
          '=',
          types.memberExpression(targetObject, propertyName, true),
          propertyValue
      )))
      _0xfd8a61 = types.memberExpression(targetObject, propertyName, true)
      if (propertyName.value === 5742) {
          scopeList.push(types.expressionStatement(types.assignmentExpression(
              '=',
              types.memberExpression(targetObject, types.valueToNode(propertyName.value - 1), true),
              types.unaryExpression(propertyValue)
          )))
          _0xfd8a61 = types.memberExpression(targetObject, types.valueToNode(propertyName.value - 1), true)
      }
      _0x17e91c--
      break
      

函数定义

在虚拟机首次执行时,会记录包含函数与其指令,在函数被调用时,再获取该函数的指令并调用虚拟机执行该函数的指令

还原时。遇到函数定义先构造 window.func_* = function () {} 的语句。其中函数体是空的,在函数指令传入虚拟机时,将函数体的指令转换成 AST 节点,从而进行还原

  • case
    • 源码
      // 从字节码读取2个字节的内容,转换成有符号整数,作为函数指令个数 / 函数长度
      _0x425731 = _0x254e53(_0x30754b, _0x415185);
      // _0x57c9e7 为构造出来的函数本身
      var _0x57c9e7 = function _0x30ce91() {
          var _0x50f290 = arguments;
          return _0x30ce91['\u0399II'] > 0 || _0x30ce91["\u0399II"]++, 
              _0x31ad27(_0x30754b, _0x30ce91["II\u0399"], _0x30ce91["I\u0399I"], _0x50f290, _0x30ce91["\u0399I\u0399"], this, null, 0);
      };
      // 函数对象记录自身的信息
      // 字节码地址
      _0x57c9e7["II\u0399"] = _0x415185 + 4,
      // 字节码长度
      _0x57c9e7["I\u0399I"] = _0x425731 - 2,
      // 内部函数标识 - 原生函数还是需要字节码 vmp 还原
      _0x57c9e7["I\u0399\u0399"] = _0x1233dd,
      // 调用计数
      _0x57c9e7["\u0399II"] = 0,
      // 外部作用域
      _0x57c9e7["\u0399I\u0399"] = _0x5d7e17,
      _0x47fda0[_0x4cd76e] = _0x57c9e7,
      // 指令索引 / PC 跳过函数字节码,从而循环进行后续 case 解析
      _0x415185 += (2 * _0x425731) - 2;  // 函数
      break;
      
    • 还原:
      _0x425731 = _0x254e53(_0x30754b, _0x415185)
      // 函数名,通过计数器防止重名
      funcName = `func_${functionCounter++}`
      // 定义一个标识符节点
      var _0x57c9e7 = types.identifier(funcName)
      _0x57c9e7["II\u0399"] = _0x415185 + 4
      _0x57c9e7["I\u0399I"] = _0x425731 - 2
      _0x57c9e7["I\u0399\u0399"] = _0x1233dd
      _0x57c9e7["\u0399II"] = 0
      _0x57c9e7["\u0399I\u0399"] = _0x5d7e17
      _0x57c9e7['funcCall'] = function _0x30ce91() {
          var _0x50f290 = arguments
          return _0x30ce91['\u0399II'] > 0 || _0x30ce91["\u0399II"]++, 
              _0x31ad27(_0x30754b, _0x30ce91["II\u0399"], _0x30ce91["I\u0399I"], _0x50f290, _0x30ce91["\u0399I\u0399"], this, null, 0)
      }
      var funcBody = []
      funcBody.name = funcName
      _0x57c9e7['funcBody'] = funcBody
      globalThis[funcName] = _0x57c9e7
      _0x795c5c[_0x17e91c] = globalThis[funcName]
      funcSaver.push(globalThis[funcName])
      _0x415185 += 2 * _0x425731 - 2
      scopeList.push(
          // 构造 `window.func_1 = function () {}` 的表达式
          types.expressionStatement(
              types.assignmentExpression(
                  '=', 
                  types.memberExpression(types.identifier('window'), types.identifier(_0x57c9e7['funcBody']['name'])),
                  // 函数表达式中,函数名,参数留空。函数名以标识符为准
                  types.functionExpression(null, [], types.blockStatement(_0x57c9e7['funcBody']))
              )
          )
      )
      break
      
  • 修改代码输出的部分,调用虚拟机将未被调用的函数进行处理,将指令还原成 AST 节点
    // 虚拟机外层 函数后 ,手动调用 虚拟机外层函数 处
    var result = _0x31ad27(_0x1f8d7a, _0x4ec77a, _0x3596f9 / 2, [], _0x4ede15, _0xb2668e, undefined, undefined, programScopeList)
    // 对已声明但为调用的函数进行调用,从而得到函数内的指令,进行还原 - 否则未调用函数将是空函数
    for(var i = 0; i < funcSaver.length; i++) {
        var _0x57c9e7 = funcSaver[i]
        // 函数未被调用(该属性记录调用次数,初始值为 0)时,执行一次调用
        if (_0x57c9e7['\u0399II'] == 0) {
            _0x57c9e7['\u0399II']++  // 计数器+1
            var selfArgs = []
            for (var i = 0; i < 6; i++) {
                // 构造函数的参数节点数组
                selfArgs.push(types.memberExpression(types.identifier('arguments'), types.numericLiteral(i), true))
            }
            // 调用虚拟机,将该函数进行还原
            //         字节码         起始地址                  长度                 参数节点数组        外部作用域  
            _0x31ad27(_0x1f8d7a, _0x57c9e7['II\u0399'], _0x57c9e7['I\u0399I'], selfArgs, _0x57c9e7['\u0399I\u0399'], types.identifier('window'), null, 0, _0x57c9e7['funcBody'], [], 0)  
        }
    }
    return result
    

分支判断

示例代码: 循环结构与IF结构的条件判断case的还原

  • 源代码: 条件成立 PC + 4, 不成立根据情况跳转

    // 堆栈中取出的条件结果进行判断 (二元计算指令 `==`,`!==` 的结果压栈)
    _0x795c5c[_0x17e91c--] ? 
        // 为真是指令索引 + 4 (跳过false分支,false 分支的 4 字节跳转偏移量)
        _0x415185 += 4 : 
        // 为假时判断情况 - 使用函数读取指令中保存的 4 字节跳转偏移量(为真时被跳过的4字节)
        // 通过 <0 判断 false 分支在当前指令的前方还是后方
        (_0x425731 = _0x254e53(_0x30754b, _0x415185)) < 0 ?   
            (1, _0x415185 += 2 * _0x425731 - 2) :
             _0x415185 += 2 * _0x425731 - 2
    break
    
  • 条件 case 的还原代码: 由于原虚拟机中入栈的是条件判断的结果,而还原时入栈的是二元表达式 AST 节点,需要额外处理

    var test = _0x795c5c[_0x17e91c--]
    console.log('if判断条件', generator(test).code)
    // 将识别到的循环跳转进行记录,通过断点计算两个跳转地址,记录到 map 中
    var whileMap = {
        21178: [21404, 21162],
        20660: [20886, 20644],
        6802: [6912, 6786],
        7060: [7188, 7038],
        7336: [7456, 7314],
        4934: [5002, 4912],
        5498: [5566, 5476],
        8098: [8162, 8076],
        9148: [9178, 9142],
        22666: [22756, 22630]
    }
    // 判断是循环结构还是IF结构的条件判断
    if (Object.keys(whileMap).includes(_0x415185.toString())) {
        // ****** 循环结构的条件判断构造 ******
        var   = []
        whileStatementBody.name = 'whileStatementBody'
        // 复制堆栈,后续栈变动时,用之前复制的栈还原后续将要执行和不会执行的分支
        var stackArray3 = Array.from(_0x795c5c)
        var stackArray4 = Array.from(_0x795c5c)
        var pc1 = _0x415185 + 4
        var pc2 = _0x415185 + (2 * _0x254e53(_0x30754b, _0x415185) - 2)
        if (types.isBinaryExpression(test)) {
            var whileTest = types.binaryExpression(test.operator, types.identifier('i'), test.right)
            scopeList.push(types.expressionStatement(types.assignmentExpression('=', types.identifier('i'), test.left)))
        } else {
            var whileTest = test
        }
        // 递归调用虚拟机函数构建分支内的代码块语句 - 传入该代码块所处的作用域与代码块所在函数的参数
        _0x1233dd(_0x30754b, whileMap[_0x415185][1], (_0x415185 - 2 - whileMap[_0x415185][1]) / 2, _0x26e9ab, _0x5d7e17, _0x2c01e5, _0x4649ec1, _0x1e438f, scopeList, stackArray, stackPosition
        _0x1233dd(_0x30754b, pc1, (pc2 - pc1) / 2, _0x26e9ab, _0x5d7e17, _0x2c01e5, _0x469ec1, _0x1e438f, whileStatementBody, stackArray4, _0x17e91c)
        scopeList.push(types.whileStatement(whileTest, types.blockStatement(whileStatementBody)))
        _0x415185 = whileMap[_0x415185][0] + 4
        // debugger
        break;
    } else {
        // ****** IF结构的条件判断构造 ******
        var ifStatementBody = []
        var elseStatementBody = []
        ifStatementBody.name = 'ifStatementBody'
        ifStatementBody.parentScope = scopeList
        elseStatementBody.name = 'elseStatementBody'
        elseStatementBody.parentScope = scopeList
        var stackArray1 = Array.from(_0x795c5c)
        var pc1 = _0x415185 + 4
        var pc2 = _0x415185 + (2 * _0x254e53(_0x30754b, _0x415185) - 2)
        // 递归调用虚拟机函数构建分支内的代码块语句
        var if Result = _0x1233dd(_0x30754b, pc1, (pc2 - pc1) / 2, _0x26e9ab, _0x5d7e17, _0x2c01e5, _0x469ec1, _0x1e438f, ifStatementBody, stackArray1, _0x17e91c)
        var stackArray2 = Array.from(_0x795c5c)
        var elseResult = _0x1233dd(_0x30754b, pc2, (_0x7ea922 - pc2) / 2, _0x26e9ab, _0x5d7e17, _0x2c01e5, _0x469ec1, _0x1e438f, elseStatementBody, stackArray2, _0x17e91c)
        // if (ifStatementBody.length === 0) {
        //     if (types.isUnaryExpression(test) && test.operator === '!') {
        //         scopeList.push(types.ifStatement(test.argument, types.blockStatement(elseStatementBody), null))
        //     } else {
        //         scopeList.push(types.ifStatement(types.unaryExpression('!', test), types.blockStatement(elseStatementBody), null))
        //     }
        // }
        // if (elseStatementBody.length === 0){ 
        //     scopeList.push(types.ifStatement(test, types.blockStatement(ifStatementBody), null))
        // }
        // if (ifStatementBody.length > 0 && elseStatementBody.length > 0) {
        //     scopeList.push(types.ifStatement(test, types.blockStatement(ifStatementBody), types.blockStatement(elseStatementBody)))
        // }
        // scopeList.push(types.ifStatement(test, types.blockStatement(ifStatementBody), types.blockStatement(elseStatementBody)))
        if (ifStatementBody.length > 0) {
            scopeList.push(types.ifStatement(test, types.blockStatement(ifStatementBody), null))
        }
        scopeList.push(...elseStatementBody)
        return [0, null]
    }
    

try

  • 源代码
    _0x425731 = _0x254e53(_0x30754b, _0x415185)
    try {
        //                                                            字节码      try 开始        try 结束
        if (_0x479d2a[_0x1dcdbe][2] = 1, 1 == (_0xfd8a61 = _0x1233dd(_0x30754d, _0x415185 + 4, _0x425731 - 3, [], _0x5d7e17, _0x2c01e5, null, 0))[0]) return _0xfd8a61
    } catch (_0x37f47a) {
        if (_0x479d2a[_0x1dcdbe] && _0x479d2a[_0x1dcdbe][1] && 1 == (_0xfd8a61 = _0x1233dd(_0x30754d, _0x479d2a[_0x1dcdbe][1][0], _0x479d2a[_0x1dcdbe][1][1], [], _0x5d7e17, _0x2c01e5, _0x37f47a, 0))[0]) return _0xfd8a61;
    } finally {
        iF (_0x479d2a[_0x1dcdbe] && _0x479d2a[_0x1dcdbe][0] && 1 == (_0xfd8a61 = _0x1233dd(_0x30754d, _0x479d2a[_0x1dcdbe][0][0], _0x479d2a[_0x1dcdbe][0][1], [], _0x5d7e17, _0x2c01e5, null, 0))[0]) return _0x118a17;
        _0x479d2a[_0x1dcdbe] = 0;
        _0x1dcdbe--
    }
    _0x415185 += 2 * _0x425731 - 2
    break
    
  • 还原思路: 直接通过递归解析出 try 内的块,catch 的块,finally 的块

循环

通过分支判断与无条件跳转配合,实现循环结构。此处是无条件跳转 case 的还原

  • 源代码
    // 从字节码中读取一个整型表示跳转偏移量。小于 0 时表示 PC 往回跳,否则 PC 往后跳
    (_0x425731 = _0x254e53(_0x30754b, _0x415185)) < 0 ? (1, _0x415185 += (2 * _0x425731) - 2) : _0x415185 += 2 * _0x425731 - 2
    break
    
  • 还原: 调试并记录循环的地址到条件判断case内,统一进行处理
    _0x425731 = _0x254e53(_0x30754b, _0x415185)
    if (_0x425731 < 0) {
        console.log('循环', _0x415185, _0x415185 + 2 * _0x425731 - 2)
        return [2, [_0x415185 + 2 * _0x425731 - 2, _0x415185]]
    }
    _0x415185 += 2 * _0x425731 - 2
    break
    

迭代器

构建一个迭代器函数压栈

  • 源代码: 该 case 经 ai 分析是一个迭代器的 iter() 函数
    // 初始化 i            栈顶元素的长度                         栈顶元素
    _0x188ee8 = 0, _0x9d4026 = _0x795c5c[_0x17e91c]["length"], _0x4a74c8 = _0x795c5c[_0x17e91c];
    // 创建一个迭代器函数压栈(获取下一个元素的函数)
    _0x795c5c[++_0x17e91c] = function () {
        // 判断栈顶元素是否有下一个元素
        var _0x68079b = _0x188ee8 < _0x9d4026;
        if (_0x68079b) {
            // 获取下一个元素
            var _0x366b4a = _0x4a74c8[_0x188ee8++];
            // 将该元素压栈 - 在函数调用时压栈
            _0x795c5c[++_0x17e91c] = _0x366b4a;
        }
        // 将是否有下一个元素的标识位压栈 - 在函数调用时压栈
        _0x795c5c[++_0x17e91c] = _0x68079b;
    };
    break;
    
  • 还原: 由于 ai 分析该函数是迭代器函数,直接生成 obj[Symbol.iterator]() 的 AST 节点后压栈
    // 取出栈顶元素
    _0x4a74c8 = _0x795c5c[_0x17e91c]
    // 构建一个迭代器函数压栈  `xxx['Symbol'['iterator']]` 等效为 `xxx[Symbol.iterator]`
    _0x795c5c[++_0x17e91c] = types.memberExpression(
        _0x4a74c8,
        
        // 'Symbol'['iterator']
        types.memberExpression(
            types.stringLiteral('Symbol'),
            types.stringLiteral('iterator'),
            true
        ),
    
        // 等效写法 Symbol.iterator
        // types.memberExpression(
        //     types.identifier('Symbol'), 
        //     types.identifier('iterator'), 
        //     false // Symbol.iterator 不是计算属性,是点运算
        // ),
        
        true
    )
    break
    

Tips: 迭代器 arr[Symbol.iterator] 的说明 在遍历结构等数组元素操作时,JS 引擎底层会自动去寻找对象身上的一个特殊属性:Symbol.iterator

  • 只要一个对象有 Symbol.iterator 方法,它就是可迭代的(Iterable)。
  • 这个属性是一个方法,xxx.[Symbol.iterator]() 执行后,会返回一个迭代器对象(Iterator),这个对象里面必须有一个 next() 方法。
  • 每次调用 next(),都会返回类似 { value: 1, done: false } 的结果,直到 done: true。

函数返回

函数返回语句,此 case 直接 return,没有 break

  • 源代码
    // 将栈顶元素弹出作为返回值
    return [1, _0x795c5c[_0x17e91c--]];
    
  • 还原
    // 构造一个 return 语句,放入当前函数语句数组
    scopeList.push(types.returnStatement(_0x795c5c[_0x17e91c]))
    return [1, _0x795c5c[_0x17e91c--]]
    

JSRPC

使用 websocket 创建爬虫代码与浏览器的连接,让爬虫代码远程调用浏览器控制台 为什么使用 websocket 而不是 http 等其他形式的服务?

  • 爬虫代码通过请求浏览器,从获取浏览器内的数据。此时爬虫代码是客户端,浏览器是服务端
  • 但浏览器只能作为客户端,因此使用 websocket 长连接,即使浏览器作为客户端,也可以在收到请求后立即响应

部分代码参考 BV1mg4y1R7p9open in new window

爬虫作为服务端

这里以加密函数为例

  • 爬虫服务端: 依赖包 websockets

    • 旧版本写法
      import asyncio
      import websockets
      
      async def main(websocket):
          # 发送给浏览器的内容 - 可在此处将加密函数的入参发给浏览器
          await websocket.send('加密函数入参')
          # 接收浏览器的响应
          data = await websocket.recv()
          print(data)
      
      if __name__ == '__main__':
          # 创建 websocket 服务,传入处理参数
          server = websockets.serve(main, '127.0.0.1', 7777)
          asyncio.get_event_loop().run_until_complete(server)
          asyncio.get_event_loop().run_forever()
      
    • 新版本写法
      import asyncio
      import websockets
      
      async def handler(websocket):
          await websocket.send('加密函数入参')
          data = await websocket.recv()
          print(data)
      
      async def main():
          async with websockets.serve(handler, '127.0.0.1', 7777):
              await asyncio.Future()  # run forever
      
      if __name__ == '__main__':
          asyncio.run(main())
      
  • 浏览器客户端: 将接收到信息直接调用加密函数,将加密函数结果返回

    • 注意: 由于控制台在断点状态下无法建立ws链接,需要保证加密函数位置是全局的
    • 先定位加密函数位置。例如页面发送请求处调用加密函数的方式如下
      this['DataEncrypt']('待传入的请求体')
      
    • 在调用加密函数位置下断点,控制台注入以下代码将加密函数全局化,绑定到 window 上(或重写响应文件),方便后续调用
      window.DataEncrypt = this['DataEncrypt'];
      
    • 刷新页面并取消断点(或直接跳过断点),使浏览器进入可以 ws 连接的状态
    • 检查加密函数全局化效果: 在控制台中输入 window.DataEncrypt 查看是否全局化成功
    • 先启动服务端,然后在控制台注入以下代码来启动客户端
      const ws = new WebSocket('ws://127.0.0.1:7777')
      ws.onmessage = function (event) {
          // MessageEvent 实例
          console.log(event)  
          // MessageEvent.data 取出消息内容字符串
          ws.send(window.DataEncrypt(event.data))
      }
      

三方服务端

额外建立一个 websocket 服务,爬虫代码也作为客户端连接该三方服务。此时可通过增加爬虫客户端实现多爬虫进程的运行

websocket服务端

在多客户端连接的情况下,为了减少房间管理等问题,服务端使用广播的方式,客户端自行根据消息判断是否是自己需要处理的消息

  • node环境
    const ws = require('nodejs-websocket')
    
    const server = ws.createServer(conn => {
        // 收到文本消息
        conn.on('text', msg => {
            const key = conn.key
            // 将收到的消息广播发送给全部客户端
            // 发送方也会收到,需要自行判断是否是自己要接收的消息
            server.connections.forEach(client => {
                client.send(msg)
            })
        })
        // close
        conn.on('close', (code, reason) => {
            console.log('客户端断开', code, reason)
        })
        conn.on('error', err => {
            console.error('连接错误:', err)
        })
    }).listen(7777)
    

websocket客户端

此处使用 js_<python_client_name>_<send_msg> 表示发送给浏览器的消息。

浏览器判断该消息是自己要处理的消息后,响应 <python_client_name>_<resp_msg>

  • python环境
    • 使用 websockets
      import asyncio
      import websockets
      
      # 每个客户端设置不同的名称
      client_name = 'xxx'
      
      async def main():
          async with websockets.connect('ws://127.0.0.1:7777') as websocket:
              # 循环采集任务,持续发送消息
              for task_id in tasks:
                  await websocket.send(f'js_{client_name}_msg')
                  res = await websocket.recv()
                  # 过滤出浏览器发送给自己的消息
                  if res.startswith(client_name + '_'):
                      # 打印出浏览器响应结果
                      print(res)
      
      if __name__ == '__main__':
          asyncio.run(main())
      
    • 使用 websocket-client
      import websocket
      
      client_name = 'xxx'
      
      ws = websocket.WebSocketApp('ws://127.0.0.1:7777')
      
      def on_message(ws, message):
          # 过滤出浏览器发送给自己的消息
          if message.startswith(client_name + '_'):
              # 打印出浏览器响应结果
              print(message)
      
      ws.on_message = on_message
      ws.run_forerver()
      
  • 浏览器环境
    (function() {
        ws = new WebSocket('ws://127.0.0.1:7777')
        ws.onopen = () => {}
        ws.onclose = () => {
            console.log('服务端关闭')
        }
        ws.onerror = (e) => {
            console.log('连接错误:' + e)
        }
        ws.onmessage = (event) => {
            const data = event.data.split('_')
            // 过滤出发送给浏览器的消息
            if (data[0] == 'js') {
                // 打印出收到的消息
                console.log(event.data)
                // 将结果响应给发送人
                ws.send(data[1] + '_' + window.DataEncrypt(event.data))
            }
        }  
    })()