直奔重点

高楼大厦寻关键线索

Js文件中关于网络请求最典型的就是异步回调,将原本简单的操作复杂化,非要你等我,我等他,他还等着他的她.

最终直接结果就是整个请求流程反过来了,假设正常流程:是 A->B->C-D-E-F,那么异步请求很可能陷入这样的陷阱: F <- E <- D <- C <- B <- A

所以一层又一层的回调函数真的是难以维护,这种技术也在慢慢淘汰更新成更容易维护的方式,还是不再展开了,回到正题上来,还是先找到程序到底什么时候开始调用的吧!

ja.prototype = {initEc: function(a) {var b = "", c = this, d = void 0 != a && void 0 != a.localAddr ? a.localAddr : "";c.checkWapOrWeb();this.ec.get("RAIL_OkLJUJ", function(a) {b = a;c.getDfpMoreInfo(function() {if (!(9E5 < F("RAIL_EXPIRATION") - (new Date).getTime() & null != F("RAIL_DEVICEID") & void 0 != F("RAIL_DEVICEID") & !c.NeedUpdate())) {for (var a = "", e = "", g = c.getpackStr(b), m = [], q = [], t = [], k = [], n = 0; n < g.length; n++)"new" != g[n].value && -1 == Fb.indexOf(g[n].key) && (-1 != Gb.indexOf(g[n].key) ? q.push(g[n]) : -1 != Ib.indexOf(g[n].key) ? t.push(g[n]) : -1 != Hb.indexOf(g[n].key) ? k.push(g[n]) : m.push(g[n]));g = "";for (n = 0; n < q.length; n++)g = g + q[n].key.charAt(0) + q[n].value;q = "";for (n = 0; n < k.length; n++)q = 0 == n ? q + k[n].value : q + "x" + k[n].value;k = "";for (n = 0; n < t.length; n++)k = 0 == n ? k + t[n].value : k + "x" + t[n].value;m.push(new l("storeDb",g));m.push(new l("srcScreenSize",q));m.push(new l("scrAvailSize",k));"" != d && m.push(new l("localCode",pb(d)));e = c.hashAlg(m, a, e);a = e.key;e = e.value;a += "\x26timestamp\x3d" + (new Date).getTime();$a.getJSON("https://kyfw.12306.cn/otn/HttpZF/logdevice" + ("?algID\x3drblubbXDx3\x26hashCode\x3d" + e + a), null, function(a) {var b = JSON.parse(a);void 0 != lb && lb.postMessage(a, r.parent);for (var d in b)"dfp" == d ? F("RAIL_DEVICEID") != b[d] && (W("RAIL_DEVICEID", b[d], 1E3),c.deviceEc.set("RAIL_DEVICEID", b[d])) : "exp" == d ? W("RAIL_EXPIRATION", b[d], 1E3) : "cookieCode" == d && (c.ec.set("RAIL_OkLJUJ", b[d]),W("RAIL_OkLJUJ", "", 0))})}})}, 1)}
}

核心代码最外层函数是 initEc 函数,而该函数的写法明显是传统 js 的属性方法,因此判断挂载于该对象的属性方法应该都是完成某些相同的功能.

暂时先不着急继续寻找谁在调用 initEc 函数,先搞懂整个函数结构是什么轮廓.

function ja() {this.ec = new evercookie;this.deviceEc = new evercookie;this.cfp = new aa;this.packageString = "";this.moreInfoArray = []
}ja.prototype = {getScrWidth: function() {return new l("scrWidth",r.screen.width.toString())},...,checkWapOrWeb: function() {return "WindowsPhone" == Ha() || "iOS" == Ha() || "Android" == Ha() ? !0 : !1}
}

如果熟悉 web 开发,那么你一定不难发现这是标准的面向对象的写法,ja 函数作为构造函数内置了一大堆成员变量,并且在原型链上继承了一大堆方法.

更何况,对象属性中还有三个带有 new 关键字的构造函数,估计也是类似于 ja 这种设计思路,高楼大厦平地起,还原相关算法之路预期并不简单!

但是想一想车票真难抢还动不动访问错误,是可忍孰不可忍,还是要研究算法一劳永逸搞定 RAIL_DEVICEID 的生成逻辑,自己用算法计算实现完美伪装浏览器!

现在以 initEc 函数名继续搜素,寻找到底是谁在调用,轻而易举又找到了新的函数名: getFingerPrint

ja.prototype = {getFingerPrint: function() {var a = this;r.RTCPeerConnection || r.webkitRTCPeerConnection || r.mozRTCPeerConnection ? nb(function(b) {a.initEc(b)}) : a.initEc()}
}

同样地,不再过多停留,继续以 getFingerPrint 为关键字搜索,找到了 Pa 函数,终于不再是 ja 的方法了.

function Pa() {if (-1 == F("RAIL_EXPIRATION"))for (var a = 0; 10 > a; a++)G(function() {(new ja).getFingerPrint()}, 20 + 2E3 * Math.pow(a, 2));else(new ja).getFingerPrint();G(function() {r.setInterval(function() {(new ja).getFingerPrint()}, 3E5)}, 3E5)
}

与此同时,Pa 函数也是 js 文件的第一行代码,来都来了,那就顺便看一眼 js 的整体结构代码吧!

(function() {})();

自执行的匿名函数实现的闭包,这样的好处在于函数内的变量不会污染其他文件,更何况混淆之后的变量名称充斥着大量的变量 a,b,c,d,e,f之类的,不用闭包也不行啊!

现在继续以 Pa 为线索搜索,最终发现了函数入口,除此之外再无其他.

var mb = !1;
u.addEventListener ? u.addEventListener("DOMContentLoaded", function b() {u.removeEventListener("DOMContentLoaded", b, !1);Pa()
}, !1) : u.attachEvent && u.attachEvent("onreadystatechange", function c() {mb || "interactive" != u.readyState && "complete" != u.readyState || (u.detachEvent("onreadystatechange", c),Pa(),mb = !0)
})

js 是典型的事件驱动型编程语言,当发生什么什么事件后我要干这个,页面加载时我要开始工作了,按钮被点击了我要登录了,页面关闭时我要下班了等等诸如此类的逻辑.

上述代码实现的就是页面元素加载成功后开始执行 Pa() 函数,而 Pa 函数又会执行 (new ja).getFingerPrint() ,紧接着又会执行 initEc 函数.

现在基本流程已经大致清楚了,总结一下基本代码逻辑如下:

