JavaScript Hook · 十招實戰詳解

不動網站一行程式碼,把原本看不見的資料流全部攤在陽光下

這份教學適合誰

你只需要會按 F12。不需要懂 JavaScript 語法——每個腳本都是複製貼上就能跑的黑盒子,我會告訴你它能看到什麼、什麼時候該用它。

用途是自己除錯、學習網頁運作原理、或對自己的系統做測試。請只用在你自己的網站、你有權限的系統,或純粹的技術學習上。

一、Hook 是什麼

Hook(鉤子)就是在資料必經的管道口,裝一台監視器。

想像一棟大樓的自來水系統。水從水廠進來,經過管道,流到你家水龍頭。你站在水龍頭前面,只能看到最後流出來的水——中間加了什麼、什麼時候加的,一概不知。

現在你在某一段管道上開個小窗口,裝一台攝影機。水照流,什麼都沒改變,但你多了一個能力:看見流過去的是什麼。

網頁裡的資料也是這樣流的。網站把資料交給 JSON.stringify 轉成字串,再交給 fetch 發出去。Hook 就是在 JSON.stringifyfetch 這些「管道口」上裝監視器。

關鍵理解

Hook 不會破壞網站的功能。它只是在原始功能外面包一層,先把資料抄一份印出來,然後原封不動地交還給原始功能繼續跑。

所以你可以隨時按 F5 重新整理,一切就恢復原狀,不會留下任何痕跡。

它能解決什麼問題

開發者工具(F12)裡面已經有 Network 面板,為什麼還需要 Hook?因為 Network 面板有幾個天生的盲區:

你想知道的事Network 面板看得到嗎Hook 看得到嗎
請求發出去的地址原始參數 ✅ 可以 ✅ 可以
參數是哪一行程式碼產生的 ❌ 只是一個字串 ✅ 加一行 debugger 就能順藤摸瓜
參數被加密/編碼前的原始樣子 ❌ 只看到加密後的字串 ✅ 在 stringify 那關就攔到了
Cookie 什麼時候被寫入 ❌ 只能看最終結果 ✅ 寫入的瞬間就跳出來
WebSocket 每一條訊息的內容 ⚠️ 要手動一條一條點開 ✅ 自動即時印出來
eval 動態執行的程式碼長什麼樣 ❌ 完全看不到 ✅ 整段原始碼印出來
一句話總結

Network 面板告訴你「發生了什麼」;Hook 告訴你「它是怎麼發生的」

二、所有 Hook 的共同套路

接下來要看十個腳本,但它們的原理其實是同一個,只有四個步驟。這一節看懂了,後面十個腳本你都能自己寫出來。

假設我們想攔截 JSON.stringify。它是瀏覽器內建的函式,我們不能改瀏覽器,但可以把它從全域位置上換掉——換成我們自己寫的版本。這就是全部的魔法。

// ① 先把原生方法「存起來」
var _stringify = JSON.stringify;

// ② 用自己寫的函式,把它「換掉」
JSON.stringify = function () {

    // ③ 想做的事:把資料抄一份印出來
    console.log('進來了:', arguments[0]);

    // ④ 原樣交還給原生方法,讓網站照常運作
    return _stringify.apply(this, arguments);
};
四步口訣:存 → 換 → 看 → 還

存:把原生方法存到一個變數裡。
換:用自己寫的函式取代它。
看:在自己的函式裡,把想知道的東西印出來。
還:呼叫存下來的那個原生方法,把結果原封不動回傳出去。

為什麼第一步「存」絕對不能省

這是初學者最容易忽略、也最容易踩的坑。看下面這段錯誤示範

// ❌ 錯誤寫法:忘記先把原生方法存起來
JSON.stringify = function (data) {
    console.log('進來了:', data);
    return JSON.stringify(data);   // ← 災難在這裡
};

問自己一個問題:JSON.stringify 這個名字,現在指向誰?

答案是指向我們剛剛寫的那個函式自己。所以裡面那行 JSON.stringify(data) 其實是在呼叫自己,然後自己又呼叫自己,然後又……

Uncaught RangeError: Maximum call stack size exceeded (堆疊溢位——俗稱「爆棧」)
這是最常見的翻車點

