大家好,我叫ConardLi。在我们上周的文章中,一种奇特的JavaScript编码风格:Get,一种可以用来装逼的JavaScript编码风格,引起了网友们的热议。这其实是一种代码混淆技术,可以让你的代码更难阅读和逆向,也可以借用一些恶意爬虫和自动化分析。今天就带大家看看还有哪些代码混淆技术可以让JavaScript代码难以分析。我们以下面的代码为例:console.log("ConardLi",666);经过一些改造,它可以变成这样:它是如何工作的?来看看吧~十六进制字符串编码我们试试去JavascriptObfuscator网站,勾选EncodeStrings复选框,会得到如下代码:console["\x6C\x6F\x67"]("\x43\x6F\x6E\x61\x72\x64\x4C\x69\x20"+666)它的原理很简单,就是把字符串的每一个ASCII字符转换成十六进制形式(把函数调用换成括号的形式,比如asconsole.log->console['log']在代码混淆中也很常见),这是最简单的混淆,但它只能骗小白,我们可以很容易地逆向解决:这个技术还有一些其他变体,例如用unicode编码替换字符。https://javascriptobfuscator.com/Javascript-Obfuscator.aspx字符串数组映射还是在上面的网站,我们选择MoveStrings选项,得到的代码如下:var_0x8925=["\x43\x6F\x6E\x61\x72\x64\x4C\x69\x20","\x6C\x6F\x67"];console[_0x8925[1]](_0x8925[0]+666)有更多的字符串数组,通过不同的索引数组引入间接使用这些字符串。死码注入死码其实就是指一些无法访问的代码。我们可以在原始代码上注入一些永远无法访问的额外代码,使代码难以阅读,但也会使代码变大。这次我们试试defendjs:安装:$npminstall-ghttps://github.com/alexhorn/defendjs.git我们尝试创建一个conardli.js并将上面的代码放到这个文件中,执行以下命令:$defendjs--inputconardli.js--featuresdead_code--output。得到以下大代码:(function(){functiona(a,d){varb=newArray(0);;varc=arguments;while(true)try{switch(a){case21309:return;案例792:functione(a,b){returnArray.prototype.slice.call(a).concat(Array.prototype.slice.call(b));}functionf(){vara=arguments[0],c=Array.prototype.slice.call(arguments,1);varb=function(){returna.apply(this,c.concat(Array.prototype.slice.call(arguments)));};b.prototype=a.prototype;返回nb;}functiong(a,b){returnArray.prototype.slice.call(a,b);}函数h(b){varc={};对于(vara=0;a>>16)+String.fromCharCode(a>>16);}).join('');}functionj(){returnString.fromCharCode.apply(null,arguments);}console.log('ConardLi',666);一=21309;休息;}}赶上(二){$$defendjs$tobethrown=null;开关(a){默认值:抛出b;}}}a(792,{});}())代码量很大,其实仔细分析后会发现,其余插入的代码无法运行:最上层包裹了一个IIFE,然后还有一个函数,a,b两个参数在调用a函数时,只传入第一个参数792,然后你会发现a函数中有一个switch语句,只会执行到第二种情况,其中包含这样的语句:e,f、g、h、j和i函数没有被调用,所以只有最后一个console.log('ConardLi',666);statementwillbeexecuted...https://github.com/alexhorn/defendjsscopeconfusesus恢复代码并重新执行defendjs的作用域能力:$defendjs--inputconardli.js--featuresscope--output.(function(){{{functionb(a,b){returnArray.prototype.slice.call(a).concat(Array.prototype.slice.call(b));}functionc(){vara=arguments[0],c=Array.prototype.slice.call(arguments,1);varb=function(){returna.apply(this,c.concat(Array.prototype.slice.call(arguments)));};b.prototype=a.prototype;returnb;}functiond(a,b){returnArray.prototype.slice.call(a,b);}functione(b){varc={};对于(vara=0;a>>16)+String.fromCharCode(a>>16);}).join('');}functiong(){returnString.fromCharCode.apply(null,arguments);}}var=[];console.log('ConardLi',666);}}())这可能看起来像前一个版本的简单版本,但有一个关键区别:它引入了多个具有重复标识符的词法作用域例如,a可能是最内层作用域中第一个函数的参数,或者第二个函数中的变量,甚至是与我们的conaole.log语句相同范围内的变量。在这个简单的例子中,很容易看穿,因为最内层作用域的任何函数都不会被任何地方调用,但真正的业务代码往往非常复杂,混淆后就没那么容易看穿了。字符编码仍然使用defendjs,在我们的代码上执行如下命令:$defendjs--inputconardli.js--featuresliterals--output。得到如下代码:(function(){functionc(){varc=arguments;varb=[];b[1]='';b[1]+=a(67,111,110);b[1]+=a(97);b[1]+=a(114,100);b[1]+=a(76,105);返回b[1];}{{函数e(a,b){returnArray.prototype.slice.call(a).concat(Array.prototype.slice.call(b));}functiond(){vara=arguments[0],c=Array.prototype.slice复制代码.call(arguments,1);varb=function(){returna.apply(this,c.concat(Array.prototype.slice.call(arguments)));};b.prototype=a.prototype;returnb;}functionf(a,b){returnArray.prototype.slice.call(a,b);}functiong(b){varc={};对于(vara=0;a>>16)+String.fromCharCode(a>>16);}).join('');}functiona(){returnString.fromCharCode.apply(null,arguments);}}变量b=[];console.log(d(c,b)(),666);}}())在这种情况下,硬代码被转换Mangling是一种为了优化和混淆目的缩短变量和属性名称的转换。例如下面的代码:letsixSixSix=666;让名字=“ConardLi”;控制台日志(名称+sixSixSix);我们使用DefendJS的mangling函数:$defendjs--inputconardli.js--featuresmangle--output。得到的代码是:(function(){vara=666;varb='ConardLi!';console.log(b+a);}())两个变量都被重命名了,在这个简单的例子下面仍然是一个很好的分析。但是如果是庞大的业务代码,这会让我们的代码阅读起来非常困难。代码压缩下面,综合运用几种技术执行:defendjs--inputconardli.js--output。--features=control_flow,literals,mangle,compress得到如下代码:(function(){functiona(d,g){varb=newArray(1);;vare=arguments;while(true)t