反编译小红书vmp 代码decompile-xhs

目前在JS 方面常见的保护方式

  1. 代码混淆 类似ob混淆 jsfuck 这种的
  2. vmp

vmp 目前还是比较常见的 国内来说
腾讯 小红书 抖音 知乎 都有vmp了

我之前的解决办法 就是在关键节点打日志
然后看输出日志分析 经常有几万行的日志
看的很蛋疼

所以来研究下怎么用 ast 来还原vmp 代码

注意

需要你了解ast常⽤节点的构成和怎么生成对应的ast节点

第一步 看vmp代码结构

vmp 代码一般都会在一个循环里 还有个长的数组 代表的要执行哪个指令

在这里插入图片描述

这里b 其实就是长数组 代表走的指令了
这个d 其实是全局的常量 后面执行的时候会从这里取值

在这里插入图片描述

经过处理后 就变长了11119长度 五列的数组 其中0位置的值就代表了 要走哪条指令
在这里插入图片描述

_ace_aec23 是一个67 长度的list 里面都是函数
这里的 指令是4 代表要走 _ace_aec23 中下标4的函数
他的执行形式就介绍完了

第二步 看看一些关键的变量

在这里插入图片描述
在开始之前定义了一些 list 和object
其中 比较有用的是 _ace_dcca5 栈 和 _ace_66 变量池
在加上 前面的常量池 就够了 别的值的定义用到时 在分析

第三步 开始反编译

我的理解中 反编译vmp
就是让vmp流程中的所有操作都变成对ast 节点的操作
演示一下大概就明白了
现在开始让函数自己进行执行
当前 i = 1 opcode = [12,6,0,6,1] 就是要执行67个函数中下标为12的函数
在这里插入图片描述

很明显 这里就是让 a部分 加上b 部分 在进入_ace_1ae3c函数
在这里插入图片描述

赋值给 ace_dcca5 这个栈 一般都是赋值到下标为0 的地方
那么下面来看怎么处理这个节点

            case 12:
                let v ;
                let l = getArgs(p0, p1) // 获取到left节点的值
                let r = getArgs(p2, p3) // 获取right节点的值
                // 这里的代码是为了优化反编译结果 连续生成一个字符串的地方
                if (types.isStringLiteral(l) && types.isStringLiteral(r)) {
                    // 面对的是 l和 r 都是字符串
                    v = l.value + r.value;
                } else if (typeof l === 'string' && types.isStringLiteral(r)) {
                    // 面对的是 l 是一个变量节点 && r === 字符串
                    v = l + r.value
                } else {
                    // 其他方法就生成一个 运算符的表达式
                    v = types.binaryExpression('+', l, r)
                    // body.push(newVaria(v, res))
                }
                _ace_dcca5[0] = v;

节点的处理就大同小异的
关键是处理 if-else while break continue try-catch-finally 已经 新函数的节点

下面来看看 新函数怎么处理 首先找到vmp中处理一个新函数的地方

// 当指令为61的时候 就是新定义一个函数
function(p0, p1, p2, p3, p4, p5, p6) {
    // 切分新的指令list
    var _ace_404c = _ace_75a05.slice(_ace_34d1(p0, p1), _ace_34d1(p2, p3) + 1)
    // 复制当前的变量池
  , _ace_71423 = _ace_66;
    // 他这里是将一个匿名函数赋值到栈里了
    // 我们的处理就是直接反编译出来这个函数 将函数节点赋值给栈
    _ace_1ae3c(function() {
        // 一些准备工作 保持新函数外层一些关键点的值不跑偏
        _ace_420ea = {
            _ace_5ee37: this || _ace_4752e,
            _ace_84c79: _ace_420ea,
            _ace_b0594: arguments,
            _ace_eb1d: _ace_71423
        };
        // 进入循环 执行函数内的vmp 代码
        _ace_99485(_ace_404c);
        // 将这个_ace_420ea的值还原回去 
        _ace_420ea = _ace_420ea._ace_84c79;
        // 如果在结果时栈0 有值 就是函数有返回值
        print('res',_ace_8cba0(_ace_dcca5[0]))
        return _ace_8cba0(_ace_dcca5[0]);
    }, _ace_be07c, _ace_be07c, 0);
    return ++p4;
}

看反编译代码吧

case 61:
    // 函数指令必须从30 开始 处理栈什么的
    // 以22结束 还原栈
    // 解析为一个函数
    let fl = getArgs(p0, p1); // 函数开始位置
    let fr = getArgs(p2, p3); // 函数结束位置

    let newCode = allcode.slice(fl, fr + 1);// 切分一个新函数指令list出来
    let newBody = new MyArray(); // 新建一个函数内部的ast代码存放的body
    let v61 = newVar(true)
    let old_ace_dcca5 = _ace_dcca5.slice(); // 像他的源码 一样备份一些东西
    let old_ace_a3718 = _ace_a3718;
    let old_ace_66 = _ace_66;
    let new_ace_dcca5 = [0, 0, 0, 0, 0, 0]
    let new_ace_66 = {};
    new_ace_66._ace_66 = _ace_66
    // 开始执行新函数 newCode 的指令了
    let func47Res = main(newCode, 0, newCode.length - 1, newBody,new_ace_dcca5 ,
        new_ace_66, -1,-1,[])
    try {
        // 如果函数有返回值 就加个return 语句
        if (types.isIdentifier(new_ace_dcca5[0]) ) {
            newBody.push(types.returnStatement(new_ace_dcca5[0]))
        }
    } catch (e) {
        debugger
    }
    // 执行完跳出循环后 定义一个新函数
    body.push(types.functionDeclaration(v61, [], types.blockStatement(newBody.data)));
    // 函数变量放到栈中
    _ace_dcca5[0] = v61
    // 还原之前备份的
    _ace_dcca5 = old_ace_dcca5.slice();
    _ace_a3718 = old_ace_a3718;
    _ace_66 = old_ace_66;
    break

反编译结果大概就这样
在这里插入图片描述

整体逻辑就是这样的 去看源码中每个指令位置坐了什么事情 把他转成ast节点 做同样的事情
就出来了

最终效果

在这里插入图片描述

这个反编译完1880行 而且可以明显的看到 _webmsxyw 这个函数被挂载到了window上了
我的理解中 vmp反编译的难点 多数在于循环中 特别是有break和continue的循环 需要非常仔细的去跟它的源码

欢迎关注我的公众号 谢谢大家

在这里插入图片描述

  • 23
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值