12306 搶票系列之只要搞定RAIL_DEVICEID的來源,從此搶票不再掉線(中)

直奔重點

高樓大廈尋關鍵線索

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 編碼等等.

遇到瓶頸則略過細節

通讀全文並結合斷點調試反覆驗證猜想後,我們發現爲了最終請求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,即 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

發佈了116 篇原創文章 · 獲贊 119 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章