iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Security

走進資安現場: JavaScript資安逆向工程超實戰系列 第 23

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

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250923/20169775xH39O0UTBK.jpg

本系列文章所討論的 JavaScript 資安與逆向工程技術,旨在分享知識、探討防禦之道,並促進技術交流。
所有內容僅供學術研究與學習,請勿用於任何非法或不道德的行為。
讀者應對自己的行為負完全責任。尊重法律與道德規範是所有技術人員應共同遵守的準則。

本文同步發佈:https://nicklabs.cc/javascript-reverse-engineering-header-url-response-medium

挑戰網址

aHR0cHM6Ly93d3cubWFzaGFuZ3BhLmNvbS9wcm9ibGVtLWRldGFpbC83Lw==

解題過程

DevTools 被 debugger 斷住

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

https://ithelp.ithome.com.tw/upload/images/20250923/20169775Ep6li7evoK.jpg

查看 Call Stack

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

繼續往上追蹤 Call Stack。

https://ithelp.ithome.com.tw/upload/images/20250923/20169775kBJLBtopkx.jpg

找到觸發 debugger 的程式碼

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

https://ithelp.ithome.com.tw/upload/images/20250923/20169775ViaakiVBKA.jpg

確認觸發原因

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

https://ithelp.ithome.com.tw/upload/images/20250923/201697755CznVFswbM.jpg

Override setInterval

透過Script Furst Statement斷點在執行前進行Override

setInterval = () => {};
for(let i = 0 ; i < 999999 ; i++){
	clearInterval(i)
};

觀察 Network 請求

在 Chrome DevTools 打開 「 Network 」 分頁,並篩選 「 XHR / Fetch 」 請求。

可以看到 /data/?page=3&x=... 的 API 呼叫。

「 x 」這個就是驗證參數。

https://ithelp.ithome.com.tw/upload/images/20250923/20169775GYlm1QpJNM.jpg

查看 Request Headers

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

m:驗證參數。

ts:時間戳。

https://ithelp.ithome.com.tw/upload/images/20250923/20169775RsjQwgYX6w.jpg

查看 Initiator

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

https://ithelp.ithome.com.tw/upload/images/20250923/20169775XCgo9mO5kk.jpg

追蹤程式中的 headers

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

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

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

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

https://ithelp.ithome.com.tw/upload/images/20250923/201697755SauBBxrx4.jpg

尋找 headers 驗證參數生成位置

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

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

https://ithelp.ithome.com.tw/upload/images/20250923/201697750w1riRRKac.jpg

確認函式位置

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

https://ithelp.ithome.com.tw/upload/images/20250923/20169775ryn8umvxOE.jpg

找到產生 headers 的程式區塊

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

https://ithelp.ithome.com.tw/upload/images/20250923/20169775n2dBqq7A4P.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 成功回呼

當 ajax 請求成功時會執行 success 閉包在這裡設定斷點觀察。

在 success 函式裡,剛進入時 response 還是加密狀態。

https://ithelp.ithome.com.tw/upload/images/20250923/20169775iRBQPnFR7z.jpg

找到解密點

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

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

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

https://ithelp.ithome.com.tw/upload/images/20250923/20169775YBaQJqQgCr.jpg

追蹤實際解密函式

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

https://ithelp.ithome.com.tw/upload/images/20250923/2016977520Co6Lq95t.jpg

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

https://ithelp.ithome.com.tw/upload/images/20250923/20169775xdWodDL6eS.jpg

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

https://ithelp.ithome.com.tw/upload/images/20250923/20169775HaKEiQFL67.jpg

確認加解密實作

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

https://ithelp.ithome.com.tw/upload/images/20250923/20169775yl6jNdUPGb.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()

Github 原始碼

https://github.com/mrnick6886/ScrapingChallenges/blob/main/mashangpa/7.js


上一篇
Day 22 逆向實戰 - Header 驗證參數 + Response加密 (簡單)
系列文
走進資安現場: JavaScript資安逆向工程超實戰23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言