1. 程式人生 > >12306 搶票系列之只要搞定RAIL_DEVICEID的來源,從此搶票不再掉線(下)

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

鄭重宣告:
本文僅供學習使用,禁止用於非法用途,否則後果自負,如有侵權,煩請告知刪除,謝謝合作!

模擬偽裝

現在已經還原了演算法的實現邏輯,下一步就是如何更好地偽造自己,本文提供臨時設定的實現方式,方便在不修改之前復現程式碼的基礎上實現擴充套件,當然也可以直接在還原演算法原始碼中寫入偽造程式碼.

值得注意的是,這種 Object.defineProperty 方式只會臨時生效而且僅僅針對使用 js 程式碼獲取物件屬性的值,並不會真正修改物件屬性!

  • 設定使用者代理
  /**
   * 設定使用者代理,檢測方式: 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
    });
  }
  • 設定瀏覽器語言
  /**
   * 設定瀏覽器語言,檢測方式: navigator.language
   */
  chromeHelper.setLanguage = function(language) {
    if (!language) {
      language = "zh-CN";
    }
    Object.defineProperty(navigator, "language", {
      value: language,
      writable: false
    });
  }
  • 設定瀏覽器語言
  /**
   * 設定瀏覽器語言,檢測方式: navigator.languages
   */
  chromeHelper.setLanguages = function(languages) {
    if (!languages) {
      languages = ["zh-CN", "zh", "en"];
    }
    Object.defineProperty(navigator, "languages", {
      value: languages,
      writable: false
    });
  }
  • 設定螢幕顏色深度
  /**
   * 設定螢幕顏色深度,檢測方式: screen.colorDepth
   */
  chromeHelper.setColorDepth = function(colorDepth) {
    if (!colorDepth) {
      colorDepth = 24;
    }
    Object.defineProperty(screen, "colorDepth", {
      value: colorDepth,
      writable: false
    });
  }
  • 設定裝置畫素比率
  /**
   * 設定裝置畫素比率,檢測方式: window.devicePixelRatio
   */
  chromeHelper.setDevicePixelRatio = function(devicePixelRatio) {
    if (!devicePixelRatio) {
      devicePixelRatio = 24;
    }
    Object.defineProperty(window, "devicePixelRatio", {
      value: devicePixelRatio,
      writable: false
    });
  }
  • 設定螢幕寬度
  /**
   * 設定螢幕寬度,檢測方式: screen.width
   */
  chromeHelper.setWidth = function(width) {
    if (!width) {
      width = 1280;
    }
    Object.defineProperty(screen, "width", {
      value: width,
      writable: false
    });
  }
  • 設定螢幕高度
  /**
   * 設定螢幕高度,檢測方式: screen.height
   */
  chromeHelper.setHeight = function(height) {
    if (!height) {
      height = 800;
    }
    Object.defineProperty(screen, "height", {
      value: height,
      writable: false
    });
  }
  • 設定螢幕可用寬度
  /**
   * 設定螢幕可用寬度,檢測方式: screen.availWidth
   */
  chromeHelper.setAvailWidth = function(availWidth) {
    if (!availWidth) {
      availWidth = 1280;
    }
    Object.defineProperty(screen, "availWidth", {
      value: availWidth,
      writable: false
    });
  }
  • 設定螢幕可用高度
  /**
   * 設定螢幕可用高度,檢測方式: screen.availHeight
   */
  chromeHelper.setAvailHeight = function(availHeight) {
    if (!availHeight) {
      availHeight = 777;
    }
    Object.defineProperty(screen, "availHeight", {
      value: availHeight,
      writable: false
    });
  }
  • 設定Session儲存
  /**
   * 設定Session儲存,檢測方式: !!window.sessionStorage
   */
  chromeHelper.setSessionStorage = function(sessionStorage) {
    if (!sessionStorage) {
      sessionStorage = 1;
    }
    if (sessionStorage) {
      window.sessionStorage = 1
    } else {
      delete window.sessionStorage
    }
  }
  • 設定Local儲存
  /**
   * 設定Local儲存,檢測方式: !!window.localStorage
   */
  chromeHelper.setLocalStorage = function(localStorage) {
    if (!localStorage) {
      localStorage = 1;
    }
    if (localStorage) {
      window.localStorage = 1
    } else {
      delete window.localStorage
    }
  }
  • 設定indexedDB儲存
  /**
   * 設定indexedDB儲存,檢測方式: !!window.indexedDB
   */
  chromeHelper.setIndexedDB = function(indexedDB) {
    if (!indexedDB) {
      indexedDB = 1;
    }
    if (indexedDB) {
      window.indexedDB = 1
    } else {
      delete window.indexedDB
    }
  }
  • 設定addBehavior儲存
  /**
   * 設定addBehavior儲存,檢測方式: !!document.body.addBehavior
   */
  chromeHelper.setAddBehavior = function(addBehavior) {
    if (!addBehavior) {
      addBehavior = 1;
    }
    if (addBehavior) {
      document.body.addBehavior = 1
    } else {
      delete document.body.addBehavior
    }
  }
  • 設定Cpu型別
  /**
   * 設定Cpu型別,檢測方式: navigator.cpuClass
   */
  chromeHelper.setCpuClass = function(cpuClass) {
    if (!cpuClass) {
      cpuClass = "unknown";
    }
    Object.defineProperty(navigator, "cpuClass", {
      value: cpuClass,
      writable: false
    });
  }
  • 設定平臺型別
  /**
   * 設定平臺型別,檢測方式: navigator.platform
   */
  chromeHelper.setPlatform = function(platform) {
    if (!platform) {
      platform = "MacIntel";
    }
    Object.defineProperty(navigator, "platform", {
      value: platform,
      writable: false
    });
  }
  • 設定反追蹤
  /**
   * 設定反追蹤,檢測方式: navigator.doNotTrack
   */
  chromeHelper.setDoNotTrack = function(doNotTrack) {
    if (!doNotTrack) {
      doNotTrack = "unknown";
    }
    Object.defineProperty(navigator, "doNotTrack", {
      value: doNotTrack,
      writable: false
    });
  }
  • 設定外掛
  /**
   * 設定外掛,檢測方式: navigator.plugins
   */
  chromeHelper.setPlugins = function(plugins) {

  }
  • 設定Canvas
  /**
   * 設定Canvas,檢測方式: TODO
   */
  chromeHelper.setCanvas = function(canvas) {

  }
  • 設定Webgl
  /**
   * 設定Webgl,檢測方式: TODO
   */
  chromeHelper.setWebgl = function(webgl) {

  }
  • 設定AdBlock
  /**
   * 設定AdBlock,檢測方式: TODO
   */
  chromeHelper.setAdBlock = function(AdBlock) {

  }
  • 設定AdBlock
  /**
   * 設定AdBlock,檢測方式: TODO
   */
  chromeHelper.setAdBlock = function(AdBlock) {

  }
  • 設定字型
  /**
   * 設定字型,檢測方式: TODO
   */
  chromeHelper.setFonts = function(fonts) {

  }
  • 設定最多觸控點
  /**
   * 設定最多觸控點,檢測方式: navigator.maxTouchPoints
   */
  chromeHelper.setMaxTouchPoints = function(maxTouchPoints) {
    if (!maxTouchPoints) {
      maxTouchPoints = 0;
    }
    Object.defineProperty(navigator, "maxTouchPoints", {
      value: maxTouchPoints,
      writable: false
    });
  }
  • 設定ontouchstart事件
  /**
   * 設定ontouchstart事件,檢測方式: "ontouchstart" in window
   */
  chromeHelper.setTouchEvent = function(ontouchstart) {
    if (!ontouchstart) {
      ontouchstart = false;
    }
    if (ontouchstart) {
      window.ontouchstart = true
    } else {
      delete window.ontouchstart
    }
  }
  • 設定app程式碼名稱程式碼
  /**
   * 設定app程式碼名稱程式碼,檢測方式: navigator.appCodeName.toString()
   */
  chromeHelper.setAppCodeName = function(appCodeName) {
    if (!appCodeName) {
      appCodeName = "Mozilla";
    }
    Object.defineProperty(navigator, "appCodeName", {
      value: appCodeName,
      writable: false
    });
  }
  • 設定app程式碼名稱程式碼
  /**
   * 設定app程式碼名稱程式碼,檢測方式: navigator.appName.toString()
   */
  chromeHelper.setAppName = function(appName) {
    if (!appName) {
      appName = "Netscape";
    }
    Object.defineProperty(navigator, "appName", {
      value: appName,
      writable: false
    });
  }
  • 設定Java是否啟用
  /**
   * 設定Java是否啟用,檢測方式: navigator.javaEnabled()
   */
  chromeHelper.setJavaEnabled = function(javaEnabled) {

  }
  • 設定媒體型別
  /**
   * 設定媒體型別,檢測方式: navigator.mimeTypes
   */
  chromeHelper.setMimeTypes = function(mimeTypes) {

  }
  • 設定cookie是否啟用
  /**
   * 設定cookie是否啟用,檢測方式: navigator.cookieEnabled
   */
  chromeHelper.setCookieEnabled = function(cookieEnabled) {
    if (!cookieEnabled) {
      cookieEnabled = true;
    }
    Object.defineProperty(navigator, "cookieEnabled", {
      value: cookieEnabled,
      writable: false
    });
  }
  • 設定是否線上
  /**
   * 設定是否線上,檢測方式: navigator.onLine.toString()
   */
  chromeHelper.setOnLine = function(onLine) {
    if (!onLine) {
      onLine = true;
    }
    Object.defineProperty(navigator, "onLine", {
      value: onLine,
      writable: false
    });
  }
  • 新增歷史記錄
  /**
   * 新增歷史記錄,檢測方式: window.history
   */
  chromeHelper.pushHistory = function(newUrls) {
    for (url in newUrls) {
      history.pushState(null, '', url);
    }
  }