(function() {var mb = !1;u.addEventListener ? u.addEventListener("DOMContentLoaded", function b() {u.removeEventListener("DOMContentLoaded", b, !1);Pa()}, !1) : u.attachEvent && u.attachEvent("onreadystatechange", function c() {mb || "interactive" != u.readyState && "complete" != u.readyState || (u.detachEvent("onreadystatechange", c),Pa(),mb = !0)})function Pa() {if (-1 == F("RAIL_EXPIRATION"))for (var a = 0; 10 > a; a++)G(function() {(new ja).getFingerPrint()}, 20 + 2E3 * Math.pow(a, 2));else(new ja).getFingerPrint();G(function() {r.setInterval(function() {(new ja).getFingerPrint()}, 3E5)}, 3E5)}function ja() {this.ec = new evercookie;this.deviceEc = new evercookie;this.cfp = new aa;this.packageString = "";this.moreInfoArray = []}ja.prototype = {getFingerPrint: function() {var a = this;r.RTCPeerConnection || r.webkitRTCPeerConnection || r.mozRTCPeerConnection ? nb(function(b) {a.initEc(b)}) : a.initEc()},initEc: function(a) {var b = "", c = this, d = void 0 != a && void 0 != a.localAddr ? a.localAddr : "";c.checkWapOrWeb();this.ec.get("RAIL_OkLJUJ", function(a) {b = a;c.getDfpMoreInfo(function() {if (!(9E5 < F("RAIL_EXPIRATION") - (new Date).getTime() & null != F("RAIL_DEVICEID") & void 0 != F("RAIL_DEVICEID") & !c.NeedUpdate())) {for (var a = "", e = "", g = c.getpackStr(b), m = [], q = [], t = [], k = [], n = 0; n < g.length; n++)"new" != g[n].value && -1 == Fb.indexOf(g[n].key) && (-1 != Gb.indexOf(g[n].key) ? q.push(g[n]) : -1 != Ib.indexOf(g[n].key) ? t.push(g[n]) : -1 != Hb.indexOf(g[n].key) ? k.push(g[n]) : m.push(g[n]));g = "";for (n = 0; n < q.length; n++)g = g + q[n].key.charAt(0) + q[n].value;q = "";for (n = 0; n < k.length; n++)q = 0 == n ? q + k[n].value : q + "x" + k[n].value;k = "";for (n = 0; n < t.length; n++)k = 0 == n ? k + t[n].value : k + "x" + t[n].value;m.push(new l("storeDb",g));m.push(new l("srcScreenSize",q));m.push(new l("scrAvailSize",k));"" != d && m.push(new l("localCode",pb(d)));e = c.hashAlg(m, a, e);a = e.key;e = e.value;a += "\x26timestamp\x3d" + (new Date).getTime();$a.getJSON("https://kyfw.12306.cn/otn/HttpZF/logdevice" + ("?algID\x3drblubbXDx3\x26hashCode\x3d" + e + a), null, function(a) {var b = JSON.parse(a);void 0 != lb && lb.postMessage(a, r.parent);for (var d in b)"dfp" == d ? F("RAIL_DEVICEID") != b[d] && (W("RAIL_DEVICEID", b[d], 1E3),c.deviceEc.set("RAIL_DEVICEID", b[d])) : "exp" == d ? W("RAIL_EXPIRATION", b[d], 1E3) : "cookieCode" == d && (c.ec.set("RAIL_OkLJUJ", b[d]),W("RAIL_OkLJUJ", "", 0))})}})}, 1)}}
})();

从以上代码分析中,相信你会发现相关逻辑应该兼容 IE 浏览器,同时设置了定时程序反复更新 cookie 值,并且还有远程 RTC 保持通信,不得不说做得还真不错,不愧是国民出行的代步工具啊!

精力有限,这里选择最简单的一种情况进行算法还原过程的研究,浏览器选择谷歌 Chrome 浏览器,这样就可以屏蔽关于 IE 的兼容性补丁处理,同时也不考虑 RTCPeerConnection 的情况,于是乎,代码逻辑简化成这样:

(function() {document.addEventListener("DOMContentLoaded", Pa,false)function Pa() {(new ja).getFingerPrint();}function ja() {this.ec = new evercookie;this.deviceEc = new evercookie;this.cfp = new aa;this.packageString = "";this.moreInfoArray = []}ja.prototype = {getFingerPrint: function() {this.initEc()},initEc: function(a) {var b = "", c = this, d = void 0 != a && void 0 != a.localAddr ? a.localAddr : "";c.checkWapOrWeb();this.ec.get("RAIL_OkLJUJ", function(a) {b = a;c.getDfpMoreInfo(function() {if (!(9E5 < F("RAIL_EXPIRATION") - (new Date).getTime() & null != F("RAIL_DEVICEID") & void 0 != F("RAIL_DEVICEID") & !c.NeedUpdate())) {for (var a = "", e = "", g = c.getpackStr(b), m = [], q = [], t = [], k = [], n = 0; n < g.length; n++)"new" != g[n].value && -1 == Fb.indexOf(g[n].key) && (-1 != Gb.indexOf(g[n].key) ? q.push(g[n]) : -1 != Ib.indexOf(g[n].key) ? t.push(g[n]) : -1 != Hb.indexOf(g[n].key) ? k.push(g[n]) : m.push(g[n]));g = "";for (n = 0; n < q.length; n++)g = g + q[n].key.charAt(0) + q[n].value;q = "";for (n = 0; n < k.length; n++)q = 0 == n ? q + k[n].value : q + "x" + k[n].value;k = "";for (n = 0; n < t.length; n++)k = 0 == n ? k + t[n].value : k + "x" + t[n].value;m.push(new l("storeDb",g));m.push(new l("srcScreenSize",q));m.push(new l("scrAvailSize",k));"" != d && m.push(new l("localCode",pb(d)));e = c.hashAlg(m, a, e);a = e.key;e = e.value;a += "\x26timestamp\x3d" + (new Date).getTime();$a.getJSON("https://kyfw.12306.cn/otn/HttpZF/logdevice" + ("?algID\x3drblubbXDx3\x26hashCode\x3d" + e + a), null, function(a) {var b = JSON.parse(a);void 0 != lb && lb.postMessage(a, r.parent);for (var d in b)"dfp" == d ? F("RAIL_DEVICEID") != b[d] && (W("RAIL_DEVICEID", b[d], 1E3),c.deviceEc.set("RAIL_DEVICEID", b[d])) : "exp" == d ? W("RAIL_EXPIRATION", b[d], 1E3) : "cookieCode" == d && (c.ec.set("RAIL_OkLJUJ", b[d]),W("RAIL_OkLJUJ", "", 0))})}})}, 1)}}
})();

所以现在问题的核心在于搞清楚 initEc 函数的数据流向,还原算法实现过程不是梦!

断点调试追踪调用栈

静态分析程序结构后开始断电调试观察一下数据流向,做到心中有数,同时为了该过程具有可重复性需要保持每一次操作环境一致.

具体而言,首先 Chrome 浏览器处于无痕模式,接着是每次试验时清空站点缓存,最后就可以愉快刷新当前网页等待进入下一轮断电调试了.

提前在关键点打入断点(鼠标左键点击行号),然后等待程序进入调试模式,稍等一会后进入断点可以一步一步看到程序运行的值,在调试区还可以监控变量的值.

当然也可以有函数调用栈的关系,这一切只是辅助手段,最关键还是要靠自己分析弄清楚函数的调用顺序流程,原则上先大后小,先整体再细节.

函数最后会发送 ajax 请求获取 cookie 并写入本地以及 cookie 中,亲测数据如下:

{"exp":"1582097104310","cookieCode":"FGH8SO9zGaWtwuld2jrurRzwmZKeXABx","dfp":"EKLLyLS1K7tqtcuZ6LEPYoUKsxmVNyrAlWNLDi3P-gA-tJMLkTxMuhsRNHEhbk7ntCFCsIpymD57I4AyfPUoWB4D_a_Fe5usS8sfJxP_OJjoun5QjAfgDBBmDLh_m2OeRVN2NnRK0-paM6dCSVKdjFGILKUOJYWT"}

一次请求完成后顺利生成了 cookie 也写入了本地缓存中,如果不清空那么进入下一次断点的流程就和这一次不一样了,所以为了可重复操作,再次断点调试时需要还原操作环境.

首次加载时变量 a 并没有值,一不小心进入下一个过程时,这一次 a 已经有值了,多次试验后搞清楚了数据流向也明白了如何还原操作环境,保持实验结果的一致性.

经过多次重复试验,先将基本数据流向还原如下:

