某Boss網(wǎng)站的反反爬機制詳解

近日出于學習的目的對某Boss網(wǎng)站的反爬機制進行了分析和逆向,終于完全搞定了,記錄總結(jié)下方便日后學習!
本代碼請僅用于 純技術(shù)研究的 用途,請勿用于商業(yè)用途或 非法用途,如果因使用者非法使用造成的法律問題與本作者無關(guān)!

涉及技術(shù)

核心js加密文件進行了全逆向,爬取方案不需要瀏覽器參與,純 python+js 即可!

本爬取方案最終通過Python實現(xiàn)了完全自動化爬取職位信息列表頁,并存儲到一個txt文件中(需要的可自行擴展數(shù)據(jù)庫實現(xiàn)),為了簡單起見未使用多線程,使用的技術(shù)有:

  • python3
  • requests
  • execjs 執(zhí)行js腳本
  • re 正則表達式
  • BeautifulSoup 解析列表頁面提取職位信息
  • jsbeautify.js 為增加可讀性,對加密js進行格式化處理

網(wǎng)站正常訪問流程

  1. 某Boss網(wǎng)站在訪問頁面時都需要攜帶一個 __zp_stoken__=xxxxxx 這樣的cookie

  2. 每次訪問頁面都會更新這個__zp_stoken__,(另外,每次__zp_stoken__其實可以使用數(shù)次(貌似<5次),具體次數(shù)沒有試驗)

    • 返回的頁面會攜帶 __zp_sseed____zp_sname____zp_snts__ 三個cookie (通過查看response 的 set-cookie部分)
    • 基于這三個cookie值計算新的__zp_stoken__存入cookie中
    • 這三個cookie值消費完后會即刻刪除,所以在瀏覽器中一般看不到這三個cookie值
  3. 如果不攜帶cookie __zp_stoken__或者__zp_stoken__無效,就會返回一個302跳轉(zhuǎn)

    • 302跳轉(zhuǎn)Location 為 https://www.zhipin.com/web/common/security-check.html?seed=r9L%2BjsS5vloxpthkin%2F2Yskecz1G198Nk%2B4RCkA2YiA%3D&name=d05572cd&ts=1587092342543&callbackUrl=%2Fc101280600%2F%3Fquery%3Dpython%26page%3D2%26ka%3Dpage-2&srcReferer=

    • Location 中seed、name、ts為關(guān)鍵參數(shù),用于計算 __zp_stoken__

    • 這個頁面中的js所做的工作就是為

      • 按照name加載核心加密js文件
      • 設(shè)置cookie中的 __zp_stoken__
      • 精簡后的核心js為
                  var getQueryString = function(name) {
                      var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
                      var r = window.location.search.substr(1).match(reg);
                      if (r != null) return unescape(r[2]);
                      return null;
                  };
                 
                 var seed = decodeURIComponent(getQueryString("seed")) || "";
                  var ts = getQueryString("ts");
                  var fileName = getQueryString("name");
                 
                 var expiredate = new Date().getTime() + 32 * 60 * 60 * 1000 * 2;
                 var code = "";
                 var ABC = window.ABC || frame.contentWindow.ABC;
                 try {
                     //code = new ABC().z(seed, parseInt(ts)+(480+new Date().getTimezoneOffset())*60*1000);
                     code = new ABC().z(seed, parseInt(ts));//+(480+new Date().getTimezoneOffset())*60*1000 時區(qū)相對于北京的偏移
                 } catch (e) {}
                 if (code ) Cookie.set("__zp_stoken__", code, expiredate, COOKIE_DOMAIN, "/"); //該方法會做一次 encodeURIComponent
      

對于2或3 拿到seed、name、ts后的流程基本一樣,都是調(diào)用new ABC().prototype.z(seed,ts)函數(shù),生成 __zp_stoken__ 并做一次 encodeURIComponent 后存放到cookie中。

