目前在JS 方面常见的保护方式
- 代码混淆 类似ob混淆 jsfuck 这种的
- 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的循环 需要非常仔细的去跟它的源码