
關注微信公眾號:K哥爬蟲,QQ交流群:808574309,持續分享爬蟲進階、JS/安卓逆向等技術干貨!
什么是 Hook?
Hook 中文譯為鈎子,Hook 實際上是 Windows 中提供的一種用以替換 DOS 下“中斷”的系統機制,Hook 的概念在 Windows 桌面軟件開發很常見,特別是各種事件觸發的機制,在對特定的系統事件進行 Hook 后,一旦發生已 Hook 事件,對該事件進行 Hook 的程序就會收到系統的通知,這時程序就能在第一時間對該事件做出響應。在程序中將其理解為“劫持”可能會更好理解,我們可以通過 Hook 技術來劫持某個對象,把某個對象的程序拉出來替換成我們自己改寫的代碼片段,修改參數或替換返回值,從而控制它與其他對象的交互。
通俗來講,Hook 其實就是攔路打劫,馬邦德帶着老婆,出了城,吃着火鍋,還唱着歌,突然就被麻匪劫了,張麻子劫下縣長馬邦德的火車,搖身一變化身縣長,帶着手下趕赴鵝城上任。Hook 的過程,就是張麻子頂替馬邦德的過程。

JS 逆向中的 Hook
在 JavaScript 逆向中,替換原函數的過程都可以被稱為 Hook,以下先用一段簡單的代碼理解 Hook 的過程:
function a() {
console.log("I'm a.");
}
a = function b() {
console.log("I'm b.");
};
a() // I'm b.
直接覆蓋原函數是最簡單的做法,以上代碼將 a 函數進行了重寫,再次調用 a 函數將會輸出 I'm b.,如果還想執行原來 a 函數的內容,可以使用中間變量進行儲存:
function a() {
console.log("I'm a.");
}
var c = a;
a = function b() {
console.log("I'm b.");
};
a() // I'm b.
c() // I'm a.
此時,調用 a 函數會輸出 I'm b.,調用 c 函數會輸出 I'm a.。
這種原函數直接覆蓋的方法通常只用來進行臨時調試,實用性不大,但是它能夠幫助我們理解 Hook 的過程,在實際 JS 逆向過程中,我們會用到更加高級一點的方法,比如 Object.defineProperty()。
Object.defineProperty()
基本語法:Object.defineProperty(obj, prop, descriptor),它的作用就是直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,接收的三個參數含義如下:
obj:需要定義屬性的當前對象;
prop:當前需要定義的屬性名;
descriptor:屬性描述符,可以取以下值:
| 屬性名 | 默認值 | 含義 |
|---|---|---|
| get | undefined | 存取描述符,目標屬性獲取值的方法 |
| set | undefined | 存取描述符,目標屬性設置值的方法 |
| value | undefined | 數據描述符,設置屬性的值 |
| writable | false | 數據描述符,目標屬性的值是否可以被重寫 |
| enumerable | false | 目標屬性是否可以被枚舉 |
| configurable | false | 目標屬性是否可以被刪除或是否可以再次修改特性 |
通常情況下,對象的定義與賦值是這樣的:
var people = {}
people.name = "Bob"
people["age"] = "18"
console.log(people)
// { name: 'Bob', age: '18' }
使用 Object.defineProperty() 方法:
var people = {}
Object.defineProperty(people, 'name', {
value: 'Bob',
writable: true // 是否可以被重寫
})
console.log(people.name) // 'Bob'
people.name = "Tom"
console.log(people.name) // 'Tom'
在 Hook 中,使用最多的是存取描述符,即 get 和 set。
get:屬性的 getter 函數,如果沒有 getter,則為 undefined,當訪問該屬性時,會調用此函數,執行時不傳入任何參數,但是會傳入 this 對象(由於繼承關系,這里的 this 並不一定是定義該屬性的對象),該函數的返回值會被用作屬性的值。
set:屬性的 setter 函數,如果沒有 setter,則為 undefined,當屬性值被修改時,會調用此函數,該方法接受一個參數,也就是被賦予的新值,會傳入賦值時的 this 對象。
用一個例子來演示:
var people = {
name: 'Bob',
};
var count = 18;
// 定義一個 age 獲取值時返回定義好的變量 count
Object.defineProperty(people, 'age', {
get: function () {
console.log('獲取值!');
return count;
},
set: function (val) {
console.log('設置值!');
count = val + 1;
},
});
console.log(people.age);
people.age = 20;
console.log(people.age);
輸出:
獲取值!
18
設置值!
獲取值!
21
通過這樣的方法,我們就可以在設置某個值的時候,添加一些代碼,比如 debugger;,讓其斷下,然后利用調用棧進行調試,找到參數加密、或者參數生成的地方,需要注意的是,網站加載時首先要運行我們的 Hook 代碼,再運行網站自己的代碼,才能夠成功斷下,這個過程我們可以稱之為 Hook 代碼的注入,以下將介紹幾種主流的注入方法。
Hook 注入的幾種方法
以下以某奇藝 cookie 中的 __dfp 值為例,來演示具體如何注入 Hook。
1、Fiddler 插件注入
來到某奇藝首頁,可以看到其 cookie 里面有個 __dfp 值:

如果直接搜索是搜不到的,我們想通過 Hook 的方式,讓在生成 __dfp 值的地方斷下,就可以編寫如下自執行函數:
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {
set: function (val) {
if (val.indexOf('__dfp') != -1) {
debugger;
}
console.log('Hook捕獲到cookie設置->', val);
cookieTemp = val;
return val;
},
get: function () {
return cookieTemp;
},
});
})();
if (val.indexOf('__dfp') != -1) {debugger;} 的意思是檢索 __dfp 在字符串中首次出現的位置,等於 -1 表示這個字符串值沒有出現,反之則出現。如果出現了,那么就 debugger 斷下,這里要注意的是不能寫成 if (val == '__dfp') {debugger},因為 val 傳過來的值類似於 __dfp=xxxxxxxxxx,這樣寫是無法斷下的。
有了代碼該如何使用呢?也就是怎么注入 Hook 代碼呢?這里推薦 Fiddler 抓包工具搭配編程貓的插件使用,插件可以在公眾號輸入關鍵字【Fiddler插件】獲取,其原理可以理解為攔截 —> 加工 —> 放行的一個過程,利用 Fiddler 替換響應,在 Fiddler 攔截到數據后,在源碼第一行插入 Hook 代碼,由於 Hook 代碼是一個自執行函數,那么網頁一旦加載,就必然會先運行 Hook 代碼。安裝完成后如下圖所示,打開抓包,點擊開啟注入 Hook:

瀏覽器清除 cookie 后重新進入某奇藝的頁面,可以看到成功斷下,在 console 控制台可以看到捕獲的一些 cookie 值,此時的 val 就是 __dfp 的值,接下來在右側的 Call Stack 調用棧里就可以看到一些函數的調用過程,依次向上跟進就能夠找到最開始 __dfp 生成的地方。

2、TamperMonkey 注入
TamperMonkey 俗稱油猴插件,是一款免費的瀏覽器擴展和最為流行的用戶腳本管理器,支持很多主流的瀏覽器, 包括 Chrome、Microsoft Edge、Safari、Opera、Firefox、UC 瀏覽器、360 瀏覽器、QQ 瀏覽器等等,基本上實現了腳本的一次編寫,所有平台都能運行,可以說是基於瀏覽器的應用算是真正的跨平台了。用戶可以在 GreasyFork、OpenUserJS 等平台直接獲取別人發布的腳本,功能眾多且強大,比如視頻解析、去廣告等。
我們依舊以某奇藝的 cookie 為例來演示如何編寫 TamperMonkey 腳本,首先去應用商店安裝 TamperMonkey,安裝過程不再贅述,然后點擊圖標,添加新腳本,或者點擊管理面板,再點擊加號新建腳本,寫入以下代碼:
// ==UserScript==
// @name Cookie Hook
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Cookie Hook 腳本示例
// @author K哥爬蟲
// @match *
// @icon https://www.kuaidaili.com/img/favicon.ico
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {
set: function (val) {
if (val.indexOf('__dfp') != -1) {
debugger;
}
console.log('Hook捕獲到cookie設置->', val);
cookieTemp = val;
return val;
},
get: function () {
return cookieTemp;
},
});
})();

