You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
varfun1=function(name){console.log('hello, '+name);};varfun2=function(name,age){console.log(name+' is '+age+' years old');}varname='xiao.ming';fun1(name);fun2(name,8);
var_0x62fae={_0xe82ae: function(_0x63aec,_0x678ec){return_0x63aec(_0x678ec);},_0xeca4f: function(_0x92352,_0x3c412,_0xae362){return_0x92352(_0x3c412,_0xae362)},_0x2374a: 'xiao.ming',_0x5482a: 'hello, ',_0x837ce: ' is ',_0x3226e: ' years old'};varfun1=function(name){console.log(_0x62fae._0x5482a+name);};varfun2=function(name,age){console.log(name+_0x62fae._0x837ce+age+_0x62fae._0x3226e);}varname=_0x62fae._0x2374a;_0x62fae._0xe82ae(name);_0x62fae._0x2374a(name,0x8);
前言
在安全攻防战场中,前端代码都是公开的,那么对前端进行加密有意义吗?可能大部分人的回答是,
毫无意义
,不要自创加密算法,直接用HTTPS吧。但事实上,即使不了解密码学,也应知道是有意义
的,因为加密前
和解密后
的环节,是不受保护的。HTTPS只能保护传输层,此外别无用处。而加密环节又分:
本文主要列举一些我见到的,我想到的一些加密方式,其实确切的说,应该叫混淆,不应该叫加密。
那么,代码混淆的具体原理是什么?其实很简单,就是去除代码中尽可能多的有意义的信息,比如注释、换行、空格、代码负号、变量重命名、属性重命名(允许的情况下)、无用代码的移除等等。因为代码是公开的,我们必须承认没有任何一种算法可以完全不被破解,所以,我们只能尽可能增加攻击者阅读代码的成本。
语法树AST混淆
在保证代码原本的功能性的情况下,我们可以对代码的AST按需进行变更,然后将变更后的AST在生成一份代码进行输出,达到混淆的目的,我们最常用的uglify-js就是这样对代码进行混淆的,当然
uglify-js
的混淆只是主要进行代码压缩,即我们下面讲到的变量名混淆。变量名混淆
将变量名混淆成阅读比较难阅读的字符,增加代码阅读难度,上面说的
uglify-js
进行的混淆,就是把变量混淆成了短名(主要是为了进行代码压缩),而现在大部分安全方向的混淆,都会将其混淆成类16进制变量名,效果如下:混淆后:
注意事项:
eval语法,eval函数中可能使用了原来的变量名,如果不对其进行处理,可能会运行报错,如下:
如果不对eval中的console.log(test)进行关联的混淆,则会报错。不过,如果eval语法超出了静态分析的范畴,比如:
这种咋办呢,可能要进行遍历AST找到其运行结果,然后在进行混淆,不过貌似成本比较高。
全局变量的编码,如果代码是作为SDK进行输出的,我们需要保存全局变量名的不变,比如:
$
变量是放在全局下的,混淆过后如下:那么如果依赖这一段代码的模块,使用
$('id')
调用自然会报错,因为这个全局变量已经被混淆了。常量提取
将JS中的常量提取到数组中,调用的时候用数组下标的方式调用,这样的话直接读懂基本不可能了,要么反AST处理下,要么一步一步调试,工作量大增。
以上面的代码为例:
混淆过后:
当然,我们可以根据需求,将数组转化为二位数组、三维数组等,只需要在需要用到的地方获取就可以。
常量混淆
将常量进行加密处理,上面的代码中,虽然已经是混淆过后的代码了,但是
hello
字符串还是以明文的形式出现在代码中,可以利用JS中16进制编码会直接解码的特性将关键字的Unicode进行了16进制编码。如下:结合常量提取得到混淆结果:
当然,除了JS特性自带的Unicode自动解析以外,也可以自定义一些加解密算法,比如对常量进行base64编码,或者其他的什么rc4等等,只需要使用的时候解密就OK,比如上面的代码用base64编码后:
运算混淆
将所有的逻辑运算符、二元运算符都变成函数,目的也是增加代码阅读难度,让其无法直接通过静态分析得到结果。如下:
混淆后:
当然除了逻辑运算符和二元运算符以外,还可以将函数调用、静态字符串进行类似的混淆,如下:
上面的例子中,fun1和fun2内的字符串相加也会被混淆走,静态字符串也会被前面提到的
字符串提取
抽取到数组中(我就是懒,这部分代码就不写了)。需要注意的是,我们每次遇到相同的运算符,需不需要重新生成函数进行替换,这就按个人需求了。
语法丑化
将我们常用的语法混淆成我们不常用的语法,前提是不改变代码的功能。例如for换成do/while,如下:
动态执行
将静态执行代码添加动态判断,运行时动态决定运算符,干扰静态分析。
如下:
混淆过后:
流程混淆
对执行流程进行混淆,又称控制流扁平化,为什么要做混淆执行流程呢?因为在代码开发的过程中,为了使代码逻辑清晰,便于维护和扩展,会把代码编写的逻辑非常清晰。一段代码从输入,经过各种if/else分支,顺序执行之后得到不同的结果,而我们需要将这些执行流程和判定流程进行混淆,让攻击者没那么容易摸清楚我们的执行逻辑。
控制流扁平化又分顺序扁平化、条件扁平化,
顺序扁平化
顾名思义,将按顺序、自上而下执行的代码,分解成数个分支进行执行,如下代码:
流程图如下:
混淆过后代码如下:
混淆过后的流程图如下:
流程看起来
扁
了。条件扁平化
条件扁平化的作用是把所有if/else分支的流程,全部扁平到一个流程中,在流程图中拥有相同的入口和出口。
如下面的代码:
如上代码,流程图是这样的
控制流扁平化后代码如下:
混淆后的流程图如下:
直观的感觉就是代码变
扁
了,所有的代码都挤到了一层当中,这样做的好处在于在让攻击者无法直观,或通过静态分析的方法判断哪些代码先执行哪些后执行,必须要通过动态运行才能记录执行顺序,从而加重了分析的负担。需要注意的是,在我们的流程中,无论是顺序流程还是条件流程,如果出现了块作用域的变量声明(const/let),那么上面的流程扁平化将会出现错误,因为switch/case内部为块作用域,表达式被分到case内部之后,其他case无法取到const/let的变量声明,自然会报错。
不透明谓词
上面的switch/case的判断是通过数字(也就是谓词)的形式判断的,而且是透明的,可以看到的,为了更加的混淆视听,可以将case判断设定为表达式,让其无法直接判断,比如利用上面代码,改为不透明谓词:
谓词用a、b、c三个变量组成,甚至可以把这三个变量隐藏到全局中定义,或者隐藏在某个数组中,让攻击者不能那么轻易找到。
脚本加壳
将脚本进行编码,运行时 解码 再 eval 执行如:
但是实际上这样意义并不大,因为攻击者只需要把alert或者console.log就原形毕露了
改进方案:利用
Function / (function(){}).constructor
将代码当做字符串传入,然后执行,如下:如上代码,可以对code进行加密混淆,例如aaencode,原理也是如此,我们举个例子
利用aaencode混淆过后,代码如下:
这段代码看起来很奇怪,不像是JavaScript代码,但是实际上这段代码是用一些看似表情的符号,声明了一个16位的数组(用来表示16进制位置),然后将code当做字符串遍历,把每个代码符号通过
string.charCodeAt
取这个16位的数组下标,拼接成代码。大概的意思就是把代码当做字符串,然后使用这些符号的拼接代替这一段代码(可以看到代码里有很多加号),最后,通过(new Function(code))('_')
执行。仔细观察上面这一段代码,把代码最后的
('_')
去掉,在运行,你会直接看到源代码,然后Function.constructor
存在(゚Д゚)
变量中,感兴趣的同学可以自行查看。除了aaencode,jjencode原理也是差不多,就不做解释了,其他更霸气的jsfuck,这些都是对代码进行加密的,这里就不详细介绍了。
反调试
由于JavaScript自带
debugger
语法,我们可以利用死循环性的debugger
,当页面打开调试面板的时候,无限进入调试状态。定时执行
在代码开始执行的时候,使用
setInterval
定时触发我们的反调试函数。随机执行
在代码生成阶段,随机在部分函数体中注入我们的反调试函数,当代码执行到特定逻辑的时候,如果调试面板在打开状态,则无限进入调试状态。
内容监测
由于我们的代码可能已经反调试了,攻击者可以会将代码拷贝到自己本地,然后修改,调试,执行,这个时候就需要添加一些检测进行判定,如果不是正常的环境执行,那让代码自行失败。
代码自检
在代码生成的时候,为函数生成一份Hash,在代码执行之前,通过函数 toString 方法,检测代码是否被篡改
环境自检
检查当前脚本的执行环境,例如当前的URL是否在允许的白名单内、当前环境是否正常的浏览器。
如果为Nodejs环境,如果出现异常环境,甚至我们可以启动木马,长期跟踪。
废代码注入
插入一些永远不会发生的代码,让攻击者在分析代码的时候被这些无用的废代码混淆视听,增加阅读难度。
废逻辑注入
与废代码相对立的就是有用的代码,这些有用的代码代表着被执行代码的逻辑,这个时候我们可以收集这些逻辑,增加一段判定来决定执行真逻辑还是假逻辑,如下:
可以看到,所有的console.log都是我们的执行逻辑,这个时候可以收集所有的console.log,然后制造假判定来执行真逻辑代码,收集逻辑注入后如下:
判定逻辑中生成了一些字符串,在没有使用字符串提取的情况下,这是可以通过代码静态分析来得到真实的执行逻辑的,或者我们可以使用上文讲到的动态执行来决定执行真逻辑,可以看一下使用字符串提取和变量名编码后的效果,如下:
求值陷阱
除了注入执行逻辑以外,还可以埋入一个隐蔽的陷阱,在一个
永不到达
且无法静态分析
的分支里,引用该函数,正常用户不会执行,而 AST 遍历求值时,则会触发陷阱!陷阱能干啥呢?加壳干扰
在代码用eval包裹,然后对eval参数进行加密,并埋下陷阱,在解码时插入无用代码,干扰显示,大量换行、注释、字符串等大量特殊字符,导致显示卡顿。
结束
大概我想到的混淆就包括这些,单个特性使用的话,混淆效果一般,各个特性组合起来用的话,最终效果很明显,当然这个看个人需求,毕竟混淆是个双刃剑,在增加了阅读难度的同时,也增大了脚本的体积,降低了代码的运行效率。
参考文献
代码混淆之道——控制流扁平与不透明谓词理论篇
The text was updated successfully, but these errors were encountered: