【JS 逆向百例】瀏覽器外掛 Hook 實戰,亞航加密引數分析

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

宣告

本文章中所有內容僅供學習交流,抓包內容、敏感網址、資料介面均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關,若有侵權,請聯絡我立即刪除!

逆向目標

  • 目標:亞航 airasia 航班狀態查詢,請求頭 Authorization 引數
  • 主頁:aHR0cHM6Ly93d3cuYWlyYXNpYS5jb20vZmxpZ2h0c3RhdHVzLw==
  • 介面:aHR0cHM6Ly9rLmFwaWFpcmFzaWEuY29tL2ZsaWdodHN0YXR1cy9zdGF0dXMvb2QvdjMv
  • 逆向引數:

    • Request Headers:authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI......

逆向過程

抓包分析

來到航班狀態查詢頁面,隨便輸入出發地和目的地,點選查詢航班,例如查詢澳門到吉隆坡的航班,MFM 和 KUL 分別是澳門和吉隆坡國際機場的程式碼,查詢介面由最基本的 URL + 機場程式碼 + 日期組成,類似於:https://xxxxxxxxxx/MFM/KUL/28... ,其中請求頭 Request Headers 裡有個 authorization 引數,通過觀察發現,不管是清除 cookie 還是更換瀏覽器,此引數的值是一直不變的,經過測試,直接複製該引數到程式碼裡也是可行的,但本次我們的目的是通過編寫瀏覽器外掛來 Hook 這個引數,找到它生成的地方。

有關 Hook 的詳細知識,在 K 哥前期的文章有詳細介紹:JS 逆向之 Hook,吃著火鍋唱著歌,突然就被麻匪劫了!

01.png

瀏覽器外掛 Hook

瀏覽器外掛事實上叫做瀏覽器擴充套件(extensions),它能夠增強瀏覽器功能,比如遮蔽廣告、管理瀏覽器代理、更改瀏覽器外觀等。

既然是通過編寫瀏覽器外掛的方式進行 Hook,那麼首先我們肯定是要簡單瞭解一下如何編寫瀏覽器外掛了,編寫瀏覽器外掛也有對應的規範,在以前,不同瀏覽器的外掛編寫方式都不太一樣,到現在基本上都和 Google Chrome 外掛的編寫方式一樣了,Google Chrome 的外掛除了能執行在 Chrome 瀏覽器之外,還可以執行在所有 webkit 核心的國產瀏覽器,比如 360 極速瀏覽器、360 安全瀏覽器、搜狗瀏覽器、QQ 瀏覽器等等,另外,Firefox 火狐瀏覽器也有很多人使用,火狐瀏覽器外掛的開發方式變化了很多次,但是從 2017 年 11 月底開始,外掛必須使用 WebExtensions APIs 進行構建,其目的也是為了和其他瀏覽器統一,一般的 Google Chrome 外掛也能直接執行在火狐瀏覽器上,但是火狐瀏覽器外掛需要要經過 Mozilla 簽名後才能安裝,否則只能臨時除錯,重啟瀏覽器後外掛就沒有了,這一點較為不便。

一個瀏覽器外掛的開發說簡單也簡單,說複雜也複雜,不過對於我們做爬蟲逆向的開發人員來說,我們主要是利用外掛對程式碼進行 Hook,我們只需要知道一個外掛是由一個 manifest.json 和一個 JavaScript 指令碼檔案組成的就夠了,接下來 K 哥以本案例中請求頭的 authorization 引數為例,帶領大家開發一個 Hook 外掛。當然,如果你想深入研究瀏覽器外掛的開發,可以參考 Google Chrome 擴充套件文件Firefox Browser 擴充套件文件

按照 Google Chrome 外掛的開發規範,首先新建一個資料夾,該資料夾下包含一個 manifest.json 檔案和一個 JS Hook 指令碼,當然,如果你想為你的外掛配置一個圖示的話,也可以將圖示放到該資料夾下,圖示格式官方建議 PNG,也可以是 WebKit 支援的任何格式,包括 BMP、GIF、ICO 和 JPEG 等,注意:manifest.json 檔名不可更改!正常的外掛目錄類似如下結構:

JavaScript Hook
    ├─ manifest.json        // 配置檔案,檔名不可更改
    ├─ icons.png            // 圖示
    └─ javascript_hook.js   // Hook 指令碼,檔名順便取

manifest.json

