JS代码指令拆分

发布于 2023-07-01  153 次阅读


回顾上篇,和传统的编译型程序不同,JavaScript应用程序是带有语法属性的文本代码,天然的不具有本地操作指令的原子特性。因此,虚拟化保护JavaScript代码的关键挑战在于需要先对目标代码进行特殊的拆分转化,破坏可读的语法属性,提取具有原子操作特性的中间代码,然后才能设计相应的虚拟指令和处理函数,并最终进行指令编码完成虚拟化的操作。

上期文章:深入了解JS 加密技术及JSVMP保护原理分析

指令拆分主要是为了能够将目标JavaScript代码的语法结构拆分细化,破坏可读语法结构,从而得到能够用一组原子操作组合还原的中间代码序列。我们通过模拟本地指令的调度方式,选择一种基于栈的指令架构设计具体的拆分过程。一般来说,虚拟机的架构通常有两类,基于栈的虚拟机和基于寄存器的虚拟机。采用不同的虚拟机架构,虚拟指令集的设计会有很大的不同,例如图下展示了“a=b+c;”这条语句在两种不同架构类型的虚拟机的指令对比。基于寄存器架构的虚拟机,由于用到的寄存器较多,可以用较少的指令实现复杂的功能,因此解释过程也更加的复杂。而基于栈的虚拟机围绕栈来进行计算,虚拟化的过程较为固定和简单,指令的寻址方式也更简单,因此本文中采用基于栈的虚拟机结构来构建最终虚拟指令集。

语句拆分

指令的拆分主要通过抽象语法树的操作来实现,首先对目标代码进行分析,提取目标代码的抽象语法树。生成抽象语法树的工具有很多种,如压缩工具UglifyJS自带的parser,还有Esprima在线工具等。本文实现中采用的是由Mozilla提供的Rhino引擎的Parser类来分析提取JavaScript代码的抽象语法树。

拿到抽象语法树以后,以每个语句块为单位将其划分为多个子树,并通过后序遍历的方式对每个语句块进行指令拆分。若目标为计算操作,即在语法树中的节点类型为中缀表达式“InfixExpression”,则如图上左侧代码所示,通过将操作数和运算符分离,拆分成基于栈架构的中间指令形式。

对于对象及其属性方法的操作,如示例中的“document.write(str)”,需要做特殊的处理,在抽象语法树中,该子树结构主要由“PropertyGet”和 “FunctionCall”节点组成,在JavaScript,我们可以改变点(.)方式访问属性(PropertyGet)为方括号([])访问方式(ElementGet),全局变量和DOM对象默认是“window”对象的成员,这样就可以将上述代码按照下图中所示的形式,拆分成一个“ElementGet”和一个“FunctionCall”,并且此时所有的对象和属性作为常量参数存在,更加便于我们做进一步的处理。

结构拆分

上述的拆分过程是针对每个子树都是一个独立语句块的情况。但是JavaScript是一种脚本语言,存在循环和分支等高级的代码结构,每个循环子树和条件分支子树中可能会存在多条语句。

在执行语句拆分之前,需要现将分支和循环等结构平铺拆分,下图展示了条件分支结构的拆分过程,通过结构的拆分,一个IF子树拆分成了多个表达式子树,每个子树都只含有一个语句块。其中的“ifjmp”和“elsepart”标记是为了便于虚拟映射过程中在此处插入分支跳转指令和标的跳转目的地址,保证程序执行的正确性。循环结构拆分操作类似,除了对条件模块和循环体的拆分,还需要在循环体结束部分增加一个向后的强制跳转指令,确保循环逻辑的还原。

经过上述的指令拆分过程,我们最终可以得到一段失去了语法属性的中间代码,此时的中间代码是一种脚本代码的低级表达形式,每一条指令都具有原子操作特性,但不具有执行能力。

JS一键VMP加密 jsvmp.com


点击体验一键VMP加密 |下滑查看JSVMP相关文章