(function() {ja.prototype = {// C:initEcinitEc: function(a) {this.ec.get("RAIL_OkLJUJ", function(a) {c.getDfpMoreInfo(function() {})}, 1)},// c.getDfpMoreInfo:AgetDfpMoreInfo: function(a) {}}// this.ec.get("RAIL_OkLJUJ":Bwindow.evercookie = window.Evercookie = function(a) {this.get = function(a, b, c) {}}
})();

异步请求 C <- B <- A 换算成实际情况是 : initEc 函数依赖于 this.ec.get("RAIL_OkLJUJ" 函数,等到 window.evercookie.get 运行完成后会调用 c.getDfpMoreInfo 函数,等到 getDfpMoreInfo 函数运行结束后会调用函数核心关键代码.

除了总的来看是各种异步请求相互回调之外,不少细节中也充斥着大量的回调函数,以 getDfpMoreInfo 函数为例,居然要收集这么多信息才开始做自己的事情!

  ja.prototype = {getDfpMoreInfo: function(a) {var b = this;this.moreInfoArray = [];b.cfp.get(function(c, d) {b.moreInfoArray.push(b.getCanvansCode(c + ""));for (var e in d) {c = d[e].key;var f = d[e].value + "";switch (c) {case "session_storage":b.moreInfoArray.push(b.getSessionStorage(f));break;case "local_storage":b.moreInfoArray.push(b.getLocalStorage(f));break;case "indexed_db":b.moreInfoArray.push(b.getIndexedDb(f));break;case "open_database":b.moreInfoArray.push(b.getOpenDatabase(f));break;case "do_not_track":b.moreInfoArray.push(b.getDoNotTrack(f));break;case "ie_plugins":b.moreInfoArray.push(b.getPlugins(f));break;case "regular_plugins":b.moreInfoArray.push(b.getPlugins());break;case "adblock":b.moreInfoArray.push(b.getAdblock(f));break;case "has_lied_languages":b.moreInfoArray.push(b.getHasLiedLanguages(f));break;case "has_lied_resolution":b.moreInfoArray.push(b.getHasLiedResolution(f));break;case "has_lied_os":b.moreInfoArray.push(b.getHasLiedOs(f));break;case "has_lied_browser":b.moreInfoArray.push(b.getHasLiedBrowser(f));break;case "touch_support":b.moreInfoArray.push(b.getTouchSupport(f));break;case "js_fonts":b.moreInfoArray.push(b.getJsFonts(f))}}"function" == typeof a && a()})}}function aa(a) {if (!(this instanceof aa))return new aa(a);this.options = this.extend(a, {detectScreenOrientation: !0,swfPath: "flash/compiled/FontList.swf",sortPluginsFor: [/palemoon/i],swfContainerId: "fingerprintjs2",userDefinedFonts: []});this.nativeForEach = Array.prototype.forEach;this.nativeMap = Array.prototype.map}aa.prototype = {get: function(a) {var b = [], b = this.userAgentKey(b), b = this.languageKey(b), b = this.colorDepthKey(b), b = this.pixelRatioKey(b), b = this.screenResolutionKey(b), b = this.availableScreenResolutionKey(b), b = this.timezoneOffsetKey(b), b = this.sessionStorageKey(b), b = this.localStorageKey(b), b = this.indexedDbKey(b), b = this.addBehaviorKey(b), b = this.openDatabaseKey(b), b = this.cpuClassKey(b), b = this.platformKey(b), b = this.doNotTrackKey(b), b = this.pluginsKey(b), b = this.canvasKey(b), b = this.webglKey(b), b = this.adBlockKey(b), b = this.hasLiedLanguagesKey(b), b = this.hasLiedResolutionKey(b), b = this.hasLiedOsKey(b), b = this.hasLiedBrowserKey(b), b = this.touchSupportKey(b), c = this;this.fontsKey(b, function(b) {var d = [];c.each(b, function(a) {var b = a.value;"undefined" !== typeof a.value.join && (b = a.value.join(";"));d.push(b)});var f = c.x64hash128(d.join("~~~"), 31);return a(f, b)})}}

getDfpMoreInfo 函数的执行过程中首先会运行 b.cfp.get 函数,通过搜索追根溯源发现是 aa.prototype.get 函数,这个函数除了获取浏览器简单信息外还涉及到字体的加密处理: var f = c.x64hash128(d.join("~~~"), 31);

然而想要继续分析 getDfpMoreInfo 函数需要先弄清楚 b.cfp.get(function(c, d) {getDfpMoreInfo: function(a) { 中的回调函数参数到底是什么,因此必须从头到尾逐一分析,这也是异步请求的陷阱.

看似请求逻辑一气呵成,真的要维护时却困难重重,想要分析 C 必须先分析 B,没想到 B 又要依赖于 A.

所以下一步的操作就是先从突破口 initEc 函数顺藤摸瓜,找到最初的函数 A 再根据断点调试看看是如何回调一步步回到 initEc 函数的,也就是将异步请求改造成同步请求的过程.

一步一步慢慢转同步

(function() {ja.prototype = {// C:initEcinitEc: function(a) {this.ec.get("RAIL_OkLJUJ", function(a) {c.getDfpMoreInfo(function() {})}, 1)},// c.getDfpMoreInfo:AgetDfpMoreInfo: function(a) {}}// this.ec.get("RAIL_OkLJUJ":Bwindow.evercookie = window.Evercookie = function(a) {this.get = function(a, b, c) {}}
})();

顺着这条路继续分析 getDfpMoreInfo 的调用过程,可以添加更多的调用细节,因此现在完善成如下代码:

其中约定标记为字母表 A,B,C的同步调用顺序,对应异步请求 C <- B <- A,深入分析其中的 A 时,依然采用 C,B,A的标记方式.

为了表现出层次性,第二层的 C,B,A 可以表示为 AC,AB,AA,以此类推.

这样最终技能通过层级调用关系形成调用树状图,最终效果大致如下:

.
├── C
│   ├── CC
│   ├── CB
│   └── CA
├── B
│   ├── BC
│   ├── BB
│   └── BA
└── A├── AC├── AB└── AA

调用栈树状图浏览异步函数调用顺序一目了然,仿照数据结构的栈结构进行设计,后进先出是最外层的 C,然后发现 C 还要依赖B,B 要依赖 A,执行完返回上一层继续执行,这个设计感觉很棒啊,为自己点赞!

(function() {ja.prototype = {// CinitEc: function(a) {this.ec.get("RAIL_OkLJUJ", function(a) {c.getDfpMoreInfo(function() {})}, 1)},// AgetDfpMoreInfo: function(a) {}}// ACaa.prototype = {get: function(a) {}}// Bwindow.evercookie = window.Evercookie = function(a) {this.get = function(a, b, c) {}}})();

然而实际分析过程中发现同级请求不总是三级 ABC的形式,这里可以根据实际情况按照这个思路自行分析研究再结合断点调试验证猜想.

  • step 1 : 获取基本信息并在获取加密字体后回调
aa.prototype = {get: function(a) {var b = [], b = this.userAgentKey(b), b = this.languageKey(b), b = this.colorDepthKey(b), b = this.pixelRatioKey(b), b = this.screenResolutionKey(b), b = this.availableScreenResolutionKey(b), b = this.timezoneOffsetKey(b), b = this.sessionStorageKey(b), b = this.localStorageKey(b), b = this.indexedDbKey(b), b = this.addBehaviorKey(b), b = this.openDatabaseKey(b), b = this.cpuClassKey(b), b = this.platformKey(b), b = this.doNotTrackKey(b), b = this.pluginsKey(b), b = this.canvasKey(b), b = this.webglKey(b), b = this.adBlockKey(b), b = this.hasLiedLanguagesKey(b), b = this.hasLiedResolutionKey(b), b = this.hasLiedOsKey(b), b = this.hasLiedBrowserKey(b), b = this.touchSupportKey(b), c = this;this.fontsKey(b, function(b) {var d = [];c.each(b, function(a) {var b = a.value;"undefined" !== typeof a.value.join && (b = a.value.join(";"));d.push(b)});var f = c.x64hash128(d.join("~~~"), 31);return a(f, b)})}}

var f = c.x64hash128(d.join("~~~"), 31); 涉及到一系列的加密操作,暂时不用管,最重要的下面这句 return a(f, b) 会执行回调函数继续下一个逻辑.

  • step 2 : 获取浏览器更多信息并在最后回调
  ja.prototype = {getDfpMoreInfo: function(a) {var b = this;this.moreInfoArray = [];b.cfp.get(function(c, d) {b.moreInfoArray.push(b.getCanvansCode(c + ""));for (var e in d) {c = d[e].key;var f = d[e].value + "";switch (c) {case "session_storage":b.moreInfoArray.push(b.getSessionStorage(f));break;case "local_storage":b.moreInfoArray.push(b.getLocalStorage(f));break;case "indexed_db":b.moreInfoArray.push(b.getIndexedDb(f));break;case "open_database":b.moreInfoArray.push(b.getOpenDatabase(f));break;case "do_not_track":b.moreInfoArray.push(b.getDoNotTrack(f));break;case "ie_plugins":b.moreInfoArray.push(b.getPlugins(f));break;case "regular_plugins":b.moreInfoArray.push(b.getPlugins());break;case "adblock":b.moreInfoArray.push(b.getAdblock(f));break;case "has_lied_languages":b.moreInfoArray.push(b.getHasLiedLanguages(f));break;case "has_lied_resolution":b.moreInfoArray.push(b.getHasLiedResolution(f));break;case "has_lied_os":b.moreInfoArray.push(b.getHasLiedOs(f));break;case "has_lied_browser":b.moreInfoArray.push(b.getHasLiedBrowser(f));break;case "touch_support":b.moreInfoArray.push(b.getTouchSupport(f));break;case "js_fonts":b.moreInfoArray.push(b.getJsFonts(f))}}"function" == typeof a && a()})}}

b.cfp.get(function(c, d) {}) 函数就是上一步的 aa.prototype.get() 函数.

  • step 3 : 打包参数前先获取浏览器机器码
ja.prototype = {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()]}}

该函数来自于 initEc 函数中 c.getDfpMoreInfo( 回调函数里的 g = c.getpackStr(b),this.getMachineCode() 心机很深,暗藏玄机.

  • step 4 : 打包参数前再组合更多信息并重新排序
ja.prototype = {getpackStr: function(a) {var b = [], b = [], b = this.getMachineCode(), b = b.concat(this.moreInfoArray);null != a && void 0 != a && "" != a && 32 == a.length && b.push(new l("cookieCode",a));b.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";});return b}}

该函数同样来自于 initEc 函数中 c.getDfpMoreInfo( 回调函数里的 g = c.getpackStr(b),值得学习研究!

  • step 5 : 重新分类浏览器信息并加密生成请求参数
ja.prototype = {initEc: function(a) {var b = "", c = this, d = void 0 != a && void 0 != a.localAddr ? a.localAddr : "";c.checkWapOrWeb();this.ec.get("RAIL_OkLJUJ", function(a) {b = a;c.getDfpMoreInfo(function() {if (!(9E5 < F("RAIL_EXPIRATION") - (new Date).getTime() & null != F("RAIL_DEVICEID") & void 0 != F("RAIL_DEVICEID") & !c.NeedUpdate())) {for (var a = "", e = "", g = c.getpackStr(b), m = [], q = [], t = [], k = [], n = 0; n < g.length; n++)"new" != g[n].value && -1 == Fb.indexOf(g[n].key) && (-1 != Gb.indexOf(g[n].key) ? q.push(g[n]) : -1 != Ib.indexOf(g[n].key) ? t.push(g[n]) : -1 != Hb.indexOf(g[n].key) ? k.push(g[n]) : m.push(g[n]));g = "";for (n = 0; n < q.length; n++)g = g + q[n].key.charAt(0) + q[n].value;q = "";for (n = 0; n < k.length; n++)q = 0 == n ? q + k[n].value : q + "x" + k[n].value;k = "";for (n = 0; n < t.length; n++)k = 0 == n ? k + t[n].value : k + "x" + t[n].value;m.push(new l("storeDb",g));m.push(new l("srcScreenSize",q));m.push(new l("scrAvailSize",k));"" != d && m.push(new l("localCode",pb(d)));e = c.hashAlg(m, a, e);a = e.key;e = e.value;a += "\x26timestamp\x3d" + (new Date).getTime();$a.getJSON("https://kyfw.12306.cn/otn/HttpZF/logdevice" + ("?algID\x3drblubbXDx3\x26hashCode\x3d" + e + a), null, function(a) {var b = JSON.parse(a);void 0 != lb && lb.postMessage(a, r.parent);for (var d in b)"dfp" == d ? F("RAIL_DEVICEID") != b[d] && (W("RAIL_DEVICEID", b[d], 1E3),c.deviceEc.set("RAIL_DEVICEID", b[d])) : "exp" == d ? W("RAIL_EXPIRATION", b[d], 1E3) : "cookieCode" == d && (c.ec.set("RAIL_OkLJUJ", b[d]),W("RAIL_OkLJUJ", "", 0))})}})}, 1)}}

e = c.hashAlg(m, a, e); 加密过程比基本信息的加密更加复杂,但是总体来说难度也不大,主要涉及到字符串反转,分段组装,哈希算法以及 base64 编码等等.

遇到瓶颈则略过细节

通读全文并结合断点调试反复验证猜想后,我们发现为了最终请求https://kyfw.12306.cn/otn/HttpZF/logdevice 时的参数可真的为呕心沥血,费心尽力啊!

总体来说,获取浏览器各种信息并且还涉及兼容 IE 浏览器而处理的各种补丁,对于原始信息较长时采用独特的加密算法进行加密处理,仅此还不够,还存在判断是否伪造浏览器参数的相关逻辑判断,真的是大写的服!

关于获取浏览器的相关信息反而很简单,只要结合如何识别是否说谎的代码一起设置就能绕过这部分逻辑,比如说常见的设置浏览器用户代码如下:

  aa.prototype = {userAgentKey: function(a) {this.options.excludeUserAgent || a.push({key: "user_agent",value: this.getUserAgent()});return a},getUserAgent: function() {var a = g.userAgent;return a = a.replace(/\&|\+|\?|\%|\#|\/|\=/g, "")}}ja.prototype = {getUserAgent: function() {var a = g.userAgent, a = a.replace(/\&|\+/g, "");return new l("userAgent",a.toString())}}

不同对象对同一个用户代理的处理逻辑不同,类似上述例子还有很多,简单到处都是陷阱,不看源码根本就不知道,看完以后你还会吐槽 12306 技术垃圾吗?

接下来的是三个加密算法,一个是最初基本信息加密,一个字体信息加密,还有一个是最终分类信息加密.

  function ba(a) {for (var b = [], c = (1 << ca) - 1, d = 0; d < a.length * ca; d += ca)b[d >> 5] |= (a.charCodeAt(d / ca) & c) << d % 32;a = a.length * ca;b[a >> 5] |= 128 << a % 32;b[(a + 64 >>> 9 << 4) + 14] = a;a = 1732584193;for (var c = -271733879, d = -1732584194, e = 271733878, f = 0; f < b.length; f += 16) {var h = a, p = c, g = d, m = e;a = D(a, c, d, e, b[f + 0], 7, -680876936);e = D(e, a, c, d, b[f + 1], 12, -389564586);d = D(d, e, a, c, b[f + 2], 17, 606105819);c = D(c, d, e, a, b[f + 3], 22, -1044525330);a = D(a, c, d, e, b[f + 4], 7, -176418897);e = D(e, a, c, d, b[f + 5], 12, 1200080426);d = D(d, e, a, c, b[f + 6], 17, -1473231341);c = D(c, d, e, a, b[f + 7], 22, -45705983);a = D(a, c, d, e, b[f + 8], 7, 1770035416);e = D(e, a, c, d, b[f + 9], 12, -1958414417);d = D(d, e, a, c, b[f + 10], 17, -42063);c = D(c, d, e, a, b[f + 11], 22, -1990404162);a = D(a, c, d, e, b[f + 12], 7, 1804603682);e = D(e, a, c, d, b[f + 13], 12, -40341101);d = D(d, e, a, c, b[f + 14], 17, -1502002290);c = D(c, d, e, a, b[f + 15], 22, 1236535329);a = C(a, c, d, e, b[f + 1], 5, -165796510);e = C(e, a, c, d, b[f + 6], 9, -1069501632);d = C(d, e, a, c, b[f + 11], 14, 643717713);c = C(c, d, e, a, b[f + 0], 20, -373897302);a = C(a, c, d, e, b[f + 5], 5, -701558691);e = C(e, a, c, d, b[f + 10], 9, 38016083);d = C(d, e, a, c, b[f + 15], 14, -660478335);c = C(c, d, e, a, b[f + 4], 20, -405537848);a = C(a, c, d, e, b[f + 9], 5, 568446438);e = C(e, a, c, d, b[f + 14], 9, -1019803690);d = C(d, e, a, c, b[f + 3], 14, -187363961);c = C(c, d, e, a, b[f + 8], 20, 1163531501);a = C(a, c, d, e, b[f + 13], 5, -1444681467);e = C(e, a, c, d, b[f + 2], 9, -51403784);d = C(d, e, a, c, b[f + 7], 14, 1735328473);c = C(c, d, e, a, b[f + 12], 20, -1926607734);a = A(c ^ d ^ e, a, c, b[f + 5], 4, -378558);e = A(a ^ c ^ d, e, a, b[f + 8], 11, -2022574463);d = A(e ^ a ^ c, d, e, b[f + 11], 16, 1839030562);c = A(d ^ e ^ a, c, d, b[f + 14], 23, -35309556);a = A(c ^ d ^ e, a, c, b[f + 1], 4, -1530992060);e = A(a ^ c ^ d, e, a, b[f + 4], 11, 1272893353);d = A(e ^ a ^ c, d, e, b[f + 7], 16, -155497632);c = A(d ^ e ^ a, c, d, b[f + 10], 23, -1094730640);a = A(c ^ d ^ e, a, c, b[f + 13], 4, 681279174);e = A(a ^ c ^ d, e, a, b[f + 0], 11, -358537222);d = A(e ^ a ^ c, d, e, b[f + 3], 16, -722521979);c = A(d ^ e ^ a, c, d, b[f + 6], 23, 76029189);a = A(c ^ d ^ e, a, c, b[f + 9], 4, -640364487);e = A(a ^ c ^ d, e, a, b[f + 12], 11, -421815835);d = A(e ^ a ^ c, d, e, b[f + 15], 16, 530742520);c = A(d ^ e ^ a, c, d, b[f + 2], 23, -995338651);a = E(a, c, d, e, b[f + 0], 6, -198630844);e = E(e, a, c, d, b[f + 7], 10, 1126891415);d = E(d, e, a, c, b[f + 14], 15, -1416354905);c = E(c, d, e, a, b[f + 5], 21, -57434055);a = E(a, c, d, e, b[f + 12], 6, 1700485571);e = E(e, a, c, d, b[f + 3], 10, -1894986606);d = E(d, e, a, c, b[f + 10], 15, -1051523);c = E(c, d, e, a, b[f + 1], 21, -2054922799);a = E(a, c, d, e, b[f + 8], 6, 1873313359);e = E(e, a, c, d, b[f + 15], 10, -30611744);d = E(d, e, a, c, b[f + 6], 15, -1560198380);c = E(c, d, e, a, b[f + 13], 21, 1309151649);a = E(a, c, d, e, b[f + 4], 6, -145523070);e = E(e, a, c, d, b[f + 11], 10, -1120210379);d = E(d, e, a, c, b[f + 2], 15, 718787259);c = E(c, d, e, a, b[f + 9], 21, -343485551);a = N(a, h);c = N(c, p);d = N(d, g);e = N(e, m)}b = [a, c, d, e];a = rb ? "0123456789ABCDEF" : "0123456789abcdef";c = "";for (d = 0; d < 4 * b.length; d++)c += a.charAt(b[d >> 2] >> d % 4 * 8 + 4 & 15) + a.charAt(b[d >> 2] >> d % 4 * 8 & 15);return c}aa.prototype = {x64hash128: function(a, b) {a = a || "";b = b || 0;for (var c = a.length % 16, d = a.length - c, e = [0, b], f = [0, b], h, p, g = [2277735313, 289559509], m = [1291169091, 658871167], l = 0; l < d; l += 16)h = [a.charCodeAt(l + 4) & 255 | (a.charCodeAt(l + 5) & 255) << 8 | (a.charCodeAt(l + 6) & 255) << 16 | (a.charCodeAt(l + 7) & 255) << 24, a.charCodeAt(l) & 255 | (a.charCodeAt(l + 1) & 255) << 8 | (a.charCodeAt(l + 2) & 255) << 16 | (a.charCodeAt(l + 3) & 255) << 24],p = [a.charCodeAt(l + 12) & 255 | (a.charCodeAt(l + 13) & 255) << 8 | (a.charCodeAt(l + 14) & 255) << 16 | (a.charCodeAt(l + 15) & 255) << 24, a.charCodeAt(l + 8) & 255 | (a.charCodeAt(l + 9) & 255) << 8 | (a.charCodeAt(l + 10) & 255) << 16 | (a.charCodeAt(l + 11) & 255) << 24],h = this.x64Multiply(h, g),h = this.x64Rotl(h, 31),h = this.x64Multiply(h, m),e = this.x64Xor(e, h),e = this.x64Rotl(e, 27),e = this.x64Add(e, f),e = this.x64Add(this.x64Multiply(e, [0, 5]), [0, 1390208809]),p = this.x64Multiply(p, m),p = this.x64Rotl(p, 33),p = this.x64Multiply(p, g),f = this.x64Xor(f, p),f = this.x64Rotl(f, 31),f = this.x64Add(f, e),f = this.x64Add(this.x64Multiply(f, [0, 5]), [0, 944331445]);h = [0, 0];p = [0, 0];switch (c) {case 15:p = this.x64Xor(p, this.x64LeftShift([0, a.charCodeAt(l + 14)], 48));case 14:p = this.x64Xor(p, this.x64LeftShift([0, a.charCodeAt(l + 13)], 40));case 13:p = this.x64Xor(p, this.x64LeftShift([0, a.charCodeAt(l + 12)], 32));case 12:p = this.x64Xor(p, this.x64LeftShift([0, a.charCodeAt(l + 11)], 24));case 11:p = this.x64Xor(p, this.x64LeftShift([0, a.charCodeAt(l + 10)], 16));case 10:p = this.x64Xor(p, this.x64LeftShift([0, a.charCodeAt(l + 9)], 8));case 9:p = this.x64Xor(p, [0, a.charCodeAt(l + 8)]),p = this.x64Multiply(p, m),p = this.x64Rotl(p, 33),p = this.x64Multiply(p, g),f = this.x64Xor(f, p);case 8:h = this.x64Xor(h, this.x64LeftShift([0, a.charCodeAt(l + 7)], 56));case 7:h = this.x64Xor(h, this.x64LeftShift([0, a.charCodeAt(l + 6)], 48));case 6:h = this.x64Xor(h, this.x64LeftShift([0, a.charCodeAt(l + 5)], 40));case 5:h = this.x64Xor(h, this.x64LeftShift([0, a.charCodeAt(l + 4)], 32));case 4:h = this.x64Xor(h, this.x64LeftShift([0, a.charCodeAt(l + 3)], 24));case 3:h = this.x64Xor(h, this.x64LeftShift([0, a.charCodeAt(l + 2)], 16));case 2:h = this.x64Xor(h, this.x64LeftShift([0, a.charCodeAt(l + 1)], 8));case 1:h = this.x64Xor(h, [0, a.charCodeAt(l)]),h = this.x64Multiply(h, g),h = this.x64Rotl(h, 31),h = this.x64Multiply(h, m),e = this.x64Xor(e, h)}e = this.x64Xor(e, [0, a.length]);f = this.x64Xor(f, [0, a.length]);e = this.x64Add(e, f);f = this.x64Add(f, e);e = this.x64Fmix(e);f = this.x64Fmix(f);e = this.x64Add(e, f);f = this.x64Add(f, e);return ("00000000" + (e[0] >>> 0).toString(16)).slice(-8) + ("00000000" + (e[1] >>> 0).toString(16)).slice(-8) + ("00000000" + (f[0] >>> 0).toString(16)).slice(-8) + ("00000000" + (f[1] >>> 0).toString(16)).slice(-8)}}ja.prototype = {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 == hb[e] ? e : hb[e]) + "\x3d" + f)}a = c;c = "";d = a.length;for (e = 0; e < d; e++)f = a.charAt(e).charCodeAt(0),c = 127 === f ? c + String.fromCharCode(0) : c + String.fromCharCode(f + 1);a = c;c = a.length;d = 0 == c % 3 ? parseInt(c / 3) : parseInt(c / 3) + 1;3 > c || (e = a.substring(0, 1 * d),f = a.substring(1 * d, 2 * d),a = a.substring(2 * d, c) + e + f);a = Qa(a);a = Qa(a);c = R.SHA256(a).toString(R.enc.Base64);c = R.SHA256(c).toString(R.enc.Base64);return new l(b,c)}}

这三个算法看起来比较吓人,实际上只要耐心调试是可以慢慢还原的,不过是字母的各种排列组合顺序而已,谁让他没有加密能让我们看到源码呢,仅仅的变量名称替换是难不倒聪明才智的开发者的,更何况这部分和业务逻辑关心不大,暂且略过吧.

算法复现

算法整体采用闭包设计面向对象的编程风格,基于原型链特性实现原始对象的加密逻辑,添加特有方法用于临时修改浏览器相关信息,最后将自定义对象 chromeHelper 直接挂载于 window 属性,方便外部调用.

/*** chrome 浏览器简化版,主要还原初次加载 RAIL_OkLJUJ 和 RAIL_DEVICEID 的基本流程,如许更新 RAIL_DEVICEID 需要结合 RAIL_OkLJUJ 一起加密,仅仅多增加一个 cookieCode 参数而已,除此之外并无特殊之处,不再赘述.* @author: snowdreams1006* @wechat: snowdreams1006(雪之梦技术驿站)*/
(function(window) {/*** 默认空构造函数*/function chromeHelper() {}/*** 设置用户代理,检测方式: navigator.userAgent*/chromeHelper.setUserAgent = function(userAgent) {if (!userAgent) {userAgent = "Mozilla5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit537.36 (KHTML, like Gecko) Chrome80.0.3987.87 Safari537.36";}Object.defineProperty(navigator, "userAgent", {value: userAgent,writable: false});}/*** 基于原型链实现面向对象编程的继承特性*/chromeHelper.prototype = {/*** 获取初始化浏览器设备信息,来源于initEc中的e = c.hashAlg(k, a, e);*/encryptedFingerPrintInfo: function() {// 获取分类后的浏览器指纹信息classifiedBrowserFingerPrintInfoArr = this.getClassifiedBrowserFingerPrintInfo();encryptedFingerPrintInfoMap = this.hashAlg(classifiedBrowserFingerPrintInfoArr, "", "");return encryptedFingerPrintInfoMap;}}/*** 直接挂载在全局变量 window 对象方便直接调用.*/window.chromeHelper = chromeHelper;
})(window);

step 1 : 获取基本信息

chromeHelper.prototype = {/*** 获取浏览器基本信息,来源于getDfpMoreInfo中的b.cfp.get和aa的get*/getBasicInfoArr: function() {// 基本信息,若数据无效则返回 void 0,即 undefinedvar basicInfoArr = [];// 用户代理 basicInfoArr.push(this.getUserAgentKeyAndValue(1));// 语言basicInfoArr.push(this.getLanguageKeyAndValue(1));// 颜色深度 basicInfoArr.push(this.getColorDepthKeyAndValue(1));// 像素比例basicInfoArr.push(this.getPixelRatioKeyAndValue(1));// 屏幕分辨率basicInfoArr.push(this.getScreenResolutionKeyAndValue(1));// 可用屏幕分辨率basicInfoArr.push(this.getAvailableScreenResolutionKeyAndValue(1));// 时区偏移量basicInfoArr.push(this.getTimezoneOffsetKeyAndValue(1));// Session存储basicInfoArr.push(this.getSessionStorageKeyAndValue(1));// Local存储basicInfoArr.push(this.getLocalStorageKeyAndValue(1));// IndexedDb存储basicInfoArr.push(this.getIndexedDbKeyAndValue(1));// websql存储basicInfoArr.push(this.getOpenDatabaseKeyAndValue(1));// cpu类型basicInfoArr.push(this.getCpuClassKeyAndValue(1));// 平台类型basicInfoArr.push(this.getPlatformKeyAndValue(1));// 反追踪basicInfoArr.push(this.getDoNotTrackKeyAndValue(1));// 插件basicInfoArr.push(this.getPluginsKeyAndValue(1));// TODO 画布basicInfoArr.push(this.getCanvasKeyAndValue(0));// webgl画布basicInfoArr.push(this.getWebglKeyAndValue(1));// adBlock广告拦截basicInfoArr.push(this.getAdBlockKeyAndValue(1));// 说谎语言basicInfoArr.push(this.getHasLiedLanguagesKeyAndValue(1));// 说谎分辨率basicInfoArr.push(this.getHasLiedResolutionKeyAndValue(1));// 说谎操作系统basicInfoArr.push(this.getHasLiedOsKeyAndValue(1));// 说谎浏览器basicInfoArr.push(this.getHasLiedBrowserKeyAndValue(1));// 触摸支持basicInfoArr.push(this.getTouchSupportKeyAndValue(1));// 字体basicInfoArr.push(this.getFontsKeyAndValue(1));return basicInfoArr;}
}

step 2 : 加密基本信息

chromeHelper.prototype = {/*** 加密浏览器基本信息,来源于aa的get中var f = c.x64hash128(d.join("~~~"), 31);*/encryptedBasicInfoArr: function(basicInfoArr) {// 剔除无效 undefined 数据并处理数组对象concatArr = [];for (i = 0; i < basicInfoArr.length; i++) {var basicInfoValue = basicInfoArr[i].value;// 值对应的也有可能是数组,针对这种情况也转成字符串.if ("undefined" !== typeof basicInfoValue) {if (Object.prototype.toString.call(basicInfoValue) === '[object Array]') {basicInfoValue = basicInfoValue.join(";");}concatArr.push(basicInfoValue);}}// 加密基本信息return this.x64hash128(concatArr.join("~~~"), 31);}
}

step 3 : 获取更多信息

chromeHelper.prototype = {/*** 获取浏览器更多信息,来源于getpackStr中的b = b.concat(this.moreInfoArray);*/getDfpMoreInfo: function(basicInfoArr, encryptedStr) {// 更多信息var moreInfoArr = [];// 添加画布信息moreInfoArr.push(this.getCanvansCode(encryptedStr + ""));// 添加浏览器本地存储累以及语言插件类信息for (var index in basicInfoArr) {var name = basicInfoArr[index].key;var value = basicInfoArr[index].value + "";switch (name) {case "session_storage":moreInfoArr.push(this.getSessionStorageCode(value));break;case "local_storage":moreInfoArr.push(this.getLocalStorageCode(value));break;case "indexed_db":moreInfoArr.push(this.getIndexedDbCode(value));break;case "open_database":moreInfoArr.push(this.getOpenDatabaseCode(value));break;case "do_not_track":moreInfoArr.push(this.getDoNotTrackCode(value));break;case "regular_plugins":moreInfoArr.push(this.getPluginsCode());break;case "adblock":moreInfoArr.push(this.getAdblockCode(value));break;case "has_lied_languages":moreInfoArr.push(this.getHasLiedLanguagesCode(value));break;case "has_lied_resolution":moreInfoArr.push(this.getHasLiedResolutionCode(value));break;case "has_lied_os":moreInfoArr.push(this.getHasLiedOsCode(value));break;case "has_lied_browser":moreInfoArr.push(this.getHasLiedBrowserCode(value));break;case "touch_support":moreInfoArr.push(this.getTouchSupportCode(value));break;case "js_fonts":moreInfoArr.push(this.getJsFontsCode(value));break;}}return moreInfoArr;}
}

step 4 : 获取机器信息

chromeHelper.prototype = {/*** 机器码信息,来源于getpackStr中的this.getMachineCode()*/getMachineCode: function() {// 机器码信息,若数据无效则返回 newvar machineCodeArr = [];// uuid代码machineCodeArr.push(this.getUUIDCode());// cookie代码machineCodeArr.push(this.getCookieCode());// 用户代理代码machineCodeArr.push(this.getUserAgentCode(1));// 源高度代码machineCodeArr.push(this.getScrHeightCode(1));// 源宽度代码machineCodeArr.push(this.getScrWidthCode(1));// 可用高度代码machineCodeArr.push(this.getScrAvailHeightCode(1));// 可用宽度代码machineCodeArr.push(this.getScrAvailWidthCode(1));// 颜色深度代码machineCodeArr.push(this.getMd5ScrColorDepthCode(1));// 源设备XDPI代码machineCodeArr.push(this.getScrDeviceXDPICode());// app代码名称代码machineCodeArr.push(this.getAppCodeNameCode(1));// app名称代码machineCodeArr.push(this.getAppNameCode(1));// Java是否启用代码machineCodeArr.push(this.getJavaEnabledCode(1));// 媒体类型代码machineCodeArr.push(this.getMimeTypesCode(1));// 平台代码machineCodeArr.push(this.getPlatformCode(1));// app次版本代码machineCodeArr.push(this.getAppMinorVersionCode());// 浏览器语言代码machineCodeArr.push(this.getBrowserLanguageCode(1));// Cookie是否启用代码machineCodeArr.push(this.getCookieEnabledCode(1));// Cpu类型代码machineCodeArr.push(this.getCpuClassCode());// 是否在线代码machineCodeArr.push(this.getOnLineCode(1));// 系统语言代码machineCodeArr.push(this.getSystemLanguageCode());// 用户语言代码machineCodeArr.push(this.getUserLanguageCode());// 时区偏移代码machineCodeArr.push(this.getTimeZoneCode(1));// flash版本代码machineCodeArr.push(this.getFlashVersionCode(1));// 历史记录条数代码machineCodeArr.push(this.getHistoryListCode(1));// 自定义ID代码machineCodeArr.push(this.getCustIdCode());// 发送平台代码machineCodeArr.push(this.getSendPlatformCode());return machineCodeArr;}
}

step 5 : 合成指纹信息

chromeHelper.prototype = {/*** 获取浏览器原始指纹信息,来源于initEc中的l = c.getpackStr(b)*/getOriginBrowserFingerPrintInfo: function() {// 浏览器指纹信息var originBrowserFingerPrintArr = [];// 基本信息,用于生成更多信息var basicInfoArr = this.getBasicInfoArr();// 基本信息加密摘要var encryptedStr = this.encryptedBasicInfoArr(basicInfoArr);// 更多信息,用于组合机器码信息var moreInfoArr = this.getDfpMoreInfo(basicInfoArr, encryptedStr);// 机器码信息var machineCodeArr = this.getMachineCode(moreInfoArr);// 组合信息并重新排序originBrowserFingerPrintArr = this.concatMachineCodeAndDfpMoreInfo(machineCodeArr, moreInfoArr);return originBrowserFingerPrintArr;}
}
chromeHelper.prototype = {/*** 组合机器码和浏览器更多信息构成原始指纹,来源于getpackStr中的getpackStr*/concatMachineCodeAndDfpMoreInfo: function(machineCodeArr, moreInfoArr) {// 机器码合并更多信息var tempArr = machineCodeArr.concat(moreInfoArr);// 重新排序tempArr.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";});return tempArr;}
}

step 6 : 重新分类指纹

chromeHelper.prototype = {/*** 获取浏览器指纹信息,来源于initEc中的k.push(new p("scrAvailSize",h));*/getClassifiedBrowserFingerPrintInfo: function() {// 浏览器指纹信息var originBrowserFingerPrintArr = this.getOriginBrowserFingerPrintInfo();// 分类键名var Gb = "appCodeName appMinorVersion appName cpuClass onLine systemLanguage userLanguage historyList hasLiedLanguages hasLiedResolution hasLiedOs hasLiedBrowser".split(" "),Hb = ["scrAvailWidth", "scrAvailHeight"],Ib = ["scrDeviceXDPI", "scrColorDepth", "scrWidth", "scrHeight"],Jb = ["sessionStorage", "localStorage", "indexedDb", "openDatabase"];// 本地存储类,键名对应 Jbvar storeDbArr = [];// 屏幕实际尺寸类,键名对应 Ibvar srcScreenSizeArr = [];// 屏幕可用尺寸类,键名对应 Hbvar scrAvailSizeArr = [];// 其他类也是分类后的浏览器指纹信息var classifiedBrowserFingerPrintArr = []// 提取出本地存储类,屏幕实际尺寸类,屏幕可用尺寸类以及其他类for (var i = 0; i < originBrowserFingerPrintArr.length; i++) {var browserFingerPrint = originBrowserFingerPrintArr[i];var name = browserFingerPrint.key;var value = browserFingerPrint.value;"new" != value && -1 == Gb.indexOf(name) && (-1 != Jb.indexOf(name) ? storeDbArr.push(browserFingerPrint) : -1 != Hb.indexOf(name) ? scrAvailSizeArr.push(browserFingerPrint) : -1 != Ib.indexOf(name) ? srcScreenSizeArr.push(browserFingerPrint) : classifiedBrowserFingerPrintArr.push(browserFingerPrint));}// 本地存储storeDb = "";for (i = 0; i < storeDbArr.length; i++) {storeDb = storeDb + storeDbArr[i].key.charAt(0) + storeDbArr[i].value;}// 屏幕实际尺寸srcScreenSize = "";for (i = 0; i < srcScreenSizeArr.length; i++) {srcScreenSize = 0 == i ? srcScreenSize + srcScreenSizeArr[i].value : srcScreenSize + "x" + srcScreenSizeArr[i].value;}// 屏幕可用尺寸scrAvailSize = "";for (i = 0; i < scrAvailSizeArr.length; i++) {scrAvailSize = 0 == i ? scrAvailSize + scrAvailSizeArr[i].value : scrAvailSize + "x" + scrAvailSizeArr[i].value;}// 添加到其他类构成完整的指纹信息classifiedBrowserFingerPrintArr.push({"key": "storeDb","value": storeDb});classifiedBrowserFingerPrintArr.push({"key": "srcScreenSize","value": srcScreenSize});classifiedBrowserFingerPrintArr.push({"key": "scrAvailSize","value": scrAvailSize});return classifiedBrowserFingerPrintArr;}
}

step 7 : 加密分类指纹

chromeHelper.prototype = {/*** 获取初始化浏览器设备信息,来源于initEc中的e = c.hashAlg(k, a, e);*/encryptedFingerPrintInfo: function() {// 获取分类后的浏览器指纹信息classifiedBrowserFingerPrintInfoArr = this.getClassifiedBrowserFingerPrintInfo();encryptedFingerPrintInfoMap = this.hashAlg(classifiedBrowserFingerPrintInfoArr, "", "");return encryptedFingerPrintInfoMap;}
}

未完待续

实际上通过上中两篇文章已经分析差不多了,但是为了教程的严谨性还是决定再更新最后一篇,下一篇将介绍如何使用以及回顾展望整个流程,感谢你的阅读.

关注雪之梦技术驿站不迷路,动动小手期待最后一篇哟!

如果你觉得本文对你有所帮助,请随手点个赞再走呗或者关注下公众号「雪之梦技术驿站」定期更新优质文章哟!

12306 抢票系列之只要搞定RAIL_DEVICEID的来源,从此抢票不再掉线(中)相关推荐

  1. 12306 抢票系列之只要搞定RAIL_DEVICEID的来源,从此抢票不再掉线(上)

    郑重声明: 本文仅供学习使用,禁止用于非法用途,否则后果自负,如有侵权,烦请告知删除,谢谢合作! 开篇明义 本文针对自主开发的抢票脚本在抢票过程中常常遇到的请求无效等问题,简单分析了 12306 网站 ...

  2. 微服务系列 —— 一小时搞定Eureka

    微服务系列  -- 一小时搞定Eureka 一.什么是Eureka Eureka是Netflix公司开源的产品,它是一种基于REST( Representational State Transfer  ...

  3. android 打开外置摄像头驱动程序,嵌入式er日常系列!终于搞定android驱动USB摄像头了!...

    原标题:嵌入式er日常系列!终于搞定android驱动USB摄像头了! 感谢网上的大神分享经验,终于解决了让我头疼好久的USB摄像头问题,讨论的前提是你的USB摄像头是UVC兼容的(如今大部分摄像头兼 ...

  4. 如何体验4G极限速度?一部Mate30系列5G轻松搞定

    当三大运营商在11月1日对全国50个城市支持办理5G套餐后,不少用户还是对至少每月百元起的5G套餐资费望而却步,而且有一说一,现在起步价5G套餐,包含的总流量并不高. 就拿中国移动5G套餐举例,128 ...

  5. 五分钟轻松搞定产品需求文档!这可能史上最全PRD文档模板…

    本文由  @JustWu 原创发布于社区 为什么写这篇文章? 第一:写PMCAFF的PRD文档,大家都是用户,比较好参考与理解,方便大家来找我写的不好的地方. 第二:我在自学PRD文档的编写过程中,总 ...

  6. 文本文档代码大全简单_简单4步搞定PC版微信多开,不再重复切换

    现在微信已经成为工作.学习中不可或缺的社交沟通工具,而很多人也不止拥有一个微信号.目前很多手机厂商都自带了应用双开的功能,所以在移动端这个问题已经被满足了,那么在电脑端呢?即使现在有很多辅助多开的工具 ...

  7. 降龙十八掌搞定rt3070 USB WIFI模块在android2.3平台上上网[基于x210开发板]

    第一掌:编译KO文件,生成rt3070sta.ko 编译时提示如下错误: 错误: ./include/generated/autoconf.h:708: fatal error: /home/lqm/ ...

  8. 搞定这几点!还怕关键词排名上不去?

    导语:iOS 13从发布迄今已经历7个测试版本,距离正式版发布只剩一个月左右时间.我们知道,每当大的系统版本更迭,App Store都会有一系列的震动--暂时锁榜就是其中之一.在锁榜到来之前,前一阶段 ...

  9. 像背单词一样搞定机器学习关键概念:300张小抄表满足你的所有AI好奇

    入坑数据科学和人工智能的同学都知道,机器学习是一个集合了计算机.统计学和数学知识的交叉领域,除了日常练习,也需要很多枯燥的记忆和理解.单纯读书不容易串联概念,又容易忘记. 可能你和文摘菌一样,读了无数 ...

  10. 读书笔记:《搞定3--平衡工作和生活的艺术》

    这本<搞定3>是在来回上海的飞机上看了一半,回来后慢慢看完的.感觉这本书把<搞定1>的思路理得更加清晰了,用一张图可以表示出文章的大纲结构. 前三章 讲述了GTD现象.为什么有 ...

最新文章

  1. 适合小小白的完整建设流程
  2. interface接口_golang 基础(Four) 接口进阶
  3. idea中不小心把文件夹删了
  4. 北京数码视讯s905l固件_神州数码与MAXHUB构建更紧密的伙伴关系,共同开启可持续发展之旅...
  5. 深入浅出谈cuda 书_入门和基础——9本关于美学的书
  6. 一个毕业生对大学爱情和奋斗的思考!
  7. 另一个域的cookie_一定要知道的第一方Cookie和第三方Cookie
  8. 【转】.net框架读书笔记---CLR内存管理\垃圾收集(二)
  9. 「深度」千篇一律的智能音箱,为何它们就是对显示屏“不感冒”?
  10. 华为杯数学建模优秀论文_数学建模经典例题(2016年国赛B题与优秀论文)
  11. php move函数,php – 在null上调用成员函数move()
  12. 量子计算和量子加密的基础问答
  13. 信息系统项目管理师历年试题分析与解答(android版)
  14. 蛋白质互作工具开发笔记(一)——整体计划实施
  15. AV1编码器优化技术
  16. 图像二值化方法及适用场景分析(OTSU Trangle 自适应阈值分割)
  17. 个人主页【阿飞算法】
  18. 中国日报聚焦游戏陪玩 直播平台加速拓展百亿市场
  19. Android8.1修改packageinstaller安装指定应用不弹窗静默安装
  20. 无法激活windows应用商店应用程序,应用进程已启动,但激活请求失败,错误为“应用未启动”

热门文章

  1. 零线断了为什么会带电, 使验电笔氖管发光
  2. uniapp报错:Browserslist: caniuse-lite is outdated. Please run next command `npm update`
  3. [渝粤教育] 同济大学 外科手术技能教学 参考 资料
  4. 解决:Field xxMapper in xx.service.impl.xxServiceImpl required a bean of type 'xx.mapper.xxMapper'...
  5. canvas卡通兔子萝卜飞行动画
  6. easyExcel导出下拉选择框,多sheet数据excle导入导出
  7. java自行车(java自行车)
  8. Mycat分库分表优缺点分析
  9. 使用jib发布代码流程
  10. 计算机二级python看什么书好_2021年全国计算机二级Python备考指导