逆向目標 猿人學 - 反混淆刷題平臺 Web 第二題:js 混淆,動態 cookie 目標:提取全部 5 頁發佈日熱度的值,計算所有值的加和 主頁:https://match.yuanrenxue.com/match/2 介面:https://match.yuanrenxue.com/api/mat ...
逆向目標
-
猿人學 - 反混淆刷題平臺 Web 第二題:js 混淆,動態 cookie
-
目標:提取全部 5 頁發佈日熱度的值,計算所有值的加和
-
逆向參數:
- Cookie 參數:m
逆向過程
抓包分析
進入網頁,點擊右鍵查看頁面源代碼,搜索不到直播間相關數據信息,證明是通過 ajax 載入的數據,ajax 載入有特殊的請求類型 XHR,打開開發者人員工具,刷新網頁進行抓包,會跳轉到虛擬機中,進入無限 debugger,過無限 debugger 的方式在往期文章中有詳細介紹,感興趣的可以去閱讀學習一下,這裡直接在 debugger 行右鍵選擇 never pause here,然後下一步斷點即可過掉:
在 Network 的篩選欄中選擇 XHR,數據介面為 2,在響應預覽中可以看到當前頁各手機發佈日的熱度:
這時候點擊第二頁,會彈出提示框:cookie 失效,正在重置頁面,證明 cookie 是有時效性的,並且會進行校驗:
cookie 中有個關鍵加密參數 m,其內容如下:
逆向分析
通過 hook cookie 中 m 參數的方式對其進行定位,hook 的方式有很多種,可以閱讀 K 哥往期文章,對其有詳細介紹,這裡使用編程貓 Fiddler 插件進行 hook,相關插件在 K哥爬蟲公眾號發送【Fiddler插件】即可獲取,Hook 代碼如下:
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {
set: function (val) {
if (val.indexOf('m') != -1) {
debugger;
}
console.log('Hook捕獲到cookie設置->', val);
cookieTemp = val;
return val;
},
get: function () {
return cookieTemp;
},
});
})();
勾選開啟框,啟動 Fiddler 進行 hook 註入:
刷新網頁,如果進入無限 debugger,則按上述方式解決,不過直接通過 m 參數定位並不是最好的方案,因為該 cookie 中還有其他參數包含 m 字母,位置不對則刷新網頁,這裡成功斷在 m 參數的值生成的位置:
向上跟棧到 _0xdad69f (2:18) 處,然後點擊左下角 { } 格式化代碼,會跳轉到 2:formatted 文件的第 4943 行,該行內容如下:
document[$dbsm_0x42c3(qqLQOq, iOiqII) + $dbsm_0x42c3(q1IoqQ, QQlLlq)] = _0x5500bb['\x4e\x74\x44' + '\x72\x43'](_0x5500bb[$dbsm_0x42c3(qqqQoq, oqQiiO) + '\x6d\x65'](_0x5500bb[$dbsm_0x42c3(Ioo0ql, olq0Oq) + '\x6d\x65'](_0x5500bb[$dbsm_0x42c3(qOIqQi, OOqIQi) + '\x72\x44'](_0x5500bb[$dbsm_0x42c3(Q1qoqQ, lILOOq) + '\x72\x44'](_0x5500bb[$dbsm_0x42c3(qOO1Q0, oiqlQQ) + '\x72\x44'](Ql1OO0, _0x5500bb['\x7a\x76\x67' + '\x6c\x77'](_0x3c9ca8)), Qoqq0I), _0x5500bb[$dbsm_0x42c3(iqOiQ0, QOiq0Q) + '\x47\x6b'](_0x313b78, _0x160e3a)), lOo0QQ), _0x160e3a), _0x5500bb[$dbsm_0x42c3(qiOOiO, liQIoQ) + '\x4e\x5a']),
控制台列印後可知這裡就是 cookie 中 m 參數值生成的位置:
在控制臺中進一步列印分析下其他部分含義:
m 參數值的格式如下:
0ef478cf61e0749d7444c7997c917679|1663213224000
可以依此將代碼進行簡化:
_0x5500bb[$dbsm_0x42c3(iqOiQ0, QOiq0Q) + '\x47\x6b'](_0x313b78,_0x160e3a) + lOo0QQ + _0x160e3a
控制台列印驗證,結果匹配:
接下來先跟進到 _0x5500bb[$dbsm_0x42c3(iqOiQ0, QOiq0Q) + '\x47\x6b'] 中,滑鼠選中後點擊進入:
在該文件的第 3911 行,內容如下:
_0x434ddb[$dbsm_0x42c3(Iooo0l, Qq1oqI) + '\x47\x6b'] = function(_0x105ffe, _0x733be0) {
return _0x105ffe(_0x733be0);
}
返回值為 _0x105ffe(_0x733be0)
,該函數傳入的參數為 _0x313b78
和 _0x160e3a
,所以可以進一步改寫:
_0x313b78(_0x160e3a) + lOo0QQ + _0x160e3a
_0x160e3a 為時間戳,因此 m 參數的值是將時間戳作為參數傳入 _0x313b78 函數後加密得到的,所以需要進一步跟進到 _0x313b78 函數定義的位置,同樣滑鼠選中,點擊即可跳轉到第 4933 行,到 node 環境中調試,初步代碼為:
function _0x313b78(_0x575158, _0x1fa91a, _0x1cf5de) {
// 以下部分內容過長,此處省略
// 完整代碼關註 GitHub:https://github.com/kgepachong/crawler
}
var _0x160e3a = Date.parse(new Date());
var m = _0x313b78(_0x160e3a) + lOo0QQ + _0x160e3a;
console.log(m);
運行後會提示 _0x5500bb 未定義,到原文件中 ctrl + f 局部搜索這個函數,在第 3940 行:
_0x5500bb = _0x434ddb
補上運行後會提示 _0x434ddb
未定義,搜索後發現 _0x434ddb
在第 2817 行定義為一個空對象,後面向其中傳入了很多值,類似於一個大數組,不能只補 _0x434ddb = {};
,需要把傳值部分補進去,不然後面運行時會出現些報錯,經測試有的部分不要也可以,但是細扣就很麻煩了,直接全補即可,這就很多了,從第 2817 行一直扣到第 3939 行,補完後接著運行程式,這次又提示 $dbsm_0x42c3
未定義,接著搜找其定義位置,在第 94 行,補了後提示 OooIi1 未定義,在第 209 行,需要從第 209 行到第 2816 行全部補上,不然會提示其中某一個未定義,同樣的,雖然經調試有的不需要也行,但是一個個調麻煩且沒有必要,補完後接著運行又會提示 $dbsm_0x123c
未定義:
其在第 22 行,是個大數組,補了之後運行程式後發現卡住了,一段時間後程度報錯:
這個報錯可能是記憶體資源耗盡導致程式崩潰了,將這部分代碼複製到瀏覽器中進行調試,開啟一個新頁面,打開開發者人員工具,在 Sources 中選擇 Snippets,新建一個腳本,將已經扣下來的代碼粘貼進去,在第一行寫入 debugger;手動打斷點調試,ctrl + s 保存文件後點擊右下角按鈕運行腳本即會在第一行斷住:
點擊單步調試,一步步查看是哪裡出了問題:
點了幾步後,卡了一下,跳到第 2711 行,是個 for 迴圈,右側出現紅框報錯,意思是潛在的記憶體崩潰,即單步調試斷到到此處時程式臨近記憶體崩潰:
接著往後單步調試,會發現一直在第 2712 行和第 2713 行間來回執行,到後來甚至瀏覽器崩潰了,所以問題出在 WxzuQr 對象中出現了無限迴圈,直至耗盡了記憶體資源:
這部分內容在 $dbsm_0x42c3 函數中,接下來需要研究一下崩潰原因,右側堆棧中向上跟棧,上兩步分別通過構造函數創建了兩個實例對象 WjJIeN 和 vnuqco,WjJIeN 部分如下:
_0x11a714['prototype']['WjJIeN'] = function(_0x4859ef) {
if (!Boolean(~_0x4859ef)) {
return _0x4859ef;
}
return this['WxzuQr'](this['yewpLt']);
}
這裡進行了一個 if 判斷,~ 為按位取反,意思是如果 !Boolean(~_0x4859ef) 的值為 false,則執行 WxzuQr 的無限迴圈行為,直至程式崩潰,接著跟進到 vnuqco 部分,查看 _0x4859ef 是啥,對什麼進行了判斷:
_0x11a714['prototype']['vnuqco'] = function() {
_0x2940ac = new RegExp(this['PuKGlh'] + this['CTXIfT']),
_0x3fba94 = _0x2940ac['test'](this['XxpyjG']['toString']()) ? --this['yHmSUE'][0x1] : --this['yHmSUE'][0x0];
return this['WjJIeN'](_0x3fba94);
}
返回值中給 WjJIeN 傳入的參數為 _0x3fba94,其定義在第 2699 行,是個三目表達式:
_0x2940ac['test'](this['XxpyjG']['toString']()) ? --this['yHmSUE'][0x1] : --this['yHmSUE'][0x0];
到控制台列印輸出一下,看看該行各部分什麼含義:
--this['yHmSUE'][0x1]
的值固定為 -1,而每運行一次 this['yHmSUE'][0x0]
的值即減一:
console.log(!Boolean(~-1)) // true
console.log(!Boolean(~-2)) // false
所以只有當 _0x2940ac['test'](this['XxpyjG']['toString']())
的值為 true 時才不會進入無限迴圈,在控制台列印下 this['XxpyjG']['toString']()
部分內容:
這個函數在第 2689 行,再來看看對其進行了怎樣的判斷,跟進到 _0x2940ac 定義位置,在第 2698 行,是個正則表達式對象,控制臺中列印後可知道表達式為:
/\w+ *\(\) *{\w+ *['|"].+['|"];? *}/
- / ... /:正則表達式限制符,字面量寫法,防轉義,中間正則表達式部分若存在 \d 不會將 \d 的 \ 當作轉義字元
*\(\)
:匹配零個或多個括弧,\ 為轉義字元*{ 、*}
:匹配前後大括弧- \w+:匹配一個或多個字母數字字元
- .+:貪婪匹配任意字元
- ['|"]:匹配單引號或者雙引號
- ;?:匹配零個或一個分號
所以匹配樣式大致如下:XXX( ){ XXX ' XXX ' ;},並不匹配換行符、製表符、空格等,沒格式化的代碼會被壓縮成一行,所以這裡相當於格式化檢測,由於一開始進行了格式化操作,因此判斷結果為 false,從而進入了無限迴圈,導致程式崩潰,所以只需要將這部分內容壓縮為一行即可,檢驗一下:
沒有格式化後列印出的結果為 true,即不會調用到 WxzuQr 對象,從而進入無限迴圈,修改後再次運行程式,結束了嗎,當然沒有,上個問題倒是解決了,又出現了以下報錯:
報錯在第 3854 行,內容如下:
_0x5500bb[$dbsm_0x42c3(QoLq0i, q0Oqqo) + '\x5a\x49']
接著在瀏覽器中進行調試,在這一行上面打上 debugger;然後運行腳本,斷住後列印分析一下:
'\x5a\x49' 即 ‘ZI’,QoLq0i、q0Oqqo 為定值,因此問題出在 $dbsm_0x42c3
函數中,其實如果對 OB 混淆瞭解的話會知道這種混淆方式有一些特征,其一般由三部分組成:大數組、移位自執行函數、解密字元串函數,大數組我們之前已經找到了,就是 $dbsm_0x123c
,而 $dbsm_0x42c3
是解密字元串函數,這裡差了個移位自執行函數,缺東西自然結果會不對,需要找到將其補上,在第 23 行到第 93 行,夾在 $dbsm_0x123c
和 $dbsm_0x42c3
之間,補完後運行程式,又到了熟悉的卡住,過了一會後報錯:
報錯在第 27 行,放到瀏覽器中進行調試,還是在開頭打上 debugger;運行後單步向下執行,點了幾下熟悉的卡住,然後跳到第 24 行 for 迴圈處:
右側出現熟悉的警告提示,證明又進入到無限迴圈了,果不其然,過了一會瀏覽器頁面就崩潰了:
根據之前的經驗,看看是不是哪又有個格式化檢測導致進入到這個迴圈里,果不其然,在第 55 行:
這裡是對 removeCookie 處的代碼進行了格式化檢測,同樣將函數體部分寫成一行即可:
'removeCookie': function() {return 'dev';},
接著運行,又提示 _0x3c9ca8 未定義,ctrl + f 局部搜索找到函數定義位置扣下來即可,運行後又提示 _0x1316f4 未定義,這個扣下來之後記得將後面的自執行的括弧刪掉,接著會提示 _0x12a78e 未定義,扣下來的時候同樣記得刪掉末尾的括弧,再接著就沒什麼特別需要註意的了,差哪個函數補哪個就行了,到後面提示 navigator 未定義,簡單地補瀏覽器環境即可,node 環境下 window 設置為 global:
var window = global;
window.navigator = {};
自然不會這麼輕易的結束了,運行後又會提示 _0x184fb0 未定義,跟之前一樣,搜到扣下來即可,後面就是漫長的補函數的過程,沒別的技巧,就是需要耐心,手都 cv 酸了,直到出現如下報錯:
報錯提示 history 未定義,這是個瀏覽器對象,顯示在 console.log 處報錯,在 console.log 行打斷點調試,運行到這裡時會跳轉到虛擬機中,其中代碼如下:
history.pushState 是向瀏覽器的會話歷史中添加記錄,當使用 console.log 輸出結果的時候,就會執行 history.pushState,但是我們並沒有 history 環境,所以會報錯,補了 history 環境後運行程式發現一直卡著,仔細看代碼才發現有個 while 迴圈,最離譜的是裡面的 for 迴圈設置了 1100000 次,幾乎可以說是在不間斷檢測,等不得等到猴年馬月去了,這裡直接將 console.log 賦值給一個變數替換掉即可,記得放到前面:
var result = console.log;
至此,終於結束了!成功列印出 m 參數的值:
這個題倒是不難,逆向下來思路也很清晰,但是扣代碼的過程繁雜且坑不少,還是很值得大家上手去練習的。
完整代碼
bilibili 關註 K 哥爬蟲,小助理手把手視頻教學:https://space.bilibili.com/1622879192
GitHub 關註 K 哥爬蟲,持續分享爬蟲相關代碼!歡迎 star !https://github.com/kgepachong/
以下只演示部分關鍵代碼,不能直接運行!
JavaScript 代碼
var window = global;
window.navigator = {};
var result = console.log;
// 以下部分內容過長,此處省略
// 完整代碼關註 GitHub:https://github.com/kgepachong/crawler
function _0x313b78(_0x575158, _0x1fa91a, _0x1cf5de) {
if (_0x5500bb[$dbsm_0x42c3(QoLq0i, q0Oqqo) + '\x5a\x49'](_0x5500bb[$dbsm_0x42c3(LQOI0Q, QqOI00) + '\x73\x42'], _0x5500bb[$dbsm_0x42c3(Q00oiq, QIioOo) + '\x5a\x76'])) {
VWQQuv['\x6f\x4f\x61' + '\x68\x47'](debuggerProtection, Q0LiqQ);
} else {
_0x5500bb[$dbsm_0x42c3(i1lQqq, q110Lq) + '\x62\x45'](_0x3c9ca8);
return _0x1fa91a ? _0x1cf5de ? _0x5500bb[$dbsm_0x42c3(iqqLQO, LoOOOq) + '\x4b\x6b'](_0x21cf21, _0x1fa91a, _0x575158) : _0x5500bb['\x72\x71\x75' + '\x4b\x51'](y, _0x1fa91a, _0x575158) : _0x1cf5de ? _0x5500bb[$dbsm_0x42c3(qLQQ1q, I1oOQ1) + '\x4d\x6e'](_0x443ca7, _0x575158) : _0x5500bb[$dbsm_0x42c3(qLLoQi, iO0OQo) + '\x4d\x6e'](_0x184fb0, _0x575158);
}
}
function getCookieM(){
var _0x160e3a = Date.parse(new Date());
var m = _0x313b78(_0x160e3a) + lOo0QQ + _0x160e3a;
return m;
}
// var _0x160e3a = Date.parse(new Date());
// var m = _0x313b78(_0x160e3a) + lOo0QQ + _0x160e3a;
// result(m);
Python 代碼
# =======================
# --*-- coding: utf-8 --*--
# @Time : 2022/9/8
# @Author : 微信公眾號:K哥爬蟲
# @FileName: yrx5.py
# @Software: PyCharm
# =======================
import execjs
import requests
import re
def get_cookie_m():
heat_total = 0
for page_num in range(1, 6):
with open('yrx2.js', 'r', encoding='utf-8') as f:
encrypt = f.read()
cookie_m = execjs.compile(encrypt).call('getCookie')
headers = {
"user-agent": "yuanrenxue,project",
}
cookies = {
"sessionid": " 填入自己的 sessionid ",
"m": cookie_m
}
url = "https://match.yuanrenxue.com/api/match/2?page=%s" % page_num
response = requests.get(url, headers=headers, cookies=cookies)
for i in range(10):
value = response.json()['data'][i]
heat = re.findall(r"'value': (.*?)}", str(value))[0]
heat_total += int(heat)
print(heat_total)
if __name__ == '__main__':
get_cookie_m()