術(shù)語說明

  • seed 是服務(wù)端發(fā)送的一個種子,由服務(wù)端控制生成
  • name 是對應(yīng)加密js的名字,這個name在每次訪問時會動態(tài)變化,也及時獲取的加密js不固定,由服務(wù)端控制生成,8位數(shù)字或字母的組合
  • ts 是timestamp時間戳,由服務(wù)端控制生成
  • __zp_stoken__ 通過name指定的加密js 計算出來 cookie值,由客戶端基于服務(wù)器提供seed和ts來生成,每次發(fā)送請求需攜帶有效的 __zp_stoken__
  • 投毒 js代碼中會對環(huán)境進行很多檢測,如果識別為非正常瀏覽器,就會對某些參數(shù)進行修改,造成最終結(jié)果錯誤,成為投毒
  • 解毒 對代碼中的投毒代碼進行處理
  • zz 加密js中的一個變量,對應(yīng)最終生成的 __zp_stoken__ ,是難得一見的具備可讀性的變量
  • ABC 加密js中的一個對象,也是生成最終 __zp_stoken__ 的入口
  • 常量池 加密js中的第一行,就是定義了一個常量字符串數(shù)組(base64加密),其中存放的都是代碼中用到字符串
  • 常量池偏移 常量池在進行字符串替換時,并不是直接按下標替換的,而是做了一個偏移(不同的js不一樣)
  • 運算符函數(shù) 就是將一些簡單的運算符轉(zhuǎn)成了函數(shù) 如+-*/等等,原始js中存在大量的運算符函數(shù)
  • 運算符函數(shù)替換 對js中的運算符函數(shù) 替代成原始 操作符,并干掉運算符函數(shù)的過程就是運算符函數(shù)替換
  • switch扁平化 原始js中很多switch case語句用來控制函數(shù)的執(zhí)行流程,而判斷的內(nèi)容來源于某個數(shù)組,這個數(shù)組又是根據(jù)常量池中的某個常量按照|進行分割形成的,其實switch最終執(zhí)行流程是固定,而按照這個數(shù)據(jù)對switch case進行函數(shù)順序編排及替換就成為 switch扁平化

爬取方案

爬取主流程

爬取方案的思路為

  1. 訪問頁面獲取302跳轉(zhuǎn)的Location,提取seed name ts核心參數(shù)
  2. 根據(jù)name請求對應(yīng)加密js文件
  3. 使用jsbeautify對加密js文件進行格式化
  4. 對加密js進行運算法替換以及解毒處理
  5. 調(diào)用js生成 __zp_stoken__
  6. 通過 __zp_stoken__ 爬取指定列表頁數(shù)據(jù)
  7. 解析頁數(shù),提取職位信息追加到result.txt中
  8. 解析頁面響應(yīng)的頭部信息,獲取seed name ts
  9. 根據(jù)seed name ts 再次 從第二步開始重復(fù)

調(diào)用代碼案例

# 對指定的頁面進行遍歷翻頁,將結(jié)果寫入指定文件
provider={}
path="C:/boss/"  # 需要確保jsbeautify.js在該目錄下,中間過程文件、最終文件都在該目錄下
for i in range(10):
    url="https://www.zhipin.com/c101280600/?query=python&page="+str(i)+"&ka=page-"+str(i)
    text=bossSpider(url,provider)
    result=extractJobInfo(text)
    resultFileName=path+"result.txt"
    with open(resultFileName,'a',encoding='UTF-8') as f:
        f.write(result)

結(jié)果示例

解毒過程說明

以下說明過程都以temp.js為例,temp.js 是 對3d0a2109.js進行最簡化處理后的核心代碼

debugger投毒處理

在調(diào)試時會頻繁出現(xiàn)debugger斷點,這是js中進行斷點投毒,其中有定時任務(wù)頻繁執(zhí)行,無敵循環(huán)debugger,嚴重影響本地調(diào)試。

需要對temp.js 中全局變量 _0x1c69['0x3af'-0x0]的值進行替換 (全局變量名稱會根據(jù)js的不同而不同)

只需要將js中的固定字符串 YkZGNWFtTjJhWHhrWTFGU0lIQnJkR2NnZENBeUlHTjRiQ0E1ZEdsNFVWSXdJSGQ0ZFdwNmVuaG5NQ0JuZUdscVoyTWdZM2hzSURsMGFYaFJVaUJXSUhRZ015QmFXVmt3Y2xGU1VnPT0=

替換為YkZGNWFtTjJhWHhrWTFGU0lIQnJkR2NnZENBeUlHTjRiQ0E1ZEdsNFVWSXdJQ0FnSUNBZ0lDQWdNQ0JuZUdscVoyTWdZM2hzSURsMGFYaFJVaUJXSUhRZ015QmFXVmt3Y2xGU1VnPT0=

解釋說明

常量池中原始字符串

_0x1c69['0x3af'-0x0]="YkZGNWFtTjJhWHhrWTFGU0lIQnJkR2NnZENBeUlHTjRiQ0E1ZEdsNFVWSXdJQ0FnSUNBZ0lDQWdNQ0JuZUdscVoyTWdZM2hzSURsMGFYaFJVaUJXSUhRZ015QmFXVmt3Y2xGU1VnPT0=";

經(jīng)過一次base64接碼后

