声明本文所有内容仅供学习交流,不作任何其他用途,不提供完整代码,抓包内容、敏感URL、数据接口等,严禁脱敏用于商业和非法用途,否则由此产生的一切后果与作者无关!本文未经许可禁止转载,修改后禁止二次传播。对于因未经授权使用本文所解释的技术而导致的任何事故,作者概不负责。如有侵权,请第一时间联系作者公众号【K爬虫哥】删除!前言睿数动态安全Botgate(机器人防火墙)以“动态安全”技术为核心,通过动态封装、动态验证、动态混淆、动态token等技术,不断动态改变服务器网页的底层代码,增加服务器行为的“不可预测性”。实现从客户端到服务端的全方位“主动防护”,为各类Web和HTML5提供强大的安全防护。锐数Botgate多用于政府、企业、金融、运营商等行业。一度被视为防攀爬天花板。随着近年来逆向高手越来越多,相关的逆向文章也层出不穷。真正到了人均睿数时代,也感谢难陀、懒神等逆向大师揭开了睿数的神秘面纱,总结出来的经验为后人少走了很多弯路。传递瑞士号码的方法基本有以下几种:自动化工具(隐藏特征值)、RPC远程调用、JS逆向(硬编码和补充环境),本文介绍JS逆向硬编码,尽可能介绍各种细节。锐书的特点及不同版本的区别对于绝大多数使用锐书的网站来说,有以下特点(可能有特殊版本不一样,先只看主流):1.打开开发者工具(F12)两个典型的无限调试器会依次出现:2.瑞书的JS混淆代码中,变量和方法名大多类似_$xx,还有很多if-else控制流。新版睿数可能还有jsvmp和很多三目表达式的情况:3.看请求,会出现三个典型的请求。第一次请求的响应码是202(瑞士号3代4代)或者412(瑞士号5代),然后单独请求一个JS文件,然后再重新请求页面,并且其他后续XHR请求将有一个后缀。这个后缀的值是JS生成的,每次都会变。后缀值的第一个数字是锐数的版本,比如MmEwMD=4xxxxx是锐数4代,bX3Xf9nD=5xxxxx是锐数5代:4.看cookies,3代4代Ruishu的有两个以T和S结尾的cookie,以S开头的cookie是第一个201请求返回的以T开头的cookie由JS生成,动态变化。T和S前面一般都是80或者443的数字,同一个版本的首位数字可以不改吗?我们在分析JS的时候可以找到这些问题的答案)例如:FSSBBIl1UgzbN7N80T=37Na97B.nWX3....:数字80是http协议Number的默认端口,对应http请求,其值的第一位是3,代表第3代瑞士号码;FSSBBIl1UgzbN7N443T=4a.tr1kEXk.....:数字443是https协议的默认端口号,对应https请求,它的值是第一个数字是4,代表第4代睿数。5代瑞士号也有两个以T和S结尾的cookie,但是一些特殊的5代瑞士号也以O和P结尾。同理,第一个412请求返回的是以O开头的,以版本结尾P开头的是JS生成的,cookie值的第一个数字也是瑞士数字版本。与3、4代不同,5代没有端口号,例如:vsKWUwn3HsfIO=57C6DwDUXS.......:以O结尾,其值的第一位是5,表示5代瑞士号;WvY7XhIMu0fGT=53.9fybty……:以T结尾,其值的第一位是5,代表第5代瑞士号码。5.看条目。锐书有一个在虚拟机VM中加载1w+行代码的过程。不同版本加载这段代码的入口不同(这个入口在哪里?如何定位?在后续的逆向分析介绍中会详细介绍),示例如下:3代:_$aW=_$c6[_$l6()](_$wc,_$mo);,_$c6其实就是eval,_$l6()其实就是call;4代:ret=_$DG.call(_$6a,_$YK);,_$DG其实就是eval,带关键字ret,call是明文;5thgeneration:5代的种类更多。最初和4代类似,比如ret=_$Yg.call(_$kc,_$mH);,带ret关键字,call是明文,也有没有ret关键字的版本,比如_$ap=_$j5.call(_$_T,_$gp);,有的像3代一样混淆,比如:_$x8=_$mP[_$nU[15]](_$z3,_$Ec);,_$mP其实就是eval,_$nU[15]其实就是call,混淆调用和三代的区别在于五代是在一个中取值得到的大批;当然,要准确区分不同的版本,结合各种条件,最重要的还是要看页面的内部实现逻辑和代码结构。比如4代有一个生成假cookie的步骤,而5代没有。虽然有些特殊版本好像是5th,但是加上了jsvmp和三目表情,和传统的5th有所区别。偶尔愚人节突然出现新版本,就会不一样。对每个版本进行分析后,就很容易区分了。本例cookie入口位置为睿数4代网站:aHR0cDovL3d3dy5mYW5nZGkuY29tLmNuL25ld19ob3VzZS9uZXdfaG91c2VfZGV0YWlsLmh0bWw=先通过无限调试器(不过没关系,后面的分析基本没什么作用),右键Neverpausehereneverstophere:定位cookies,Hook是首选,通过Fiddler等抓包工具、油猴脚本、浏览器插件等注入如下Hook代码:(function(){//严格模式检查所有错误'usestrict';//document是要hook的对象这里是hook的cookievarcookieTemp="";Object.defineProperty(document,'cookie',{//hookset方法也是赋值set的方法:function(val){//这是你可以在下面这行代码快速设置断点//快速定位到设置cookie的代码console.log('Hookcapturesthecookiesetting->',val);debugger;cookieTemp=val;returnval;},//hook的get方法也是获取值的方法get:function(){returncookieTemp;}});})();Hook发现会出现两次生成cookie的情况。打散后往上栈,可以看到组装好的cookie代码类似如下结构:仔细观察两个cookie生成的地方,分别往栈上走,会发现两个cookies有两种不同的获取方式,如下图:这里的代码存在于VM虚拟机中,IIFE是自执行的代码,我们要顺栈看看这些VM代码是从哪里加载的,顺栈到首页(202页)有一个调用位置:我们文章开头介绍的位置是这样分析的,这个位置通常作为分析锐数的切入点。图中_$te其实就是eval方法。传入的第一个参数_$fY是Window对象,第二个对象_$F8就是我们前面看到的那个。VM虚拟机中的IIFE自执行代码知道瑞书的大概入口后,我们也可以在事件监听器中使用Script断点,一直到202页面直到下一个断点(F8),然后搜索call关键字可以快速定位入口。Scriptbreakpoint中的两个选项,第一个表示在JS脚本第一条语句运行时断,第二个表示JS由于内容安全策略被阻塞时断。一般可以选择第一种,如下图:文件结构及逻辑分析cookies的生成,我们要观察202页的代码,meta标签有一个content内容,指的是一个文件类似于c。FxJzG50F.dfe1675.js的JS文件后面是一个自执行的JS,如下图:part1中meta标签的内容每次都变,part2引用的外部JS也存在于不同的pages但是,同一个网站的同一个页面的JS中的内容一般是固定不变的,不会发生变化。第三部分,自执行代码每次只改变变量名,整体逻辑不变。后面我们推导代码的时候,也会用到这里的一些方法。自执行代码中也有很多if-else控制流程。开头的数组,如上图中的_$Dk,用于控制后续的控制流程。引用的c.FxJzG50F.dfe1675.js直接打开是乱码,自执行JS的主要作用是将JS乱码恢复到VM中1w+行正常代码,定义了一个全局变量window.$_ts并赋了很多值,这个变量在后续的VM中非常有用,meta标签的content内容在VM中也会用到。由于很多值和变量是动态变化的,肯定不利于我们的分析,所以我们需要在本地固定一套代码。打断跟栈会更方便。只需要将202页的代码和页面对应的外链js文件,比如c.FxJzG50F.dfe1675.js保存一份到本地,使用浏览器自带的覆盖重写功能,或者浏览器插件ReRes,或抓包工具(如Fiddler的AutoResponder)的响应替换功能替换。VM中的代码是生成cookie的主要代码,其中包含很多if-else控制流,这无疑增加了分析代码的成本。这里,我们可以使用AST技术来做反混淆。例如,Nanda将if-else控制流转换成了switch-case。同一个控制流下的代码放在同一个case下,然后在调用入口处,本地替换VM代码。详情可以参考南大的文章:《某数4代逻辑分析》,有兴趣的可以试试。不知道AST的可以看之前的文章《逆向进阶,利用 AST 技术还原 JavaScript 混淆代码》。以后有时间的话,K哥写一下AST还原睿数代码的实战。在这篇文章中,我们将选择努力!VM代码和$_ts变量的获取之前我们已经了解了VM代码和$_ts的重要性,所以我们的第一步就是想办法获取它们。至于什么时候有用,文章后面再说,复制外链JS,即c.FxJzG50F.dfe1675.js的代码和202页的自执行代码上传到文件即可,并且可以直接在本地运行。环境需要稍微补上,缺的补上。大致补上窗口、位置、文档即可。具体内容可以直接在浏览器控制台中使用copy()命令进??行复制,然后我们就可以通过Hookeval直接获取VM代码。一般补充环境代码如下:vareval_js=""window={$_ts:{},eval:function(data){eval_js=data}}location={"ancestorOrigins":{},"href":"http://www.desensitization.com.cn/new_house/new_house_detail.html","来源":"http://www.desensitization.com.cn","协议":"http:","host":"www.desensitization.com.cn","hostname":"www.takeoffminprocessing.com.cn","port":"","pathname":"/new_house/new_house_detail.html","search":"","hash":""}document={"scripts":["script","script"]}观察$_ts的key和value,和浏览器中获取的是一样的:注:c.FxJzG50F.dfe1675.js外链JS如果直接下载,用an打开editor会自动编码,和原来的数据不一样,导致运行时出错,这里建议直接在浏览器在线访问这个文件,手动复制,或者复制响应中的内容抓包软件,观察以下两种情况,第一种情况可能导致运行出错,第二种情况正常:推演代码之前说了这么多,现在终于可以进入正题了,推演代码,找张好椅子,准备坐屁股,这时候你键盘有用的只有F11,连续单步调试,只有一个需要一些细节,工作结束了!推导代码的步骤太多,不可能每一步都写截图,只写比较重要的,万一有遗漏,那也没办法。首先,在我们替换的202页面中,在执行代码的开头手动添加调试器,一进入页面就断掉,方便后续分析:通过我们前面的分析,我们已经知道入口是callplace,赶紧搜索下断点:通过我们前面的分析,我们也知道有两个地方会产生cookie。快速搜索(5),第二个搜索结果为入口:假cookie生成逻辑先单步跟随假cookie,虽然是假的,但会在后续生成真cookie时用到。following的时候会走到这样的逻辑:一步会调用_$8e()方法,然后_$8e=_$Q9,_$Q9嵌套在_$d0中,查找调用_$d0的地方,然后发现是代码的开头:那么传入的参数_$Wn是什么?单步后续是一个方法,功能是取202页面的content内容,那我们直接在本地删除_$Wn方法,直接传给content的值就够了,如下图所示:另外我们发现代码中有很多情况是通过数组中的index取值,比如中_$PV[68]的值上图,其实是一个字符串内容,显然我们要找到这个数组的来源,直接搜索_$PV=,就可以找到疑似定义和赋值:所以我们要看这个_$iL方法,传入了一个很长的字符串,闯入来看一看。果然生成了_$PV,它是一个725位的数组:接下来,在推导代码的过程中,你会经常遇到一个变量,就是本文中的_$sX:你熟悉吗??这个值就是我们之前得到的$_ts变量。一开始我们可以看到window.$_ts被赋值给了_$sX:继续往下走,会得出如下逻辑:这里会有六个数组,它们已经有了值,所以我们要找出他们来自哪里。搜索任意一个数组名,都会找到定义和赋值:赋值明明是调用了_$rv方法,然后搜索_$rv方法,发现是在开头调用的:有后续没什么特别的,一直单步执行,最后有个join('')操作,生成假cookie:接下来就是生成cookie的名称FSSBBIl1UgzbN7N80T,然后将cookie赋值给document.cookie,然后给localStorage中的$_ck赋值,直接复制localStorage的内容,影响不大真正的cookie生成逻辑是一步到位真正的cookie,就是_$ZN(768,1);在本文中。可以看出进入了一个无穷无尽的if-else控制流程:本地进程在这里应该怎么处理?我的方法是使用_$Hn及其值来命名函数。function_$Hn768(){}表示所有使用768号控制流的方法,继续往下看。生成真实cookie的方法基本在747号控制流中。后续我们主要看747号控制流的步骤。从747号控制流中推导出来的代码大致如下:取一个fakecookie,按照747号控制流,会有一个进入709号控制流的步骤,会把之前生成的fakecookie经过一系列操作后返回一个数组:至此我们已经在本地同步了扣减代码.如果正常,返回的数组应该是一样的(后面的数据会不一样,有一些时间戳之类的参数参与计算):自动化工具检测继续按照747号控制流程,会进入268号控制流,再进入154号控制流,会为自动化工具做一些检测,如下图:这里,一个变量_$iL,如果检测失败,则为1,然后把这个变量赋值给_$aW,所以我们在本地保持不变,可以为false(其实如果不使用自动化工具,这部分不需要直接返回falsetest.):20位核心数组继续走268号控制流程,将进入668号控制流程。668号控制流有两个操作,一个是生成一个16位的数组,一个是在$_ts中取4个变量,加上前16位后,组成一个20位的数组。20位数组的最后4位是睿数核心。如果映射关系错误,则请求不会通过。到了第五代,这部分的处理逻辑会更加复杂。这不是简单的取$_ts中的键值对。扣代码的时候可能会发现,为什么在本地取值的时候,取出来的不是数字,而是字符串?就像下面这种情况:其实我们一开始拿到的$_ts值已经处理了两次。我们以第一个_$sX._$Xb为例,直接搜索_$sX._$Xb,可以找到这样一个地方:很明显这里_$sX._$Xb被重新赋值了,我们可以看到等号右边,_$sX._$Xb被取了一次,它的值为_$Rm。和我们初始的$_ts里面对应的值是一样的,然后我们要看_$sX["_$Rm"]在哪。直接搜索发现一开始赋值了一个方法,通过调用这个方法生成一个新的值:其他三个值都是同一个套路,赋值的代码是:_$sX._$Xb=_$sX[_$sX._$Xb](_$BH,_$DP);_$sX._$oI=_$sX[_$sX._$oI](_$ZJ,_$DS)_$sX._$EN=_$sX[_$sX._$EN]();_$sX._$D9=_$sX[_$sX._$D9](_$iL);实际上应该是:_$sX._$Xb=_$sX["_$Rm"](_$BH,_$DP);_$sX._$oI=_$sX["_$Nw"](_$ZJ,_$DS)_$sX._$EN=_$sX["_$Uh"]();_$sX._$D9=_$sX["_$ci"](_$iL);更进一步,它实际上是:_$sX._$Xb=_$1k(_$BH,_$DP);_$sX._$oI=_$jH(_$ZJ,_$DS)_$sX._$EN=_$9M();_$sX._$D9=_$oL(_$iL);静态分析是没有问题的,我们可以先修复它,但是这些值在实际应用中是动态的,那我们应该怎么处理呢?先多看几张对比一下,发现其中的规律:可以发现每次对应的位置都不一样,但其实同一个位置点进去的方法是一样的,也就是说只有方法名和变量名改变。实现的逻辑不变,所以我们只要知道这四个值对应的位置,就可以得到正确的值。在本地,我们可以这样做:1.首先使用正则模式匹配四个值,如:[_$sX._$Xb,_$sX._$oI,_$sX._$EN,_$sX._$D9];2、然后匹配VM代码开头的20条赋值语句,如:_$sX._$RH=_$wI;_$sX._$i5=_$n5;ETC。;3.然后用$_ts得到这四个值对应的值,相当于:_$sX._$Xb=_$ts._$Xb=_$Rm;然后找到这4个值定义的方法在20条赋值语句中的位置,相当于:find_$sX._$Rm=_$1k;20条赋值语句中的位置为7(索引从0开始)4、我们知道这4个方法在20条赋值语句中的位置,那么我们可以直接匹配对应的局部位置的名称,进行动态替换。当然,前提是我们在本地扣除了一组代码出来:经过这样的处理,可以保证这四个值的准确性。除了上面说的20位数组用到$_ts的4个值外,还有其他地方用到$_ts的值。还有7个值也是用到的,直接搜索就可以定位到。这7个值比较简单,每次取$_ts中第2、3、4、15、16、17、19位的值。同理,找到对应的位置,进行动态替换:Notes特别注意VM代码的开头,会直接调用执行一些方法。一些变量的值就是通过这些方法生成的。当你一步步follow的时候,你会发现一些参数不正确或者没有,那么你一开始就得注意这些方法,可能一开始就生成了。后缀MmEwMD生成逻辑后续其他XHR请求都有后缀。这个后缀的值也是JS生成的,每次都会变。当然,不同网站的后缀名不一定相同。本例为MmEwMD,先设置一个XHR断点,当XHR请求包含MmEwMD=时断点,然后刷新网页:可以看到传给l.open()的URL还是正常的,然后去l.sendafterbreak()有一个后缀。看l.open()其实就是xhr.open(),明显和正常的不一样。这个方法也在VM代码中。应该是重写的方法,可以和普通的比较一下。对比一下:按照VM代码看,在_$sd(arguments[1])方法之后,变成了一个带后缀的完整链接:跟在_$sd方法之后,在Processing之前做一些url,有一个进入779号控制流程的流程,其实就是最原始的生成cookie的步骤,照着做就行了。善用Watch跟踪功能开发者工具的Watch功能可以持续跟踪某个变量的值。对于这种控制流比较多的情况,设置相应的变量跟踪可以让你知道你现在在哪个控制流中,以及数组中产生的变化,让你不跟随不知道自己在哪里要去。结果验证,如果整个过程没问题,正确扣code,并且携带正确的cookie和suffix,就可以访问成功:
