什么是 JSVMP?
JSVMP 全称 Virtual Machine based code Protection for JavaScript,即 JS 代码虚拟化保护方案。
除了匡开圆的论文以外,还有以下文章也值得学习:
JSVMP 的概念最早应该是由西北大学2015级硕士研究生匡开圆,在其2018年的学位论文中提出的,论文标题为:《基于 WebAssembly 的 JavaScript 代码虚拟化保护方法研究与实现》,同年还申请了国家专利,专利名称:《一种基于前端字节码技术的 JavaScript 虚拟化保护方法》,网上可以直接搜到,也可在公众号【K哥爬虫】后台回复 JSVMP,免费获取原版高清无水印的论文和专利。 本文就简单介绍一下 JSVMP,想要详细了解,当然还是建议去读一下这篇论文。

JSVMP 的核心是在 JavaScript 代码保护过程中引入代码虚拟化思想,实现源代码的虚拟化过程,将目标代码转换成自定义的字节码,这些字节码只有特殊的解释器才能识别,隐藏目标代码的关键逻辑。在匡开圆的论文中,利用 WebAssembly 技术实现了特殊的虚拟解释器,通过编译隐藏解释器的执行逻辑。JSVMP 的保护流程如下图所示:

一个完整的 JSVMP 保护系统,大致的架构应该是这样子的:服务器端读取 JavaScript 代码 —> 词法分析 —> 语法分析 —> 生成AST语法树 —> 生成私有指令 —> 生成对应私有解释器,将私有指令加密与私有解释器发送给浏览器,然后一边解释,一边执行。

就目前来讲,JSVMP 的逆向方法有三种(自动化不算):RPC 远程调用,补环境,日志断点还原算法,其中日志断点也称为插桩,找到关键位置,输出关键参数的日志信息,从结果往上倒推生成逻辑,以达到算法还原的目的,RPC 技术K哥以前写过文章,补环境的方式以后有时间再写,本文主要介绍如何使用插桩来还原算法。
实现原理
JavaScript示例
JSVMP的核心是在JavaScript代码保护过程中引入代码虚拟化思想,实现源代码的虚拟化过程,将目标代码转换成自定义的字节码,这些字节码只有特殊的解释器才能识别。
自定义字节码:任何组不成的字符串/数组/对象
常见自定义字节码:数组、字符串
js无法真正vmp化。通过写VMP,实现jsvmp化的过程,感受vmp是怎么实现的。
一段示例JavaScript代码:
var a = window.document.all;
var b = typeof a;
var c = document.all.length;
window.sign1 = b;
window.sign2 = c;
这段代码都是赋值指令,我们把这五行代码进行拆解:
1. var a(声明一个变量,变量名为a) //声明变量
2. window.document (获取当前作用域/全局变量 document) //取window下的全局变量
3. window.document.all (取document下的属性all) //取变量的操作
4. var a = document.all; (将document.all 财值给变量 a) //赋值
5. var b (声明一个变至,变量名力b) //声明
6. typeof a (取得typeof a的值) //typeof
7. var b = typeof a (将 typeof a的值赋值给 b) //赋值
8. var c (声明一个变量,变量名为c) //声明
9. 重复 2·3: 取document.all 后,document.all的 length属性 => (此处可优化为 a.length) //取变量的属性
10.var c = document.all.length; (将 document.all.length 赋值给c) //赋值
11.将 b 赋值给 window.sign1 //赋值
12.将 c 赋值信给 window.sign2 //赋值
通过拆解之后,我们就明确了上面的代码究竟是在做些什么事情。那么接下来,我们总结相关操作并设置序号
- 166:声明
- 188:对象属性取值
- 222:赋值
- 355:typeof
将十二个步骤进行抽象化:
1. 166 ---> a
2. 188 ---> windows, document ***寄存进内存
3. 188 ---> 寄存内存(也就是 windows.document), all ***寄存进内存,windows.document.all已寄存,windows.document的值就删掉了
4. 222 ---> 寄存内存(也就是 windows.document.all), a
5. 166 ---> b
6. 355 ---> a *** 寄存进内存
7. 222 ---> 寄存内存(也就是 typeof a), b
8. 166 ---> c
9. 188 ---> a, length *** 寄存进内存
10.222 ---> 寄存内存(也就是 a.lrngth), c
11.222 ---> b, window.sign1
12.222 ---> c, window.sign2
接下来,我们把上面十个操作抽象成指令集(指令数组)
用最后一个的 1,0 来表示结果是否存储为寄存器临时变量。
为了方便我们学习和表示,我们把最后的两步全局变量赋值抽象成一个新指令,记为 888
还有一个问题,有一些值,需要从变量里面取值。所以我们需要进行一定的标记。
166 ---> a
188 ---> window, document *** 寄存进内存 ----- 【盒子】
新增一个888指令:全局变量的赋值
888 ---> b sign1
等同于
window.sign1 = b
[166, 'a']
[188, ['window'], 'document', 1]
[188, 寄存内存, 'all', 1]
[222, 寄存内存, 'a']
[166, 'b']
[355, ['a'], 1]
[222, 寄存内存, 'b']
[166, c]
[188, ['a'], 'length', 1]
[222, 寄存内存, 'c']
[888, ['b'], 'sign1']
[888, ['c'], 'sign2']
指令写完后,我们开始按照指令操作写解释器
166:声明变量
188:取变量的属性
222:赋值
255:typeof操作
888:全局变量赋值操作
JSVMP代码:
debugger;
!function (a){
let variable = {'window': window}; //变量池
let register; //寄存器
let left;
let right;
let instruct; // 指令码
function analysis(a){
//如果是寄存器 —— 给寄存内存起了个重复率低的字符串名称
if (a==='a_a'){
return register
}
//如果是非字符串,即数组
if (typeof a !== 'string'){
return variable[a[0]]
}
else{
return a
}
}
for(i of a){
instruct = i[0];
switch(instruct){
case 166:
variable[i[1]] = void 0;
break;
case 188:
left = analysis(i[1]);
right = analysis(i[2]);
//判断处理结果是否进寄存器
if (i.at(-1) === 1 && i.length === 4){
register = left[right]
}
else{
left[right]
}
break;
case 222:
left = analysis(i[1])
right = analysis(i[2])
variable[right] = left;
break;
case 355:
if (i.at(-1) === 1 && i.length === 3){
register = typeof analysis(i[1])
}
break;
case 888:
window[i[2]] = analysis(i[1])
break;
}
}
}([
[166, 'a'],
[188, ['window'], 'document', 1],
[188, 'a_a', 'all', 1],
[222, 'a_a', 'a'],
[166, 'b'],
[355, ['a'], 1],
[222, 'a_a', 'b'],
[166, 'c'],
[188, ['a'], 'length', 1],
[222, 'a_a', 'c'],
[888, ['b'], 'sign1'],
[888, ['c'], 'sign2'],
])
i.at()方法:
i.at意为数组按下标取值,和i[index]比可以传负值,类似Python的list[-1]
插桩(插入日志断点)
- 指令集的操作
- 关键指令的操作
-
指令集数组

再加密措施
-
数组不显示明文
即:[166, 'a']实际显示为:[166, 97] (String.forcharCode(97)为'a')
-
数组栈是随机栈,有一个控制系统,先压栈再解栈
1 ---> 2 ---> 3 ---> 4 ---> 5 ---> 6
2 ---> 1 ---> 6 ---> 3 ---> 5 ---> 4
-
不止一个寄存器
-
外层加壳
jsvmp加壳后再处理,如js盾
JSVMP的弊端
- 速度慢,核心算法存储数量有限
- 安全性上限地(它难度下限非常高,但是上限非常低,因为安全性上升比如性能指数下降
- 容易插桩/猜测
JSVMP的优势
- 安全性下限高
- 可以搭配其他安全措施使用,只加固核心检测点
- 难以还原