鐵人賽 Day 23 逆向實戰 - Header 驗證參數 + Url 驗證參數 + Response加密 (中等)

鐵人賽 Day 23 逆向實戰 - Header 驗證參數 + Url 驗證參數 + Response加密 (中等)
鐵人賽 Day 23 逆向實戰 - Header 驗證參數 + Url 驗證參數 + Response加密 (中等)

本系列文章所討論的 JavaScript 資安與逆向工程技術,旨在分享知識、探討防禦之道,並促進技術交流。

所有內容僅供學術研究與學習,請勿用於任何非法或不道德的行為。

讀者應對自己的行為負完全責任。尊重法律與道德規範是所有技術人員應共同遵守的準則。

 

aHR0cHM6Ly93d3cubWFzaGFuZ3BhLmNvbS9wcm9ibGVtLWRldGFpbC83Lw==

DevTools 會直接停在這行往上追蹤 Call Stack。

1.jpg

Function(arguments[0] + "bugger")(),代表 debugger 是被動態建構出來的。

繼續往上追蹤 Call Stack。

2.jpg

找到觸發debugger的地方但因為會一直觸發所以還需要繼續往上追蹤 Call Stack。

3.jpg

發現是因為 setInterval 所以導致程式會不停觸發斷點。

4.jpg

透過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 」這個就是驗證參數。

5.jpg

查看 Request Headers 可以看到兩個關鍵欄位。

m:驗證參數。

ts:時間戳。

6.jpg

在 Initiator 頁籤看到這個請求是由 pagination7.js 裡的 N.ajax 所發出。

7.jpg

在此行下斷點重新切換分頁讓程式斷在這行。

程式經過些微的混淆必須步步檢視,並且看到 Y 變數中的 headers 已有驗證參數。

也就表示說在這一步的時候驗證參數已經生成完了,所以必須往上找生成邏輯。

在斷點的上方可以看到有個函式需要把 Y 帶入,我們在上方再下個斷點進行觀察。

8.jpg

重新換頁斷住後使用單步執行詳細觀察 Y 是有變化。

確認 I(Y) 會加上 headers 的驗證餐數。

9.jpg

滑鼠放在 I 上顯示其為一個函式,點擊後可跳到定義處。

10.jpg

觀察到關鍵字如 headers、m、ts、url,因此可以確認此處是生成驗證參數的地方。

11.jpg
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 還是加密狀態。

12.jpg

我們使用單步執行慢慢觀察在哪一行程式被解密成功。

當 B[yF(0x222)] 執行後 response 就會被解密。

可以在 Console 將 I 輸出驗證是否解密成功。

13.jpg

滑鼠放在 d 上顯示其為一個函式,點擊後可跳到定義處。

14.jpg

因為混淆的關係所以看起來很複雜,但只要仔細觀察步步調適可以得知,實際上是透過 xxxxoooo 才成功解密 Response。

15.jpg

滑鼠放在 xxxxoooo 上顯示其為一個函式,點擊後可跳到定義處。

16.jpg

最終在程式內找到 CryptoJS.AES 的呼叫,驗證這就是實際進行解密的核心程式碼。

17.jpg
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

 

作者頭像
Nick

擅長從前端的互動設計到後端的資料處理,都能親自規劃與實作。對我來說開發網站不只是完成功能,而是打造一個能被真正使用、體驗順暢的平台。我喜歡把複雜的技術轉化成簡單好懂的成果,並在這個過程中持續學習與挑戰自己。