改寫內建函式時,必須先留一份原始版本在手裡,否則你就把唯一的出口也堵死了,整個頁面會直接壞掉。

所以十個腳本裡,每一行 var _xxx = 原生方法; 都是這個用意。看到底線開頭的變數名,就知道那是「原生版本」。

為什麼要用 .apply(this, arguments)

兩個原因,都很實際:

寫法作用
arguments 「照單全收」——不管對方傳了幾個參數、什麼型別,原封不動全部轉交。你不需要知道原本的函式怎麼用
.apply(this, ...) 保留 this 的指向。有些方法靠 this 才知道自己該操作哪個物件,弄丟了就會報錯。
好記的說法

arguments 像快遞代收站:不管送來多少件、什麼形狀,先全收下,再原樣轉給收件人。

這正是 Hook 能做到「不破壞網站功能」的關鍵——我們只是路過看了一眼,包裹本身碰都沒碰。

不是所有東西都能這樣換

有三種情況需要特別處理,後面的腳本裡都會遇到:

情況為什麼麻煩哪個腳本遇到
方法定義在原型
(如 Storage.prototype.setItem
要改的是原型,不是實例。改對了,所有實例一起生效。 05、06
屬性不是方法
(如 document.cookie
不能用賦值,要用 Object.defineProperty 重寫 getter / setter。 03、10
建構式
(如 new WebSocket(...)
換掉之後要記得把原型鏈和靜態常數一起搬過去,否則頁面判斷會出錯。 06

不用擔心記不住——腳本都已經幫你處理好了,這裡只是讓你知道為什麼每個腳本的開頭長得不太一樣

三、怎麼在瀏覽器裡跑起來

Chrome 和 Edge 操作完全一樣。整個流程五步:

  1. 打開目標網頁,按 F12 打開開發者工具
  2. 頂部選 Sources(原始碼) 分頁
  3. 左側欄找到 Snippets(程式碼片段),點 + New snippet
  4. 把某個 .js 檔案的全部內容貼進去
  5. Ctrl + Enter 執行,然後切到 Console(主控台) 看輸出
想停止 Hook?

直接按 F5 重新整理頁面就恢復了。
不用卸載、不用清理,重新整理 = 一切歸零。所以放心大膽地試。

3.1 一次裝上全部十個

不用開十個 Snippet。把它們全部貼進同一個 Snippet 就行——每個檔案都用 (function(){ ... })(); 獨立包起來了,彼此不會污染,也不會打架。

貼好之後按一次 Ctrl + Enter,十個 Hook 同時生效。主控台會連續印出十行 [Hook] xxx 已劫持,看到就代表成功了。

3.2 最重要的坑:時機

裝晚了,就什麼都看不到了

如果你的目標是頁面一載入就執行的邏輯(例如啟動時就發出的請求),那必須趕在它之前把 Hook 裝好,否則那幾次呼叫早就跑完了,你只會看到一個空蕩蕩的主控台。

正確的做法:

  1. 先按上面的步驟把 Snippet 準備好,但先不要執行
  2. F5 重新整理頁面
  3. 頁面剛開始載入時,立刻回到 Sources 面板,選中 Snippet,按 Ctrl + Enter

多試兩次就能卡準時機。技巧是:重新整理後不要等頁面畫完,載入轉圈的時候就可以按了。

3.3 進階:讓它自動在每次載入時執行

如果同一個網站你要反覆研究,每次手動卡時機很煩。可以在 Snippet 上按右鍵 → 選「Run」旁邊的執行選項,或者把腳本內容貼進 Chrome 擴充功能(如 Tampermonkey)設為「document-start 時執行」——這樣就永遠不會錯過開頭。

不過對於學習和臨時排查,手動三步已經夠用了。

四、十個腳本逐一詳解

先給你一張總覽表,方便按需取用。每一項下面都有詳細說明和下載按鈕

#鉤住什麼解決什麼問題建議
01JSON.stringify / parse看資料「加密前 / 解密後」的明文⭐ 必備
02XMLHttpRequest / fetch看請求發了什麼、回應回了什麼⭐ 必備
03document.cookie看 cookie 何時被寫入、寫入什麼常用
04eval / Function揪出動態生成的「加密程式碼」進階
05localStorage / sessionStorage看瀏覽器儲存的讀寫時機常用
06WebSocket看即時通訊收發的每一條訊息按需
07atob / btoa看 Base64 編解碼前後的內容⭐ 必備
08setTimeout / setInterval看定時器裡跑的是什麼程式碼按需
09Math.random / Date.now固定隨機數與時間戳,複現請求⚠️ 高頻
10Object.defineProperty揪出隱藏的 getter / setter 邏輯⚠️ 高頻

01JSON stringify / parse

看清楚「加密前」和「解密後」的明文資料——排查介面參數最常用的一招。

鉤住什麼JSON.stringify(物件→字串)、JSON.parse(字串→物件)
什麼時候用參數是一坨看不懂的字串,但你想知道送出去之前它長什麼樣

網站要把資料發出去之前,通常會先把物件轉成 JSON 字串。所以「轉換的那一刻」就是資料最完整、最可讀的那一刻。在這裡攔下來,等於在包裹封箱之前先開箱看一眼。

var _stringify = JSON.stringify;
var _parse = JSON.parse;

JSON.stringify = function () {
    var result = _stringify.apply(this, arguments);
    console.log('[JSON.stringify] 輸入 →', arguments[0]);
    console.log('[JSON.stringify] 輸出 →', result);
    return result;
};

JSON.parse = function () {
    console.log('[JSON.parse] 輸入 →', arguments[0]);
    var result = _parse.apply(this, arguments);
    console.log('[JSON.parse] 輸出 →', result);
    return result;
};
[JSON.stringify] 輸入 → {uid: "8831", token: "a3f9...", ts: 1735689600} [JSON.stringify] 輸出 → {"uid":"8831","token":"a3f9...","ts":1735689600} [JSON.parse] 輸入 → {"code":0,"data":{"name":"張三"}} [JSON.parse] 輸出 → {code: 0, data: {…}}
小技巧:直接用主控台看物件

主控台裡印出來的物件是可以點開的。看到 {…} 就點它,能一層層展開,比看字串舒服得多。

02XHR 與 fetch 網路請求

頁面到底往哪個地址、發了什麼、收到什麼——一個腳本全交代。

鉤住什麼XMLHttpRequest.prototype.open / sendwindow.fetch
什麼時候用幾乎所有情況。不確定從哪下手時,先裝這一個

這是十個腳本裡最實用的一個。因為網站絕大多數的行為,最後都要變成一個網路請求。抓住了請求,就抓住了主線。

它同時鉤了兩種東西:XMLHttpRequest(老式寫法,但至今仍大量存在)和 fetch(現代寫法)。兩個都蓋住,才不會漏。

XHR 的部分

XHR 需要鉤兩個方法,因為資訊分散在兩處:open 負責記錄「往哪發」,send 負責記錄「發了什麼」。

var _open = XMLHttpRequest.prototype.open;
var _send = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.open = function (method, url) {
    // 先把 method 和 url 記在這個實例身上,send 的時候才拿得到
    this.__hookInfo = { method: method, url: url };
    return _open.apply(this, arguments);
};

XMLHttpRequest.prototype.send = function (body) {
    var self = this;
    var info = this.__hookInfo || {};
    console.log('[XHR 請求] ' + info.method + ' ' + info.url, '\n參數:', body);

    // 等回應回來了再印
    this.addEventListener('load', function () {
        console.log('[XHR 回應] ' + info.url, '\n結果:', self.responseText);
    });

    return _send.apply(this, arguments);
};

fetch 的部分

fetch 有一個很關鍵的細節:回應內容只能被讀取一次。你讀走了,頁面自己就拿不到了,功能會直接壞掉。所以必須先 clone() 一份再讀。

var _fetch = window.fetch;

window.fetch = function () {
    var args = arguments;
    var url = args[0];
    var init = args[1] || {};

    console.log('[fetch 請求] ' + url, '\n參數:', init.body);

    return _fetch.apply(this, args).then(function (response) {
        // 必須 clone 一份再讀!直接讀會把資料流用掉,頁面就拿不到結果了
        response.clone().text().then(function (text) {
            console.log('[fetch 回應] ' + url, '\n結果:', text);
        });
        return response;
    });
};
這個 clone() 不能省

fetch 的回應是一個一次性資料流,就像一張只能播放一次的光碟。你播了,別人就沒得播。

response.clone() 就是「先燒一張備份光碟」——你播備份的,正本留給頁面。少了這一步,網站會出現各種詭異的錯誤。

[XHR 請求] POST /api/v2/order/list 參數: {"page":1,"sign":"7f3a9c..."} [XHR 回應] /api/v2/order/list 結果: {"code":0,"data":[...]}

03document.cookie Cookie 讀寫

看 cookie 在什麼時候、被哪一行程式碼寫進去。

鉤住什麼document.cookie 的 getter 與 setter
什麼時候用要搞清楚登入憑證(token / session)是怎麼產生、怎麼更新的

很多網站的關鍵憑證藏在 cookie 裡,而且是由 JavaScript 動態生成的——每次請求前算一次,寫進 cookie,再發出去。你想知道它是怎麼算的,就得先知道它什麼時候被寫入

document.cookie 不是普通方法,它是屬性。屬性不能用賦值的方式換掉,要用 Object.defineProperty 重新定義它的讀取(get)和寫入(set)行為。

// 關鍵:先拿到原生的 getter / setter(它們定義在 Document.prototype 上)
var desc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');

Object.defineProperty(document, 'cookie', {
    configurable: true,

    get: function () {
        var value = desc.get.call(document);
        console.log('[cookie 讀取] →', value);
        return value;
    },

    set: function (value) {
        console.log('[cookie 寫入] →', value);
        // 轉交給原生 setter,保證頁面原本的 cookie 邏輯不受影響
        return desc.set.call(document, value);
    }
});
[cookie 寫入] → token=eyJhbGciOi...; path=/; expires=... [cookie 讀取] → token=eyJhbGciOi...; _ga=GA1.2...
一個必須知道的限制

這種寫法只能看到「由 JavaScript 讀寫」的 cookie

伺服器透過回應標頭 Set-Cookie 種下的 cookie,是瀏覽器底層直接處理的,不會經過 JavaScript,所以這裡看不到。那種要去 Application 面板的 Cookies 區看。

為什麼不直接覆蓋,非要用 getter / setter

如果偷懶寫成 document.cookie = 我的函式,會直接弄壞 cookie 功能——頁面之後所有讀寫 cookie 的操作都會失敗,登入狀態當場消失。

defineProperty 重寫 getter / setter,才能做到「看一眼,但行為完全不變」。這是本腳本最關鍵的地方。

04eval 與 Function 動態程式碼

揪出「加密函式」的原文——很多混淆過的網站,祕密就藏在這裡。

鉤住什麼window.evalwindow.Function(建構式)
什麼時候用找遍所有檔案都找不到加密邏輯,懷疑它是「動態生成」的

有些網站為了防分析,會把關鍵邏輯寫成一段字串,等到執行时才用 evalnew Function 把它變成真正能跑的程式碼。

這種情況下,你在 Sources 面板裡翻遍所有檔案都找不到——因為那段程式碼根本不在任何檔案裡,它是臨時拼出來的。

var _eval = window.eval;

window.eval = function (code) {
    console.log('[eval 執行] →', code);
    debugger;                  // 直接在這裡斷下
    return _eval.call(this, code);
};

var _Function = window.Function;

function HookedFunction() {
    console.log('[new Function] →', arguments);
    debugger;
    return _Function.apply(this, arguments);
}

// 保持原型鏈,否則 instanceof 之類的判斷會出問題
HookedFunction.prototype = _Function.prototype;
window.Function = HookedFunction;

裝上之後重新整理頁面,主控台會直接把那段「隱形程式碼」整段印出來。這往往是整個分析過程中最有突破性的一刻。

這個腳本比較「兇」,要有心理準備

它改寫的是全域的 Function 建構式,個別網站可能會因此報錯或功能異常

這是正常現象,按 F5 重新整理就恢復了。建議:先單獨裝這一個,確認網站還能跑,再去裝其他的。

另外它內建了 debugger(沒被註解掉),所以一觸發就會暫停。如果停得太頻繁,把 debugger; 那兩行註解掉即可。

05localStorage / sessionStorage 瀏覽器儲存

看 token、使用者資訊、快取資料「什麼時候」被存進去。

鉤住什麼Storage.prototypesetItem / getItem / removeItem
什麼時候用知道資料存在 localStorage,但想知道它是何時、被什麼操作觸發寫入的

Application 面板可以看到 localStorage 裡有什麼,但看不到什麼時候寫進去的。而時機往往才是關鍵——因為寫入的那一刻,就是資料產生的那一刻。

這裡有個很漂亮的技巧:localStoragesessionStorage 都是 Storage 的實例,所以只要改 Storage.prototype,兩個就一起管住了,不用寫兩遍。

var _setItem = Storage.prototype.setItem;

function which(storage) {
    return storage === window.localStorage ? 'localStorage' : 'sessionStorage';
}

Storage.prototype.setItem = function (key, value) {
    console.log('[' + which(this) + ' 寫入] ' + key + ' =', value);
    return _setItem.apply(this, arguments);
};
[localStorage 寫入] token = eyJhbGciOiJIUzI1NiIs... [localStorage 寫入] userInfo = {"id":8831,"name":"張三","vip":true} [sessionStorage 寫入] cartCount = 3
為什麼要判斷 this

which(this) 是在判斷「這次呼叫是從哪個儲存物件發出的」。

因為兩個儲存共用同一個原型方法,不判斷的話輸出會混在一起,你分不清到底是哪一個被寫了。這個小設計能省下很多困惑。

06WebSocket 即時通訊

聊天、直播、股價走的是 WebSocket——這些資料在 Network 面板裡藏得很深。

鉤住什麼WebSocket.prototype.sendwindow.WebSocket 建構式
什麼時候用網站有即時更新的內容,但你找不到資料從哪來

WebSocket 建立連線之後,資料是雙向持續流動的,不像普通請求一問一答。在 Network 面板裡要一條一條手動點開訊息,很繁琐。裝上這個腳本,每一條訊息都會自動即時印出來。

它需要鉤兩個地方:發送send 方法,接收則要在連線建立時掛上監聽。

// 發送:改原型上的 send
var _send = WebSocket.prototype.send;

WebSocket.prototype.send = function (data) {
    console.log('[WS 發送] →', data);
    return _send.apply(this, arguments);
};

// 接收:要攔截建構式,才能在連線建立時掛監聽
var _WebSocket = window.WebSocket;

function HookedWebSocket(url, protocols) {
    var ws = (protocols === undefined)
      ? new _WebSocket(url)
      : new _WebSocket(url, protocols);

    ws.addEventListener('message', function (event) {
        console.log('[WS 接收] ←', event.data);
    });

    return ws;
}

// 原型鏈和靜態常數都要搬過去,否則會破壞頁面對 WebSocket 的判斷
HookedWebSocket.prototype = _WebSocket.prototype;
HookedWebSocket.CONNECTING = 0;
HookedWebSocket.OPEN = 1;
HookedWebSocket.CLOSING = 2;
HookedWebSocket.CLOSED = 3;

window.WebSocket = HookedWebSocket;
那四行常數不能省

WebSocket.CONNECTINGOPEN 這些是寫在建構式本身上的常數,不是原型上的。

頁面很常用 if (ws.readyState === WebSocket.OPEN) 這種方式判斷連線狀態。如果我們換掉建構式卻忘了搬常數,WebSocket.OPEN 就會變成 undefined,判斷永遠不成立——連線會詭異地一直重連或直接失效。

[WS 連接] wss://push.example.com/feed [WS 發送] → {"type":"subscribe","channel":"price:btc"} [WS 接收] ← {"type":"price","btc":67231.5,"ts":1735689600}

07atob / btoa Base64 編解碼

看到一串以 eyJ 開頭的亂碼?這招直接變回明文。

鉤住什麼window.atob(解碼)、window.btoa(編碼)
什麼時候用參數看起來是亂碼,但你懷疑它只是被「編碼」而沒有真正加密
先記住這個辨認技巧

看到一串字元,eyJ 開頭,而且結尾常帶 =——那幾乎可以確定是 Base64。

因為 eyJ 解碼之後正好是 {",而 JSON 物件就是這樣開頭的。這是最好用的肉眼判斷法。

Base64 不是加密,它只是一種編碼方式,任何人都能還原。但很多網站會用它來讓參數「看起來像亂碼」,順便避開一些簡單的字串過濾。

var _atob = window.atob;
var _btoa = window.btoa;

window.atob = function (str) {
    var result = _atob.call(window, str);
    console.log('[atob 解碼]', '\n輸入:', str, '\n輸出:', result);
    return result;
};

window.btoa = function (str) {
    var result = _btoa.call(window, str);
    console.log('[btoa 編碼]', '\n輸入:', str, '\n輸出:', result);
    return result;
};
[atob 解碼] 輸入: eyJ1aWQiOiI4ODMxIiwidmlwIjp0cnVlfQ== 輸出: {"uid":"8831","vip":true}
最強組合:07 + 01

Base64 解出來的東西通常就是 JSON。所以 0701 一起裝,你會同時看到「解碼後的字串」和「解析後的物件」——明文就是肉眼完全可讀的了。

08setTimeout / setInterval 定時器

看定時器裡跑的究竟是什麼程式碼——常常是核心業務邏輯的入口。

鉤住什麼setTimeout / setInterval / clearTimeout / clearInterval
什麼時候用網站有輪詢、倒數計時、延遲上報,你找不到它的邏輯寫在哪

定時器有個特性:程式碼是以「函式」的形式傳進去的。而函式可以透過 .toString() 把自己變成原始碼字串——所以我們能直接把「待會要執行的程式碼」印出來。

這個腳本還做了一個貼心設計:用一張表記住「定時器編號 → 對應的程式碼」,這樣 clearTimeout 清除時,能直接告訴你被清掉的是哪一段程式碼

var _setTimeout = window.setTimeout;
var idMap = new Map();    // 定時器編號 → 函式原始碼

function sourceOf(fn) {
    return (typeof fn === 'function') ? fn.toString() : String(fn);
}

window.setTimeout = function (fn, delay) {
    var id = _setTimeout.apply(this, arguments);
    var src = sourceOf(fn);
    idMap.set(id, src);
    console.log('[setTimeout] ' + delay + 'ms', '\n', src);
    return id;
};

window.clearTimeout = function (id) {
    console.log('[clearTimeout] 清除 ' + id, '\n對應程式碼:', idMap.get(id) || '(未知)');
    idMap.delete(id);
    return _clearTimeout.apply(this, arguments);
};
[setTimeout] 3000ms function () { refreshToken(); pollOrders(); } [clearTimeout] 清除 42 對應程式碼: function () { refreshToken(); pollOrders(); }
這個腳本在找什麼

看到 refreshToken()pollOrders() 這類函式名,就等於拿到了下一步的關鍵字

接著在 Sources 面板按 Ctrl + Shift + F 全域搜尋這個名字,就能直接跳到定義它的地方。這是最快的尋路方式。

09Math.random / Date.now 隨機數與時間戳

把隨機值和時間戳固定住,同一個請求就能反覆複現。

鉤住什麼Math.randomDate.now
什麼時候用簽名參數每次都不一樣,導致你沒辦法重複測試同一個請求

很多簽名演算法會故意摻入隨機數時間戳,目的是防止請求被重放。但對做測試的人來說,這正是最煩的地方——每次算出來的簽名都不同,根本沒辦法比對。

把這兩個值固定住,同一個請求的簽名就會每次都一樣,於是就能反覆複現、逐步比對了。

var _random = Math.random;

Math.random = function () {
    var value = _random.apply(this, arguments);
    console.log('[Math.random] → ' + value);
    // 想固定就改成:return 0.123456;
    return value;
};

var _now = Date.now;

Date.now = function () {
    var value = _now.apply(this, arguments);
    console.log('[Date.now] → ' + value + '  (' + new Date(value).toLocaleString() + ')');
    // 想固定就改成:return 1735689600000;
    return value;
};

怎麼固定

把腳本裡那兩行註解打開、改成直接回傳定值就好:

Math.random = function () { return 0.123456; };
Date.now    = function () { return 1735689600000; };
⚠️ 這個腳本會把主控台刷爆

Math.randomDate.now 的呼叫極其頻繁——動畫、計時、框架內部到處都在用,一秒可能幾百上千次。

三個絕對不要:

  • 不要開 debugger(會直接卡死)
  • 不要長時間開著 console.log(主控台會爆掉,頁面也變慢)
  • 不要忘記關——定位到問題就立刻 F5 重新整理

正確用法:先裝 02 找到問題請求,再臨時裝上 09 確認簽名組成,確認完馬上關掉。

10Object.defineProperty 隱藏的 getter / setter

抓出「讀取某個屬性時偷偷觸發加密」這種隱藏邏輯。

鉤住什麼Object.defineProperty只記錄 getter / setter
什麼時候用某個屬性的值來歷不明——明明沒看見誰設定它,讀出來卻有值

JavaScript 允許給物件定義「訪問器屬性」——表面上是一個普通屬性,但實際上每次讀取或寫入都會執行一段函式。

這意味著網站可以做到:你讀 obj.sign 的時候,它才現場算一遍加密。這種邏輯在程式碼裡用肉眼很難發現,因為它長得就像一個普通屬性。

var _defineProperty = Object.defineProperty;

Object.defineProperty = function (target, prop, descriptor) {
    // 只記錄「訪問器屬性」,也就是有 get 或 set 的
    if (descriptor && (descriptor.get || descriptor.set)) {
        console.log('[defineProperty] 定義訪問器屬性:' + String(prop),
                    '\n目標物件:', target,
                    '\n描述符:', descriptor);
    }
    return _defineProperty.apply(this, arguments);
};
[defineProperty] 定義訪問器屬性:sign 目標物件: {…} 描述符: {get: ƒ, set: ƒ, enumerable: true, configurable: true}
為什麼只印 getter / setter,不全部印

defineProperty 的呼叫量非常大——現代框架在初始化時就會呼叫成千上萬次,全部印出來會把主控台徹底淹掉,什麼都找不到。

但真正藏邏輯的,恰恰只有「訪問器屬性」這一種。過濾掉 99% 的噪音,留下那 1% 的重點——這是這個腳本最聰明的地方。

⚠️ 高頻腳本,用完請關

雖然已經過濾過,但訪問器屬性仍然可能不少。看到需要的資訊就馬上 F5 重新整理,不要一直開著。

五、組合技:實戰配方

十個腳本不需要一次全裝。依照你的目標挑選組合,效果更好也更清爽。

5.1 按目標挑組合

你的目標裝這些說明
只想看介面發了什麼 02 單獨用就夠,最常用
參數是亂碼,想還原明文 01 + 02 + 07 黃金三件套,涵蓋 JSON 與 Base64 兩大常見編碼
想找加密函式在哪 02 → 加 debugger 看呼叫棧 見下面 5.2 的完整流程
登入憑證怎麼產生的 03 + 05 同時蓋住 cookie 和儲存
完全不知道從哪下手 全部裝上 先看到全貌,再逐一收窄

5.2 實戰流程:三步找出「加密參數」怎麼算出來的

這是最經典的需求,也是這十個腳本最主要的用途。完整流程:

第一步:先找到那個請求

裝上 02,重新整理頁面,然後在網頁上做一次會觸發目標請求的操作(例如點「下一頁」)。主控台會印出請求的網址和參數。

找到那一行,記住它的 URL參數長相

第二步:判斷參數是不是「只是被編碼了」

看參數的樣子:

第三步:順藤摸瓜,找到產生它的函式

這是關鍵的一步。回到 02 的腳本裡,找到這一行:

console.log('[XHR 請求] ' + info.method + ' ' + info.url, '\n參數:', body);

在它下面加一行:

debugger;

然後重新整理頁面、重複剛才的操作。程式會自動暫停在那一行。這時候看右側的 Call Stack(呼叫棧)面板——它列出了「是誰呼叫了這一行」,一層一層往上排。

由上往下逐層點開,每一層都看看它的程式碼。你要找的那個加密函式,就在這條鏈條上的某一層裡。

這就是「順藤摸瓜」

你不需要讀懂整個專案幾萬行壓縮程式碼。你只需要沿著呼叫棧往上走幾層,就能直達源頭。

找到之後,在 Sources 面板對那個檔案按 Ctrl + Shift + F 全域搜尋函式名,就能反覆研究它。

5.3 全域搜尋:最被低估的技巧

找到任何一個函式名或關鍵字之後,在 Sources 面板按 Ctrl + Shift + F 開啟全域搜尋,輸入那個名字。

它會在所有已載入的檔案裡搜尋,包含被壓縮成一行的檔案。這是從「一個名字」擴散到「完整邏輯」最快的路徑。

搜尋時的小技巧

壓縮過的程式碼全部擠在一行裡,直接開啟會看到一團亂。點搜尋結果之後按 Ctrl + F 在檔案內再搜一次關鍵字,然後按 { }(美化)按鈕把程式碼排版展開,會好讀很多。

六、常見坑速查

坑 1:裝好了,但主控台什麼都沒有

原因:裝晚了。目標程式碼在你裝 Hook 之前就已經跑完了。

解決:按 F5 重新整理,在頁面剛開始載入時立刻執行 Snippet。見 第三節 3.2

坑 2:頁面白屏,或功能變得怪怪的

原因:Hook 腳本和網站本身的程式碼起了衝突。最常見的是 04(改寫了全域 Function)。

解決:F5 重新整理,一切恢復,這屬於正常現象。要繼續用就單獨裝、逐個排除。

坑 3:主控台瘋狂刷屏,瀏覽器卡死

原因:鉤到了呼叫極其頻繁的函式,通常是 09(Math.random / Date.now)或 10(defineProperty)。

解決:關掉這兩個腳本,只留需要的。真的要用,就縮短使用時間——確認到需要的資訊就立刻重新整理

坑 4:改了 document.cookie 之後,登入狀態消失了

原因:用了錯誤的寫法(直接賦值覆蓋),把 cookie 功能弄壞了。

解決:本目錄的 03 已經用原生的 getter / setter 正確處理,不會有這個問題。直接下載本頁的版本即可。

坑 5:fetch 的請求有印出來,回應卻沒有

原因:通常是漏掉了 response.clone(),或者網站用的是 XHR 而不是 fetch。

解決:本頁的 02 同時鉤了 XHR 和 fetch,兩種都涵蓋。確認你下載的是完整版本。

坑 6:debugger 一開就卡在那裡,按繼續又馬上停

原因:那個函式被呼叫得太頻繁,每次呼叫都觸發斷點。

解決:不要用 debugger,改用 console.log 觀察。或者用 Chrome 的條件斷點:在 Sources 面板對斷點按右鍵 → Edit breakpoint → 輸入條件(例如 info.url.includes('order')),只有符合條件時才停。

一句話總表

症狀原因解決
沒有輸出裝晚了F5 後立刻重裝
頁面白屏腳本衝突F5 恢復,逐個排查
主控台刷屏高頻函式關掉 09 / 10
cookie 不見了覆蓋寫法錯誤用本頁的 03
回應沒印出來漏了 clone / 型別不符用本頁的 02
斷點停不住呼叫太頻繁改用條件斷點

七、下載全部

十個腳本可以一次打包下載。每個檔案都已經寫好完整的註解說明,可以直接放進 Snippet 使用。

打包下載

包含全部 10 個腳本 + 一份純文字版使用說明。

單獨下載

01 JSON 02 XHR/fetch 03 cookie 04 eval 05 storage 06 WebSocket 07 Base64 08 定時器 09 隨機/時間 10 defineProperty


三句話總結

  1. Hook 就是在資料必經的管道口裝監視器——看得到,但什麼都不改變。
  2. 所有腳本都是同一個套路:存下原生方法 → 換成自己的 → 看一眼 → 原樣還回去。
  3. 它的價值是把 Network 面板看不到的東西(原始明文、產生時機、呼叫來源)攤開來。

建議的學習順序

  1. 先裝 02,找個你常用的網站,看看它的請求長什麼樣——建立感覺
  2. 再加上 0107,看看能不能把參數還原成明文——建立信心
  3. 找一個真的看不懂的參數,用 5.2 的三步流程追一次——這個過程走通一次,你就掌握了

不用一次學完十個。02 用熟,就已經能解決八成的問題了。