不動網站一行程式碼,把原本看不見的資料流全部攤在陽光下
你只需要會按 F12。不需要懂 JavaScript 語法——每個腳本都是複製貼上就能跑的黑盒子,我會告訴你它能看到什麼、什麼時候該用它。
用途是自己除錯、學習網頁運作原理、或對自己的系統做測試。請只用在你自己的網站、你有權限的系統,或純粹的技術學習上。
Hook(鉤子)就是在資料必經的管道口,裝一台監視器。
想像一棟大樓的自來水系統。水從水廠進來,經過管道,流到你家水龍頭。你站在水龍頭前面,只能看到最後流出來的水——中間加了什麼、什麼時候加的,一概不知。
現在你在某一段管道上開個小窗口,裝一台攝影機。水照流,什麼都沒改變,但你多了一個能力:看見流過去的是什麼。
網頁裡的資料也是這樣流的。網站把資料交給 JSON.stringify 轉成字串,再交給 fetch 發出去。Hook 就是在 JSON.stringify 和 fetch 這些「管道口」上裝監視器。
Hook 不會破壞網站的功能。它只是在原始功能外面包一層,先把資料抄一份印出來,然後原封不動地交還給原始功能繼續跑。
所以你可以隨時按 F5 重新整理,一切就恢復原狀,不會留下任何痕跡。
開發者工具(F12)裡面已經有 Network 面板,為什麼還需要 Hook?因為 Network 面板有幾個天生的盲區:
| 你想知道的事 | Network 面板看得到嗎 | Hook 看得到嗎 |
|---|---|---|
| 請求發出去的地址和原始參數 | ✅ 可以 | ✅ 可以 |
| 參數是哪一行程式碼產生的 | ❌ 只是一個字串 | ✅ 加一行 debugger 就能順藤摸瓜 |
| 參數被加密/編碼前的原始樣子 | ❌ 只看到加密後的字串 | ✅ 在 stringify 那關就攔到了 |
| Cookie 什麼時候被寫入 | ❌ 只能看最終結果 | ✅ 寫入的瞬間就跳出來 |
| WebSocket 每一條訊息的內容 | ⚠️ 要手動一條一條點開 | ✅ 自動即時印出來 |
被 eval 動態執行的程式碼長什麼樣 |
❌ 完全看不到 | ✅ 整段原始碼印出來 |
Network 面板告訴你「發生了什麼」;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) 其實是在呼叫自己,然後自己又呼叫自己,然後又……
改寫內建函式時,必須先留一份原始版本在手裡,否則你就把唯一的出口也堵死了,整個頁面會直接壞掉。
所以十個腳本裡,每一行 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 操作完全一樣。整個流程五步:
.js 檔案的全部內容貼進去直接按 F5 重新整理頁面就恢復了。
不用卸載、不用清理,重新整理 = 一切歸零。所以放心大膽地試。
不用開十個 Snippet。把它們全部貼進同一個 Snippet 就行——每個檔案都用 (function(){ ... })(); 獨立包起來了,彼此不會污染,也不會打架。
貼好之後按一次 Ctrl + Enter,十個 Hook 同時生效。主控台會連續印出十行 [Hook] xxx 已劫持,看到就代表成功了。
如果你的目標是頁面一載入就執行的邏輯(例如啟動時就發出的請求),那必須趕在它之前把 Hook 裝好,否則那幾次呼叫早就跑完了,你只會看到一個空蕩蕩的主控台。
正確的做法:
多試兩次就能卡準時機。技巧是:重新整理後不要等頁面畫完,載入轉圈的時候就可以按了。
如果同一個網站你要反覆研究,每次手動卡時機很煩。可以在 Snippet 上按右鍵 → 選「Run」旁邊的執行選項,或者把腳本內容貼進 Chrome 擴充功能(如 Tampermonkey)設為「document-start 時執行」——這樣就永遠不會錯過開頭。
不過對於學習和臨時排查,手動三步已經夠用了。
先給你一張總覽表,方便按需取用。每一項下面都有詳細說明和下載按鈕。
| # | 鉤住什麼 | 解決什麼問題 | 建議 |
|---|---|---|---|
01 | JSON.stringify / parse | 看資料「加密前 / 解密後」的明文 | ⭐ 必備 |
02 | XMLHttpRequest / fetch | 看請求發了什麼、回應回了什麼 | ⭐ 必備 |
03 | document.cookie | 看 cookie 何時被寫入、寫入什麼 | 常用 |
04 | eval / Function | 揪出動態生成的「加密程式碼」 | 進階 |
05 | localStorage / sessionStorage | 看瀏覽器儲存的讀寫時機 | 常用 |
06 | WebSocket | 看即時通訊收發的每一條訊息 | 按需 |
07 | atob / btoa | 看 Base64 編解碼前後的內容 | ⭐ 必備 |
08 | setTimeout / setInterval | 看定時器裡跑的是什麼程式碼 | 按需 |
09 | Math.random / Date.now | 固定隨機數與時間戳,複現請求 | ⚠️ 高頻 |
10 | Object.defineProperty | 揪出隱藏的 getter / setter 邏輯 | ⚠️ 高頻 |
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;
};
主控台裡印出來的物件是可以點開的。看到 {…} 就點它,能一層層展開,比看字串舒服得多。
網路請求頁面到底往哪個地址、發了什麼、收到什麼——一個腳本全交代。
| 鉤住什麼 | XMLHttpRequest.prototype.open / send、window.fetch |
|---|---|
| 什麼時候用 | 幾乎所有情況。不確定從哪下手時,先裝這一個 |
這是十個腳本裡最實用的一個。因為網站絕大多數的行為,最後都要變成一個網路請求。抓住了請求,就抓住了主線。
它同時鉤了兩種東西:XMLHttpRequest(老式寫法,但至今仍大量存在)和 fetch(現代寫法)。兩個都蓋住,才不會漏。
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 有一個很關鍵的細節:回應內容只能被讀取一次。你讀走了,頁面自己就拿不到了,功能會直接壞掉。所以必須先 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() 就是「先燒一張備份光碟」——你播備份的,正本留給頁面。少了這一步,網站會出現各種詭異的錯誤。
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);
}
});
這種寫法只能看到「由 JavaScript 讀寫」的 cookie。
伺服器透過回應標頭 Set-Cookie 種下的 cookie,是瀏覽器底層直接處理的,不會經過 JavaScript,所以這裡看不到。那種要去 Application 面板的 Cookies 區看。
如果偷懶寫成 document.cookie = 我的函式,會直接弄壞 cookie 功能——頁面之後所有讀寫 cookie 的操作都會失敗,登入狀態當場消失。
用 defineProperty 重寫 getter / setter,才能做到「看一眼,但行為完全不變」。這是本腳本最關鍵的地方。
動態程式碼揪出「加密函式」的原文——很多混淆過的網站,祕密就藏在這裡。
| 鉤住什麼 | window.eval、window.Function(建構式) |
|---|---|
| 什麼時候用 | 找遍所有檔案都找不到加密邏輯,懷疑它是「動態生成」的 |
有些網站為了防分析,會把關鍵邏輯寫成一段字串,等到執行时才用 eval 或 new 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; 那兩行註解掉即可。
瀏覽器儲存看 token、使用者資訊、快取資料「什麼時候」被存進去。
| 鉤住什麼 | Storage.prototype 的 setItem / getItem / removeItem |
|---|---|
| 什麼時候用 | 知道資料存在 localStorage,但想知道它是何時、被什麼操作觸發寫入的 |
Application 面板可以看到 localStorage 裡有什麼,但看不到什麼時候寫進去的。而時機往往才是關鍵——因為寫入的那一刻,就是資料產生的那一刻。
這裡有個很漂亮的技巧:localStorage 和 sessionStorage 都是 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);
};
this
which(this) 是在判斷「這次呼叫是從哪個儲存物件發出的」。
因為兩個儲存共用同一個原型方法,不判斷的話輸出會混在一起,你分不清到底是哪一個被寫了。這個小設計能省下很多困惑。
即時通訊聊天、直播、股價走的是 WebSocket——這些資料在 Network 面板裡藏得很深。
| 鉤住什麼 | WebSocket.prototype.send、window.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.CONNECTING、OPEN 這些是寫在建構式本身上的常數,不是原型上的。
頁面很常用 if (ws.readyState === WebSocket.OPEN) 這種方式判斷連線狀態。如果我們換掉建構式卻忘了搬常數,WebSocket.OPEN 就會變成 undefined,判斷永遠不成立——連線會詭異地一直重連或直接失效。
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;
};
Base64 解出來的東西通常就是 JSON。所以 07 和 01 一起裝,你會同時看到「解碼後的字串」和「解析後的物件」——明文就是肉眼完全可讀的了。
定時器看定時器裡跑的究竟是什麼程式碼——常常是核心業務邏輯的入口。
| 鉤住什麼 | 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);
};
看到 refreshToken()、pollOrders() 這類函式名,就等於拿到了下一步的關鍵字。
接著在 Sources 面板按 Ctrl + Shift + F 全域搜尋這個名字,就能直接跳到定義它的地方。這是最快的尋路方式。
隨機數與時間戳把隨機值和時間戳固定住,同一個請求就能反覆複現。
| 鉤住什麼 | Math.random、Date.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.random 和 Date.now 的呼叫極其頻繁——動畫、計時、框架內部到處都在用,一秒可能幾百上千次。
三個絕對不要:
debugger(會直接卡死)console.log(主控台會爆掉,頁面也變慢)正確用法:先裝 02 找到問題請求,再臨時裝上 09 確認簽名組成,確認完馬上關掉。
隱藏的 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 的呼叫量非常大——現代框架在初始化時就會呼叫成千上萬次,全部印出來會把主控台徹底淹掉,什麼都找不到。
但真正藏邏輯的,恰恰只有「訪問器屬性」這一種。過濾掉 99% 的噪音,留下那 1% 的重點——這是這個腳本最聰明的地方。
雖然已經過濾過,但訪問器屬性仍然可能不少。看到需要的資訊就馬上 F5 重新整理,不要一直開著。
十個腳本不需要一次全裝。依照你的目標挑選組合,效果更好也更清爽。
| 你的目標 | 裝這些 | 說明 |
|---|---|---|
| 只想看介面發了什麼 | 02 |
單獨用就夠,最常用 |
| 參數是亂碼,想還原明文 | 01 + 02 + 07 |
黃金三件套,涵蓋 JSON 與 Base64 兩大常見編碼 |
| 想找加密函式在哪 | 02 → 加 debugger 看呼叫棧 |
見下面 5.2 的完整流程 |
| 登入憑證怎麼產生的 | 03 + 05 |
同時蓋住 cookie 和儲存 |
| 完全不知道從哪下手 | 全部裝上 | 先看到全貌,再逐一收窄 |
這是最經典的需求,也是這十個腳本最主要的用途。完整流程:
裝上 02,重新整理頁面,然後在網頁上做一次會觸發目標請求的操作(例如點「下一頁」)。主控台會印出請求的網址和參數。
找到那一行,記住它的 URL 和參數長相。
看參數的樣子:
{"uid":"8831"} → 根本沒加密,這一步就結束了,你已經拿到答案eyJ 開頭 → 是 Base64,裝 07 就能還原a3f9c2...)→ 是真的加密,進第三步這是關鍵的一步。回到 02 的腳本裡,找到這一行:
console.log('[XHR 請求] ' + info.method + ' ' + info.url, '\n參數:', body);
在它下面加一行:
debugger;
然後重新整理頁面、重複剛才的操作。程式會自動暫停在那一行。這時候看右側的 Call Stack(呼叫棧)面板——它列出了「是誰呼叫了這一行」,一層一層往上排。
由上往下逐層點開,每一層都看看它的程式碼。你要找的那個加密函式,就在這條鏈條上的某一層裡。
你不需要讀懂整個專案幾萬行壓縮程式碼。你只需要沿著呼叫棧往上走幾層,就能直達源頭。
找到之後,在 Sources 面板對那個檔案按 Ctrl + Shift + F 全域搜尋函式名,就能反覆研究它。
找到任何一個函式名或關鍵字之後,在 Sources 面板按 Ctrl + Shift + F 開啟全域搜尋,輸入那個名字。
它會在所有已載入的檔案裡搜尋,包含被壓縮成一行的檔案。這是從「一個名字」擴散到「完整邏輯」最快的路徑。
壓縮過的程式碼全部擠在一行裡,直接開啟會看到一團亂。點搜尋結果之後按 Ctrl + F 在檔案內再搜一次關鍵字,然後按 { }(美化)按鈕把程式碼排版展開,會好讀很多。
原因:Hook 腳本和網站本身的程式碼起了衝突。最常見的是 04(改寫了全域 Function)。
解決:按 F5 重新整理,一切恢復,這屬於正常現象。要繼續用就單獨裝、逐個排除。
原因:鉤到了呼叫極其頻繁的函式,通常是 09(Math.random / Date.now)或 10(defineProperty)。
解決:關掉這兩個腳本,只留需要的。真的要用,就縮短使用時間——確認到需要的資訊就立刻重新整理。
原因:用了錯誤的寫法(直接賦值覆蓋),把 cookie 功能弄壞了。
解決:本目錄的 03 已經用原生的 getter / setter 正確處理,不會有這個問題。直接下載本頁的版本即可。
原因:通常是漏掉了 response.clone(),或者網站用的是 XHR 而不是 fetch。
解決:本頁的 02 同時鉤了 XHR 和 fetch,兩種都涵蓋。確認你下載的是完整版本。
原因:那個函式被呼叫得太頻繁,每次呼叫都觸發斷點。
解決:不要用 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
02,找個你常用的網站,看看它的請求長什麼樣——建立感覺01 和 07,看看能不能把參數還原成明文——建立信心不用一次學完十個。把 02 用熟,就已經能解決八成的問題了。