使用示例

親測構造請求 /otn/HttpZF/logdevice時,關於引數 algID 經常性發生變化,因此無法提供靜態的請求方法,建議根據實際情況實時改變.

通過翻閱原始碼實現,最終發現關於傳送請求的程式碼是這樣的:

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\x3dmBxuYhGXYR\x26hashCode\x3d" + e + a), null, function(a) {
    var b = JSON.parse(a);
    void 0 != mb && mb.postMessage(a, r.parent);
    for (var d in b)
        "dfp" == d ? G("RAIL_DEVICEID") != b[d] && (V("RAIL_DEVICEID", b[d], 1E3),
        c.deviceEc.set("RAIL_DEVICEID", b[d])) : "exp" == d ? V("RAIL_EXPIRATION", b[d], 1E3) : "cookieCode" == d && (c.ec.set("RAIL_OkLJUJ", b[d]),
        V("RAIL_OkLJUJ", "", 0))
})

其中,引數 a 表示的是加密後的瀏覽器指紋資訊,(new Date).getTime() 是當前時間戳,而 algID\x3dmBxuYhGXYR\x26hashCode\x3d 這部分的 algID 演算法引數是暫時性靜態的,比如今天一段時間都是 mBxuYhGXYR 而第二天這個值就變成其他值了.