manifest.json 是一個 Chrome 外掛中最重要也是必不可少的檔案,它用來配置所有和外掛相關的配置,必須放在根目錄。其中,manifest_version、name、version 這 3 個引數是必不可少的,本案例中,manifest.json 檔案配置如下:(完整配置參考 Chrome manifest file format

{
    "name": "JavaScript Hook",          // 外掛名稱
    "version": "1.0",                   // 外掛版本
    "description": "JavaScript Hook",   // 外掛描述
    "manifest_version": 2,              // 清單版本,必須是2或者3
    "icons": {                          // 外掛圖示
        "16": "/icons.png",             // 圖示路徑,外掛圖示不同尺寸也可以是同一張圖
        "48": "/icons.png",
        "128": "/icons.png"
    },
    "content_scripts": [{
        "matches": ["<all_urls>"],      // 匹配所有地址
        "js": ["javascript_hook.js"],   // 注入的程式碼檔名和路徑,如果有多個,則依次注入
        "all_frames": true,             // 允許將內容指令碼嵌入頁面的所有框架中
        "permissions": ["tabs"],        // 許可權申請,tabs 表示標籤
        "run_at": "document_start"      // 程式碼注入的時間
    }]
}

這裡需要注意以下幾點:

  • manifest_version:配置清單版本,目前支援 2 和 3,2 將會在將來被逐步淘汰,將來也可能推出 4 或者更高版本。可以在官網檢視 Manifest V2Manifest V3 的區別,3 有更高的隱私安全要求,這裡推薦使用 2。
  • content_scripts:Chrome 外掛中向頁面注入指令碼的一種形式,包括地址匹配(支援正規表示式),要注入的 JS、CSS 指令碼,程式碼注入的時間(建議 document_start,網頁開始載入時就注入)等。

javascript_hook.js

javascript_hook.js 檔案裡就是 Hook 程式碼了:

var hook = function () {
    var org = window.XMLHttpRequest.prototype.setRequestHeader;
    window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
        if (key == 'Authorization') {
            debugger;
        }
        return org.apply(this, arguments);
    }
}
var script = document.createElement('script');
script.textContent = '(' + hook + ')()';
(document.head || document.documentElement).appendChild(script);
script.parentNode.removeChild(script);

XMLHttpRequest.setRequestHeader() 是設定 HTTP 請求頭部的方法,定義了一個變數 org 來儲存原始方法,window.XMLHttpRequest.prototype.setRequestHeader 這裡有個原型物件 prototype,所有的 JavaScript 物件都會從一個 prototype 原型物件中繼承屬性和方法,具體可以參考菜鳥教程 JavaScript prototype 的介紹。一旦程式在設定請求頭中的 Authorization 時,就會進入我們的 Hook 程式碼,通過 debugger 斷下,最後依然將所有引數返回給 org,也就是 XMLHttpRequest.setRequestHeader() 這個原始方法,保證資料正常傳輸。然後建立 script 標籤,script 標籤內容是將 Hook 函式變成 IIFE 自執行函式,然後將其插入到網頁中。

到此我們瀏覽器外掛就編寫完成了,接下來介紹如何在 Google Chrome 和 Firefox Browser 中使用。

Google Chrome

在瀏覽器位址列輸入 chrome://extensions 或者依次點選右上角【自定義及控制 Google Chrome】—>【更多工具】—>【擴充套件程式】,進入擴充套件程式頁面,再依次選擇開啟【開發者模式】—>【載入已解壓的擴充套件程式】,選擇整個 Hook 外掛資料夾(資料夾裡應包含 manifest.json、javascript_hook.js 和圖示檔案),如下圖所示:

02.png

Firefox Browser

火狐瀏覽器不能直接安裝未經過 Mozilla 簽名認證的外掛,只能通過除錯附加元件的方式進行安裝。外掛的格式必須是 .xpi、.jar、.zip 的,所以需要我們將 manifest.json、javascript_hook.js 和圖示檔案一起打包,打包需要注意不要包含頂層目錄,直接全選右鍵壓縮即可,否則在安裝時會提示 does not contain a valid manifest。

在瀏覽器位址列輸入 about:addons 或者依次點選右上角【開啟應用程式選單】—>【擴充套件和主題】,也可以直接使用快捷鍵 Ctrl + Shift + A 來到擴充套件頁面,在管理您的擴充套件目錄旁有個設定按鈕,點選選擇【除錯附加元件】,在臨時擴充套件專案下,選擇【臨時載入附加元件】,選擇 Hook 外掛的壓縮包即可。

也可以直接在瀏覽器位址列輸入 about:debugging#/runtime/this-firefox,直接進入到臨時擴充套件頁面,如下圖所示:

03.png

自此,瀏覽器 Hook 外掛我們就開發安裝完畢了,重新來到航班查詢頁面,隨便輸入出發地和目的地,點選查詢航班,就可以看到此時已經成功斷下:

04.png

TamperMonkey 外掛 Hook

