python-js逆向破解12306网站并获取cookie字段RAIL_DEVICEID

python模拟登录12306时必须要有的一个cookie参数就是RAIL_DEVICEID,否则是登录不成功的,那么这个RAIL_DEVICEID是从哪里来的呢?下面我们来一步步分析。

1、RAIL_DEVICEID如何获取

首先可以用chrome浏览器打开12306网站,先清空浏览器的缓存,打开开发者工具,然后刷新,找到RAIL_DEVICEID的值,再全局搜索,可以发现这个值是服务器返回的,所以我们应该分析ajax请求的url是如何生成的。

2、定位ajax请求在JS的位置

在知道这个ajax请求的url之后,可以全局搜索url中的关键字段,例如HttpZF/logdevice这部分(需一点点尝试),此时可以发现在GetJS这个文件中找到了这部分url字段,那么我们就已经定位到js生成url的位置了。

3、分析url请求链接的拼接字段

接下来,我们来分析这个url是如何拼接成的,从js中扒下来的url拼接代码是:

1
"https://kyfw.12306.cn/otn/HttpZF/logdevice" + ("?algID\x3dBikJbiWz0u\x26hashCode\x3d" + e + a)

这里总共是四个部分,第一部分是固定不变的,第二部分是每次请求获取GetJS文件时服务器返回的,也就是会变的,可以从GetJS文件中提取(其中\x3d是”=”,\x26是”&”),第三部分和第四部分都是js生成的,可以通过断点调试找到它们的生成过程。

4、url中e和a如何生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (var a = "", e = "", g = c.getpackStr(b), k = [], q = [], t = [], l = [], p = 0; p < g.length; p++)
"new" != g[p].value && -1 == Eb.indexOf(g[p].key) && (-1 != Bb.indexOf(g[p].key) ? q.push(g[p]) : -1 != Db.indexOf(g[p].key) ? t.push(g[p]) : -1 != Cb.indexOf(g[p].key) ? l.push(g[p]) : k.push(g[p]));
g = "";
for (p = 0; p < q.length; p++)
g = g + q[p].key.charAt(0) + q[p].value;
q = "";
for (p = 0; p < l.length; p++)
q = 0 == p ? q + l[p].value : q + "x" + l[p].value;
l = "";
for (p = 0; p < t.length; p++)
l = 0 == p ? l + t[p].value : l + "x" + t[p].value;
k.push(new n("storeDb",g));
k.push(new n("srcScreenSize",q));
k.push(new n("scrAvailSize",l));
"" != d && k.push(new n("localCode",pb(d)));
e = c.hashAlg(k, a, e);
a = e.key;
e = e.value;
a += "\x26timestamp\x3d" + (new Date).getTime();
$a.getJSON("https://kyfw.12306.cn/otn/HttpZF/logdevice" + ("?algID\x3dBikJbiWz0u\x26hashCode\x3d" + e + a), null, function(a) {

上面是生成e和a的js代码,其实就在url拼接的上面,这里可以看到,ea分别对应另一个变量的valuekey,而这个变量是调用c.hashAlg方法生成,传入了k,a,e这三个值,通过断点或者观察代码,可以发现其实ae都是空字符串,所以只要得到k的值即可,这里我们可以将这一段代码扒下来,封装成一个函数,把最终的url赋值给一个变量,然后返回,用python执行这段代码,就可以得到url了。但是,这里有几个外部变量和外部函数,我们要如何去拿到它们呢?

5、分析外部变量和外部函数

在封装成函数后,这里需要得到的是c.getpackStrc.hashAlg以及n,pb这四个函数和d,Eb,Bb,Db,Cb这五个变量,我们来一一分析它们,先分析n,pb这两个函数,可以通过断点调试找到它们,只要把它们拿出来添加到js代码中即可,可以直接调用,这里不多赘述;d是在前面赋值的,调试后可发现它其实是本地局域网的ip地址,可以写死;Eb,Bb,Db,Cb是不变的,同样可以写死;而c.getpackStr通过断点调试,可以发现它最终返回的是一个数组,其中每个元素都是通过n函数得到的一个实例对象,而且这些都是浏览器的相关信息,因此我们可以猜测它是不变,返回值赋值给g变量,此时我们可以通过断点拿到g的值,然后在代码中写死这个g变量。
注:这里g变量中有一个cookieCode的值是一段字符串,这其实是个坑,这个值就是在第一步中服务器返回的,有可能你看了发现并没有,那是因为只要g变量中有这个值,那么服务器就不返回该值,所以这个在最开始是并没有值的,此时返回的内容就会有这个值,因此我们可以直接将这个值删除。

6、调用hashAlg函数执行加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
hashAlg: function(a, b, c) {
a.sort(function(a, b) {
var c, d;
if ("object" === typeof a && "object" === typeof b && a && b)
return c = a.key,
d = b.key,
c === d ? 0 : typeof c === typeof d ? c < d ? -1 : 1 : typeof c < typeof d ? -1 : 1;
throw "error";
});
for (var d = 0; d < a.length; d++) {
var e = a[d].key.replace(RegExp("%", "gm"), "")
, f = ""
, f = "string" == typeof a[d].value ? a[d].value.replace(RegExp("%", "gm"), "") : a[d].value;
"" !== f && (c += e + f,
b += "\x26" + (void 0 == gb[e] ? e : gb[e]) + "\x3d" + f)
}
a = R.SHA256(c).toString(R.enc.Base64);
c = a.length;
d = a.split("");
for (e = 0; e < parseInt(c / 2); e++)
0 == e % 2 && (f = a.charAt(e),
d[e] = d[c - 1 - e],
d[c - 1 - e] = f);
a = d.join("");
c = a.length;
d = "";
d = 0 == a.length % 2 ? a.substring(c / 2, c) + a.substring(0, c / 2) : a.substring(c / 2 + 1, c) + a.charAt(c / 2) + a.substring(0, c / 2);
a = Qa(d);
c = Qa(a);
c = R.SHA256(c).toString(R.enc.Base64);
return new n(b,c)
},

c.hashAlg才是js加密的重头戏,上面是通过断点调试得到的hashAlg的函数,这里就是生成url加密部分的代码,我们同样把它扒下来封装成函数添加到刚刚的js代码中,老办法,找外部函数或变量,这里传进来的就是第4步中的k,a,e,只要在js代码中调用这个hashAlg即可,然后就是R.SHA256,R.enc.Base64以及gb,Qa,n,gb也是一个固定不变的字典,所以可以写死,Qa可以直接扒下来添加到代码中调用,n在第5步已经添加了;而R.SHA256R.enc.Base64是两个加密算法,可以下载CryptoJS,然后将这两个算法的代码复制粘贴到我们的js代码中,将这里R替换成CryptoJS即可,这里提供CryptoJS文件的下载地址:
https://code.google.com/archive/p/crypto-js/downloads
注:需要科学上网

7、如何处理返回的动态JS加密算法

这里基本上我们的js代码已经处理完成了,可以直接使用pythonexecjs执行js语句,调用函数,获得url,再使用url去获取RAIL_DEVICEID的值,但是,这里有一个问题,其它的js代码都是不变,而在第6步中的hashAlg函数中,其中的加密算法是会变的,在for循环结束后到return之间的内容是变化,而且Qa也会随之变化,那么如何根据这个变化来随时变换js代码,我是通过搜索,定位,抠取代码,然后拼接来解决这个问题,但是这里由于某些原因,不便将具体思路分享出来。