hashCode 引數的值就是程式執行結果的 value 值,最後面的變數 a 代表的是剩下的瀏覽器指紋資訊,即執行結果的 key.

假設此時此刻為例,演示如何使用該 js 檔案:

function ajax(req){
    var xhr=new XMLHttpRequest();
    xhr.onreadystatechange=function(){
        if(xhr.readyState===4){
            req.success&&req.success(xhr.responseText,xhr.status);
        }
    }
    req.method=req.method?req.method.toUpperCase():'GET';
    var data=null;
    var url=req.url;
    if(req.data){
        var arg='';
        for(var n in req.data){
            arg+=n+'='+encodeURIComponent(req.data[n])+'&'
        }
        arg=arg.slice(0,-1);
        if(req.method==='GET'){
            url=url+'?'+arg;
        }else{
            data=arg;
        }
    }
    if(req.headers){
        for(var h in req.headers){
            var v=req.headers[h];
            xhr.setRequestHeader(h,v);
        }
    }
    xhr.open(req.method,url);
    xhr.send(data);
}

e = chromeHelper.prototype.encryptedFingerPrintInfo();
a = e.key;
e = e.value;
a += "\x26timestamp\x3d" + (new Date).getTime();
ajax({
    url:"https://kyfw.12306.cn/otn/HttpZF/logdevice" + ("?algID\x3dmBxuYhGXYR\x26hashCode\x3d" + e + a),
    success:function(data){
        console.log("data",data);

        startIndex = "callbackFunction('".length;
        endIndex = data.lastIndexOf("')");
        jsonStrData = data.substring(startIndex,endIndex);
        console.log("jsonStrData",jsonStrData);

        jsonData = JSON.parse(jsonStrData);
        console.log("jsonData",jsonData);

        exp = jsonData.exp;
        cookieCode = jsonData.cookieCode;
        dfp = jsonData.dfp;
        console.log("RAIL_DEVICEID::: "+dfp+" RAIL_EXPIRATION::: " + exp +" RAIL_OkLJUJ::: " + cookieCode);
    }
});

