為惠濟等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計制作服務,及惠濟網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務為成都網(wǎng)站設(shè)計、網(wǎng)站制作、惠濟網(wǎng)站設(shè)計,以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務,秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務。我們深信只要達到每一位用戶的要求,就會得到認可,從而選擇與我們長期合作。這樣,我們也可以走得更遠!
關(guān)注微信公眾號:K哥爬蟲,持續(xù)分享爬蟲進階、JS/安卓逆向等技術(shù)干貨!
本文章中所有內(nèi)容僅供學習交流,抓包內(nèi)容、敏感網(wǎng)址、數(shù)據(jù)接口均已做脫敏處理,嚴禁用于商業(yè)用途和非法用途,否則由此產(chǎn)生的一切后果均與作者無關(guān),若有侵權(quán),請聯(lián)系我立即刪除!
題目本身不是很難,但是其中有很多坑,主要是反 Hook 操作和本地聯(lián)調(diào)補環(huán)境,本文會詳細介紹每一個坑,并不只是一筆帶過,寫得非常詳細!
通過本文你將學到:
首先觀察到點擊翻頁,URL 并沒有發(fā)生變化,那么一般就是 Ajax 請求,每一次請求有些參數(shù)會改變,熟練的按下 F12 準備查找加密參數(shù),會發(fā)現(xiàn)立馬斷住,進入無限 debugger 狀態(tài),往上跟一個棧,可以發(fā)現(xiàn) debugger 字樣,如下圖所示:
這種情況在K哥以前的案例中也有,當時我們是直接重寫這個 JS,把 debugger 字樣給替換掉就行了,但是本題很顯然是希望我們以 Hook 的方法來過掉無限 debugger,除了 debugger 以外,我們注意到前面還有個 constructor 字樣,在 JavaScript 中它叫構(gòu)造方法,一般在對象創(chuàng)建或者實例化時候被調(diào)用,它的基本語法是:constructor([arguments]) { ... }
,詳細介紹可參考 MDN 構(gòu)造方法,在本案例中,很明顯 debugger 就是 constructor 的 arguments 參數(shù),因此我們可以寫出以下 Hook 代碼來過掉無限 debugger:
// 先保留原 constructor
Function.prototype.constructor_ = Function.prototype.constructor;
Function.prototype.constructor = function (a) {
// 如果參數(shù)為 debugger,就返回空方法
if(a == "debugger") {
return function (){};
}
// 如果參數(shù)不為 debugger,還是返回原方法
return Function.prototype.constructor_(a);
};
注入 Hook 代碼的方法也有很多,比如直接在瀏覽器開發(fā)者工具控制臺輸入代碼(刷新網(wǎng)頁會失效)、Fiddler 插件注入、油猴插件注入、自寫瀏覽器插件注入等,這些方法在K哥以前的文章都有介紹,今天就不再贅述。
本次我們使用 Fiddler 插件注入,注入以上 Hook 代碼后,會發(fā)現(xiàn)會再次進入無限 debugger,setInterval,很明顯的定時器,他有兩個必須的參數(shù),第一個是要執(zhí)行的方法,第二個是時間參數(shù),即周期性調(diào)用方法的時間間隔,以毫秒為單位,詳細介紹可參考菜鳥教程 Window setInterval(),同樣我們也可以將其 Hook 掉:
// 先保留原定時器
var setInterval_ = setInterval
setInterval = function (func, time){
// 如果時間參數(shù)為 0x7d0,就返回空方法
// 當然也可以不判斷,直接返回空,有很多種寫法
if(time == 0x7d0)
{
return function () {};
}
// 如果時間參數(shù)不為 0x7d0,還是返回原方法
return setInterval_(func, time)
}
將兩段 Hook 代碼粘貼到瀏覽器插件里,開啟 Hook,重新刷新頁面就會發(fā)現(xiàn)已經(jīng)過掉了無限 debugger。
過掉無限 debugger 后,我們隨便點擊一頁,抓包可以看到是個 POST 請求,F(xiàn)orm Data 里,page
是頁數(shù),count
是每一頁數(shù)據(jù)量,_signature
是我們要逆向的參數(shù),如下圖所示:
我們直接搜索 _signature
,只有一個結(jié)果,其中有個 window.get_sign()
方法就是設(shè)置 _signature
的函數(shù),如下圖所示:
這里問題來了?。?!我們再看看本題的題目,JS 混淆加密,反 Hook 操作,作者也再三強調(diào)本題是考驗 Hook 能力!并且到目前為止,我們好像還沒有遇到什么反 Hook 手段,所以,這樣直接搜索 _signature
很顯然太簡單了,肯定是要通過 Hook 的方式來獲取 _signature
,并且后續(xù)的 Hook 操作肯定不會一帆風順!
話不多說,我們直接寫一個 Hook window._signature
的代碼,如下所示:
(function() {
//嚴謹模式 檢查所有錯誤
'use strict';
//window 為要 hook 的對象,這里是 hook 的 _signature
var _signatureTemp = "";
Object.defineProperty(window, '_signature', {
//hook set 方法也就是賦值的方法
set: function(val) {
console.log('Hook 捕獲到 _signature 設(shè)置->', val);
debugger;
_signatureTemp = val;
return val;
},
//hook get 方法也就是取值的方法
get: function()
{
return _signatureTemp;
}
});
})();
將兩個繞過無限 debugger 的 Hook 代碼,和這個 Hook _signature
的代碼一起,使用 Fiddler 插件一同注入(這里注意要把繞過 debugger 的代碼放在 Hook _signature
代碼的后面,否則有可能不起作用,這可能是插件的 BUG),重新刷新網(wǎng)頁,可以發(fā)現(xiàn)前端的一排頁面的按鈕不見了,打開開發(fā)者工具,可以看到右上角提示有兩個錯誤,點擊可跳轉(zhuǎn)到出錯的代碼,在控制臺也可以看到報錯信息,如下圖所示:
整個 1.js 代碼是經(jīng)過了 sojson jsjiami v6 版本混淆了的,我們將里面的一些混淆代碼在控制臺輸出一下,然后手動還原一下這段代碼,有兩個變量 i1I1i1li
和 illllli1
,看起來費勁,直接用 a
和 b
代替,如下所示:
(function() {
'use strict';
var a = '';
Object["defineProperty"](window, "_signature", {
set: function(b) {
a = b;
return b;
},
get: function() {
return a;
}
});
}());
是不是很熟悉?有 get 和 set 方法,這不就是在進行 Hook window._signature
操作嗎?整個邏輯就是當 set 方法設(shè)置 _signature
時,將其賦值給 a,get 方法獲取 _signature
時,返回 a,這么操作一番,實際上對于 _signature
沒有任何影響,那這段代碼存在的意義是啥?為什么我們添加了自己的 Hook 代碼就會報錯?
來看看報錯信息:Uncaught TypeError: Cannot redefine property: _signature
,不能重新定義 _signature
?我們的 Hook 代碼在頁面一加載就運行了 Object.defineProperty(window, '_signature', {})
,等到網(wǎng)站的 JS 再次 defineProperty
時就會報錯,那很簡單嘛,既然不讓重新定義,而且網(wǎng)站自己的 JS Hook 代碼不會影響 _signature
,直接將其刪掉不就行了嘛!這個地方大概就是反 Hook 操作了。
保存原 1.js 到本地,刪除其 Hook 代碼,使用 Fiddler 的 AutoResponder 功能替換響應(替換方法有很多,K哥以前的文章同樣有介紹),再次刷新發(fā)現(xiàn)異常解除,并且成功 Hook 到了 _signature
。
成功 Hook 之后,直接跟棧,直接把方法暴露出來了:window._signature = window.byted_acrawler(window.sign())
先來看看 window.sign()
,選中它其實就可以看到是 13 位毫秒級時間戳,我們跟進 1.js 去看看他的實現(xiàn)代碼:
我們將部分混淆代碼手動還原一下:
window["sign"] = function sign() {
try {
div = document["createElement"];
return Date["parse"](new Date())["toString"]();
} catch (IIl1lI1i) {
return "abcdefghigklmnopqrstuvwxyz";
}
}
這里就要注意了,有個坑給我們埋下了,如果直接略過,覺得就一個時間戳沒啥好看的,那你就大錯特錯了!注意這是一個 try-catch 語句,其中有一句 div = document["createElement"];
,有一個 HTML DOM Document 對象,創(chuàng)建了 div 標簽,這段代碼如果放到瀏覽器執(zhí)行,沒有任何問題,直接走 try 語句,返回時間戳,如果在我們本地 node 執(zhí)行,就會捕獲到 document is not defined
,然后走 catch 語句,返回的是那一串數(shù)字加字母,最后的結(jié)果肯定是不正確的!
解決方法也很簡單,在本地代碼里,要么去掉 try-catch 語句,直接 return 時間戳,要么在開頭定義一下 document,再或者直接注釋掉創(chuàng)建 div 標簽的這行代碼,但是K哥在這里推薦直接定義一下 document,因為誰能保證在其他地方也有類似的坑呢?萬一隱藏得很深,沒發(fā)現(xiàn),豈不是白費力氣了?
然后再來看看 window.byted_acrawler()
,return 語句里主要用到了 sign()
也就是 window.sign()
方法和 IIl1llI1()
方法,我們跟進 IIl1llI1()
方法可以看到同樣使用了 try-catch 語句,nav = navigator[liIIIi11('2b')];
和前面 div 的情況如出一轍,同樣的這里也建議直接定義一下 navigator,如下圖所示:
到這里用到的方法基本上分析完畢,我們將 window、document、navigator 都定義一下后,本地運行一下,會提示 window[liIIIi11(...)] is not a function
:
我們?nèi)ゾW(wǎng)頁里看看,會發(fā)現(xiàn)這個方法其實就是一個定時器,沒有太大作用,直接注釋掉即可:
經(jīng)過以上操作以后,再次本地運行,會提示 window.signs is not a function
,出錯的地方是一個 eval 語句,我們?nèi)g覽器看一下這個 eval 語句,發(fā)現(xiàn)明明是 window.sign()
,為什么本地就變成了 window.signs()
,平白無故多了個 s 呢?
造成這種情況的原因只有一個,那就是本地與瀏覽器的環(huán)境差異,混淆的代碼里肯定有環(huán)境檢測,如果不是瀏覽器環(huán)境的話,就會修改 eval 里的代碼,多加了一個 s,這里如果你直接刪掉包含 eval 語句的整個函數(shù)和上面的 setInterval 定時器,代碼也能正常運行,但是,K哥一向是追求細節(jié)的!多加個 s 的原因咱必須得搞清楚呀!
我們在本地使用 PyCharm 進行調(diào)試,看看到底是哪里給加了個 s,出錯的地方是這個 eval 語句,我們點擊這一行,下個斷點,右鍵 debug 運行,進入調(diào)試界面(PS:原代碼有無限 debugger,如果不做處理,PyCharm 里調(diào)試同樣也會進入無限 debugger,可以直接把前面的 Hook 代碼加到本地代碼前面,也可以直接刪除對應的函數(shù)或變量):
左側(cè)是調(diào)用棧,右側(cè)是變量值,整體上和 Chrome 里面的開發(fā)者工具差不多,詳細用法可參考 JetBrains 官方文檔,主要介紹一下圖中的 8 個按鈕:
我們點擊步入按鈕(Step Into),會進入到 function IIlIliii()
,這里同樣使用了 try-catch 語句,繼續(xù)下一步,會發(fā)現(xiàn)捕獲到了異常,提示 Cannot read property 'location' of undefined
,如下圖所示:
我們輸出一下各個變量的值,手動還原一下代碼,如下:
function IIlIliii(II1, iIIiIIi1) {
try {
href = window["document"]["location"]["href"];
check_screen = screen["availHeight"];
window["code"] = "gnature = window.byted_acrawler(window.sign())";
return '';
} catch (I1IiI1il) {
window["code"] = "gnature = window.byted_acrawlers(window.signs())";
return '';
}
}
這么一來,就發(fā)現(xiàn)了端倪,在本地我們并沒有 document、location、href、availHeight 對象,所以就會走 catch 語句,變成了 window.signs()
,就會報錯,這里解決方法也很簡單,可以直接刪掉多余代碼,直接定義為不帶 s 的那串語句,或者也可以選擇補一下環(huán)境,在瀏覽器里看一下 href 和 screen 的值,定義一下即可:
var window = {
"document": {
"location": {
"href": "http://spider.wangluozhe.com/challenge/1"
}
},
}
var screen = {
"availHeight": 1040
}
然后再次運行,又會提示 sign is not defined
,這里的 sign()
其實就是 window.sign()
,也就是下面的 window[liIIIi11('a')]
方法,任意改一種寫法即可:
再次運行,沒有錯誤了,我們可以自己寫一個方法來獲取 _signature
:以下寫法二選一,都可以:
function getSign(){
return window[liIIIi11('9')](window[liIIIi11('a')]())
}
function getSign(){
return window.byted_acrawler(window.sign())
}
// 測試輸出
console.log(getSign())
我們運行一下,發(fā)現(xiàn)在 Pycharm 里并沒有任何輸出,同樣的我們在題目頁面的控制臺輸出一下 console.log
,發(fā)現(xiàn)被置空了,如下圖所示:
看來他還對 console.log
做了處理,其實這種情況問題不大,我們直接使用 Python 腳本來調(diào)用前面我們寫的 getSign()
方法就能得到 _signature
的值了,但是,再次重申,K哥一向是追求細節(jié)的!我就得找到處理 console.log
的地方,把它變?yōu)檎#?/p>
這里我們?nèi)匀皇褂?Pycharm 來調(diào)試,進一步熟悉本地聯(lián)調(diào),在 console.log(getSign())
語句處下個斷點,一步一步跟進,會發(fā)現(xiàn)進到了語句 var IlII1li1 = function() {};
,查看此時變量值,發(fā)現(xiàn) console.log
、console.warn
等方法都被置空了,如下圖所示:
再往下一步跟進,發(fā)現(xiàn)直接返回了,這里有可能第一次運行 JS 時就會對 console 相關(guān)命令進行方法置空處理,所以先在疑似對 console 處理的方法里面下幾個斷點,再重新調(diào)試,會發(fā)現(xiàn)會走到 else 語句,然后直接將 IlII1li1 也就是空方法,賦值給 console 相關(guān)命令,如下圖所示:
定位到了問題所在,我們直接把 if-else 語句注釋掉,不讓它置空即可,然后再次調(diào)試,發(fā)現(xiàn)就可以直接輸出結(jié)果了:
調(diào)用 Python 攜帶 _signature 挨個計算每一頁的數(shù)據(jù),最終提交成功:
GitHub 關(guān)注 K 哥爬蟲,持續(xù)分享爬蟲相關(guān)代碼!歡迎 star !https://github.com/kgepachong/
以下只演示部分關(guān)鍵代碼,不能直接運行!完整代碼倉庫地址:https://github.com/kgepachong/crawler/
var window = {
"document": {
"location": {
"href": "http://spider.wangluozhe.com/challenge/1"
}
},
}
var screen = {
"availHeight": 1040
}
var document = {}
var navigator = {}
var location = {}
// 先保留原 constructor
Function.prototype.constructor_ = Function.prototype.constructor;
Function.prototype.constructor = function (a) {
// 如果參數(shù)為 debugger,就返回空方法
if(a == "debugger") {
return function (){};
}
// 如果參數(shù)不為 debugger,還是返回原方法
return Function.prototype.constructor_(a);
};
// 先保留原定時器
var setInterval_ = setInterval
setInterval = function (func, time){
// 如果時間參數(shù)為 0x7d0,就返回空方法
// 當然也可以不判斷,直接返回空,有很多種寫法
if(time == 0x7d0)
{
return function () {};
}
// 如果時間參數(shù)不為 0x7d0,還是返回原方法
return setInterval_(func, time)
}
var iil = 'jsjiami.com.v6'
, iiIIilii = [iil, '\x73\x65\x74\x49\x6e\x74\x65\x72\x76\x61\x6c', '\x6a\x73\x6a', ...];
var liIIIi11 = function(_0xe, _0x3cbe90) {
_0xe = ~~'0x'['concat'](_0xe);
var _0x636e4d = iiIIilii[_0xe];
return _0x636e4d;
};
(function(_0xd, _0xfd26eb) {
var _0x1bba22 = 0x0;
for (_0xfd26eb = _0xd['shift'](_0x1bba22 >> 0x2); _0xfd26eb && _0xfd26eb !== (_0xd['pop'](_0x1bba22 >> 0x3) + '')['replace'](/[fnwRwdGKbwKrRFCtSC=]/g, ''); _0x1bba22++) {
_0x1bba22 = _0x1bba22 ^ 0x661c2;
}
}(iiIIilii, liIIIi11));
// window[liIIIi11('0')](function() {
// var l111IlII = liIIIi11('1') + liIIIi11('2');
// if (typeof iil == liIIIi11('3') + liIIIi11('4') || iil != l111IlII + liIIIi11('5') + l111IlII[liIIIi11('6')]) {
// var Ilil11iI = [];
// while (Ilil11iI[liIIIi11('6')] > -0x1) {
// Ilil11iI[liIIIi11('7')](Ilil11iI[liIIIi11('6')] ^ 0x2);
// }
// }
// iliI1lli();
// }, 0x7d0);
(function() {
var iiIIiil = function() {}();
var l1liii11 = function() {}();
window[liIIIi11('9')] = function byted_acrawler() {};
window[liIIIi11('a')] = function sign() {};
(function() {}());
// (function() {
// 'use strict';
// var i1I1i1li = '';
// Object[liIIIi11('1f')](window, liIIIi11('21'), {
// '\x73\x65\x74': function(illllli1) {
// i1I1i1li = illllli1;
// return illllli1;
// },
// '\x67\x65\x74': function() {
// return i1I1i1li;
// }
// });
// }());
var iiil1 = 0x0;
var l11il1l1 = '';
var ii1Ii = 0x8;
function i1Il11i(iiIll1i) {}
function I1lIIlil(l11l1iIi) {}
function lllIIiI(IIi1lIil) {}
// 此處省略 N 個函數(shù)
window[liIIIi11('37')]();
}());
function iliI1lli(lil1I1) {
function lili11I(l11I11l1) {
if (typeof l11I11l1 === liIIIi11('38')) {
return function(lllI11i) {}
[liIIIi11('39')](liIIIi11('3a'))[liIIIi11('8')](liIIIi11('3b'));
} else {
if (('' + l11I11l1 / l11I11l1)[liIIIi11('6')] !== 0x1 || l11I11l1 % 0x14 === 0x0) {
(function() {
return !![];
}
[liIIIi11('39')](liIIIi11('3c') + liIIIi11('3d'))[liIIIi11('3e')](liIIIi11('3f')));
} else {
(function() {
return ![];
}
[liIIIi11('39')](liIIIi11('3c') + liIIIi11('3d'))[liIIIi11('8')](liIIIi11('40')));
}
}
lili11I(++l11I11l1);
}
try {
if (lil1I1) {
return lili11I;
} else {
lili11I(0x0);
}
} catch (liIlI1il) {}
}
;iil = 'jsjiami.com.v6';
// function getSign(){
// return window[liIIIi11('9')](window[liIIIi11('a')]())
// }
function getSign(){
return window.byted_acrawler(window.sign())
}
console.log(getSign())
# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-12-01
# @Author : 微信公眾號:K哥爬蟲
# @FileName: challenge_1.py
# @Software: PyCharm
# ==================================
import execjs
import requests
challenge_api = "http://spider.wangluozhe.com/challenge/api/1"
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cookie": "將 cookie 值改為你自己的!",
"Host": "spider.wangluozhe.com",
"Origin": "http://spider.wangluozhe.com",
"Referer": "http://spider.wangluozhe.com/challenge/1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36",
"X-Requested-With": "XMLHttpRequest"
}
def get_signature():
with open('challenge_1.js', 'r', encoding='utf-8') as f:
ppdai_js = execjs.compile(f.read())
signature = ppdai_js.call("getSign")
print("signature: ", signature)
return signature
def main():
result = 0
for page in range(1, 101):
data = {
"page": page,
"count": 10,
"_signature": get_signature()
}
response = requests.post(url=challenge_api, headers=headers, data=data).json()
for d in response["data"]:
result += d["value"]
print("結(jié)果為: ", result)
if __name__ == '__main__':
main()