關注微信公眾號: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
});
})();