_0x25ec['mdGvrp']['0x3af'-0x0]="bFF5amN2aXxkY1FSIHBrdGcgdCAyIGN4bCA5dGl4UVIwICAgICAgICAgMCBneGlqZ2MgY3hsIDl0aXhRUiBWIHQgMyBaWVkwclFSUg==";

這個比較特殊還需要再經(jīng)過一次解密才能得到明文,也就是在調(diào)試時看到那個拼命無敵debugger

(function() {var a = new Date(); debugger; return new Date() - a > 100;}()) 

為了優(yōu)雅的搞掉他,我對解密函數(shù)進行了你想,寫了加密函數(shù)encry,然后將明文中的debugger替換成空格,則給base64編碼得到無毒的字符串常量用于替換

btoa(btoa(encry("(function() {var a = new Date();         ; return new Date() - a > 100;}())")));//去掉debugger  注意要兩次base64加密

//YkZGNWFtTjJhWHhrWTFGU0lIQnJkR2NnZENBeUlHTjRiQ0E1ZEdsNFVWSXdJQ0FnSUNBZ0lDQWdNQ0JuZUdscVoyTWdZM2hzSURsMGFYaFJVaUJXSUhRZ015QmFXVmt3Y2xGU1VnPT0=

本地調(diào)試時只需進行這一步替換就能解決到debugger問題了。

域名投毒處理

temp.js的 1572行,涉及域名投毒,本地調(diào)試或執(zhí)行js時獲取域名是不對的,而這個域名是需要參與最終cookie計算的,所以需要處理,處理方式比較暴力簡單,強制寫成正確的域名即可,把其余代碼注釋掉。

case '4': _0x476e10 ="https://www.zhipin.com/";
            //_0x476e10 = ((typeof top) == ( _0x261f6f[_0x40b4('0x345')])) && ((typeof top[_0x40b4('0x199')]) == ( _0x261f6f[_0x40b4('0x345')])) && ((/\w+:\/\/(.*?)\//[_0x40b4('0x37c')](top[_0x40b4('0x199')][_0x40b4('0x8b')])) != ( null)) ? /\w+:\/\/(.*?)\//[_0x40b4('0x37c')](top[_0x40b4('0x199')][_0x40b4('0x8b')])[0x0] : _0x261f6f[_0x40b4('0x3ab')];

typeof 類的投毒處理(按調(diào)用順序處理)

在js中會對系統(tǒng)環(huán)境進行多種判斷,如果識別為非正常瀏覽就會投毒!

這些判斷包括windows、document、global、title、setInterval等,為了可讀性我進行了操作符函數(shù)替換后再做的如下處理(其實可以不用進行函數(shù)替換的),主要投毒位置記錄如下,具體代碼處理時的策略和下方標注略有差異。

74行

74行判斷條件需要保證為true 或者 干掉投毒函數(shù)setInterval_ =false,ss 第9位置1

注釋79-84行

        Date[ptt][gm] = function() { //投毒 
修改Data.prototype.getMonth()函數(shù)固定返回12
            return ((((0x1) << ( 0x3))) | ( 0x4));
        }
        ;
        setInterval_ = ![];
        ss = ((ss) | ( ((0x1) << ( 0x8))));

以上代碼全部注釋掉

214 行

214行判斷global process 等對象,不存在則投毒 ss 0xc+1置1

注釋215行

原始代碼

if (((typeof global) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof process) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof child_process) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof Buffer) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof sessionStorage) == ( _0x2629ad[_0x2b8b('0x173')]))) {//干掉或注釋下面的內(nèi)容
        ss = ((ss) | ( ((0x1) << ( 0xc))));
    }

解析后

    if (((typeof global) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof process) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof child_process) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof Buffer) != ( _0x2629ad[_0x2b8b('0x173')])) || ((typeof sessionStorage) == ( _0x2629ad[_0x2b8b('0x173')]))) {
        ss = ((ss) | ( ((0x1) << ( 0xc))));
    }

284-287行

