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

1,001 阅读18分钟

直奔重点

高楼大厦寻关键线索

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 浏览器处于无痕模式,接着是每次试验时清空站点缓存,最后就可以愉快刷新当前网页等待进入下一轮断电调试了.

12306-algorithm-web-js-source-getjs-debug.png

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

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

12306-algorithm-web-js-source-getjs-debug-watch.png

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

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

12306-algorithm-web-js-source-getjs-debug-finally.png

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

12306-algorithm-web-js-source-getjs-debug-write.png

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

12306-algorithm-web-js-source-getjs-debug-repeat.png

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

(function() {
  ja.prototype = {
    // C:initEc
    initEc: function(a) {
      this.ec.get("RAIL_OkLJUJ", function(a) {
          c.getDfpMoreInfo(function() {

          })
      }, 1)
    },
    // c.getDfpMoreInfo:A
    getDfpMoreInfo: function(a) {
        
    }
  }

  // this.ec.get("RAIL_OkLJUJ":B
  window.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:initEc
    initEc: function(a) {
      this.ec.get("RAIL_OkLJUJ", function(a) {
          c.getDfpMoreInfo(function() {

          })
      }, 1)
    },
    // c.getDfpMoreInfo:A
    getDfpMoreInfo: function(a) {
        
    }
  }

  // this.ec.get("RAIL_OkLJUJ":B
  window.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 = {
    // C
    initEc: function(a) {
      this.ec.get("RAIL_OkLJUJ", function(a) {
          c.getDfpMoreInfo(function() {

          })
      }, 1)
    },
    // A
    getDfpMoreInfo: function(a) {
        
    }
  }

  // AC
  aa.prototype = {
    get: function(a) {
        
    }
  }

  // B
  window.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 编码等等.

遇到瓶颈则略过细节

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

总体来说,获取浏览器各种信息并且还涉及兼容 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,即 undefined
      var 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() {
      // 机器码信息,若数据无效则返回 new
      var 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"];

      // 本地存储类,键名对应 Jb
      var storeDbArr = [];
      // 屏幕实际尺寸类,键名对应 Ib
      var srcScreenSizeArr = [];
      // 屏幕可用尺寸类,键名对应 Hb
      var 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;
    }
}

未完待续

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

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

如果你觉得本文对你有所帮助,欢迎点赞留言告诉我,你的鼓励是我继续创作的动力,不妨顺便关注下个人公众号「雪之梦技术驿站」,定期更新优质文章哟!

雪之梦技术驿站.png