前言
最近学Android逆向学到so层了,汇编真的是看的脑壳疼。感觉也好久没做js逆向了,基本上就是大的做不来,小的太简单。。想起之前听人提到过12306的设备码,就打开网站找了找,也算是巩固一下JS调试方面的知识。
RAIL_DEVICEID是12306网站Cookie中的重要部分,也是用户进行操作的重要凭证,包括了一系列的浏览器信息及验证等。开发者为应对不同浏览器的差异,在Cookies、Web SQL、evercookie、session、localStorage中都存储了REIL_DEVICEID。今天我们就来分析下这个参数的生成过程。
其实12306除了RAIL_DEVICEID以外,还有RAIL_OkLJUJ,也是cookie中的重要内容,并且在不更新浏览器以及电脑环境配置的情况下,其值不会改变。换句话说,这个RAIL_OkLJUJ才是唯一标识设备的信息。个人认为RAIL_OkLJUJ的优先级是要比RAIL_DEVICEID高的,当然,进行用户操作的时候哪个是决定性的参数不是我们今天的重点,买票时信息的加密等到以后有机会再看吧。
逆向分析过程
首先用chrome浏览器打开12306,打开F12开发者工具,多次刷新页面,然后查找Application中的cookie,拿到RAIL_DEVICEID:
切换到Network,搜索cookie,你会惊奇的发现。。
除了图片和一些JS的加载,什么都没有,这串代码也只是作为cookie与服务器交互。那是当然的啦,因为我之前打开过12306,本地cookie已经有DEVICEID的信息,不需要向服务器重新请求,此时需要清空网页的Cookie。
点击清除,全部删掉。然后重新刷新网页,再重复上一步的步骤:Application拿DEVICEID,复制网络请求搜索后:
可以看到这个请求,我们需要的DEVICEID就在这个请求的响应结果中,也就是DEVICEID通过请求返回了一个jsonp,前端JS再把deviceid解析出来赋值到cookie中:
看下参数,有很多,不太方便看,把所有参数复制出来。开启一个无痕模式的浏览器,打开12306抓这个请求,再把参数复制出来,把两个请求的参数对比一下。
一样的参数我们可以先不管,把注意点关注在不同的参数上。timestamp是时间戳,也不看。那么有algID、hashCode、VySQ,看似是三个加密参数。再对比响应内容:
好像发现了点什么。这个cookieCode和VySQ是不是一模一样。第一次请求中我只删除了cookie中的RAIL_DEVICEID,并没有动过其他参数,在请求中就传入了VySQ,响应中也没有返回cookieCode,也就是VySQ的值。第二次因为打开了一个无痕窗口,是没有任何cookie,在请求参数里没有再传这个值,反而在响应中出现了这个值。那么按这个思路找,果然,在session、local storage、以及web sqlite中均找到了同样的值。key是RAIL_OkLJUJ。这也是我说RAIL_OkLJUJ的优先级应当会大于RAIL_DEVICEID的原因。RAIL_OkLJUJ才是唯一标识一个设备信息的值。
下面步入正题,对RAIL_DEVICEID的生成进行分析。先全局搜索url看看,也就是logdevice,发现只有一处,那么直接定位到这里的代码。
找到initEC方法,可以看到调用了方法请求logdevice接口,拼接了参数:algID\x3dQv19wsqY3f\x26hashCode\x3d,“\x3d”与“\x26”都是utf-8编码,解码以后分别对应着“=”和“&”,此处是对参数进行了拼接。那么拿到了algID,值为Qv19wsqY3f。拿着这个id和上面参数中右边的参数图对比,证实拿到的algID是对的。但是这个algID是服务器随机返回的,下次请求的时候就不一定是这个值了,后面还原的时候需要注意。不管怎样,第一个参数已经得到。
按上图的位置打两个断点,清除所有cookie,刷新页面进入断点调试。一定要清除cookie后再进入断点调试,否则是不会断在这里的。
可以看到几个局部变量,对应的value值貌似都在请求的参数中。上面代码中向列表k 中push了这么多东西,看来k就是参数数组了。控制台打印看看:
这时的参数列表还没有被混淆,key就对应着是哪些值,大概能猜到是什么内容。
按下F8继续运行,程序走过c.hashAlg方法,在右边的调试窗口中点开Scope,可以查看执行到此处的所有局部变量,发现此时的变量 e 已经有值:
上下对比不难看出,变量 e ,就是参数中的 hashCode ,变量 a 就是将列表 k 混淆后并序列化的字符串。hashAlg是一个加密函数,进行了一系列的加密操作,得到混淆的字符串 a与加密的结果e。
先不看这个hashAlg,这个函数是在整个DEVICEID生成过程中最复杂的一个,经过一大坨的赋值、调用、回调。我们先找找k的生成,点开Call Stack,查看整个过程的方法堆栈调用,这里按时间倒序展示了所有用到的函数调用,对于12306这种复杂混合调用的网站,要比正常的断点及静态分析省力很多。
第一行也就是我们刚刚打断点的位置,已经分析过了。点击到第二行,对照着代码跳到的位置以及local中保存的临时参数值分析:
调用了getDfpMoreInfo方法,ja.prototype={}使用了原型链向ja添加方法。这里值得注意的是数组 d :
通过for 和 一轮的switch-case调用N个方法得到的一个新的参数列表。和上面的 k 数组还是有一些差异的。
同时方法结束执行了方法 a ,a是什么?这里做了一个判断,如果a是一个function,那么就执行a,a 又是传过来的参数,那就是回调了。也就是initEC中的这个部分:
再看第三行的调用:
get方法。也就是图19中getDfpMoreInfo的b.cfp.get(),与getDfpMoreInfo类似,这里返回的 a(f, b) 依然是回调函数。同时在回调中传了两个值,经过 x64hash128 处理后的加密参数 f ,和调用一些获取本机信息方法得到的数组 b,分别对应着getDfpMoreInfo中的形参c和形参 d。
到这里可能有些同学会问,c不是"js_fonts"吗,怎么和上面得到的f不一样?这是一个调试的问题,因为在我们执行到getDfpMoreInfo中的时候,c已经被重新赋值了,并不是我们最开始传进来的那个被加密处理的参数c,上面显示的实际上是整个方法调用完c的最终值。如果打断点一步一步调试,会观察出c值的变化。
x64hash128,又是一个加密算法,对字体信息进行了加密,也是调用了很多的方法。这个也略过,有耐心的同学可以打断点对照着还原,这个不是很复杂。
再看第四行的调用:
看样子是对字体的一些处理,也不卖关子了,如果尝试直接抓12306的查询车票,会发现是乱码,这个方法就是与字体加密相关。查看这一步的scope,好像并没有我们感兴趣的东西:
调用栈中的内容就是这些。大体的轮廓已经有了,对每一步的内容还需要细化。
正向还原调用过程
逆向分析完了,接下来理一下正向的调用过程,将异步转为同步。
回到initEc,一直往上翻,先看看他是谁的孩子,这么嚣张!
原来是ja,和getDfpMoreInfo同属一家。全局搜索function ja,看看ja是什么东西。找到下面这个部分:
再在文件中搜索ja,找找谁调用了他。追踪到了Qa,这里我单步调试过,正常情况H("RAIL_EXPIRATION")是会返回当前时间戳,I方法其实就是setTimeout,他的第二个参数是一个数字,代表延迟的执行时间。也就是无论如何最终都会执行到ja的getFingerPrint()方法:
顺着这个思路找:
这部分已经比较清晰了,在所有DOM加载完成时调用Qa,Qa再调用ja的getFingerPrint方法,getFingerPrint调用了initEc
接下来我们可以回到initEc中,多打几个断点一步一步分析每一个参数的来源。
前面也说了2853行的function是一个异步回调,这步真正运行的是this.ec.get,F11单步进入
继续进入
试图从本地资源中取RAIL_OkLJUJ。
可以按F9查看每一步的取值过程。这里就略过了,如果清Cookie时连带着RAIL_OkLJUJ一起清掉,这里就不会有返回值。我这边没有清掉RAIL_OkLJUJ,所以执行到这里取到了值。
执行到2854行,将取得的RAIL_OkLJUJ赋值给b。然后调用getDfpMoreInfo方法,也就是图23中get()方法获取这些参数的过程。
然后是对数组l的赋值,l是通过执行c.getpackStr(b)而来的,找一下这个方法做了什么:
调用了getMachineCode方法,直译过来:获取机器码,到底是不是这样呢,看一下就知道:
getMachineCode: function() {
return [this.getUUID(), this.getCookieCode(), this.getUserAgent(), this.getScrHeight(), this.getScrWidth(), this.getScrAvailHeight(), this.getScrAvailWidth(), this.md5ScrColorDepth(), this.getScrDeviceXDPI(), this.getAppCodeName(), this.getAppName(), this.getJavaEnabled(), this.getMimeTypes(), this.getPlatform(), this.getAppMinorVersion(), this.getBrowserLanguage(), this.getCookieEnabled(), this.getCpuClass(), this.getOnLine(), this.getSystemLanguage(), this.getUserLanguage(), this.getTimeZone(), this.getFlashVersion(), this.getHistoryList(), this.getCustId(), this.getSendPlatform()
] },
这个方法很长,截图放不下,直接复制代码过来了。调用了许多方法,获取了与机器配置、浏览器有关的一系列信息。再与moreInfoArray拼接之后,返回给了l。这里面又提到了cookieCode,之前我们也证实过,cookieCode就是RAIL_OklJUJ。
最后合成一个整个的参数列表f执行到上面的2857行后,把所有参数都压进l中:
还是在这层循环中,对l进行了筛选、重排序,最后把结果赋给了k。
执行完循环得到的 k,经过这一步,把长度为41的 l ,变成了长度为17的 k :
图40中代码2859行到2871行的地方,获取了浏览器窗口大小及分辨率的信息,并添加到数组 k 中。经过这些调用,获得了最终的 k。接下来就是调用hashAlg算出散列值,再拼接成完整url的过程了。
再提一点就是对key值的混淆处理,对应代码在hashAlg中,\x26和\x3d,上面说过的,还记得吗,\x26是"&",\x3d是"=",很明显又是在拼接了:
全局搜索ib = ,找到以下定义:
我们重新整理一下同步的调用过程:
页面全部加载->执行Qa->执行ja.getFingerPrint->执行ja.initEc->执行ja.get获取浏览器基本信息->执行getDfpMoreInfo获取更多信息->调用getpackStr获取机器码->重排列、加密、组合->发送请求
至此,所有部分全理通了,剩下的就是把这些零散的方法读懂、分离、复现、组合。其实12306的这些部分都没有太难的地方,相信静下心来调试,大家都能够还原每一步的代码。整体过程中还有很多小的获取浏览器参数的方法没有讲,不需要也没必要。最混乱的一层是在hashAlg中,这一层还能再往上追踪出很多层,分析步骤与前面类似。
我是使用了node进行了代码的复现与测试,用https模块发起请求。因为都是用js,所以不需要把每一句代码都严格的抠出来,这也是node调试js的优势。需要注意的一点,algID一定要先去请求Get.js,用正则把algID匹配出来,再拼到url中,这个algID并不是写死的!最后附上一个运行结果:
总结:
12306的设备码总体难度还好,一长串的调用与回调把整个过程的逻辑复杂化,很容易让人没有耐心继续找下去。混淆也只是混淆了变量,很多重要的方法名都没有做混淆,也没有对网站进行反调试处理,很多方法看见名字就能猜到他做了些什么了。总结的话,还是要细心吧,看不懂的地方就多打几个断点运行看看,浏览器开发者工具提供的Call Stack、Scope、Watch三个窗口,是调试这类网站的一大利器,要比单纯的静态分析和一步一步的断点调试方便的多。以前在我找的一些js加密中,很少有需要用到这些,甚至自己都快忘了有这几个东西,果然人还是要“Stay hungry”啊。
把整个过程理清以后,回头发现参数的生成真的没有那么难,异步都弄清了,还原成同步调用的方式相信大家都没什么问题。
本人能力有限,研究过程中难免会对过程中的某些细节造成疏漏,如果有一些关键点是我没有注意到的,还望各位看官老爷们能提出来帮助我改正。感谢这些强大的调试工具,能够让我这个三流大学出身的渣渣能得以一窥顶级工程师的风采。
最后的最后:
声明:本文分析过程仅供学习,并无任何个人以及商业或其他用途。如有不慎侵权,请联系我删除。