回顧展望

在還原演算法實現過程中,充分複習了 web 前端開發的除錯技巧,針對通篇無意義的變數命名方式,有效的應對方式就是採用正則表示式精確匹配進行查詢.

同時,為了驗證自己的猜想是否正確,還需要結合斷點除錯,如果存在反除錯手段,那麼只能靠自己硬啃壓縮混淆程式碼了,我只能說:考驗真正技術的時候,到了!

本文給我們留下了不少啟發供後續工作學習使用,從開發者的角度上來講:

  • 不完全依靠現成加密技術,哪怕真正加密時沒自己實現也要在加密前後實現自己的混淆邏輯.

例如重新打亂字串,將字串分隔成三份,按照首尾中或者尾中首等反人類次序重新排序等.

  • 重複使用同一資料時也不一定要抽象成同一個方法,不同物件呼叫不同的處理邏輯,更是讓人防不勝防,陷入思維慣性誤區.

例如獲取使用者代理採用不同的正則表示式進行替換,獲取瀏覽器語言時採用另外的途徑驗證上一步獲取結果是否造假等.

  • 無序更勝似有序,看似規整優美的程式碼是給開發人員看的而不該給機器看,一定不能使用原始碼上線而是要加密處理或者其他處理.

只有要基本的開發經驗很容易一葉知秋,進而判斷相關技術棧,因為技術都是通用的方案,非常容易複製,打造獨特的技術流會增大破解成本,進而嚇退一部分菜鳥小白.

  • 前端和後端需要密切配合協同協作,缺少統一指揮難以避免一方或者多方偷懶進而暴露我方陣地.

重要的業務邏輯肯定是放在後端進行處理,哪怕前端已經處理過相同邏輯也不能偷懶,更要保證前後端處理邏輯的一致性.

攻防是矛與盾,作為攻克方要做的一點就是打鐵還需自身硬,多瞭解不同的技術棧才能做到有的放矢而不至於臨陣脫逃,望而卻步!

最後祝大家搶票愉快,需要買票時人人都有票,再也不需要搶票回家,人生苦短何必浪費生命去搶票?

再次宣告,本文僅供學習研究,一切用作它途的行為均與本人無關,如有侵權,煩請告知,謝謝合作.

參考資料

  • Chrome - JavaScript除錯技巧總結(瀏覽器除錯JS)
  • 如何使用Chrome DevTools花式打斷點
  • 【譯】Chrome瀏覽器開發者工具的13個有趣技巧——希望你已經掌握
  • HTML5前端資料庫——Web SQL Database
  • localStorage相容ie6/7 用addBehavior 實現
  • 前端儲存之indexedDB
  • localstorage || globalStorage || userData
  • navigator,JS檢測瀏覽器外掛