JS 逆向之 Hook,吃著火鍋唱著歌,突然就被麻匪劫了!

K哥爬蟲發表於2021-10-04
關注微信公眾號:K哥爬蟲,QQ交流群:808574309,持續分享爬蟲進階、JS/安卓逆向等技術乾貨!

什麼是 Hook?

Hook 中文譯為鉤子,Hook 實際上是 Windows 中提供的一種用以替換 DOS 下“中斷”的系統機制,Hook 的概念在 Windows 桌面軟體開發很常見,特別是各種事件觸發的機制,在對特定的系統事件進行 Hook 後,一旦發生已 Hook 事件,對該事件進行 Hook 的程式就會收到系統的通知,這時程式就能在第一時間對該事件做出響應。在程式中將其理解為“劫持”可能會更好理解,我們可以通過 Hook 技術來劫持某個物件,把某個物件的程式拉出來替換成我們自己改寫的程式碼片段,修改引數或替換返回值,從而控制它與其他物件的互動。

通俗來講,Hook 其實就是攔路打劫,馬邦德帶著老婆,出了城,吃著火鍋,還唱著歌,突然就被麻匪劫了,張麻子劫下縣長馬邦德的火車,搖身一變化身縣長,帶著手下趕赴鵝城上任。Hook 的過程,就是張麻子頂替馬邦德的過程。

01.png

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:屬性描述符,可以取以下值:

屬性名預設值含義
getundefined存取描述符,目標屬性獲取值的方法
setundefined存取描述符,目標屬性設定值的方法
valueundefined資料描述符,設定屬性的值
writablefalse資料描述符,目標屬性的值是否可以被重寫
enumerablefalse目標屬性是否可以被列舉
configurablefalse目標屬性是否可以被刪除或是否可以再次修改特性

通常情況下,物件的定義與賦值是這樣的:

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 值:

02.png

如果直接搜尋是搜不到的,我們想通過 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:

03.png

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

04.png

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;
    },
  });
})();

05.png

主體的 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 值的來源。

06.png

3、瀏覽器外掛注入

瀏覽器外掛官方叫法應該是瀏覽器擴充套件(Extension),瀏覽器外掛能夠增強瀏覽器功能,同樣也能夠幫助我們 Hook,瀏覽器外掛的編寫並不複雜,以 Chrome 外掛為例,只需要保證專案下有一個 manifest.json 檔案即可,它用來設定所有和外掛相關的配置,必須放在根目錄。其中 manifest_versionnameversion 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 的擴充套件程式, 開啟開發者模式,載入已解壓的擴充套件程式,選擇建立的資料夾即可:

07.png

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

08.png

常用 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
    });
})();

相關文章