我们突破了机器人的保护



最近, incapsula已开始出现在许多外国站点上,该系统提高了站点的安全性,工作速度,同时又大大增加了软件开发人员的生命。 该系统的本质是使用JavaScript的全面保护,顺便说一下,许多DDOS机器人已经学会了执行甚至绕过CloudFlare。 今天,我们将学习封装,编写JS脚本反混淆器,并教我们的DDOS机器人如何解决它!

下面的屏幕快照中的网站是作为文章的一个很好的示例,在可疑主题的论坛上,与其他网站并没有什么不同,许多人正在为之寻找暴力手段,但是我还有另外一项任务-用于自动执行网站各种动作的软件。

让我们从检查查询开始,因为只有两个查询,这是第一个查询:



此请求将加载一个自然混淆的脚本:



将eval更改为document.write()可以使代码更具可读性:



我不知道为什么,但是用于格式化输出代码的自动工具破坏了此代码,因此我不得不用手对其进行格式化,并为变量指定普通名称。 对于所有这些操作,我使用了一个简单的记事本++和一个文本替换功能,因此我们可以继续进行第一行的检查:

var _0x3a59=['wpgXPQ==','cVjDjw==','Tk/DrFl/','GMOMd8K2w4jCpw==','wpkwwpE=','w6zDmmrClMKVHA==', ... ,'w4w+w5MGBQI=','w6TDr8Obw6TDlTJQaQ==']; 

这是一个包含加密函数名称和其他字符串的数组,因此我们需要寻找一个解密该数组的函数,无需走太远:

混淆代码
 var Decrypt=function(base64_Encoded_Param,Key_Param){ var CounterArray=[], CRC=0x0, TempVar, result='', EncodedSTR=''; base64_Encoded_Param=atob(base64_Encoded_Param); for(var n=0,input_length=base64_Encoded_Param.length;n<input_length;n++){ EncodedSTR+='%'+('00'+base64_Encoded_Param.charCodeAt(n).toString(0x10)).slice(-0x2); } base64_Encoded_Param=decodeURIComponent(EncodedSTR); for(var n=0x0;n<0x100;n++){ CounterArray[n]=n; } for(n=0x0;n<0x100;n++){ CRC=(CRC+CounterArray[n]+Key_Param.charCodeAt(n%Key_Param.length))%0x100; TempVar=CounterArray[n]; CounterArray[n]=CounterArray[CRC]; CounterArray[CRC]=TempVar; } n=0x0; CRC=0x0; for(var i=0x0;i<base64_Encoded_Param.length;i++){ n=(n+0x1)%0x100; CRC=(CRC+CounterArray[n])%0x100; TempVar=CounterArray[n]; CounterArray[n]=CounterArray[CRC]; CounterArray[CRC]=TempVar; result+=String.fromCharCode(base64_Encoded_Param.charCodeAt(i)^CounterArray[(CounterArray[n]+CounterArray[CRC])%0x100]); } return result; }; 


如果我们单独调用它:

 ParamDecryptor('0x0', '0Et]') 

这个结果将远远超出我们的预期。 另一个功能是要怪:

 var shift_array=function(number_of_shifts){ while(--number_of_shifts){ EncodedParams['push'](EncodedParams['shift']()); } }; 

这是在一个非常意外的地方-在开始时就调用的函数,并检查cookie。 显然,开发人员因此“保护”了cookie支票免于被切掉。 如您所见-没什么复杂的,循环仅将数组移动指定数量的元素,在本例中为223个元素。 这个魔术数字223是从哪里来的? 我从调用Cookie验证功能的电话中获得了这个数字,它看起来像0xdf并遵循以下路线:

 //  function(EncodedParams,EncodedParamsShifts); //  (AllParams,0xdf); //0xdf => 223 //    var _0x5e622e=function(_0x486a40,_0x1de600){_0x486a40(++_0x1de600);}; _0x5e622e(shift_array,EncodedParamsShifts); //     : shift_array(++EncodedParamsShifts); 

自然,它每次都会改变,谁会怀疑...

现在剩下的就是替换所有电话

 var _0x85e545=this[ParamDecryptor('0x0', '0Et]')]; 



 var _0x85e545=this['window']; 

或者,更好的是,

 var ThisWindow=this.window; 

