javascript
【JS 逆向百例】网洛者反爬练习平台第一题:JS 混淆加密,反 Hook 操作
關注微信公眾號:K哥爬蟲,持續分享爬蟲進階、JS/安卓逆向等技術干貨!
文章目錄
- 聲明
- 寫在前面
- 逆向目標
- 繞過無限 debugger
- Hook 參數
- 逆向參數
- PyCharm 本地聯調
- 完整代碼
- JavaScript 加密關鍵代碼架構
- Python 計算關鍵代碼
聲明
本文章中所有內容僅供學習交流,抓包內容、敏感網址、數據接口均已做脫敏處理,嚴禁用于商業用途和非法用途,否則由此產生的一切后果均與作者無關,若有侵權,請聯系我立即刪除!
寫在前面
題目本身不是很難,但是其中有很多坑,主要是反 Hook 操作和本地聯調補環境,本文會詳細介紹每一個坑,并不只是一筆帶過,寫得非常詳細!
通過本文你將學到:
逆向目標
- 目標:網洛者反反爬蟲練習平臺第一題:JS 混淆加密,反 Hook 操作
- 鏈接:http://spider.wangluozhe.com/challenge/1
- 簡介:本題要提交的答案是100頁的所有數據并加和,要求以 Hook 的方式完成此題,不要以 AST、扣代碼等方式解決,不要使用 JS 反混淆工具進行解密。(Hook 代碼的寫法和用法,K哥以前文章有,本文不再詳細介紹)
繞過無限 debugger
首先觀察到點擊翻頁,URL 并沒有發生變化,那么一般就是 Ajax 請求,每一次請求有些參數會改變,熟練的按下 F12 準備查找加密參數,會發現立馬斷住,進入無限 debugger 狀態,往上跟一個棧,可以發現 debugger 字樣,如下圖所示:
這種情況在K哥以前的案例中也有,當時我們是直接重寫這個 JS,把 debugger 字樣給替換掉就行了,但是本題很顯然是希望我們以 Hook 的方法來過掉無限 debugger,除了 debugger 以外,我們注意到前面還有個 constructor 字樣,在 JavaScript 中它叫構造方法,一般在對象創建或者實例化時候被調用,它的基本語法是:constructor([arguments]) { ... },詳細介紹可參考 MDN 構造方法,在本案例中,很明顯 debugger 就是 constructor 的 arguments 參數,因此我們可以寫出以下 Hook 代碼來過掉無限 debugger:
// 先保留原 constructor Function.prototype.constructor_ = Function.prototype.constructor; Function.prototype.constructor = function (a) {// 如果參數為 debugger,就返回空方法if(a == "debugger") {return function (){};}// 如果參數不為 debugger,還是返回原方法return Function.prototype.constructor_(a); };注入 Hook 代碼的方法也有很多,比如直接在瀏覽器開發者工具控制臺輸入代碼(刷新網頁會失效)、Fiddler 插件注入、油猴插件注入、自寫瀏覽器插件注入等,這些方法在K哥以前的文章都有介紹,今天就不再贅述。
本次我們使用 Fiddler 插件注入,注入以上 Hook 代碼后,會發現會再次進入無限 debugger,setInterval,很明顯的定時器,他有兩個必須的參數,第一個是要執行的方法,第二個是時間參數,即周期性調用方法的時間間隔,以毫秒為單位,詳細介紹可參考菜鳥教程 Window setInterval(),同樣我們也可以將其 Hook 掉:
// 先保留原定時器 var setInterval_ = setInterval setInterval = function (func, time){// 如果時間參數為 0x7d0,就返回空方法// 當然也可以不判斷,直接返回空,有很多種寫法if(time == 0x7d0){return function () {};}// 如果時間參數不為 0x7d0,還是返回原方法return setInterval_(func, time) }將兩段 Hook 代碼粘貼到瀏覽器插件里,開啟 Hook,重新刷新頁面就會發現已經過掉了無限 debugger。
Hook 參數
過掉無限 debugger 后,我們隨便點擊一頁,抓包可以看到是個 POST 請求,Form Data 里,page 是頁數,count 是每一頁數據量,_signature 是我們要逆向的參數,如下圖所示:
我們直接搜索 _signature,只有一個結果,其中有個 window.get_sign() 方法就是設置 _signature 的函數,如下圖所示:
這里問題來了!!!我們再看看本題的題目,JS 混淆加密,反 Hook 操作,作者也再三強調本題是考驗 Hook 能力!并且到目前為止,我們好像還沒有遇到什么反 Hook 手段,所以,這樣直接搜索 _signature 很顯然太簡單了,肯定是要通過 Hook 的方式來獲取 _signature,并且后續的 Hook 操作肯定不會一帆風順!
話不多說,我們直接寫一個 Hook window._signature 的代碼,如下所示:
(function() {//嚴謹模式 檢查所有錯誤'use strict';//window 為要 hook 的對象,這里是 hook 的 _signaturevar _signatureTemp = "";Object.defineProperty(window, '_signature', {//hook set 方法也就是賦值的方法 set: function(val) {console.log('Hook 捕獲到 _signature 設置->', val);debugger;_signatureTemp = val;return val;},//hook get 方法也就是取值的方法 get: function(){return _signatureTemp;}}); })();將兩個繞過無限 debugger 的 Hook 代碼,和這個 Hook _signature 的代碼一起,使用 Fiddler 插件一同注入(這里注意要把繞過 debugger 的代碼放在 Hook _signature 代碼的后面,否則有可能不起作用,這可能是插件的 BUG),重新刷新網頁,可以發現前端的一排頁面的按鈕不見了,打開開發者工具,可以看到右上角提示有兩個錯誤,點擊可跳轉到出錯的代碼,在控制臺也可以看到報錯信息,如下圖所示:
整個 1.js 代碼是經過了 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 方法設置 _signature 時,將其賦值給 a,get 方法獲取 _signature 時,返回 a,這么操作一番,實際上對于 _signature 沒有任何影響,那這段代碼存在的意義是啥?為什么我們添加了自己的 Hook 代碼就會報錯?
來看看報錯信息:Uncaught TypeError: Cannot redefine property: _signature,不能重新定義 _signature?我們的 Hook 代碼在頁面一加載就運行了 Object.defineProperty(window, '_signature', {}),等到網站的 JS 再次 defineProperty 時就會報錯,那很簡單嘛,既然不讓重新定義,而且網站自己的 JS Hook 代碼不會影響 _signature,直接將其刪掉不就行了嘛!這個地方大概就是反 Hook 操作了。
保存原 1.js 到本地,刪除其 Hook 代碼,使用 Fiddler 的 AutoResponder 功能替換響應(替換方法有很多,K哥以前的文章同樣有介紹),再次刷新發現異常解除,并且成功 Hook 到了 _signature。
逆向參數
成功 Hook 之后,直接跟棧,直接把方法暴露出來了:window._signature = window.byted_acrawler(window.sign())
先來看看 window.sign(),選中它其實就可以看到是 13 位毫秒級時間戳,我們跟進 1.js 去看看他的實現代碼:
我們將部分混淆代碼手動還原一下:
window["sign"] = function sign() {try {div = document["createElement"];return Date["parse"](new Date())["toString"]();} catch (IIl1lI1i) {return "123456789abcdefghigklmnopqrstuvwxyz";} }這里就要注意了,有個坑給我們埋下了,如果直接略過,覺得就一個時間戳沒啥好看的,那你就大錯特錯了!注意這是一個 try-catch 語句,其中有一句 div = document["createElement"];,有一個 HTML DOM Document 對象,創建了 div 標簽,這段代碼如果放到瀏覽器執行,沒有任何問題,直接走 try 語句,返回時間戳,如果在我們本地 node 執行,就會捕獲到 document is not defined,然后走 catch 語句,返回的是那一串數字加字母,最后的結果肯定是不正確的!
解決方法也很簡單,在本地代碼里,要么去掉 try-catch 語句,直接 return 時間戳,要么在開頭定義一下 document,再或者直接注釋掉創建 div 標簽的這行代碼,但是K哥在這里推薦直接定義一下 document,因為誰能保證在其他地方也有類似的坑呢?萬一隱藏得很深,沒發現,豈不是白費力氣了?
然后再來看看 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:
我們去網頁里看看,會發現這個方法其實就是一個定時器,沒有太大作用,直接注釋掉即可:
PyCharm 本地聯調
經過以上操作以后,再次本地運行,會提示 window.signs is not a function,出錯的地方是一個 eval 語句,我們去瀏覽器看一下這個 eval 語句,發現明明是 window.sign(),為什么本地就變成了 window.signs(),平白無故多了個 s 呢?
造成這種情況的原因只有一個,那就是本地與瀏覽器的環境差異,混淆的代碼里肯定有環境檢測,如果不是瀏覽器環境的話,就會修改 eval 里的代碼,多加了一個 s,這里如果你直接刪掉包含 eval 語句的整個函數和上面的 setInterval 定時器,代碼也能正常運行,但是,K哥一向是追求細節的!多加個 s 的原因咱必須得搞清楚呀!
我們在本地使用 PyCharm 進行調試,看看到底是哪里給加了個 s,出錯的地方是這個 eval 語句,我們點擊這一行,下個斷點,右鍵 debug 運行,進入調試界面(PS:原代碼有無限 debugger,如果不做處理,PyCharm 里調試同樣也會進入無限 debugger,可以直接把前面的 Hook 代碼加到本地代碼前面,也可以直接刪除對應的函數或變量):
左側是調用棧,右側是變量值,整體上和 Chrome 里面的開發者工具差不多,詳細用法可參考 JetBrains 官方文檔,主要介紹一下圖中的 8 個按鈕:
我們點擊步入按鈕(Step Into),會進入到 function IIlIliii(),這里同樣使用了 try-catch 語句,繼續下一步,會發現捕獲到了異常,提示 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 '';} }這么一來,就發現了端倪,在本地我們并沒有 document、location、href、availHeight 對象,所以就會走 catch 語句,變成了 window.signs(),就會報錯,這里解決方法也很簡單,可以直接刪掉多余代碼,直接定義為不帶 s 的那串語句,或者也可以選擇補一下環境,在瀏覽器里看一下 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())我們運行一下,發現在 Pycharm 里并沒有任何輸出,同樣的我們在題目頁面的控制臺輸出一下 console.log,發現被置空了,如下圖所示:
看來他還對 console.log 做了處理,其實這種情況問題不大,我們直接使用 Python 腳本來調用前面我們寫的 getSign() 方法就能得到 _signature 的值了,但是,再次重申,K哥一向是追求細節的!我就得找到處理 console.log 的地方,把它變為正常!
這里我們仍然使用 Pycharm 來調試,進一步熟悉本地聯調,在 console.log(getSign()) 語句處下個斷點,一步一步跟進,會發現進到了語句 var IlII1li1 = function() {};,查看此時變量值,發現 console.log、console.warn 等方法都被置空了,如下圖所示:
再往下一步跟進,發現直接返回了,這里有可能第一次運行 JS 時就會對 console 相關命令進行方法置空處理,所以先在疑似對 console 處理的方法里面下幾個斷點,再重新調試,會發現會走到 else 語句,然后直接將 IlII1li1 也就是空方法,賦值給 console 相關命令,如下圖所示:
定位到了問題所在,我們直接把 if-else 語句注釋掉,不讓它置空即可,然后再次調試,發現就可以直接輸出結果了:
調用 Python 攜帶 _signature 挨個計算每一頁的數據,最終提交成功:
完整代碼
GitHub 關注 K 哥爬蟲,持續分享爬蟲相關代碼!歡迎 star !https://github.com/kgepachong/
**以下只演示部分關鍵代碼,不能直接運行!**完整代碼倉庫地址:https://github.com/kgepachong/crawler/
JavaScript 加密關鍵代碼架構
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) {// 如果參數為 debugger,就返回空方法if(a == "debugger") {return function (){};}// 如果參數不為 debugger,還是返回原方法return Function.prototype.constructor_(a); };// 先保留原定時器 var setInterval_ = setInterval setInterval = function (func, time){// 如果時間參數為 0x7d0,就返回空方法// 當然也可以不判斷,直接返回空,有很多種寫法if(time == 0x7d0){return function () {};}// 如果時間參數不為 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(_0x11145e, _0x3cbe90) {_0x11145e = ~~'0x'['concat'](_0x11145e);var _0x636e4d = iiIIilii[_0x11145e];return _0x636e4d; }; (function(_0x52284d, _0xfd26eb) {var _0x1bba22 = 0x0;for (_0xfd26eb = _0x52284d['shift'](_0x1bba22 >> 0x2); _0xfd26eb && _0xfd26eb !== (_0x52284d['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 個函數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())Python 計算關鍵代碼
# ================================== # --*-- coding: utf-8 --*-- # @Time : 2021-12-01 # @Author : 微信公眾號:K哥爬蟲 # @FileName: challenge_1.py # @Software: PyCharm # ==================================import execjs import requestschallenge_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 signaturedef main():result = 0for 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("結果為: ", result)if __name__ == '__main__':main()總結
以上是生活随笔為你收集整理的【JS 逆向百例】网洛者反爬练习平台第一题:JS 混淆加密,反 Hook 操作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Python CheckiO 题解】T
- 下一篇: 招商银行现金分期放款的时间 招行现金分期