JavaScript高手进阶:详解Eval加密
在JavaScript编程中,涉及到代码加密,在混淆加密时代之前,用的最多的应该是种Eval加密。
加密后的特征是以:eval(function(p,a,c,k,e,r)为代码开始,相信很多人都见过这种代码。
Eval加密效果例程
这是一种非常古老的技术。早在约2004年,一名南非的JavaScript程序员dean.edwards发明了这种加密技术:
本文将探索该加密技术的实现原理,并给出解密方法。
首先,直接上源码,该源码为Eval加密的变种,加密效果一样。
Eval加密完整源码示例
a=62;
function encode(js_code) {
var code = js_code;
code = code.replace(/[\r\n]+/g, '');
code = code.replace(/'/g, "\\'");
var tmp = code.match(/\b(\w+)\b/g);
tmp.sort();
var dict = [];
var i, t = '';
for(var i=0; i<tmp.length; i++) {
if(tmp[i] != t) dict.push(t = tmp[i]);
}
var len = dict.length;
var ch;
for(i=0; i<len; i++) {
ch = num(i);
code = code.replace(new RegExp('\\b'+dict[i]+'\\b','g'), ch);
if(ch == dict[i]) dict[i] = '';
}
return "eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[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}(" + "'"+code+"',"+a+","+len+",'"+ dict.join('|')+"'.split('|'),0,{}))";
}
function num(c) {
return(c<a?'':num(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36));
}
console.log(encode("var str ='jshaman.com'; console.log(str); var str ='jshaman.com'; console.log(str);"));
执行效果
运行加密后的代码,以测试正确性:
在NodeJS环境中运行:
正确输出,与源代码“var str =’jshaman.com’; console.log(str); var str =’jshaman.com’; console.log(str);”实现的效果一致。
接下来,详解加密代码,剖析其加密原理。
Eval加密详细剖析
用于测试的代码仅一行:
在之前展示的代码中增加console.log()用于调试分析:
用于去除回车换行,以及变单引号为斜杠加单引号的两句正则表达式:
此时输出:
Match用于在字符串中查找指定字符,返回为数组。
正则表达式中的\b匹配一个单词边界,也就是指单词和空格间的位置,或换行以后的起始位置。
\w匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。
运行到这里,输出:[‘var’, ‘str’, ‘jshaman’, ‘com’, ‘console’, ‘log’, ‘str’, ‘var’, ‘str’, ‘jshaman’, ‘com’, ‘console’, ‘log’, ‘str’],可以理解为将代码进行了分词。
接下来的for循环,用于除重,可以达到压缩代码的目的:
重点是dict.push,用的很精妙。(啊?)
运行时输出:
可以看到,原本14个数组成员,去重后,缩减成了6个成员。
接下来,是加密的重点部分:
在for循环中,使用正则表达式,将原代码中的关键字替换。
替换的内容来自于num函数的返回值:
这个num函数,返回的是:空格或参数除以62的整数结果加参数除以62的除数大于35时数字编码对应的字符或以36为基数的toString()字符。这也就是:Base62算法!
接下来再用正则表达式,结合base62算法,替换代码中每个字符串部分为dict中的数组序号。
看这时输出,在循环中原始代码已逐渐变为加密形式:
最后,再与Eval语句拼接,实现解密并运行:
这便得到了最终形态:
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[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}('5 4 =\'2.0\'; 1.3(4); 5 4 =\'2.0\'; 1.3(4);',62,6,'com|console|jshaman|log|str|var'.split('|'),0,{}))
到此,已经完成加密,生成了加密代码。
接下来,再对解密方法进行说明,将加密代码还原。
解密方法
解密之前,先对加密代码进行美化,手动换行、增加缩进,增加代码可读性:
美化后的代码,可以看到大体分为几部分:
- E函数,是base62的解码部分。
- while循环是关键字替换,还原出原始代码。
- 最下方的字符是加密后的base62编码,以及字符串数组等,是做为参数传递给匿名function(p,a,c,k,e,d)。
为了理解的更清晰,如上图,增加console.log语句,打印出各参数。
执行这段代码,输出如下:
在图中可以看到,变量p中存储的便是原始代码。
到此,等于已经完成解密。
另有一个更简单的方法是,见到此类代码,将起始处的eval改为console.log或document.write或alert都可直接输出源始代码。(我之前刚好碰到混淆的JS,用的是“document.write”搭配调试的控制台查看)
如下图,源码被输出:
可见此种加密方法,虽然看起来够吓人,但破解十分容易。
在之前的文章《JavaScript黑暗技巧:变异的Eval》中,曾讲过对这种eval加密增强的办法:采用变异eval名称、用JShaman对加密代码二次混淆加密,可以极大的提升保护强度,实现JS代码的不可逆加密。