我按常规进行了最后一次转换。 哦,是的,他们完全忘记了以下几行:

\x77\x6f\x4a\x33\x58\x68\x6b\x59\x77\x34\x44\x44\x6e\x78\x64\x70

到普通视图。 没什么复杂的,这是一个常规的UrlEncode,将\ x更改为%并解码以得到以下行:

woJ3XhkYw4DDnxdp

然后我开始替换所有电话

 ParamDecryptor('0x0', '0Et]') 

使用我模块中的自写函数将已解密的行复制到 是的,代码并没有闪烁的光芒,截止日期很紧急(就像昨天通常需要的那样),我懒得思考, 因为我曾经使用鼠标编程 ,但是它仍然可以正常工作:



我从源代码几乎1v1重写了代码。

接下来,另一种混淆代码的方法引起了我的注意。 我必须编写一个相当大的函数来搜索此类调用:

 case'7':while(_0x30fe16["XNg"](_0x13d8ee,_0x5a370d)) 

并用更简单的类似物代替它们:

很棒的功能列表
 var _0x30fe16={ 'XNg':function _0x19aabd(_0x425e3c,_0x481cd6){return _0x425e3c<_0x481cd6;}, 'sUd':function _0x320363(_0xa24206,_0x49d66b){return _0xa24206&_0x49d66b;}, 'wMk':function _0x32974a(_0x2cdcf4,_0x250e85){return _0x2cdcf4>>_0x250e85;}, 'FnU':function _0x22ce98(_0x2f5577,_0x4feea7){return _0x2f5577<<_0x4feea7;}, 'mTe':function _0x35a8bc(_0x11fecf,_0x29718e){return _0x11fecf&_0x29718e;}, 'doo':function _0x5ce08b(_0x4e5976,_0x4757ea){return _0x4e5976>>_0x4757ea;}, 'vmP':function _0x5d415c(_0x39dc96,_0x59022e){return _0x39dc96<<_0x59022e;}, 'bGL':function _0xd49b(_0x7e8c9f,_0x301346){return _0x7e8c9f|_0x301346;}, 'rXw':function _0x4dfb4d(_0x39d33a,_0x36fd1e){return _0x39d33a<<_0x36fd1e;}, 'svD':function _0x387610(_0x3cd4f7,_0x58fd9e){return _0x3cd4f7&_0x58fd9e;}, 'cuj':function _0x472c54(_0x4e473a,_0x26f3fd){return _0x4e473a==_0x26f3fd;}, 'OrY':function _0x3c6e85(_0x445d0b,_0x1caacf){return _0x445d0b|_0x1caacf;}, 'AKn':function _0x4dac5b(_0x521c05,_0x27b6bd){return _0x521c05>>_0x27b6bd;}, 'gtj':function _0x5416f0(_0x3e0965,_0x560062){return _0x3e0965&_0x560062;} }; 


