经过前面对目标代码的指令拆分,我们得到了JavaScript代码的低级表达形式,仔细分析可以发现,这段低级中间代码是由几类具有特定功能的原子操作组成。这些原子操作指令可以类比编译程序中的汇编指令,经过分类,我们可以设计出一套语义完备的虚拟指令集来完成对JavaScript代码的虚拟化过程。首先对中间代码进行分类,在传统二进制程序中,指令常被分为数据转移指令、控制转移指令和算数逻辑运算指令三类。目标JavaScript代码经过指令拆分和字符转移后得到的中间代码也包含这三类操作,且由于JavaScript对象的存在,中间代码中还包含了针对对象和属性的原子操作,因此将拆分得到的中间代码扩充划分为四类,具体如表所示。
其中,Handler的主体是通过“EM_ASM”宏在C中内联JavaScript代码实现,数据的读取和调度执行在WASM中,而最终的功能实现由JavaScript完成。除了数据转移指令,大部分的虚拟指令是没有显式参数的,通过隐式的方式按照操作需求在栈顶获取操作的对象,并将最终的执行结果保存到栈中。数据转移操作的特征是将操作数转移到特定的容器中以达到一定的目的。由于我们采用的是基于栈的虚拟机架构,所有的数据都经过栈,寄存器在运算过程中只需要起到临时存储中间值的作用,所以我们需要设计两类数据转移虚拟指令,一类是加载指令“lod”,其作用是将特定存储单元中的数值或者常量压入栈顶。另一类是保存指令“stor”,它的作用是从栈顶获取计算结果,并将结果保存到特定的存储单元。表1中给出了部分数据转移操作的虚拟指令示例,它们都具有操作数,并且根据数据来源不同划分为四类寻址方式:“立即数寻址”、“字符串数组寻址”、“寄存器寻址”和“外部变量寻址”。这里的寻址并非通常意义上二进制代码中的寻址,本文的处理中,字符串数组寻址表示指令的操作对象是以参数byte为索引的数组元素“VMA[byte]”(字符串常量)。而外部变量的定义是指函数的参数和代码中的全局变量和外来变量等,统一保存在一个变量池“Varlist[]”中,因此这种寻址方式表示指令的操作对象是以参数byte为索引的外部变量“Varlist[byte]”。而保存指令的操作对象只能是存储空间,因此“stor”指令只有两种寻址方式。属性操作是JavaScript代码虚拟化过程非常重要的原子操作,其主要的作用是将已经拆分的对象、对象的属性和属性方法的参数拼接来还原目标JavaScript代码功能。在基于栈架构的虚拟机中,该类操作直接从栈顶获取操作对象执行计算过程。除了表1中示例的指令,“set”指令也属于属性操作类别,其功能是实现对对象属性的赋值。特别的,“call”指令由于方法调用存在参数个数不确定性,Handler需要额外的参数表明该方法参数的个数,确保虚拟化过程中的语义完备性,此外还有一类模拟自定义函数调用的“fcall”指令,其核心的操作为一个函数对象调用,如“a(b)”,不包含对对象和属性获取,同样也需要引入参数说明函数调用的参数个数。
指令拆分获得中间代码分支结构明确,跳转的目的地址可以直接获得。因此本文中将控制转移操作按照跳转条件划分为:直接跳转和条件跳转。如表1中所示,“jmp”属于直接跳转,执行该指令,则从字节码中读取相应的目的地址参数参数,直接执行跳转任务。而条件跳转指令“je”,相应的条件判断无论复杂与否,都在该指令执行之前完成计算并将条件结果压栈,所以只需从栈顶获取判断结果,如果符合条件则执行跳转任务。由于虚拟指令最终被编码成字节码,所以跳转的实现就是通过修改虚拟计数器“VPC”来完成,所以这里的参数就是跳转指令到目的指令地址的偏移。算术逻辑运算操作和属性操作类似,在基于栈的虚拟机架构下,没有显式的操作数,所有的参数都已经由加载指令完成压栈,在栈顶按需取操作数完成相应运算过程并将计算结果压栈即可。
JS一键VMP加密 jsvmp.com