在安全攻防战场中,前端代码都是公开的,那么对前端进行加密有意义吗?可能大部分人的回答是,毫无心义
,不要自创加密算法,直接用HTTPS吧。但事实上,即便不了解密码学,也应知道是有意义
的,由于加密前
和解密后
的环节,是不受保护的。HTTPS只能保护传输层,此外别无用处。html
而加密环节又分:前端
本文主要列举一些我见到的,我想到的一些加密方式,其实确切的说,应该叫混淆,不该该叫加密。git
那么,代码混淆的具体原理是什么?其实很简单,就是去除代码中尽量多的有意义的信息,好比注释、换行、空格、代码负号、变量重命名、属性重命名(容许的状况下)、无用代码的移除等等。由于代码是公开的,咱们必须认可没有任何一种算法能够彻底不被破解,因此,咱们只能尽量增长攻击者阅读代码的成本。github
原文地址算法
在保证代码本来的功能性的状况下,咱们能够对代码的AST按需进行变动,而后将变动后的AST在生成一份代码进行输出,达到混淆的目的,咱们最经常使用的uglify-js就是这样对代码进行混淆的,固然uglify-js
的混淆只是主要进行代码压缩,即咱们下面讲到的变量名混淆。npm
将变量名混淆成阅读比较难阅读的字符,增长代码阅读难度,上面说的uglify-js
进行的混淆,就是把变量混淆成了短名(主要是为了进行代码压缩),而如今大部分安全方向的混淆,都会将其混淆成类16进制变量名,效果以下:数组
var test = 'hello';
混淆后:浏览器
var _0x7deb = 'hello';
注意事项:安全
eval语法,eval函数中可能使用了原来的变量名,若是不对其进行处理,可能会运行报错,以下:函数
var test = 'hello'; eval('console.log(test)');
若是不对eval中的console.log(test)进行关联的混淆,则会报错。不过,若是eval语法超出了静态分析的范畴,好比:
var test = 'hello'; var variableName = 'test'; eval('console.log(' + variableName + ')');
这种咋办呢,可能要进行遍历AST找到其运行结果,而后在进行混淆,不过貌似成本比较高。
全局变量的编码,若是代码是做为SDK进行输出的,咱们须要保存全局变量名的不变,好比:
<script> var $ = function(id) { return document.getElementById(id); }; </script>
$
变量是放在全局下的,混淆事后以下:
<script> var _0x6482fa = function(id) { return document.getElementById(id); }; </script>
那么若是依赖这一段代码的模块,使用$('id')
调用天然会报错,由于这个全局变量已经被混淆了。
将JS中的常量提取到数组中,调用的时候用数组下标的方式调用,这样的话直接读懂基本不可能了,要么反AST处理下,要么一步一步调试,工做量大增。
以上面的代码为例:
var test = 'hello';
混淆事后:
var _0x9d2b = ['hello']; var _0xb7de = function (_0x4c7513) { var _0x96ade5 = _0x9d2b[_0x4c7513]; return _0x96ade5; }; var test = _0xb7de(0);
固然,咱们能够根据需求,将数组转化为二位数组、三维数组等,只须要在须要用到的地方获取就能够。
将常量进行加密处理,上面的代码中,虽然已是混淆事后的代码了,可是hello
字符串仍是以明文的形式出如今代码中,能够利用JS中16进制编码会直接解码的特性将关键字的Unicode进行了16进制编码。以下:
var test = 'hello';
结合常量提取获得混淆结果:
var _0x9d2b = ['\x68\x65\x6c\x6c\x6f']; var _0xb7de = function (_0x4c7513) { _0x4c7513 = _0x4c7513 - 0x0; var _0x96ade5 = _0x9d2b[_0x4c7513]; return _0x96ade5; }; var test = _0xb7de('0x0');
固然,除了JS特性自带的Unicode自动解析之外,也能够自定义一些加解密算法,好比对常量进行base64编码,或者其余的什么rc4等等,只须要使用的时候解密就OK,好比上面的代码用base64编码后:
var _0x9d2b = ['aGVsbG8=']; // base64编码后的字符串 var _0xaf421 = function (_0xab132) { // base64解码函数 var _0x75aed = function(_0x2cf82) { // TODO: 解码 }; return _0x75aed(_0xab132); } var _0xb7de = function (_0x4c7513) { _0x4c7513 = _0x4c7513 - 0x0; var _0x96ade5 = _0xaf421(_0x9d2b[_0x4c7513]); return _0x96ade5; }; var test = _0xb7de('0x0');
将全部的逻辑运算符、二元运算符都变成函数,目的也是增长代码阅读难度,让其没法直接经过静态分析获得结果。以下:
var i = 1 + 2; var j = i * 2; var k = j || i;
混淆后:
var _0x62fae = { _0xeca4f: function(_0x3c412, _0xae362) { return _0x3c412 + _0xae362; }, _0xe82ae: function(_0x63aec, _0x678ec) { return _0x63aec * _0x678ec; }, _0x2374a: function(_0x32487, _0x3a461) { return _0x32487 || _0x3a461; } }; var i = _0x62fae._0e8ca4f(1, 2); var j = _0x62fae._0xe82ae(i, 2); var k = _0x62fae._0x2374a(i, j);
固然除了逻辑运算符和二元运算符之外,还能够将函数调用、静态字符串进行相似的混淆,以下:
var fun1 = function(name) { console.log('hello, ' + name); }; var fun2 = function(name, age) { console.log(name + ' is ' + age + ' years old'); } var name = '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' }; var fun1 = function(name) { console.log(_0x62fae._0x5482a + name); }; var fun2 = function(name, age) { console.log(name + _0x62fae._0x837ce + age + _0x62fae._0x3226e); } var name = _0x62fae._0x2374a; _0x62fae._0xe82ae(fun1, name); _0x62fae._0xeca4f(fun2, name, 0x8);
上面的例子中,fun1和fun2内的字符串相加也会被混淆走,静态字符串也会被前面提到的字符串提取
抽取到数组中(我就是懒,这部分代码就不写了)。
须要注意的是,咱们每次遇到相同的运算符,需不须要从新生成函数进行替换,这就按我的需求了。
将咱们经常使用的语法混淆成咱们不经常使用的语法,前提是不改变代码的功能。例如for换成do/while,以下:
for (i = 0; i < n; i++) { // TODO: do something } var i = 0; do { if (i >= n) break; // TODO: do something i++; } while (true)
将静态执行代码添加动态判断,运行时动态决定运算符,干扰静态分析。
以下:
var c = 1 + 2;
混淆事后:
function _0x513fa(_0x534f6, _0x85766) { return _0x534f6 + _0x85766; } function _0x3f632(_0x534f6, _0x534f6) { return _0x534f6 - _0x534f6; } // 动态断定函数 function _0x3fa24() { return true; } var c = _0x3fa24() ? _0x513fa(1, 2) : _0x3f632(1, 2);
对执行流程进行混淆,又称控制流扁平化,为何要作混淆执行流程呢?由于在代码开发的过程当中,为了使代码逻辑清晰,便于维护和扩展,会把代码编写的逻辑很是清晰。一段代码从输入,通过各类if/else分支,顺序执行以后获得不一样的结果,而咱们须要将这些执行流程和断定流程进行混淆,让攻击者没那么容易摸清楚咱们的执行逻辑。
控制流扁平化又分顺序扁平化、条件扁平化,
顾名思义,将按顺序、自上而下执行的代码,分解成数个分支进行执行,以下代码:
(function () { console.log(1); console.log(2); console.log(3); console.log(4); console.log(5); })();
流程图以下:
混淆事后代码以下:
(function () { var flow = '3|4|0|1|2'.split('|'), index = 0; while (!![]) { switch (flow[index++]) { case '0': console.log(3); continue; case '1': console.log(4); continue; case '2': console.log(5); continue; case '3': console.log(1); continue; case '4': console.log(2); continue; } break; } }());
混淆事后的流程图以下:
流程看起来扁
了。
条件扁平化的做用是把全部if/else分支的流程,所有扁平到一个流程中,在流程图中拥有相同的入口和出口。
以下面的代码:
function modexp(y, x, w, n) { var R, L; var k = 0; var s = 1; while(k < w) { if (x[k] == 1) { R = (s * y) % n; } else { R = s; } s = R * R % n; L = R; k++; } return L; }
如上代码,流程图是这样的
控制流扁平化后代码以下:
function modexp(y, x, w, n) { var R, L, s, k; var next = 0; for(;;) { switch(next) { case 0: k = 0; s = 1; next = 1; break; case 1: if (k < w) next = 2; else next = 6; break; case 2: if (x[k] == 1) next = 3; else next = 4; break; case 3: R = (s * y) % n; next = 5; break; case 4: R = s; next = 5; break; case 5: s = R * R % n; L = R; k++; next = 1; break; case 6: return L; } } }
混淆后的流程图以下:
直观的感受就是代码变扁
了,全部的代码都挤到了一层当中,这样作的好处在于在让攻击者没法直观,或经过静态分析的方法判断哪些代码先执行哪些后执行,必需要经过动态运行才能记录执行顺序,从而加剧了分析的负担。
须要注意的是,在咱们的流程中,不管是顺序流程仍是条件流程,若是出现了块做用域的变量声明(const/let),那么上面的流程扁平化将会出现错误,由于switch/case内部为块做用域,表达式被分到case内部以后,其余case没法取到const/let的变量声明,天然会报错。
上面的switch/case的判断是经过数字(也就是谓词)的形式判断的,并且是透明的,能够看到的,为了更加的混淆视听,能够将case判断设定为表达式,让其没法直接判断,好比利用上面代码,改成不透明谓词:
function modexp(y, x, w, n) { var a = 0, b = 1, c = 2 * b + a; var R, L, s, k; var next = 0; for(;;) { switch(next) { case (a * b): k = 0; s = 1; next = 1; break; case (2 * a + b): if (k < w) next = 2; else next = 6; break; case (2 * b - a): if (x[k] == 1) next = 3; else next = 4; break; case (3 * a + b + c): R = (s * y) % n; next = 5; break; case (2 * b + c): R = s; next = 5; break; case (2 * c + b): s = R * R % n; L = R; k++; next = 1; break; case (4 * c - 2 * b): return L; } } }
谓词用a、b、c三个变量组成,甚至能够把这三个变量隐藏到全局中定义,或者隐藏在某个数组中,让攻击者不能那么轻易找到。
将脚本进行编码,运行时 解码 再 eval 执行如:
eval (…………………………..……………. ……………. !@#$%^&* ……………. .…………………………..……………. )
可是实际上这样意义并不大,由于攻击者只须要把alert或者console.log就原形毕露了
改进方案:利用Function / (function(){}).constructor
将代码当作字符串传入,而后执行,以下:
var code = 'console.log("hellow")'; (new Function(code))();
如上代码,能够对code进行加密混淆,例如aaencode,原理也是如此,咱们举个例子
alert("Hello, JavaScript");
利用aaencode混淆事后,代码以下:
゚ω゚ノ= /`m´)ノ ~┻━┻ //*´∇`*/ ['_']; o=(゚ー゚) =_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); (゚Д゚) =(゚Θ゚)= (o^_^o)/ (o^_^o);(゚Д゚)={゚Θ゚: '_' ,゚ω゚ノ : ((゚ω゚ノ==3) +'_') [゚Θ゚] ,゚ー゚ノ :(゚ω゚ノ+ '_')[o^_^o -(゚Θ゚)] ,゚Д゚ノ:((゚ー゚==3) +'_')[゚ー゚] }; (゚Д゚) [゚Θ゚] =((゚ω゚ノ==3) +'_') [c^_^o];(゚Д゚) ['c'] = ((゚Д゚)+'_') [ (゚ー゚)+(゚ー゚)-(゚Θ゚) ];(゚Д゚) ['o'] = ((゚Д゚)+'_') [゚Θ゚];(゚o゚)=(゚Д゚) ['c']+(゚Д゚) ['o']+(゚ω゚ノ +'_')[゚Θ゚]+ ((゚ω゚ノ==3) +'_') [゚ー゚] + ((゚Д゚) +'_') [(゚ー゚)+(゚ー゚)]+ ((゚ー゚==3) +'_') [゚Θ゚]+((゚ー゚==3) +'_') [(゚ー゚) - (゚Θ゚)]+(゚Д゚) ['c']+((゚Д゚)+'_') [(゚ー゚)+(゚ー゚)]+ (゚Д゚) ['o']+((゚ー゚==3) +'_') [゚Θ゚];(゚Д゚) ['_'] =(o^_^o) [゚o゚] [゚o゚];(゚ε゚)=((゚ー゚==3) +'_') [゚Θ゚]+ (゚Д゚) .゚Д゚ノ+((゚Д゚)+'_') [(゚ー゚) + (゚ー゚)]+((゚ー゚==3) +'_') [o^_^o -゚Θ゚]+((゚ー゚==3) +'_') [゚Θ゚]+ (゚ω゚ノ +'_') [゚Θ゚]; (゚ー゚)+=(゚Θ゚); (゚Д゚)[゚ε゚]='\\'; (゚Д゚).゚Θ゚ノ=(゚Д゚+ ゚ー゚)[o^_^o -(゚Θ゚)];(o゚ー゚o)=(゚ω゚ノ +'_')[c^_^o];(゚Д゚) [゚o゚]='\"';(゚Д゚) ['_'] ( (゚Д゚) ['_'] (゚ε゚+(゚Д゚)[゚o゚]+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚Θ゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ ((゚ー゚) + (o^_^o))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚Θ゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) - (゚Θ゚))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (c^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚ー゚)+ ((o^_^o) - (゚Θ゚))+ (゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+ (゚Θ゚)+ (゚Д゚)[゚o゚]) (゚Θ゚)) ('_');
这段代码看起来很奇怪,不像是JavaScript代码,可是实际上这段代码是用一些看似表情的符号,声明了一个16位的数组(用来表示16进制位置),而后将code当作字符串遍历,把每一个代码符号经过string.charCodeAt
取这个16位的数组下标,拼接成代码。大概的意思就是把代码当作字符串,而后使用这些符号的拼接代替这一段代码(能够看到代码里有不少加号),最后,经过(new Function(code))('_')
执行。
仔细观察上面这一段代码,把代码最后的('_')
去掉,在运行,你会直接看到源代码,而后Function.constructor
存在(゚Д゚)
变量中,感兴趣的同窗能够自行查看。
除了aaencode,jjencode原理也是差很少,就不作解释了,其余更霸气的jsfuck,这些都是对代码进行加密的,这里就不详细介绍了。
因为JavaScript自带debugger
语法,咱们能够利用死循环性的debugger
,当页面打开调试面板的时候,无限进入调试状态。
在代码开始执行的时候,使用setInterval
定时触发咱们的反调试函数。
在代码生成阶段,随机在部分函数体中注入咱们的反调试函数,当代码执行到特定逻辑的时候,若是调试面板在打开状态,则无限进入调试状态。
因为咱们的代码可能已经反调试了,攻击者能够会将代码拷贝到本身本地,而后修改,调试,执行,这个时候就须要添加一些检测进行断定,若是不是正常的环境执行,那让代码自行失败。
在代码生成的时候,为函数生成一份Hash,在代码执行以前,经过函数 toString 方法,检测代码是否被篡改
function module() { // 篡改校验 if (Hash(module.toString()) != 'JkYxnHlxHbqKowiuy') { // 代码被篡改! } }
检查当前脚本的执行环境,例如当前的URL是否在容许的白名单内、当前环境是否正常的浏览器。
若是为Nodejs环境,若是出现异常环境,甚至咱们能够启动木马,长期跟踪。
插入一些永远不会发生的代码,让攻击者在分析代码的时候被这些无用的废代码混淆视听,增长阅读难度。
与废代码相对立的就是有用的代码,这些有用的代码表明着被执行代码的逻辑,这个时候咱们能够收集这些逻辑,增长一段断定来决定执行真逻辑仍是假逻辑,以下:
(function(){ if (true) { var foo = function () { console.log('abc'); }; var bar = function () { console.log('def'); }; var baz = function () { console.log('ghi'); }; var bark = function () { console.log('jkl'); }; var hawk = function () { console.log('mno'); }; foo(); bar(); baz(); bark(); hawk(); } })();
能够看到,全部的console.log都是咱们的执行逻辑,这个时候能够收集全部的console.log,而后制造假断定来执行真逻辑代码,收集逻辑注入后以下:
(function(){ if (true) { var foo = function () { if ('aDas' === 'aDas') { console.log('abc'); } else { console.log('def'); } }; var bar = function () { if ('Mfoi' !== 'daGs') { console.log('ghi'); } else { console.log('def'); } }; var baz = function () { if ('yuHo' === 'yuHo') { console.log('ghi'); } else { console.log('abc'); } }; var bark = function () { if ('qu2o' === 'qu2o') { console.log('jkl'); } else { console.log('mno'); } }; var hawk = function () { if ('qCuo' !== 'qcuo') { console.log('jkl'); } else { console.log('mno'); } }; foo(); bar(); baz(); bark(); hawk(); } })();
断定逻辑中生成了一些字符串,在没有使用字符串提取的状况下,这是能够经过代码静态分析来获得真实的执行逻辑的,或者咱们可使用上文讲到的动态执行来决定执行真逻辑,能够看一下使用字符串提取和变量名编码后的效果,以下:
var _0x6f5a = [ 'abc', 'def', 'caela', 'hmexe', 'ghi', 'aaeem', 'maxex', 'mno', 'jkl', 'ladel', 'xchem', 'axdci', 'acaeh', 'log' ]; (function (_0x22c909, _0x4b3429) { var _0x1d4bab = function (_0x2e4228) { while (--_0x2e4228) { _0x22c909['push'](_0x22c909['shift']()); } }; _0x1d4bab(++_0x4b3429); }(_0x6f5a, 0x13f)); var _0x2386 = function (_0x5db522, _0x143eaa) { _0x5db522 = _0x5db522 - 0x0; var _0x50b579 = _0x6f5a[_0x5db522]; return _0x50b579; }; (function () { if (!![]) { var _0x38d12d = function () { if (_0x2386('0x0') !== _0x2386('0x1')) { console[_0x2386('0x2')](_0x2386('0x3')); } else { console[_0x2386('0x2')](_0x2386('0x4')); } }; var _0x128337 = function () { if (_0x2386('0x5') !== _0x2386('0x6')) { console[_0x2386('0x2')](_0x2386('0x4')); } else { console[_0x2386('0x2')](_0x2386('0x7')); } }; var _0x55d92e = function () { if (_0x2386('0x8') !== _0x2386('0x8')) { console[_0x2386('0x2')](_0x2386('0x3')); } else { console[_0x2386('0x2')](_0x2386('0x7')); } }; var _0x3402dc = function () { if (_0x2386('0x9') !== _0x2386('0x9')) { console[_0x2386('0x2')](_0x2386('0xa')); } else { console[_0x2386('0x2')](_0x2386('0xb')); } }; var _0x28cfaa = function () { if (_0x2386('0xc') === _0x2386('0xd')) { console[_0x2386('0x2')](_0x2386('0xb')); } else { console[_0x2386('0x2')](_0x2386('0xa')); } }; _0x38d12d(); _0x128337(); _0x55d92e(); _0x3402dc(); _0x28cfaa(); } }());
除了注入执行逻辑之外,还能够埋入一个隐蔽的陷阱,在一个永不到达
且没法静态分析
的分支里,引用该函数,正经常使用户不会执行,而 AST 遍历求值时,则会触发陷阱!陷阱能干啥呢?
在代码用eval包裹,而后对eval参数进行加密,并埋下陷阱,在解码时插入无用代码,干扰显示,大量换行、注释、字符串等大量特殊字符,致使显示卡顿。
大概我想到的混淆就包括这些,单个特性使用的话,混淆效果通常,各个特性组合起来用的话,最终效果很明显,固然这个看我的需求,毕竟混淆是个双刃剑,在增长了阅读难度的同时,也增大了脚本的体积,下降了代码的运行效率。