获得:

 case'7':while(_0x13d8ee < _0x5a370d){ 

工作的算法非常简单,实际上,我们只是替换了变量:

  1. 在本例中,我们找到了数组的名称: _0x30fe16
  2. Parsim输入参数: _0x425e3c,_0x481cd6
  3. Parsim函数主体: _0x425e3c <_0x481cd6
  4. _0x425e3c替换为_0x13d8ee
  5. _0x481cd6替换为_0x5a370d
  6. 我们得到_0x13d8ee <_0x5a370d
  7. 用上面的代码替换_0x30fe16.XNg(_0x13d8ee,_0x5a370d)
  8. 重复直到功能结束

解析函数的名称,参数和主体是由一个常规函数完成的。 当然,在模块的最终版本中,这种方法并不是特别需要,只有1个调用稍微模糊了一些,但是客户说要做所有事情,因此做到了,此外,其他功能也变得更加清晰。 在其他站点上也有这样的设计:

显示大小写错误的代码
 var _0x4dc9f4 = { 'NTSjj': _0x3d6e1f.dEVDh, 'tZeHx': function(_0x2a40cd, _0x2faf22) { return _0x3d6e1f.JIehC(_0x2a40cd, _0x2faf22); }, 'ocgoO': "https://site/login", 'WmiOO': _0x3d6e1f.vsCuf }; //    ,      : var _0x3d6e1f = { 'dEVDh': "4|0|2|3|5|1", 'JIehC': function(_0x34757f, _0xd344e8) { return _0x34757f != _0xd344e8; }, 'vsCuf': ".countdownGroup", 'awUzV': function(_0x4b3914, _0x1f9e41) { return _0x4b3914 === _0x1f9e41; }, 'smOkd': "NSpHE", 'bvCub': function(_0x208c1d, _0x160d32) { return _0x208c1d(_0x160d32); }, 'PmBNl': function(_0x33524f, _0x29b35a) { return _0x33524f(_0x29b35a); }, 'Fhbrr': "#stopBtn", 'Vkpkf': function(_0x2de6ac, _0x31bb8b) { return _0x2de6ac + _0x31bb8b; }, 'HbSaV': function(_0x429822, _0x1a46e9) { return _0x429822 + _0x1a46e9; }, 'UsdKM': "https://site/register", 'JCXqh': "Timer started. ", 'GBXqx': function(_0x18f912, _0x5829b5) { return _0x18f912 / _0x5829b5; }, 'sSdZf': function(_0x45f64c, _0x152cb4) { return _0x45f64c(_0x152cb4); }, 'AAmKj': ".countdownTimer" }; 

如您所见,一个数组中的参数引用了另一个。 我简单地解决了这个问题:

  1. 解析所有数组
  2. 我们从代码中清除它们
  3. 将所有元素复制到关联数组(名称,值)
  4. 在递归搜索循环中,我们寻找所有嵌套函数
  5. 用嵌套函数替换嵌套函数
  6. 以相同方式将所有链接扩展到字符串

应用这种去混淆方法后,代码变得有点混乱了。 您可以通过两种方式立即注意到base64函数。 第一个:

 CharArray="ABCDE...XYZabcde...xyz0123456789+/"; 

第二个:

 if(!window["btoa"])window["btoa"]=_0x386a89; 

您不再可以撤消并继续使用其他更重要的功能,或更确切地说,转到使用Cookie的功能。 我在incap_ses_行中找到了它,并注意到了另一个混淆芯片-使用循环的代码混淆:

显示代码
 var _0x290283="4|2|5|0|3|1"["split"]('|'), _0x290611=0x0; while(!![]){ switch(_0x290283[_0x290611++]){ case'0':for(var n=0x0;n<CookieArray["length"];n++){ var _0x27e53a=CookieArray[n]["substr"](0x0,CookieArray[n]["indexOf"]('=')); var _0x4b4644=CookieArray[n]["substr"](CookieArray[n]["indexOf"]('=')+0x1,CookieArray[n]["length"]); if(_0x5ebd6a["test"](_0x27e53a)){ResultCookieArray[ResultCookieArray["length"]]=_0x4b4644;} } continue; case'1':return ResultCookieArray;continue; case'2':var _0x5ebd6a=new this.window.RegExp("^\s?incap_ses_");continue; case'3':_0x4d5690();continue; case'4':var ResultCookieArray=new this.window.Array();continue; case'5':var CookieArray=this.window.document.cookie["split"](';');continue; } break; } 


一切都非常简单:我们按照执行顺序重新排列行:4 | 2 | 5 | 0 | 3 | 1并获得原始功能。 在最终版本中也不需要这种去混淆方法,但是它不会引起大问题,所有内容都以基本方式进行解析,主要要考虑的是可以存在嵌套循环,因此我只是进行了递归搜索。

Cookies功能
 var _0x30fe16={ function _0x2829d5(){ var ResultCookieArray=new this.window.Array(); var _0x5ebd6a=new this.window.RegExp("^\s?incap_ses_"); var CookieArray=this.window.document.cookie["split"](';'); for(var n=0x0;n<CookieArray["length"];n++){ var _0x27e53a=CookieArray[n]["substr"](0x0,CookieArray[n]["indexOf"]('=')); var _0x4b4644=CookieArray[n]["substr"](CookieArray[n]["indexOf"]('=')+0x1,CookieArray[n]["length"]); if(_0x5ebd6a["test"](_0x27e53a)){ResultCookieArray[ResultCookieArray["length"]]=_0x4b4644;} } _0x4d5690(); return ResultCookieArray; } 

它只是将以incap_ses_开头的所有cookie 存储数组中 ,然后另一种方法仅通过对ASCII码求和来计算其校验和:

显示代码
 function TIncapsula.CharCRC(text: string): string; var i, crc:integer; begin crc:=0; for i:=1 to Length(text) do crc:=crc+ord(text[i]); result:=IntToStr(crc); end; function TIncapsula.GetCookieDigest: string; var i:integer; res:string; begin res:=''; for i:=0 to FCookies.Count-1 do begin if res='' then res:=CharCRC(browserinfo+FCookies[i]) else res:=res+','+CharCRC(browserinfo+FCookies[i]); end; result:=res; end; 


我们将需要进一步的校验和,现在让我们弄清楚从不同位置调用此_0x4d5690是什么样的功能。 为此,只需查看被调用的方法并为它们分配适当的名称即可:

 function CheckDebugger(){ if(new this.window.Date()["getTime"]() - RunTime) > 0x1f4){ FuckDebugger(); } } 

这个脚本的作者很天真:)

