JSVMP实现原理
JSVMP实现原理

JSVMP实现原理

什么是 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]

image-20230915114459028

插桩(插入日志断点)

  • 指令集的操作
  • 关键指令的操作
  • 指令集数组

    image-20230915140933664

image-20230915140907331

再加密措施

  1. 数组不显示明文

    即:[166, 'a']实际显示为:[166, 97] (String.forcharCode(97)为'a')

  2. 数组栈是随机栈,有一个控制系统,先压栈再解栈

    1 ---> 2 ---> 3 ---> 4 ---> 5 ---> 6

    2 ---> 1 ---> 6 ---> 3 ---> 5 ---> 4

  3. 不止一个寄存器

  4. 外层加壳

    jsvmp加壳后再处理,如js盾

JSVMP的弊端

  1. 速度慢,核心算法存储数量有限
  2. 安全性上限地(它难度下限非常高,但是上限非常低,因为安全性上升比如性能指数下降
  3. 容易插桩/猜测

JSVMP的优势

  1. 安全性下限高
  2. 可以搭配其他安全措施使用,只加固核心检测点
  3. 难以还原

发表回复

您的电子邮箱地址不会被公开。