近日出于學習的目的對某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)站正常訪問流程
某Boss網(wǎng)站在訪問頁面時都需要攜帶一個
__zp_stoken__
=xxxxxx 這樣的cookie-
每次訪問頁面都會更新這個
__zp_stoken__
,(另外,每次__zp_stoken__
其實可以使用數(shù)次(貌似<5次),具體次數(shù)沒有試驗)- 返回的頁面會攜帶
__zp_sseed__
、__zp_sname__
、__zp_snts__
三個cookie (通過查看response 的 set-cookie部分) - 基于這三個cookie值計算新的
__zp_stoken__
存入cookie中 - 這三個cookie值消費完后會即刻刪除,所以在瀏覽器中一般看不到這三個cookie值
- 返回的頁面會攜帶
-
如果不攜帶cookie
__zp_stoken__
或者__zp_stoken__
無效,就會返回一個302跳轉(zhuǎn)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扁平化
爬取方案
爬取主流程
爬取方案的思路為
- 訪問頁面獲取302跳轉(zhuǎn)的Location,提取seed name ts核心參數(shù)
- 根據(jù)name請求對應(yīng)加密js文件
- 使用jsbeautify對加密js文件進行格式化
- 對加密js進行運算法替換以及解毒處理
- 調(diào)用js生成
__zp_stoken__
- 通過
__zp_stoken__
爬取指定列表頁數(shù)據(jù) - 解析頁數(shù),提取職位信息追加到result.txt中
- 解析頁面響應(yīng)的頭部信息,獲取seed name ts
- 根據(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
-
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))));
-
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))));
-
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))));
-
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 行
- 判斷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
代碼核心流程
-
生成秘鑰
No. 1.1 將 zz5IntArray_0x4c34d0 的前17位賦值,初始化后對應(yīng)字符串 '4e227197bf508bab' 第17位對應(yīng)整數(shù)0,對應(yīng)原始代碼5890行左右,關(guān)鍵字[0xf],下斷點,即可得到秘鑰
-
生成要加密的原文
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' -
生成每次輪函數(shù)要是用的輪密鑰
No. 1.3 調(diào)用函數(shù)根據(jù) zz5IntArray_0x4c34d0 生成復(fù)雜對象 complex2Array_0x1a6b6f 包含二維數(shù)組
complex2Array_0x1a6b6f 中a開頭的屬性為數(shù)組結(jié)構(gòu),i開頭的元素為整數(shù) -
將明文分組為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ù)組
- 1.4.1 調(diào)用核心代碼encrypted16Reel_0x5de6e9 加密
-
最后一輪的加密結(jié)果進行擴展生成128位密文整數(shù)數(shù)組
No. 1.5 調(diào)用函數(shù)生成 zz5IntArray_0x4c34d0,值來源于 zz97IntArray_0x14daee 97位數(shù)組用于擴展生成128位zz整數(shù)數(shù)組 3變4
-
將密文按照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 分隔