另一个要点:

 ParamDecryptor('0x65', '\x55\xa9\xf9\x1c\x1a\xd5\xfc\x60') //result = "ca3XP6zjTSB3w3gEwMl6lqgsdEVDTV9aF4rEDQ=="; 

从这里开始,我们需要前5个字母: ca3XP ,下面我将告诉您原因。 记住,我们是根据Cookie值计算校验和的? 现在我们需要它们来获取所谓的哈希。

哈希函数
 function TIncapsula.GetDigestHash(Digest: string): string; var i:integer; CookieDigest, res:string; begin CookieDigest:=GetCookieDigest; //85530,85722 res:=''; for i:=0 to Length(Digest)-1 do begin res:=res+IntToHex(ord(Digest[i+1]) + ord(CookieDigest[i mod Length(CookieDigest)+1]),1); end; result:=res; end; 


比较:



太好了! 最后一步仍然是-接收响应的Cookie:

 ResCooka=((((ParamDecryptor(btoa(PluginsInfo),"ca3XP")+",digest=")+DigestArray)+",s=")+AllDigestHash); Set_Cookies("___utmvc",btoa(ResCooka),0x14); 

最初的原始代码将以base64编码的浏览器参数添加到移位的AllParams数组的末尾,并使用带有ca3XP密钥的ParamDecryptor函数“加密”了它们,然后删除了之前添加的元素。 我可以假定此拐杖是由于一个小功能而制成的:ParamDecryptor函数接受数组中元素的索引和密钥,这意味着您只能通过数组在其中传输字符串。 为什么不正确呢? 程序员,先生。

好吧,实际上,一切就绪,cookie都准备好了,它仍然可以安装并发送请求。 是的,您不会接受它,因为有一个小细节,我希望保持沉默。

最佳化


Delphi中的代码片段只是一个原型。 根据客户的要求,去混淆器的所有代码都已在汇编器中重写,其执行速度提高了数倍。 以下对速度也有积极影响:

  1. 在循环的一次迭代中剪切掉多余的代码和数组。 这对于大幅减少代码量并在将来加快搜索速度是必要的。
  2. 由于代码中的函数没有混合在一起,我们知道它们的大概位置,因此,如果cookie安装函数位于最后,那么您至少需要从中间查找它。
  3. 预先搜索关键功能。 甚至在清除不必要垃圾中的代码时,汇编算法也会搜索它们。
  4. 在最终版本中,我摆脱了去混淆器大约一半的功能,这些功能是理解代码所必需的,而对于机器人来说则不是必需的,因为必要的参数毫无问题地得到了解决。

结论


当我访问该站点时,我希望它能快速运行,并且使用此方法混淆JS脚本对用户不敬。 这有助于防止机器人攻击吗? 不,当然,正如您所看到的,它实际上是在整个晚上花费的,并且仅花费几个三明治和几杯茶。

本文的目的是讨论此解决方案的操作原理,并说明其无用之处。 出于明显的原因,将不会发布用于避开此保护措施的现成模块(泄漏后,可怜的游戏停止转为akamai),他需要根据这项研究(大约一年前进行并且仍然有意义)亲自进行研究,而想要窃取别人的帐目去森林。 如有任何疑问,我随时准备在评论中回答。

Source: https://habr.com/ru/post/zh-CN429662/


All Articles