作者:JShaman.com:w2sft
本文将实例讲解以下JS代码混淆加密技术:
方法名转义和转码、成员表达式转IIFE、函数标准化、数值混淆、布尔型常量值混淆、二进制表达式转为调用表达式、字符串转Unicode、局部变量变形、屏蔽输出语句,以及:无限断点、时间差检测等反调试方案。
理论层面:为什么要对JS代码进行混淆加密?
技术层面:用JS编程实现对JS代码混淆加密。
防逆向措施:检测与对抗。
专业的混淆加密:JShaman。
彩蛋:字节码加密技术。
1、问:JS代码需要考虑安全性吗?
答:当然。
2、问:为什么?
答:JS因为应用环境需要,功能设计目的等历史原因,成为了一种代码公开透明的语言。
前端JS代码,直接暴露在浏览器中,任何访问者,都可以随意查看代码。这就导致代码可以被分析、复制、盗用等,进而引发安全问题,如被利用代码bug攻击、揭露功能逻辑、复制出雷同应用等等。
互联网早些年,安全场景如上。而发展到当下,JS的应用范围更加广泛,如NodeJS的兴起,使很多后端服务、产品、项目也应用了JS。
在后端的角度,如果项目或产品,提交给第三方时,是否要交出源码?显然不妥。
假设服务器被入侵,如果部署的后端服务产品源码也是JS明文,那将导致更严重的安全问题。
更多的应用领域,如小程序开发、H5应用,含ThreeJS引擎类游戏,等,都广泛应用了JS。
在所有这些场景中,都不应该忽视JS代码的安全问题,都应该且需要对JS代码进行保护。
3、问:如何让JS代码变的安全?
答:对JS代码进行保护:混淆&加密,使代码不可读。即:它人依然可以看到代码,但看到的是加密的代码、无法理解代码,更无法修改。
深入并精准的说:通过混淆加密,使代码变的难以阅读和理解。可能有人说,混淆后机器能执行,人就能理解,只是需要的时间长短问题。这种极端的说法,从理论上来说没错,如果可以投入足够长的时间,程序员甚至可以直接用0101写代码。而从实际角度而言,一段代码如果保护后分析需要的时长,超过开发需要的时长,保护的目的就达到了,就会劝退99.9999%对它有想法的正常人类。
理论已探讨完毕,接下来步入正题,探索如何对JS代码进行混淆加密,可不仅仅是应用层面,而是全面掌握:会用、知其然,知其所在然,还要动手编码,实现:用JS对JS代码混淆加密。
接下来的内容,将在NodeJS环境中,使用JS编程,实现对JS代码的混淆加密。
确定实现方案之前,首先需要排除几种不可用方案:
混淆原理:非replace或regexp方式字符串替换,而是对JS源码进行重编译。从源码,进行词法分析、语法分析、得到AST(抽象语法树),此处是重点,得到AST后,在AST中执行关键混淆加密操作,如:字符算阵列化、字符加密、平展控制流、僵尸代码值入、反调试埋雷、花指令插入等,最后,再将AST重建为JS代码。这样就得到了一份被更改的面目全非的安全JS代码:不可读、不可理解、不可修改、不可还原。
由以上的理论可知,重点是混淆加密,而入口点及整体流程框架是AST操作。
在JS引擎之下,代码编译执行大体流程是:
JS代码→AST(抽象语法树)→ByteCode(字节码)→机器码→解释器→执行。
AST设计之初并不是用于对JS代码混淆加密,但AST却很适合这个事情。
基于AST的JS代码混淆加密大体流程:
JS代码→AST→(基于AST的混淆加密)→JS代码。
题外话:能在ByteCode阶段进行加密吗?某些情况下可以,比如NodeJS环境中的JS代码,可以编译为ByteCode。但在前端运行的JS代码,且于DOM有交互的则不理想,小总结而言,有将JS代码进行VM式的加密方法,但通用性较差,使用起来复杂。因为这些弊端,因此,不是普遍性的JS代码保护方案。
注:在本结尾,会有一个彩蛋内容,实例介绍NodeJS字节码生成及运行。
图1,NodeJS字节码效果:
回到正题,JS代码如何转化为AST?
其实,没有想象中那么复杂。得益于NodeJS成熟的生态,已经有多个已实现模块可以完成这一操作。比较流行的如:esprima、babel,都可以实现对JS代码进行词法分析、语法分析、生成AST、AST操作、从AST再生成JS代码。
图2、esprima框架demo:
如图2所示,使用esprima进行JS代码保护的原始功能框架。
代码介绍:
Esprima实现将JS代码转化为AST;
estraverse对AST节点进行遍历,混淆加密的逻辑操作都将在此环节实现;
escodegen则是将操作后的AST转为JS代码输出。
此demo代码未对AST进行任何处理,所以图中右侧的执行结果中可以看到,输出的JS代码与最初代码完全一致。
前面已经对AST进行了说明,AST具体是什么样?
一个方便的办法,是使用astexplorer.net,可以对输入的代码的AST即时同步显示:
图3、const a=1的AST:
Demo中使用的一行JS语句:“const a=1”,其AST即如图中所显示。
AST是一个JSON结构。
Program表示程序,子节点body中,是变量定义kind是“const”,字面量是“a”,值是“1”。
看似杂乱,但很规整,细看便不难理解。
demo程序里,在节点操作处可以用console输出AST,与astexplorer输出一至,不过前者更方便些。
图4、在程序中输出AST:
图5:
代码如上图,这是一个很简单的示例。
程序中,estraverse对示例代码结点进行处理,当匹配到“==”时,改为“===”。
为了明确修改节点细节,再对前后代码进行分析。由图6、图7看到,差异仅在节点中的operator。
图6、代码中使用“==”:
图7、代码中使用“===”:
parseInt方法,有两个参数,参数一是要转化的值,参数二是可选择项,是要转化的进制类型。
图8:
通过astexplorer,先了解parseInt的AST,未使用参数二时,AST如下:
图9:
如果有第二参数,则AST如下:
图10:
那么,要将parseInt转为标准形式即是要给只有一个参数的调用增加第二参数。
代码及执行结果如下:
图11:
因为初入手的原因,以上描述较为细致,后续将简化。
如:console.log转为console[log]形式。
通过在aspexplorer中比较可知,造成语句形式差异的原因是CallExpression成员中computed属性值的不同。
图12:
那么,只需修改节对应节点的computed属性值即可:
图13:
而修改的条件,则是判断AST节点是CallExpression。上面的例子中,也是使用相似的条件判断方法,找出要修改内容相对应的AST节点。
再进一步,将方法名转为十六进制字符,console[log]会成为:console['x6cx6fx67'],以此进一步降低代码可读性。
图14、增加字符串转16进制操作:
例程代码输出为:
图15:
运行混淆后的代码:
图16:
从简单的例程,可以初步学习到对AST的操作方法。
接下来,实现一个有点难度的功能。
成员表达式通常指调用对象的成员,例如 console 对象的 log 成员。
IIFE,全称为:Immediately Invoked Function Expression,在JAVAScript编程中,是指:立即调用函数表达式。
为了方便理解,先展示此功能实现后的效果:
图17:
如上图中,console的log、warn、error方法,以及字符串的toUperCase()方法,在保护后都会成为匿名自执行的函数,且方法名都以数组化的形式被另外存放,代码相比之前混乱了许多。
图18、IIFE代码执行效果:
实现方法如下:
主架构与之前略有差异,traverse方法改为replace,enter事件改为leave事件,如下图:
图19:
对变量定义结点,如console.log输出的信息,以及成员函数,如console的log方法进行操作。
图20、改写字符串定义、成员函数调用:
Add_string函数把字符串信息、方法名,都写入到一个新的字符串数组,并且把方法改为IIFE。
字符串数组建立、方法改为IIFE的具体实现如下图:
图21:
然后,把新增的数组加入到AST中,最重再重建代码:
图22:
这样就完成了本例功能。
注:本例仅供功能演示,尚有不严谨的逻辑,比如成员方法IIFE化之前,除应该判断node.type为MemberExpression,还应排除节点computed为true的情况,否则代码执行会发生错误。
正如前文中所述,能对AST进行操作的模块不止esprima,babel也是个很好的选择。
接下来的例子,将使用babel来完成。
Babel的使用方式与esprima极为相似,其代码框架如下:
图23:
同样是:JS代码→AST→节点处理→JS代码。
代码如下图所示:
图24、用Babel在AST中去除console.log节点:
匹配AST中的成员操作节点,且满足条件callee的对像名为console,属性方法名为log,如检测掉,则remove该节点。
运行效果如下图所示,测试代码中含有console.log,修改后输出中已经被去除。
图25:
严谨的考虑的话,需要注意对象挂载的识别,如global.console.log,此时remove则会剩下global,将导致语法错误,因此还应该判断父节点类型来排除这种情况。
图26、对min、number两个局部变量变形:
相当于是可设定、可配置的对某些变量进行变形。
反向思考,也可以排除对某些变量的处理,等同于白名单,类似于JShaman平台中的“保留字”功能。
图27:
EmptyStatement表示空语句AST节点。
图28:
代码及执行结果如上图,原理为:判断节字符串字面量节点是否为Unicode格式,如不是则转为Unicode。
在这几个例子中,可看到与esprima的差异,esprima使用的是enter、leave方法,Babel中是直接对要处理的节点类型操作,如上图中的StringLiteral。
更条理化的写法,上面的代码可以修改如下,这个方法被称为Babel-plugin(插件):
图29:
即BinaryExpression节点转为CallExpression。
先看效果:
图30,左侧为二进制表达式,右侧为调用表达式:
二进制表达式AST形式:
实现代码:
图31:
调用表达式AST形式:
图32:
代码中的这部分,即是将二进制表达式转化为调用表达式:
图33:
代码及效果如下图:
图34:
代码及效果如下图:
图35:
JS代码混淆加密,虽不至博大精深,但也属于高段位技术。
在此分享部分浅显方案,以展现其实用效果,用于说明混淆加密手段对于JS代码加固的有效性。此外,还有更多高端的防护手段,如JShaman应用的:平展控制流、时间限制、域名锁定、僵尸代码植入等。
图36、JShaman的JS代码保护配置功能:
对JS代码进行混淆加密之后,代码安全度得到相当的加强,但还能更进一步,为了防止不法者进行逆向分析、破解,可在代码中加入防破解对抗功能。这也是被JShaman应用的方案。
JS当中有一个debugger指令,当处于调试工具中时,如在浏览器中,会形成断点,使调试中断,利用此特性,在程序中加入无限的debugger,可使代码无法被调试。
图37、每100毫秒一个断点:
浏览器中执行效果如下,当打开“调试器”时,程序会不停的中断,导致无法跟踪代码:
图38:
代码及执行效果如下图所示:
图39:
检测原理是:
在代码中加入console.log输出和console.clear语句,未在调试工具中时,这两句代码执行是不需渲染显示的,执行耗时短,但假如在浏览器中打开了开发者工具,则会因为显示输出并清除的操作而消耗较多时间,这会被程序察觉出耗时异常,从而检测出是在被调试。
本文讲述了部分JS代码混淆加密技术及实现,更多更专业的防护方案未有尽述,这里再展示一段经JShaman保护的代码,领略专业级的JS代码安全。
图40、测试代码准备进行混淆加密:
图41、保护选项设置:
图42、混淆加密后的JS代码:
提示:JS字节码(ByteCode)加密技术,理论可行,但通用性较差,在此仅做技术介绍,不推荐做为项目或产品正式使用方案。
在NodeJS中将JS代码生成字节码,方法很简单,需借助google的V8引擎,V8引擎内置有JS虚拟机。通过v8虚拟机,将JS代码编译为字节码。全程仅需十几行代码,如下图:
图43:
关键处是cachedData,即字节码。
V8虚拟机是能够识别和直接运行该字节码的。
代码如下,如同创建字节码一样简单。
图44:
生成的字节码是非文本形式的,如强行打开,内容如下图:
图45、字节码文件内容:
JS字节码生成并运行效果如下:
图46:
代码改变世界,献给广大JS开发者。全文结束,感谢阅读。