本系列文章所討論的 JavaScript 資安與逆向工程技術,旨在分享知識、探討防禦之道,並促進技術交流。
所有內容僅供學術研究與學習,請勿用於任何非法或不道德的行為。
讀者應對自己的行為負完全責任。尊重法律與道德規範是所有技術人員應共同遵守的準則。
本文同步發佈:https://nicklabs.cc/javascript-reverse-engineering-header-url-response-medium
aHR0cHM6Ly93d3cubWFzaGFuZ3BhLmNvbS9wcm9ibGVtLWRldGFpbC83Lw==
DevTools 會直接停在這行往上追蹤 Call Stack。
Function(arguments[0] + "bugger")(),代表 debugger 是被動態建構出來的。
繼續往上追蹤 Call Stack。
找到觸發debugger的地方但因為會一直觸發所以還需要繼續往上追蹤 Call Stack。
發現是因為 setInterval 所以導致程式會不停觸發斷點。
透過Script Furst Statement斷點在執行前進行Override
setInterval = () => {};
for(let i = 0 ; i < 999999 ; i++){
clearInterval(i)
};
在 Chrome DevTools 打開 「 Network 」 分頁,並篩選 「 XHR / Fetch 」 請求。
可以看到 /data/?page=3&x=... 的 API 呼叫。
「 x 」這個就是驗證參數。
查看 Request Headers 可以看到兩個關鍵欄位。
m:驗證參數。
ts:時間戳。
在 Initiator 頁籤看到這個請求是由 pagination7.js 裡的 N.ajax 所發出。
在此行下斷點重新切換分頁讓程式斷在這行。
程式經過些微的混淆必須步步檢視,並且看到 Y 變數中的 headers 已有驗證參數。
也就表示說在這一步的時候驗證參數已經生成完了,所以必須往上找生成邏輯。
在斷點的上方可以看到有個函式需要把 Y 帶入,我們在上方再下個斷點進行觀察。
重新換頁斷住後使用單步執行詳細觀察 Y 是有變化。
確認 I(Y) 會加上 headers 的驗證餐數。
滑鼠放在 I 上顯示其為一個函式,點擊後可跳到定義處。
觀察到關鍵字如 headers、m、ts、url,因此可以確認此處是生成驗證參數的地方。
let M = new Date().getTime();
let O = md5("xialuo" + M);
// headers.ts = M
// headers.m = O
// urlX = encodeURIComponent(dd.a.SHA256(O + "xxoo"))
headers 的 ts 實際為時間戳。
headers 的 m 實際為 md5("xialuo" + 時間戳)。
url 的 x 為 encodeURIComponent(dd.a.SHA256(md5("xialuo" + 時間戳) + "xxoo"))
接下來需要持續追蹤Response的解密邏輯。
當 ajax 請求成功時會執行 success 閉包在這裡設定斷點觀察。
在 success 函式裡,剛進入時 response 還是加密狀態。
我們使用單步執行慢慢觀察在哪一行程式被解密成功。
當 B[yF(0x222)] 執行後 response 就會被解密。
可以在 Console 將 I 輸出驗證是否解密成功。
滑鼠放在 d 上顯示其為一個函式,點擊後可跳到定義處。
因為混淆的關係所以看起來很複雜,但只要仔細觀察步步調適可以得知,實際上是透過 xxxxoooo 才成功解密 Response。
滑鼠放在 xxxxoooo 上顯示其為一個函式,點擊後可跳到定義處。
最終在程式內找到 CryptoJS.AES 的呼叫,驗證這就是實際進行解密的核心程式碼。
const CryptoJS = require('crypto-js')
function generatorUrlX(headerM){
return encodeURIComponent(
CryptoJS.SHA256(headerM + "xxoo")
)
}
function generatorHeaderM(time) {
const data = "xialuo" + time
return CryptoJS.MD5(data).toString()
}
const decrypt = (encryptedHex) => {
let key = CryptoJS.enc.Utf8.parse("xxxxxxxxoooooooo");
let iv = CryptoJS.enc.Utf8.parse("0123456789ABCDEF");
let parseEncryptedHex = CryptoJS.enc.Hex.parse(encryptedHex);
let decryptBuffer = CryptoJS.AES.decrypt({
ciphertext: parseEncryptedHex
},
key,
{
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
iv: iv,
});
return decryptBuffer.toString(CryptoJS.enc.Utf8);
}
const getPage = async(page) => {
const time = new Date().getTime();
const headerM = generatorHeaderM(time);
const urlX = generatorUrlX(headerM);
const response = await fetch(`https://xxxxxxxxxx/api/problem-detail/7/data/?page=${page}&x=${urlX}`, {
"headers": {
"accept": "*/*",
"accept-language": "zh-TW,zh;q=0.9,en;q=0.8,en-US;q=0.7",
"cache-control": "no-cache",
"pragma": "no-cache",
"priority": "u=1, i",
"sec-ch-ua": "\"Not;A=Brand\";v=\"99\", \"Google Chrome\";v=\"139\", \"Chromium\";v=\"139\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"cookie": "sessionid=xxxxxxxxxx",
"Referer": "https://xxxxxxxxxx/problem-detail/7/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"m": generatorHeaderM(time),
"ts": time,
},
"body": null,
"method": "GET"
});
let json = await response.json()
json = JSON.parse(decrypt(json.r))
return json.current_array.reduce((a, b) => a + b, 0);
}
const run = async() => {
let total = 0;
for(let i = 1; i <= 20; i++){
total += (await getPage(i))
}
console.log(`total: ${total}`)
}
run()
https://github.com/mrnick6886/ScrapingChallenges/blob/main/mashangpa/7.js