前面我們已經介紹瞭如何自己編寫一個瀏覽器外掛,但是不同瀏覽器外掛的編寫始終是大同小異的,有可能你編寫的某個外掛在其他瀏覽器上執行不了,而 TamperMonkey 就可以幫助我們解決這個問題,TamperMonkey 俗稱油猴外掛,它本身就是一個瀏覽器擴充套件,是最為流行的使用者指令碼管理器,基本上支援所有帶有擴充套件功能的瀏覽器,實現了指令碼的一次編寫,所有平臺都能執行,使用者可以在 GreasyFork、OpenUserJS 等平臺直接獲取別人釋出的指令碼,功能眾多且強大,同樣的,我們也可以利用 TamperMonkey 來實現 Hook。

TamperMonkey 可以直接在各大瀏覽器擴充套件商店裡面安裝,也可以去 TamperMonkey 官網進行安裝,安裝過程這裡不再贅述。

安裝完成後點選圖示,新增新指令碼,或者點選管理皮膚,再點選加號新建指令碼,寫入以下 Hook 程式碼:

// ==UserScript==
// @name         JavaScript Hook
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  JavaScript Hook 指令碼
// @author       K哥爬蟲
// @include      *://*airasia.com/*
// @icon         https://profile.csdnimg.cn/1/B/8/3_kdl_csdn
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';
    var org = window.XMLHttpRequest.prototype.setRequestHeader;
    window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
        if (key == 'Authorization') {
            debugger;
        }
        return org.apply(this, arguments);
    };
})();

05.png

整個程式碼 JavaScript 部分是個 IIFE 立即執行函式,具體含義就不解釋了,前面瀏覽器外掛開發時已經講過,重要的是上面幾行註釋,千萬不要以為這只是簡單的註釋,可有可無,在 TamperMonkey 中,可以將這部分視為基本的配置選項,各項都有其具體含義,完整的配置選項參考 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

重新來到航班查詢頁面,啟用 TamperMonkey 指令碼,如果配置正確的話,就可以看到我們編寫的 Hook 指令碼已開啟,隨便輸入出發地和目的地,點選查詢航班,就可以看到此時已經成功斷下:

06.png

引數逆向

不管你是使用瀏覽器外掛還是 TamperMonkey 進行 Hook,此時 Hook 到的是設定請求頭 Authorization 的地方,也就是說 Authorization 的值是產生肯定經過了之前的某個函式或者方法,那麼我們跟進開發者工具的 Call Stack 呼叫棧,就一定能夠找到這個方法,跟呼叫棧是一個考驗耐心的過程,花費時間也比較多。

通常情況下,我們是挨個函式檢視其傳遞的引數有沒有包含我們目標引數,如果上一個函式裡沒有而下一個函式裡出現了,那麼大概率加密過程就在這兩個函式之間,進入上一個函式再進行單步除錯,一般就能找到加密程式碼,在本案例中,我們跟到 t.getData 函式埋下斷點進行單步除錯,可以看到其實後面在反覆呼叫 t.subscribet.call,之所以不在這兩個函式處埋下斷點,是因為迴圈過多不好除錯,而且 t.getData 通過名稱判斷也比較可疑。

07.png

重新點選登陸,來到我們剛剛埋下斷點的地方,F11 或者點選向下箭頭,進入函式內部進行單步除錯,除錯大約 7 步後,來到一個 t.getHttpHeader 函式,可以看到 Authorization 的值就是 "Bearer " + r.accessToken,我們在控制檯列印 r.accessToken 可以看到就是我們想要的值,如下圖所示:

08.png

那麼重點是這個 r.accessToken,如果你嘗試直接往上找,你會發現找了很多行也沒有找到,直接搜尋關鍵字 accessToken,可以發現在 zUnb 物件裡面是直接定義死了的,直接拿來用即可,如下圖所示:

09.png

關於出發地、目的地的各個地方的程式碼,是通過 JSON 傳遞過來的,很容易找到,可根據實際需求靈活處理,如下圖所示:

10.png

這個案例本身不難,直接搜尋還能更快定位引數位置,但是本案例重點在於如何使用瀏覽器外掛進行 Hook 操作,這對於某些無法經過搜尋得到的引數,或者搜尋結果太多難以定位的情況來說,是一個很好的解決方法。

完整程式碼

GitHub 關注 K 哥爬蟲,持續分享爬蟲相關程式碼!歡迎 star !https://github.com/kgepachong/

以下只演示部分關鍵程式碼,不能直接執行!完整程式碼倉庫地址:https://github.com/kgepachong...

Python 示例程式碼

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import requests


status_url = '脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler'


def get_flight_status(departure, destination, date):
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36',
        'authorization': '脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler'
    }
    complete_url = status_url + departure + '/' + destination + '/' + date
    response = requests.get(url=complete_url, headers=headers)
    print(response.text)


if __name__ == '__main__':
    departure = input('請輸入出發地程式碼:')
    destination = input('請輸入目的地程式碼:')
    date = input('請輸入日期(例如:29/09/2021):')

    # departure = 'MFM'
    # destination = 'KUL'
    # date = '29/09/2021'
    get_flight_status(departure, destination, date)

相關文章