284-287注釋掉 ss = ss | 31;//1-5位置1

  1. 284行判斷瀏覽器類型 確保執(zhí)行 ss = ss | (0x1 << 0x1) 倒數(shù)第2位置1,否則0xf+1位投毒置1

    原始代碼

    ((typeof window) == ( _0x2c63cc[_0x2b8b('0x395')])) && ((typeof window[_0x2b8b('0xf4')]) == ( _0x2c63cc[_0x2b8b('0x395')])) ? ((typeof window[_0x2b8b('0xf4')][_0x2b8b('0xac')]) == ( _0x2c63cc[_0x2b8b('0x11c')])) ? /phantomjs/[_0x2b8b('0x32d')](window[_0x2b8b('0xf4')][_0x2b8b('0xac')][_0x2b8b('0x407')]()) ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : window[_0x2b8b('0xf4')][_0x2b8b('0x1a8')] ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0x1)))) : ((typeof window[_0x2b8b('0xf4')][_0x2b8b('0x8')]) == ( _0x2c63cc[_0x2b8b('0x116')])) ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : !window[_0x2b8b('0xf4')][_0x2b8b('0x8')] ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0xf))));
    

    解析后代碼

    ((typeof window) == ( "object")) && ((typeof window["navigator"]) == ( "object")) ? 
      ((typeof window["navigator"]["userAgent"]) == ( "string")) ? 
          /phantomjs/["test"](window["navigator"]["userAgent"]["toLowerCase"]()) ? 
             ss = ((ss) | ( ((0x1) << ( 0xf)))) 
             : window["navigator"]["webdriver"] ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0x1)))) 
         :((typeof window["navigator"]["appVersion"]) == ( "undefined")) ? 
             ss = ((ss) | ( ((0x1) << ( 0xf)))) 
             : !window["navigator"]["appVersion"] ? ss = ((ss) | ( ((0x1) << ( 0xf)))) : ss = ((ss) | ( ((0x1) << ( 0xf)))) 
     : ss = ((ss) | ( ((0x1) << ( 0xf))));
    
  2. 285行判斷屏幕 確保執(zhí)行 ss = ((ss) | ( ((0x1) << ( 0x2)))) 第3位置1,否則0x10+1位投毒置1

    原始代碼

    ((typeof window) == ( _0x2c63cc[_0x2b8b('0x395')])) && ((typeof window[_0x2b8b('0x30c')]) == ( _0x2c63cc[_0x2b8b('0x395')])) ? ((((window[_0x2b8b('0x30c')][_0x2b8b('0x426')]) + ( window[_0x2b8b('0x30c')][_0x2b8b('0x124')]))) > ( 0x0)) ? ss = ((ss) | ( ((0x1) << ( 0x2)))) : ss = ((ss) | ( ((0x1) << ( 0x10)))) : ss = ((ss) | ( ((0x1) << ( 0x10))));
    

    解析后代碼

    ((typeof window) == ( "object")) && ((typeof window["screen"]) == ( "object")) ? 
         ((((window["screen"]["availHeight"]) + ( window["screen"]["availWidth"]))) > ( 0x0)) ?
             ss = ((ss) | ( ((0x1) << ( 0x2)))) 
             : ss = ((ss) | ( ((0x1) << ( 0x10))))
         : ss = ((ss) | ( ((0x1) << ( 0x10))));
    
  3. 286行判斷getElementById 確保執(zhí)行 ss = ((ss) | ( ((0x1) << ( 0x3)))) 第4位置1,否則0x11+1位投毒置1

    原始代碼

    ((typeof document) == ( _0x2c63cc[_0x2b8b('0x395')])) && ((typeof document[_0x2b8b('0x3dd')]) == ( fff)) ? !document[_0x2b8b('0x3dd')](_0x2c63cc[_0x2b8b('0x367')]) ? ss = ((ss) | ( ((0x1) << ( 0x3)))) : ss = ((ss) | ( ((0x1) << ( 0x11)))) : ss = ((ss) | ( ((0x1) << ( 0x11))));
    

    解析后代碼

    ((typeof document) == ( "object")) && ((typeof document["getElementById"]) == ( fff)) ? 
         !document["getElementById"]("glcanvaxs") ? 
             ss = ((ss) | ( ((0x1) << ( 0x3)))) 
             : ss = ((ss) | ( ((0x1) << ( 0x11))))
         : ss = ((ss) | ( ((0x1) << ( 0x11))));
    
  4. 287行判斷phantom 確保執(zhí)行ss = ((ss) | ( ((0x1) << ( 0x4)))) 第5位置1,否則0x12+1位投毒置1

    原始代碼

     ((typeof window) == ( _0x2c63cc[_0x2b8b('0x395')])) ? ((typeof window[_0x2b8b('0x55')]) == ( _0x2c63cc[_0x2b8b('0x116')])) ? ss = ((ss) | ( ((0x1) << ( 0x4)))) : ss = ((ss) | ( ((0x1) << ( 0x12)))) : ((typeof window) == ( _0x2c63cc[_0x2b8b('0x395')])) && ((typeof window[_0x2b8b('0x34b')]) == ( _0x2c63cc[_0x2b8b('0x116')])) ? ss = ((ss) | ( ((0x1) << ( 0x12)))) : ss = ((ss) | ( ((0x1) << ( 0x12))));
    

    解析后代碼

    ((typeof window) == ( "object")) ? 
         ((typeof window["callPhantom"]) == ( "undefined")) ? 
             ss = ((ss) | ( ((0x1) << ( 0x4)))) 
             : ss = ((ss) | ( ((0x1) << ( 0x12)))) 
         : ((typeof window) == ( "object")) && ((typeof window["_phantom"]) == ( "undefined")) ? 
             ss = ((ss) | ( ((0x1) << ( 0x12)))) 
             : ss = ((ss) | ( ((0x1) << ( 0x12))));
    