主體的 JavaScript 自執行函數和前面都是一樣的,這里需要注意的是最前面的注釋,每個選項都是有意義的,所有的選項參考 TamperMonkey 官方文檔,以下列出了比較常用、比較重要的部分選項(其中需要特別注意 @match、@include 和 @run-at 選項):
| 選項 | 含義 |
|---|---|
| @name | 腳本的名稱 |
| @namespace | 命名空間,用來區分相同名稱的腳本,一般寫作者名字或者網址就可以 |
| @version | 腳本版本,油猴腳本的更新會讀取這個版本號 |
| @description | 描述這個腳本是干什么用的 |
| @author | 編寫這個腳本的作者的名字 |
@match |
從字符串的起始位置匹配正則表達式,只有匹配的網址才會執行對應的腳本,例如 * 匹配所有,https://www.baidu.com/* 匹配百度等,可以參考 Python re 模塊里面的 re.match() 方法,允許多個實例 |
@include |
和 @match 類似,只有匹配的網址才會執行對應的腳本,但是 @include 不會從字符串起始位置匹配,例如 *://*baidu.com/* 匹配百度,具體區別可以參考 TamperMonkey 官方文檔 |
| @icon | 腳本的 icon 圖標 |
| @grant | 指定腳本運行所需權限,如果腳本擁有相應的權限,就可以調用油猴擴展提供的 API 與瀏覽器進行交互。如果設置為 none 的話,則不使用沙箱環境,腳本會直接運行在網頁的環境中,這時候無法使用大部分油猴擴展的 API。如果不指定的話,油猴會默認添加幾個最常用的 API |
| @require | 如果腳本依賴其他 JS 庫的話,可以使用 require 指令導入,在運行腳本之前先加載其它庫 |
@run-at |
腳本注入時機,該選項是能不能 hook 到的關鍵,有五個值可選:document-start:網頁開始時;document-body:body出現時;document-end:載入時或者之后執行;document-idle:載入完成后執行,默認選項;context-menu:在瀏覽器上下文菜單中單擊該腳本時,一般將其設置為 document-start |
清除 cookie,開啟 TamperMonkey 插件,再次來到某奇藝首頁,可以看到也成功被斷下,同樣的也可以跟進調用棧來進一步分析 __dfp 值的來源。

3、瀏覽器插件注入
瀏覽器插件官方叫法應該是瀏覽器擴展(Extension),瀏覽器插件能夠增強瀏覽器功能,同樣也能夠幫助我們 Hook,瀏覽器插件的編寫並不復雜,以 Chrome 插件為例,只需要保證項目下有一個 manifest.json 文件即可,它用來設置所有和插件相關的配置,必須放在根目錄。其中 manifest_version、name、version 3個參數是必不可少的,如果想要深入學習,可以參考小茗同學的博客和 Google 官方文檔。需要注意的是,火狐瀏覽器插件不一定能在其他瀏覽器上運行,而 Chrome 插件除了能運行在 Chrome 瀏覽器之外,還可以運行在所有 webkit 內核的國產瀏覽器,比如 360 極速瀏覽器、360 安全瀏覽器、搜狗瀏覽器、QQ 瀏覽器等等。我們還是以某奇藝的 cookie 來演示如何編寫一個 Chrome 瀏覽器 Hook 插件。
新建 manifest.json 文件:
{
"name": "Cookie Hook", // 插件名稱
"version": "1.0", // 插件版本
"description": "Cookie Hook", // 插件描述
"manifest_version": 2, // 清單版本,必須是2或者3
"content_scripts": [{
"matches": ["<all_urls>"], // 匹配所有地址
"js": ["cookie_hook.js"], // 注入的代碼文件名和路徑,如果有多個,則依次注入
"all_frames": true, // 允許將內容腳本嵌入頁面的所有框架中
"permissions": ["tabs"], // 權限申請,tabs 表示標簽
"run_at": "document_start" // 代碼注入的時間
}]
}
新建 cookie_hook.js 文件:
var hook = function() {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {
set: function(val) {
if (val.indexOf('__dfp') != -1) {
debugger;
}
console.log('Hook捕獲到cookie設置->', val);
cookieTemp = val;
return val;
},
get: function() {
return cookieTemp;
},
});
}
var script = document.createElement('script');
script.textContent = '(' + hook + ')()';
(document.head || document.documentElement).appendChild(script);
script.parentNode.removeChild(script);
將這兩個文件放到同一個文件夾,打開 chrome 的擴展程序, 打開開發者模式,加載已解壓的擴展程序,選擇創建的文件夾即可:

來到某奇藝頁面,清除 cookie 后重新進入,可以看到同樣也成功斷下,跟蹤調用棧就可以找到其值生成的地方:

常用 Hook 代碼總匯
除了使用上述的 Object.defineProperty() 方法,還可以直接捕獲相關接口,然后重寫這個接口,以下列出了常見的 Hook 代碼。注意:以下只是關鍵的 Hook 代碼,具體注入的方式不同,要進行相關的修改。
Hook Cookie
Cookie Hook 用於定位 Cookie 中關鍵參數生成位置,以下代碼演示了當 Cookie 中匹配到了 __dfp 關鍵字, 則插入斷點:
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {
set: function (val) {
if (val.indexOf('__dfp') != -1) {
debugger;
}
console.log('Hook捕獲到cookie設置->', val);
cookieTemp = val;
return val;
},
get: function () {
return cookieTemp;
},
});
})();
(function () {
'use strict';
var org = document.cookie.__lookupSetter__('cookie');
document.__defineSetter__('cookie', function (cookie) {
if (cookie.indexOf('__dfp') != -1) {
debugger;
}
org = cookie;
});
document.__defineGetter__('cookie', function () {
return org;
});
})();
Hook Header
Header Hook 用於定位 Header 中關鍵參數生成位置,以下代碼演示了當 Header 中包含 Authorization 關鍵字時,則插入斷點:
(function () {
var org = window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
if (key == 'Authorization') {
debugger;
}
return org.apply(this, arguments);
};
})();
Hook URL
URL Hook 用於定位請求 URL 中關鍵參數生成位置,以下代碼演示了當請求的 URL 里包含 login 關鍵字時,則插入斷點:
(function () {
var open = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function (method, url, async) {
if (url.indexOf("login") != 1) {
debugger;
}
return open.apply(this, arguments);
};
})();
Hook JSON.stringify
JSON.stringify() 方法用於將 JavaScript 值轉換為 JSON 字符串,在某些站點的加密過程中可能會遇到,以下代碼演示了遇到 JSON.stringify() 時,則插入斷點:
(function() {
var stringify = JSON.stringify;
JSON.stringify = function(params) {
console.log("Hook JSON.stringify ——> ", params);
debugger;
return stringify(params);
}
})();
Hook JSON.parse
JSON.parse() 方法用於將一個 JSON 字符串轉換為對象,在某些站點的加密過程中可能會遇到,以下代碼演示了遇到 JSON.parse() 時,則插入斷點:
(function() {
var parse = JSON.parse;
JSON.parse = function(params) {
console.log("Hook JSON.parse ——> ", params);
debugger;
return parse(params);
}
})();
Hook eval
JavaScript eval() 函數的作用是計算 JavaScript 字符串,並把它作為腳本代碼來執行。如果參數是一個表達式,eval() 函數將執行表達式。如果參數是 Javascript 語句,eval() 將執行 Javascript 語句,經常被用來動態執行 JS。以下代碼執行后,之后所有的 eval() 操作都會在控制台打印輸出將要執行的 JS 源碼:
(function() {
// 保存原始方法
window.__cr_eval = window.eval;
// 重寫 eval
var myeval = function(src) {
console.log(src);
console.log("=============== eval end ===============");
debugger;
return window.__cr_eval(src);
}
// 屏蔽 JS 中對原生函數 native 屬性的檢測
var _myeval = myeval.bind(null);
_myeval.toString = window.__cr_eval.toString;
Object.defineProperty(window, 'eval', {
value: _myeval
});
})();
Hook Function
以下代碼執行后,所有的函數操作都會在控制台打印輸出將要執行的 JS 源碼:
(function() {
// 保存原始方法
window.__cr_fun = window.Function;
// 重寫 function
var myfun = function() {
var args = Array.prototype.slice.call(arguments, 0, -1).join(","),
src = arguments[arguments.length - 1];
console.log(src);
console.log("=============== Function end ===============");
debugger;
return window.__cr_fun.apply(this, arguments);
}
// 屏蔽js中對原生函數native屬性的檢測
myfun.toString = function() {
return window.__cr_fun + ""
}
Object.defineProperty(window, 'Function', {
value: myfun
});
})();

