JS 逆向百例】猿人學系列 web 比賽第五題:js 混淆 - 亂碼增強,詳細剖析
逆向目標
- 猿人學 - 反混淆刷題平臺 Web 第五題:js 混淆,亂碼增強
- 目標:抓取全部 5 頁直播間熱度,計算前 5 名直播間熱度的加和
- 主頁:http://match.yuanrenxue.com/...
- 介面:http://match.yuanrenxue.com/...
逆向引數:
- url 請求引數:m、f
- Cookie 引數:m、RM4hZBv0dDon443M
逆向過程
抓包分析
進入網頁,點選右鍵檢視頁面原始碼,搜尋不到直播間相關資料資訊,證明是通過 ajax 載入的資料,ajax 載入有特殊的請求型別 XHR,開啟開發者人員工具,重新整理網頁進行抓包,在 Network 的篩選欄中選擇 XHR,資料介面為 5?m=XXX&f=XXX,在響應預覽中可以看到各直播間熱度資料:
介面 url 有兩個請求引數 m 和 f,現在還不知道具體怎麼來的:
本題提示 cookie 有效期僅為 50 秒鐘,即 cookie 值是在動態變化的,經過對比分析,cookie 中有兩個動態變化的引數 m 和 RM4hZBv0dDon443M,接下來需要定位到其生成的位置:
逆向分析
Cookie 加密引數分析
可以通過 Hook Cookie 的方式定位引數位置,這裡通過 Fiddler 程式設計貓外掛進行 Hook,相關外掛在 K哥爬蟲公眾號傳送【Fiddler外掛】即可獲取,Hook 程式碼如下:
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {
set: function (val) {
if (val.indexOf('RM4hZBv0dDon443M') != -1) {
debugger;
}
console.log('Hook捕獲到cookie的值->', val);
cookieTemp = val;
return val;
},
get: function () {
return cookieTemp;
},
});
})();
將以上程式碼寫入外掛中,注入 Hook:
清除網頁快取,勾選開啟框,開啟 Fiddler 進行 Hook 注入,可以發現成功斷住:
從右側堆疊中向上跟棧,會發現跟到了虛擬機器 VMXXX 中,點選右下角 { } 格式化,跳轉到了第 978 行,程式碼部分如下:
_0x3d0f3f[_$Fe] = 'R' + 'M' + '4' + 'h' + 'Z' + 'B' + 'v' + '0' + 'd' + 'D' + 'o' + 'n' + '4' + '4' + '3' + 'M=' + _0x4e96b4['_$ss'] + ';\x20path=/';
在該行打下斷點進行除錯,控制檯列印相關引數:
- _$Fe:cookie
- _ 0x4e96b4['_$ss']:RM4hZBv0dDon443M 引數加密後的值
前面各字母組成起來就是 RM4hZBv0dDon443M=,此處就是 RM4hZBv0dDon443M 引數加密後賦值給 cookie 的位置,所以關鍵的加密部分為 _0x4e96b4['_$ss']
,列印相關內容會發現 _0x4e96b4 是 window 物件,window. _$ss 即加密後的值:
直接搜尋 _$ss 沒有結果,同樣嘗試 Hook,Hook 程式碼:
(function () {
'use strict'
Object.defineProperty(window, '_$ss', {
set: function (val) {
console.log('Hook捕獲到_$ss的值->', val);
debugger;
},
});
})();
成功斷住:
同樣向上跟棧,找到其定義位置,跟到了虛擬機器中,格式化後跳到第 1229 行:
_0x4e96b4['_$' + _$UH[0x348][0x1] + _$UH[0x353][0x1]] = _0x29dd83[_$UH[0x1f]]();
在該行打下斷點除錯分析各自含義:
'_$s'
、_$UH[0x348][0x1]
、_$UH[0x353][0x1]
組合起來:'_$ss'_$UH[0x1f]()
:toString()_0x29dd83[ _$UH[0x1f]]()
:將 _0x29dd83 生成的值轉換為字串
因此關鍵的加密位置肯定在 _0x29dd83 中,往上看, _0x29dd83 定義在第 1225 行,這時候眼前一亮,看到了 mode 和 padding 兩個關鍵字,這裡大概率為 AES 或者 DES 加密,將程式碼解混淆替換後的結果如下:
_$Ww = _$Tk['enc']['utf-8']['parse'](_0x4e96b4['_$pr']['toString']()),
_0x29dd83 = _$Tk['AES'](_$Ww, _0x4e96b4['_$qF'], {
'mode': _$Tk['mode']['ECB'],
'padding': _$Tk['pad']['pkcs7']
}),
_0x4e96b4['_$ss'] = _0x29dd83['toString']();
現在就很明顯了,這裡為 AES 加密,加密內容為 _$Ww
,key 值為 _0x4e96b4['_$qF']
,加密模組為 ECB,填充方式為 pkcs7:
- CBC:Cipher Block Chaining(密碼塊連結模式),是一種迴圈模式,前一個分組的密文和當前分組的明文異或操作後再加密,這樣做的目的是增強破解難度
- PKCS7:在填充時首先獲取需要填充的位元組長度 = 塊長度 - (資料長度 % 塊長度), 在填充位元組序列中所有位元組填充為需要填充的位元組長度值
_$Ww
的值由 _0x4e96b4['_$pr']
轉換為字串後經過 utf-8 編碼得到,其與 key 值 _0x4e96b4['_$qF']
都是陣列,需要知道這兩個陣列是怎麼生成的,先 ctrl + f 搜尋 _0x4e96b4['_$qF']
,定義在第 1444 行,內容如下:
_0x4e96b4['_$qF'] = CryptoJS['enc']['Utf8'][_$UH[0xff]](_0x4e96b4['btoa'](_0x4e96b4['_$is'])['slice'](0x0, 0x10));
在該行打下斷點,控制檯列印分析一下:
由此可見,_0x4e96b4['_$qF']
是通過 CryptoJS 庫將字串經過 base64 加密後取前 16 位的結果,搜尋 _0x4e96b4['_$is']
,找到字串生成的位置,在第 674 行,由 _$yw 賦值,在上一行可以看到熟悉的 _$Fe,即 cookie,發現 cookie 中的 m 引數是在這裡定義的:
_0x3d0f3f[_$Fe] = 'm=' + _0x474032(_$yw) + ';\x20path=/';
引數 m 的值也與 _$yw
有關,m 引數是將 _$yw
經過 _0x474032
函式處理後得到,後面再專門進行分析,_$yw
定義在第 672 行:
_$yw = _0x2d5f5b()[_$UH[0x1f]]();
_$UH[0x1f]
為 “toString”,_$yw
的值是將 _0x2d5f5b()
函式的返回值轉換成了字串得到的,跟進到該函式定義的位置,搜尋後發現在第 279 行,控制檯列印後發現這裡就是時間戳,所以 _$yw
即時間戳:
因此 _0x4e96b4['_$qF']
的值是將時間戳經過 base64 加密後取了前 16 位的結果,接下來只需要知道 _0x4e96b4['_$pr']
是如何生成的,就能復現出 RM4hZBv0dDon443M 引數的加密過程,在第 1224 行打斷點除錯發現此時的 _0x4e96b4['_$pr']
陣列包含五個值:
現在就需要知道這五個值是在哪傳進去的,搜尋 _0x4e96b4['_$pr']
看看哪裡對其進行了賦值,每個都打下斷下,該陣列定義在第 270 行:
_0x4e96b4['_$pr'] = new _0x4d2d2c();
_0x4d2d2c
在第 224 行定義為 Array,所以這裡是建立了一個數組 _0x4e96b4['_$pr']
,接著往後找傳值的地方,繼續執行斷點除錯,第 1717 行的斷點運行了四次傳入了四個值:
_0x4e96b4['_$pr']['push'](_0x474032(_$Wa));
跟進 _$Wa 定義的位置,在第 1715 行,由 _0x12eaf3 函式生成,跟進到這個函式的位置,在第 275 行,返回值解混淆後如下:
Date['parse'](new Date());
再次下一步除錯斷點會跳轉到第 868 行,這時候陣列被傳入了第五個值,_$yw
為時間戳,由於 m = _0x474032(_$yw)
,所以第五個值也就是引數 m 的值,記住這裡出現的 _0x4e96b4['_$is']
:
_0x3d0f3f[_$Fe] = 'm=' + _0x474032(_$yw) + ';\x20path=/';
_0x4e96b4['_$is'] = _$yw;
_0x4e96b4['_$pr']['push'](_0x474032(_$yw));
陣列值的生成位置都找到了,跟 m 引數一樣,傳入的值都經過了 _0x474032 函式的處理,因此需要跟進 _0x474032 函式,滑鼠選中,點選即可跳轉到該函式定義的位置:
在第 455 行,返回值為三目表示式:
function _0x474032(_0x233f82, _0xe2ed33, _0x3229f9) {
return _0xe2ed33 ? _0x3229f9 ? v(_0xe2ed33, _0x233f82) : y(_0xe2ed33, _0x233f82) : _0x3229f9 ? _0x41873d(_0x233f82) : _0x37614a(_0x233f82);
}
在 return 處打下斷點除錯,_0x233f82 為傳入的 _$yw 的值,即時間戳,後面兩個引數均為 undefined,所以不妨將函式簡化下:
function _0x474032(_0x233f82, _0xe2ed33, _0x3229f9) {
return _0x37614a(_0x233f82);
}
接下來需要跟進到 _0x37614a 函式的位置:
function _0x37614a(_0x32e7c1) {
return _0x499969(_0x41873d(_0x32e7c1));
}
這裡就需要跟出 _0x499969
函式和 _0x41873d
函式的內容,接下來就是扣,缺啥補啥,缺函式補函式,缺環境補環境,若報錯提示 _$UH is not defined
,_$UH
是個大陣列,直接將其整體解混淆替換掉就行了,例如:
_$UH[0x6c] ---> "length"
或者寫成鍵值對形式:
_$UH = {
8: 'prototype',
15: 'charCodeAt',
31: 'toString',
108: 'length'
}
值得注意的是 _0x11a7a2 函式,執行時會報錯 op is not defined
,op 定義在第 308 行:
op 的值為 26,這裡直接將其定義成固定值即可,即 var op = 26;
同樣將 _0x42fb36
和 b64pad 也寫成固定值,即 _0x42fb36 = 16;
、b64pad = 1
;
除錯過程中還發現 window['_$6_']
、window['_$tT']
、window['_$Jy']
這幾個引數的值是在動態變化的,不進行改寫甚至將相關部分註釋掉,在本地 node 環境中都是可以執行出結果的,但是用 python 呼叫的話會報錯,證明在前端會對這幾個引數進行校驗,這幾個引數在 _0x11a7a2
函式中定義,該函式溯源後最終被 _0x474032
函式呼叫,_0x474032
函式對 _$yw
的值進行處理,生成了 _0x4e96b4['_$pr']
陣列的最後一個值及 m 引數的值,所以如果這幾個引數的值匹配錯誤的話會導致校驗失敗,我們只需要打斷點看 m 引數的值生成的時候,這三個引數的值是多少,然後寫成固定值就行了:
window['_$6_'] = -389564586;
window['_$tT'] = -660478335;
window['_$Jy'] = -405537848;
至此 Cookie 中 RM4hZBv0dDon443M 引數和 m 引數的生成邏輯就疏通了,以下通過 JavaScript 對其復現:
// 以下函式部分內容過長,此處省略
// 完整程式碼關注 GitHub:http://github.com/kgepachong/crawler
var CryptoJS = require('crypto-js');
function rm4Encrypt(_$yw, pr){
var value = Buffer.from(_$yw).toString('base64').slice(0, 16);
var srcs = CryptoJS.enc.Utf8.parse(pr);
var key = CryptoJS.enc.Utf8.parse(value);
var encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
var _$yw = new Date().valueOf().toString();
var _$Wa = Date.parse(new Date())
function pr(){
pr = [];
for (i = 1; i < 5; i++) {
// _$Wa 傳入四個值
pr.push(_0x474032(_$Wa))
}
// _$yw 傳入一個值
pr.push(_0x474032(_$yw));
return pr.toString();
}
var RM4hZBv0dDon443M = rm4Encrypt(_$yw, pr());
// m 為陣列傳入的最後一個值
var m = pr[4];
console.log('RM4hZBv0dDon443M 引數加密後的值為: ' + RM4hZBv0dDon443M)
console.log('m 引數的值為: ' + m)
執行結果:
請求頭引數分析
Cookie 中的引數分析完了,還有兩個請求引數 m 和 f 沒有解決,直接從介面處跟棧,從 Initiator 中跟到 request 裡:
點選右下角 { } 格式化後會跳轉到 5:formatted
檔案的第 856 行,在第 883 行的 list 中可以找到引數 m 和 f 的定義位置:
"m": window._$is,
"f": window.$_zw[23]
m 的值是 window._$is
,有沒感覺似曾相識,就是上文所說的 _0x4e96b4['_$is']
,_0x4e96b4
就是 window,所以這裡 m 的值其實就是 _$yw
;f 的值是 window.$_zw[23]
,現在需要知道 $_zw[23]
的值怎麼生成的,區域性搜尋 $_zw
會發現該陣列定義在第 611 行,接著往後找,看看陣列中的第 23 個是什麼,先控制檯列印一下內容:
第 633 行內容是第六個,順下去找會發現第 23 個的內容如下:
$_aiding.$_zw.push($_t1);
在此處打下斷點除錯驗證一下,可以發現結果是一樣的:
接下來只需要找到 $_t1
的定義位置即可,ctrl + f 區域性搜尋 $_t1
,其定義在第 613 行,是個時間戳:
let $_t1 = Date.parse(new Date());
- Date.parse(new Date()):獲取的時間戳是把毫秒改成 000 顯示,如 1662691102000
- new Date().valueOf():獲取了當前包括毫秒的時間戳,如 1662691114310
可以發現與 _$Wa 的定義方式一致,對比一下 m 和 f 兩個引數的值會發現差值接近於 50 秒,與題目中提示的 Cookie 有效期僅 50 秒鐘對應上了:
在虛擬機器檔案的第 1975 行也有個 50 秒的定時器:
至此所有引數生成的邏輯都調理清晰了,本題並不難,但是扣程式碼的過程中有許多需要注意的細節,猿人學給大家提供了一個優質的練習平臺,做題也是一個很好的自我提升的方式。
完整程式碼
bilibili 關注 K 哥爬蟲,小助理手把手影片教學:http://space.bilibili.com/16...
GitHub 關注 K 哥爬蟲,持續分享爬蟲相關程式碼!歡迎 star !http://github.com/kgepachong/
以下只演示部分關鍵程式碼,不能直接執行!
JavaScript 程式碼
var _0x4e96b4 = window = {};
var _0x1171c8 = 0x67452301;
var _0x4dae05 = -0x10325477;
var _0x183a1d = -0x67452302;
var _0xcfa373 = 0x10325476;
var _0x30bc70 = String;
// 以下函式部分內容過長,此處省略
// 完整程式碼關注 GitHub:http://github.com/kgepachong/crawler
var CryptoJS = require('crypto-js');
function rm4Encrypt(_$yw, pr){
var value = Buffer.from(_$yw).toString('base64').slice(0, 16);
var _$Ww = CryptoJS.enc.Utf8.parse(pr);
var key = CryptoJS.enc.Utf8.parse(value);
var encrypted = CryptoJS.AES.encrypt(_$Ww, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
function getParamers() {
pr = [];
for (i = 1; i < 5; i++) {
var _$Wa = Date.parse(new Date());
pr.push(_0x474032(_$Wa))
}
var _$yw = new Date().valueOf().toString();
pr.push(_0x474032(_$yw));
cookie_m = pr[4];
cookie_rm4 = rm4Encrypt(_$yw, pr.toString());
return{
"cookie_m": cookie_m,
"cookie_rm4": cookie_rm4,
"m": _$yw,
"f": Date.parse(new Date()).toString()
}
}
console.log(getParamers());
Python 程式碼
# =======================
# --*-- coding: utf-8 --*--
# @Time : 2022/9/8
# @Author : 微信公眾號:K哥爬蟲
# @FileName: yrx5.py
# @Software: PyCharm
# =======================
import execjs
import requests
import re
def encrypt_yrx5():
room_heat_all = []
for page_num in range(1, 6):
with open('yrx5.js', 'r', encoding='utf-8') as f:
encrypt = f.read()
encrypt_params = execjs.compile(encrypt).call('getParamers')
headers = {
"user-agent": "yuanrenxue,project",
}
cookies = {
# 填入自己的 sessionid
"sessionid": " your sessionid ",
"m": encrypt_params['cookie_m'],
"RM4hZBv0dDon443M": encrypt_params['cookie_rm4']
}
params = {
"m": encrypt_params['m'],
"f": encrypt_params['f']
}
url = "http://match.yuanrenxue.com/api/match/5?page=%s" % page_num
response = requests.get(url, headers=headers, cookies=cookies, params=params)
for i in range(10):
value = response.json()['data'][i]
room_heat = re.findall(r"'value': (.*?)}", str(value))[0]
room_heat_all.append(room_heat)
room_heat_all.sort(reverse=True)
top_five_total = 0
for i in range(5):
top_five_total += int(room_heat_all[i])
print(top_five_total)
if __name__ == '__main__':
encrypt_yrx5()
本文來源:JS 逆向百例】猿人學系列 web 比賽第五題:js 混淆 - 亂碼增強,詳細剖析
- OceanBase榮獲OSCAR兩項大獎,開源已成主流開發模式
- linux根據inode編號刪除檔案
- 特約專訪 | 思否 CEO 高陽帶你瞭解 Code For Better _ Hackathon 冠軍團隊背後的故事
- Nest.js快速啟動API專案
- TiDB Hackathon 2022丨總獎金池超 35 萬!邀你喚醒程式碼世界的更多可能性!
- Go 為什麼能火?歸功於這 5 個方面
- JS 逆向百例】猿人學系列 web 比賽第五題:js 混淆 - 亂碼增強,詳細剖析
- 汪源:資料分析熱詞迭出,“三個統一”值得關注
- Go 為什麼能火?歸功於這 5 個方面
- Go 為什麼能火?歸功於這 5 個方面
- Go 為什麼能火?歸功於這 5 個方面
- Go 為什麼能火?歸功於這 5 個方面
- SpringBoot Vue Flowable,模擬一個請假審批流程!
- Go 為什麼能火?歸功於這 5 個方面
- 聊聊如何利用管道模式來進行業務編排(下篇)
- Golang 單例模式與sync.Once
- Golang 單例模式與sync.Once
- 如何通俗地理解「分散式系統」;Vue是否可以在一個專案中使用多個UI框架;大廠上線流程:先上前端還是後端|極客觀點
- 第二屆 1024 中國工程師文化日議程全覽,你不能錯過的 N 個理由
- 手寫程式語言-遞迴函式是如何實現的?