206 行

  1. 判斷window對象 是this 確保執(zhí)行ss = ((ss) | ( 0x1)) 第1位置1,否則0xb+1位投毒置1,并debugger隨機生成字符串
    注意這段代碼在定時器中,會定時觸發(fā)執(zhí)行

206 207注釋掉,前面插入 ss = ss|0x1;

原始代碼

        ((typeof window) != ( _0x1e3f0e[_0x2b8b('0x241')])) && ((this) == ( window)) ? ss = ((ss) | ( 0x1)) : (ss = ((ss) | ( ((0x1) << ( 0xb)))),
        _0x327c14 = _0x2ce1dc());

解析后代碼

((typeof window) != "undefined") && ((this) == ( window)) ?
    ss = ((ss) | ( 0x1)) 
    : (ss = ((ss) | ( ((0x1) << ( 0xb)))),_0x327c14 = _0x2ce1dc());

297-300 275行

297 298 300 275行代碼判斷橫屏豎屏及調(diào)試 outer-inner如果<=160 為false ,否則為true window不存在也是true 好像沒啥影響
定時觸發(fā)

297-300行

為避免麻煩,兩個都確保為true,進入300行if判斷的else分支 var _0x5c72b8 =true;//
var _0x6c7431 =true;//

if分支會將ss的0x13+1位置1 100010000000000000011111

var _0x5c72b8 = ((typeof window) == ( _0x5ecff7[_0x2b8b('0x1e9')])) ? ((((window[_0x2b8b('0x392')]) - ( window[_0x2b8b('0x1cb')]))) > ( _0x59c9ca)) : ![];
var _0x6c7431 = ((typeof window) == ( _0x5ecff7[_0x2b8b('0x1e9')])) ? ((((window[_0x2b8b('0x306')]) - ( window[_0x2b8b('0x19e')]))) > ( _0x59c9ca)) : ![];
var _0x4f3ce8 = _0x5c72b8 ? _0x5ecff7[_0x2b8b('0x21f')] : _0x5ecff7[_0x2b8b('0x21d')]; //width差值大于160為vertical,否則horizontal
if (!((_0x6c7431) && ( _0x5c72b8)) && (((typeof window) == ( _0x5ecff7[_0x2b8b('0x1e9')])) && ((typeof window[_0x2b8b('0x157')]) == ( _0x5ecff7[_0x2b8b('0x1e9')])) && ((typeof window[_0x2b8b('0x157')][_0x2b8b('0x2c8')]) == ( _0x5ecff7[_0x2b8b('0x1e9')])) && window[_0x2b8b('0x157')][_0x2b8b('0x2c8')][_0x2b8b('0x1dd')] || _0x5c72b8 || _0x6c7431))    

解析后為

