本章的内容主要详细分析JavaScript代码常用的保护策略和相应的攻击手段,并从原理上讲解代码虚拟化的保护机制,最后通过WebAssembly实际应用分析基于其实现JavaScript代码虚拟化保护的可行性。Web前端是网站结构的前台交互部分,前端应用主要由JavaScript实现,因此本文中所提到的前端代码主要指的是JavaScript脚本代码。
上期文章: JS加密的研究背景和意义
1.JavaScript代码保护和攻击
在Web环境中,JavaScript脚本以源码形式传输,这导致了很多重要的应用逻辑对于恶意的用户是不设防的,特别是在前端攻防体系中,一旦前端数据采集和鉴别的逻辑直接暴露,恶意请求将很容易通过伪造数据来逃过检测。在常用的保护方案中,目前较为普遍的做法是加入一些如代码精简、加密和混淆等安全性策略,尽可能的增大恶意用户理解、分析和复制的难度。本节的内容主要分析这些常用的保护方法及其相应的攻击分析手段。
1.1代码精简与格式化
在前端应用发展的早期,为了减小JavaScript代码的体积,加快脚本传输和加载的速度,出现了精简压缩JavaScript代码的技术,并衍生了多种压缩工具,如GoogleClosureCompiler[4]、YUICompressor[35]和UglifyJS[36]等。精简代码可以缩小JavaScript脚本的体积,不仅加快了传输速度,压缩的代码同时也变得难以阅读。由于这种保护方法最简单,基本不会影响源代码的功能,所以现在大部分的JavaScript代码发布时都会经过精简压缩处理。
代码精简的主要方法有:变量名简化,用简单字符替换具有语义的变量名,合并多余的变量声明;删除代码注释和无意义的空白以及换行;去除或者简化可以省略的符号;删除死代码,缩短语句等。
因为这种方法的简单易用,保护后的代码也更容易被还原。主流的浏览器自带的调试器就含有格式化脚本代码的功能,图2展示了FireFox浏览器调试窗口中代码格式化的效果,点击代码标签左下角的“{}”按钮即可对代码格式进行美化,自动补齐空格、换行和缩进,使得源码变得可读。并且从图中调试器的结构也可以看出,调试器也可以对目标代码进行变量监控,断点跟踪和性能监测等复杂操作。也有类似的工具如jsbeautifier[37]等专门提供脚本代码的格式美化。
对于精简过程中压缩的变量名还原处理,Raychev等人[38]提出了一个基于学习的反混淆工具JSNice,通过分析大量的开源代码学习命名和类型的规律来构建预测引擎,用于解决JavaScript反混淆中的两类问题:预测标识符名称(语法)和预测变量类型注释(语义)。其实验结果表明可以正确恢复63%的标识符名称,并且变量类型注释的正确率可达81%。
1.2代码加密与解密
类似于二进制程序保护,在JavaScript代码的攻防体系中也常用加密手段来隐藏目标代码语义。JavaScript代码加密的主要思想是,利用某种加密方法对目标代码以字符串的形式进行加密,转换成一串无意义的字符乱码,并在脚本中嵌入解密模块,在浏览器中执行时通过解密模块还原出源代码执行。由于解密的结果是字符串形式,不是浏览器可执行的合法脚本,还需要通过一些特殊的可将字符串作为JavaScript代码执行的API(如:“eval”、“Functionconstructor”等)来解码得到的源代码字符串并执行其中的JavaScript逻辑。
分析代码加密的保护过程可知,加密后的脚本文件需要一个解密模块,并且无论形式如何变化,最终都需要依赖并调用“eval”等方法来执行解密后的逻辑。因此,代码加密的特征很明显,并且容易被还原。攻击者不需要分析加解密的细节,只需拦截最终的“eval”调用,改为打印输出字符串的方式即可得到解密后的程序源码。图3给出了一个加解密过程的示例,经过加密保护的JavaScript代码段,结构混乱难以理解,但是通过将目标代码的“eval”调用修改为“console.log”并在浏览器的调试窗口输出,很容易得到脚本的源代码。
eval(function(p,a,c,k,e,r){e=String;if(!''.replace(/^/,String)){while(c-- )r[c]=k[c]||c;k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c-- )if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('0("1, 2 3 4 5 6!");',7,7,'alert|Congratulations|you|got|the|source|code'.split('|'),0,{}))
由于单个加密方法特征明显容易被破解,因此很多时候都会通过混合多种方法进行组合或者嵌套加密的手段来增加保护的强度。还有一些特殊的加密方式,比如将JavaScript代码隐藏到其他介质的隐写技术,SaumilShah[39]实现了一个Stegosploit程序,采用一种称为Steganography的技术把目标信息隐藏到一个图片中,作者利用这个技术,把目标JavaScript代码写入图片像素,当图像加载到浏览器中,使用HTML5CANVAS元素进行解码,抽取还原载体中隐藏的目标代码。处理后的图片如果不放大仔细分析,很难发现其中问题。不过这种方法和一般加密方法一样,也需要对程序进行动态解码和执行,所以破解方法和攻击一般代码加密的方法类似。
1.3代码混淆与反混淆
代码混淆可以理解为是一组用以隐藏编程意图的保留语义的程序转换。主要的目的是降低目标代码的可读性,使攻击者难以通过手动和自动的分析手段来理解程序的逻辑。代码混淆可以作用于程序编译完成的本地代码,编译过程的中间代码以及程序的源代码。一般来说,依据混淆效果可以将混淆分为以下几种[11]:
(1)布局混淆。去除代码的书写格式(例如空格、缩进、换行等),删除注释,通过压缩代码以降低其可读性和大小,等同于前面提到的代码精简。
(2)名称混淆。将代码中的标识符名称(变量名、函数名等)替换为无意义的随机名称。例如,对于一个JavaScript变量声明语句“varname”,可以很容易猜测这个变量和姓名有关,若将变量名替换为“a”,则需要花费更多的精力分析上下文来得到其包含的语义。
(3)数据混淆。数据混淆一般是通过重用变量,内联变量或者数据加密等形式来混淆程序中的数据流。例如,在不同的命名空间中使用相同的标识符名称,或者将同一个值赋给多个名称随机的变量,使得代码中含有大量的变量和重复的变量名,可以有效的混淆数据流向,增加理解代码逻辑的难度。
(4)控制流混淆。这类混淆方法主要是改变程序的控制流。常用方法主要有以下两种:插入不透明谓词[40]和平展控制流[41]。不透明谓词法通过构造一些必真或必假的条件,这些条件静态分析时无法推断,只能在运行时确定,以此增加静态程序分支,改造目标程序控制流。控制流的平展主要是改变程序控制流的立体结构,依据条件分支将循环、嵌套条件分支等代码段分成一个个代码基本块,然后利用循环和“switch”结构将程序结构扁平化,利用“case”的取值变化来控制基本块的执行顺序。通过以上方法可以使代码控制流程复杂化,阻碍人们的理解分析。
JavaScript混淆属于源代码混淆。且一般JavaScript代码混淆所指的是上述的名称混淆、数据混淆和控制流混淆,这些操作主要通过修改抽象语法树(简称AST,AbstractSyntaxTree)实现。目前有大量的保护工具提供JavaScript代码混淆功能,还有商业平台可以提供定制的深度混淆保护。
特别的,一些恶意脚本常会利用混淆隐藏恶意代码来逃过安全检测[42],因此也有一些工作研究常用混淆保护工具的反混淆实现。比如,Metasploit[43]框架的JavaScript混淆器常被用于恶意代码混淆,而如图4所示[44],恶意代码反混淆工具JSDetox[45]
可以反混淆经过Metasploit混淆过的JavaScript代码。etacsufbo[46]是一个开源的反混淆工具,其主要思路是表达式简化,通过模拟执行获取可预测部分代码的执行结果,然后利用计算结果替换代码中复杂冗长的计算过程,该工具可以有效的反混淆Javascriptobfuscator[2]保护的代码。对于混淆保护后的代码,通常变量名混淆是不可逆的,这里可以利用前面提到的工具JSNice[38]来尝试还原,这类基于学习的方法在一定程度上可以还原代码的标识符名称。事实上,脚本代码的初衷就是简单易用,因此也导致了许多安全上的不足,传统的混淆方法在JavaScript保护中难以深入实施,保护效果无法达到传统程序代码混淆保护的强度。
1.4其他保护方式和攻击方法
一般分析程序的手段可以分为静态分析和动态分析两类,传统的加密和普通的混淆策略主要应对的是恶意静态分析,当攻击者结合动态分析,对代码进行动态的调试跟踪,可以过滤掉很多简单的保护手段。一般JavaScript代码动态调试需要依赖浏览器的调试器,由此可以有针对的添加反调试措施,主要通过检测调试器的状态并做出响应来保护代码不受恶意的动态分析[47]。类似还有代码防篡改等都可以一定程度的抵御动态分析,然而这类措施主要依靠添加特殊的功能模块,一旦这段代码被发现并移除,则很容易绕过保护。
还有一类JavaScript代码保护方法,利用一些特殊的或者常用字符编码转换原JavaScript代码,使得代码变得难以阅读,我们称之为编码保护。如编码混淆工具jjencode、aaencode[5],可以将JavaScript代码编码成由常用字符和颜文字表情字符组成的代码。但是这类方法缺点也很明显,首先有较大的体积膨胀,其次特点明显容易被发现并针对。并且编码的方式产生的保护强度有限,编码后的代码同样需要“eval”等函数才能执行,因此破解方法和攻击代码加密保护类似。如图5中所示,对于aaencode加密得到的代码,我们仅需要按照如图中上侧所示方法在调试器中先行拦截最终构造执行函数,当再次执行混淆后的代码时就可以在控制台中输出源码,结果如图下侧所示。
另外,在Web框架中,相较于客户端,服务端的安全性是可控的,可以将所有的重要逻辑全部放在服务端完成,但是因此放弃前端的优势而增加服务端负载是得不偿失的,因此采取前后端协作的方式,重要的数据和操作由后端认证和配合完成是当前较为可行的方案。