var _0x5c72b8 = ((typeof window) == ( "object")) ? ((((window["outerWidth"]) - ( window['innerWidth'))) > ( _0x59c9ca)) : ![];
var _0x6c7431 = ((typeof window) == ( "object")) ? ((((window["outerHeight"]) - ( window['innerHeight']))) > ( _0x59c9ca)) : ![];  if (! var _0x4f3ce8 = _0x5c72b8 ? _0x5ecff7[_0x2b8b('0x21f')] : _0x5ecff7[_0x2b8b('0x21d')]; //width差值大于160為vertical,否則horizontal
if (!((_0x6c7431) && ( _0x5c72b8))
    && (((typeof window) == ("object")) 
        && ((typeof window["Firebug"]) == ("object")) 
        && ((typeof window["Firebug"]["chrome"]) == ("object")) 
        && window["Firebug"]["chrome"]["isInitialized"] || _0x5c72b8 || _0x6c7431))

275行

如果是調(diào)試狀態(tài)會調(diào)用事件發(fā)布,執(zhí)行275處的相關(guān)window判斷, 確保執(zhí)行 0x1) << ( 0xd)

((typeof window) != ( _0x5b9200[_0x2b8b('0x361')])) && ((typeof window[_0x2b8b('0x327')]) == ( fff)) && ((typeof CustomEvent) == ( fff)) ? window[_0x2b8b('0x327')](new CustomEvent(_0x5b9200[_0x2b8b('0xe0')],_0x4fa1b3)) : ss = ((ss) | ( ((0x1) << ( 0xd))));

解析后

((typeof window) != "undefined") && ((typeof window["dispatchEvent"]) == ("function")) && ((typeof CustomEvent) == ( "function")) ?
    window["dispatchEvent"](new CustomEvent("bcr_change",_0x4fa1b3)) 
    : ss = ((ss) | ( ((0x1) << ( 0xd))));

調(diào)用new ABC().z()方法觸發(fā)的投毒

接下來的方法都是調(diào)用main()之后觸發(fā)

349行

349行 給ts,seed賦值 確保三元運算符為false,或者截取: 前的代碼

(_0x327c14 = _0x33b55a,_0x46dced = _0x3220f9); 其他注釋掉

((typeof document) == ( _0x15044d[_0x2b8b('0x1f5')])) && ((typeof document[_0x2b8b('0x1b3')]) != ( _0x15044d[_0x2b8b('0x1fc')])) ? _0x327c14 = _0x2ce1dc() : (_0x327c14 = _0x33b55a,
            _0x46dced = _0x3220f9);

解析后為

((typeof document) == "object") && ((typeof document["title"]) != ( "string")) ?
    _0x327c14 = _0x2ce1dc() : //生成一個隨機字符串,44位,最后一位為=,下毒
    (_0x327c14 = _0x33b55a, _0x46dced = _0x3220f9);//第一個賦值seed,第二個賦值ts

1739行

1739行 判斷moveBy 確保執(zhí)行((ss) | ( ((0x1) << ( 0x5)))) 第6位置1,否則0x14+1位投毒置1,并debugger隨機生成字符串

ss = ((ss) | ( ((0x1) << ( 0x5)))); 其余注釋掉

((typeof window) == ( _0x578d43[_0x2b8b('0x3f3')])) && /function|object/[_0x2b8b('0x32d')](typeof window[_0x2b8b('0x152')]) ? ss = ((ss) | ( ((0x1) << ( 0x5)))) : (ss = ((ss) | ( ((0x1) << ( 0x14)))),
            _0x327c14 = _0x2ce1dc());

解析后為

((typeof window) == "object") && /function|object/["test"](typeof window["moveBy"]) ? 
    ss = ((ss) | ( ((0x1) << ( 0x5)))) 
    : (ss = ((ss) | ( ((0x1) << ( 0x14)))),_0x327c14 = _0x2ce1dc());

1407行

1407行 判斷window.open是函數(shù) 確保執(zhí)行 ss| (0x1<< 0x6) 第7位置1,否則0x15+1投毒置1,并將seed置為隨機字符串

ss=ss| (0x1<< 0x6); 其余注釋掉 兩行
下面的特殊字符影響markdown顯示 xxxx 刪掉即可

!!(((typeof window) == ( _0x578d43[_0x2b8b('0x3f3')])) && (((window[_0x2b8b('0x412')]) + ( ''))[_0x2b8b('0x70')]()[_0x2b8b('0x386')](/function\s*([^(]*)\xxxx(/)!=null)) ? ss = ((ss) | ( ((0x1) << ( 0x6)))) : (_0x327c14 = _0x2ce1dc(),
            ss = ((ss) | ( ((0x1) << ( 0x15)))));

解析后為

!!((typeof window == "object") && ((window["open"]+ '')["toString"]()["match"](/function\s*([^(]*)\xxxx(/)!=null)) ? 
    ss = (ss| ( (0x1<< 0x6)))
    : (_0x327c14 = _0x2ce1dc(),ss = (ss| ( 0x1 << 0x15)));

1699行

1699行 判斷document.createElement可用 確保執(zhí)行ss = ((ss) | ( ((0x1) << ( 0x7)))) 第8位置1,否則0x17投毒置1

ss = ((ss) | ( ((0x1) << ( 0x7)))) ; 注釋掉其余的

((typeof document) == ( _0x578d43[_0x2b8b('0x3f3')])) && ((typeof document[_0x2b8b('0x391')]) == ( fff)) && ((typeof (caption = document[_0x2b8b('0x391')](_0x578d43[_0x2b8b('0x2e8')]))) == ( _0x578d43[_0x2b8b('0x3f3')])) && ((caption[_0x2b8b('0x24f')]) == ( _0x578d43[_0x2b8b('0x302')])) ? ss = ((ss) | ( ((0x1) << ( 0x7)))) : ss = ((ss) | ( ((0x1) << ( 0x16))));
            

解析后為

((typeof document) == ("object")) && ((typeof document["createElement"]) == ( fff)) && ((typeof (caption = document["createElement"]("caption"))) == ( "object")) && ((caption["tagName"]) == ( "CAPTION")) ? 
    ss = ((ss) | ( ((0x1) << ( 0x7)))) 
    : ss = ((ss) | ( ((0x1) << ( 0x16))));

1543行

1543行 判斷window["moveTo"] 確保執(zhí)行 ss = ((ss) | ( ((0x1) << ( 0x0)))),第0位置1,否則投毒0xe+1置1

ss = ((ss) | ( ((0x1) << ( 0x0))))

            !!(((typeof window) == ( _0x578d43[_0x2b8b('0x3f3')])) && window[_0x2b8b('0x137')]) ? ss = ((ss) | ( ((0x1) << ( 0x0)))) : (_0x327c14 = _0x2ce1dc(),
            ss = ((ss) | ( ((0x1) << ( 0xe)))));

解析后為

!!(((typeof window) == "object") && window["moveTo"]) ? 
                ss = ((ss) | ( ((0x1) << ( 0x0)))) 
                : (_0x327c14 = _0x2ce1dc(), ss = ((ss) | ( ((0x1) << ( 0xe)))));

88行

88行判斷setInterval需要處理,否則官方定義會投毒 Date.getMonth

強制進入if分支,定義setIntervalnew為空函數(shù)

if(true){//if (typeof setInterval == _0x2b8b('0x2ed') && setInterval[_0x2b8b('0x70')]()[_0x2b8b('0x1db')](/\s+/g, '') == _0x2b8b('0x2d3') || typeof setInterval == _0x2b8b('0x28')) {//setInterval未被替換
    setIntervalnew=function(a,b){};//setIntervalnew = setInterval;
}

20行

第20行有一處 window 不用處理
處理windows.atob()函數(shù),如果系統(tǒng)不存在該函數(shù) 官方代碼自定義了算法,所以無需處理

到這里投毒類就都處理完成了

js核心流程說明

通過逆向,發(fā)現(xiàn)js的核心流程就是一個AES加密算法。

加密模式ECB,填充方式pkcs5padding,數(shù)據(jù)塊128位,密碼4e227197bf508bab,偏移量4e227197bf508bab,輸出base64,字符集gb2312

代碼核心流程

  1. 生成秘鑰

    No. 1.1 將 zz5IntArray_0x4c34d0 的前17位賦值,初始化后對應(yīng)字符串 '4e227197bf508bab' 第17位對應(yīng)整數(shù)0,對應(yīng)原始代碼5890行左右,關(guān)鍵字[0xf],下斷點,即可得到秘鑰

  2. 生成要加密的原文

    No. 1.2 plainText_0x5bd912 為seed+'@'+"https://www.zhipin.com/@1587351053@8388833"+'@9a04'轉(zhuǎn)成的整型數(shù)組追加 4個4 和 1個0 湊夠97位
    "hQGTnOz8GDIwnLC+cm6m7eUC65EOVoF3GtqnngpvfN8="+'@'+"https://www.zhipin.com/@1587351053@8388833"+'@9a04'

  3. 生成每次輪函數(shù)要是用的輪密鑰

    No. 1.3 調(diào)用函數(shù)根據(jù) zz5IntArray_0x4c34d0 生成復(fù)雜對象 complex2Array_0x1a6b6f 包含二維數(shù)組
    complex2Array_0x1a6b6f 中a開頭的屬性為數(shù)組結(jié)構(gòu),i開頭的元素為整數(shù)

  4. 將明文分組為6組,每組128位,循環(huán)對每一組進行AES加密

    No. 1.4 循環(huán)調(diào)用函數(shù)生成 zz97IntArray_0x14daee, 值來源于plainText_0x5bd912每次取16個進行處理,循環(huán)6次,complex2Array_0x1a6b6f 也參與計算

    • 1.4.1 調(diào)用核心代碼encrypted16Reel_0x5de6e9 加密
      • No. 1.4.1.1 首次輪秘鑰加,每次計算4個元素組成一個32位數(shù)
      • No. 1.4.1.2 輪函數(shù) 循環(huán)10次處理a2中的數(shù)組
  5. 最后一輪的加密結(jié)果進行擴展生成128位密文整數(shù)數(shù)組

    No. 1.5 調(diào)用函數(shù)生成 zz5IntArray_0x4c34d0,值來源于 zz97IntArray_0x14daee 97位數(shù)組用于擴展生成128位zz整數(shù)數(shù)組 3變4

  6. 將密文按照ASCII碼轉(zhuǎn)成字符,并增加四個字符的前綴,也就最終cookie前身(再做一次encodeURIComponent就是 __zp_stoken__了)

    No. 1.6 生成zz也就是token值 前四位固定,后面由zz5IntArray_0x4c34d0按照ASCII碼轉(zhuǎn)變而來

    前四位獲取位置,原始代碼約789行 關(guān)鍵詞zz = zz = _0x1ddb83[_0x2241('0x2d0')];; 756行_0x5c82f4[_0x2241('0x2d0')] = _0x2241('0xf');

擴展思路

其實完全可以通過 js 獲取固定的秘鑰 和 固定的四字符前綴(js不同 秘鑰、前綴也不同),然后在Python中使用標準AES算法直接計算__zp_stoken__值。有興趣的同學可去實現(xiàn)!

通過官方j(luò)s解析出秘鑰和前綴 秘鑰關(guān)鍵詞[0xf] 大部分是常量賦值,但有一部分是通過date計算,前綴關(guān)鍵詞zz = 通過一次函數(shù)替換由常量池獲取,注意常量池有偏移量

自己通過AES加密獲取__zp_stoken__
明文 seed+'@'+"https://www.zhipin.com/@+ts/1000+@8388833"+@+四位前綴
token=encodeURICompent(四位前綴+生成的密文)

幾個變量說明

  • _0x14daee 97位數(shù)組用于擴展生成128位zz整數(shù)數(shù)組 3變4
  • complex2Array.a7 .a8 存放原始秘鑰
  • .a9 臨時存放
  • .a2 輪秘鑰 a2[0]存放原始秘鑰 4個拼成一個
  • .a3 逆序輪秘鑰 a3[10]存放原始秘鑰 4個拼成一個

關(guān)于算法的幾點疑問

在逆向中存在以下幾點疑問,知道的同學還望不吝賜教,我感激不盡!

  • 標準AES算法中在進行字節(jié)代換時使用的是一個S盒,通過8位二進制的高、低四位形成行、列坐標,然后在S盒中獲取替代值,只有一個S盒而且S盒中的值都是小于<256的,但在temp.js中存在四個S盒,而且S盒中存放的是32位數(shù)(有正有負數(shù)),為什么會有四個S盒?

  • 在標準AES算法中在進行列混淆時,是通過一個常量矩陣相乘進行混淆的,而在temp.js中沒看到常量矩陣的影子或者它使用的是全1的常量矩陣,這與標準的AES算法也對應(yīng)不上,不知道這個常量矩陣和上面的四個S盒做了何種融合?(因為最終的計算結(jié)果和標準AES加密一致)

  • 根據(jù)加密的調(diào)用次數(shù)不同,最終生成的__zp_stoken__也不完全相同,但都可以使用

    • 經(jīng)過觀察第一次調(diào)用生成是唯一
    • 連著調(diào)用第二次另外生成一個__zp_stoken__值,后續(xù)調(diào)用基本都是這個 __zp_stoken__
    • 偶爾會出現(xiàn)第三個__zp_stoken__
    • 這三種情況產(chǎn)生的__zp_stoken__都可以正常使用

相關(guān)代碼文件說明

  • analysis 分析的過程文件目錄
    • test.html 進行本地調(diào)試使用的頁面
    • temp.js 對3d0a2109.js進行最簡化處理后的核心代碼,進行了詳細標注
    • 3d0a2109.js 經(jīng)過chrome格式化后的原始js,以這個js為案例代碼進行逆向分析處理,python涉及行數(shù)的都是以這個原始js為藍本為基礎(chǔ)的
    • temp-3d0a2109.js temp-3d0a2109-v2.js temp-3d0a2109-v3.js 逆向分析的中間過程文件
  • jsbeautify.js python運行時需要調(diào)用的js,用于js代碼格式化
  • XBossSpider.py 最終python代碼
  • xxxxxxxx.js 運行時抓取的原始js文件
  • xxxxxxxx-step1.js 對原始js進行格式化處理后的js
  • xxxxxxxx-step2.js 對原始js進行運算符函數(shù)替換后的js
  • xxxxxxxx-step3.js 進行解毒處理后的js,也是最終python要執(zhí)行的js文件
  • result.txt 抓取后提取的職位信息,每行一個職位,每行職位的字段用 tab 分隔
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內(nèi)容