Android Native Hook技術你知道多少?

做者:xiangzhihong,地址:http://suo.im/6xocWunode



Hook 直譯過來就是「鉤子」的意思,是指截獲進程對某個 API 函數的調用,使得 API 的執行流程轉向咱們實現的代碼片斷,從而實現咱們所須要得功能,這裏的功能能夠是監控、修復系統漏洞,也能夠是劫持或者其餘惡意行爲。
程序員

相信許多新手第一次接觸 Hook 時會以爲這項技術十分神祕,只能被少數高手、黑客所掌握,那 Hook 是否是真的難以掌握?但願今天的文章能夠打消你的顧慮。web

Native Hook 的不一樣流派

對於 Native Hook 技術,咱們比較熟悉的有 GOT/PLT Hook、Trap Hook 以及 Inline Hook,下面我來逐個講解這些 Hook 技術的實現原理和優劣比較。編程

1. GOT/PLT Hook

在Chapter06-plus中,咱們使用了 PLT Hook 技術來獲取線程建立的堆棧。先來回顧一下它的整個流程,咱們將 libart.so 中的外部函數 pthread_create 替換成本身的方法 pthread_create_hook。緩存


你能夠發現,GOT/PLT Hook 主要是用於替換某個 SO 的外部調用,經過將外部函數調用跳轉成咱們的目標函數。GOT/PLT Hook 能夠說是一個很是經典的 Hook 方法,它很是穩定,能夠達到部署到生產環境的標準。安全

那 GOT/PLT Hook 的實現原理到底是什麼呢?你須要先對 SO 庫文件的 ELF 文件格式和動態連接過程有所瞭解。微信

ELF 格式

ELF(Executableand Linking Format)是可執行和連接格式,它是一個開放標準,各類 UNIX 系統的可執行文件大多采用 ELF 格式。雖然 ELF 文件自己就支持三種不一樣的類型(重定位、執行、共享),不一樣的視圖下格式稍微不一樣,不過它有一個統一的結構,這個結構以下圖所示。網絡


網上介紹 ELF 格式的文章很是多,你能夠參考《ELF 文件格式解析》。顧名思義,對於 GOT/PLT Hook 來講,咱們主要關心「.plt」和「.got」這兩個節區:架構

  • .plt。該節保存過程連接表(Procedure Linkage Table)。app

  • .got。該節保存着全局的偏移量表。

咱們也可使用readelf -S來查看 ELF 文件的具體信息。

連接過程

接下來咱們再來看看動態連接的過程,當須要使用一個 Native 庫(.so 文件)的時候,咱們須要調用dlopen("libname.so")來加載這個庫。

在咱們調用了dlopen("libname.so")以後,系統首先會檢查緩存中已加載的 ELF 文件列表。若是未加載則執行加載過程,若是已加載則計數加一,忽略該調用。而後系統會用從 libname.so 的dynamic節區中讀取其所依賴的庫,按照相同的加載邏輯,把未在緩存中的庫加入加載列表。

你可使用下面這個命令來查看一個庫的依賴:

readelf -d <library> | grep NEEDED

下面咱們大概瞭解一下系統是如何加載的 ELF 文件的。

  • 讀 ELF 的程序頭部表,把全部 PT_LOAD 的節區 mmap 到內存中。

  • 從「.dynamic」中讀取各信息項,計算並保存全部節區的虛擬地址,而後執行重定位操做。

  • 最後 ELF 加載成功,引用計數加一。

可是這裏有一個關鍵點,在 ELF 文件格式中咱們只有函數的絕對地址。若是想在系統中運行,這裏須要通過重定位。這實際上是一個比較複雜的問題,由於不一樣機器的 CPU 架構、加載順序不一樣,致使咱們只能在運行時計算出這個值。不過還好動態加載器(/system/bin/linker)會幫助咱們解決這個問題。

若是你理解了動態連接的過程,咱們再回頭來思考一下「.got」和「.plt」它們的具體含義。

  • The Global Offset Table (GOT)。簡單來講就是在數據段的地址表,假定咱們有一些代碼段的指令引用一些地址變量,編譯器會引用 GOT 表來替代直接引用絕對地址,由於絕對地址在編譯期是沒法知道的,只有重定位後纔會獲得 ,GOT 本身自己將會包含函數引用的絕對地址。

  • The Procedure Linkage Table (PLT)。PLT 不一樣於 GOT,它位於代碼段,動態庫的每個外部函數都會在 PLT 中有一條記錄,每一條 PLT 記錄都是一小段可執行代碼。通常來講,外部代碼都是在調用 PLT 表裏的記錄,而後 PLT 的相應記錄會負責調用實際的函數。咱們通常把這種設定叫做「蹦牀」(Trampoline)。

PLT 和 GOT 記錄是一一對應的,而且 GOT 表第一次解析後會包含調用函數的實際地址。既然這樣,那 PLT 的意義到底是什麼呢?PLT 從某種意義上賦予咱們一種懶加載的能力。當動態庫首次被加載時,全部的函數地址並無被解析。下面讓咱們結合圖來具體分析一下首次函數調用,請注意圖中黑色箭頭爲跳轉,紫色爲指針。

  • 咱們在代碼中調用 func,編譯器會把這個轉化爲 func@plt,並在 PLT 表插入一條記錄。

  • PLT 表中第一條(或者說第 0 條)PLT[0] 是一條特殊記錄,它是用來幫助咱們解析地址的。一般在類 Linux 系統,這個的實現會位於動態加載器,就是專欄前面文章提到的 /system/bin/linker。

  • 其他的 PLT 記錄都均包含如下信息:

-- 跳轉 GOT 表的指令(jmp *GOT[n])。
-- 爲上面提到的第 0 條解析地址函數準備參數。
-- 調用 PLT[0],這裏 resovler 的實際地址是存儲在 GOT[2] 。

在解析前 GOT[n] 會直接指向 jmp *GOT[n] 的下一條指令。在解析完成後,咱們就獲得了 func 的實際地址,動態加載器會將這個地址填入 GOT[n],而後調用 func。

若是你對上面的這個調用流程還有疑問,你能夠參考《GOT 表和 PLT 表》這篇文章,它裏面有一張圖很是清晰。

當第一次調用發生後,以後再調用函數 func 就高效簡單不少。首先調用 PLT[n],而後執行 jmp *GOT[n]。GOT[n] 直接指向 func,這樣就高效的完成了函數調用。

總結一下,由於不少函數可能在程序執行完時都不會被用到,好比錯誤處理函數或一些用戶不多用到的功能模塊等,那麼一開始把全部函數都連接好實際就是一種浪費。爲了提高動態連接的性能,咱們可使用 PLT 來實現延遲綁定的功能。

對於函數運行的實際地址,咱們依然須要經過 GOT 表獲得,整個簡化過程以下:

看到這裏,相信你已經有了如何 Hack 這一過程的初步想法。這裏業界一般會根據修改 PLT 記錄或者 GOT 記錄區分爲 GOT Hook 和 PLT Hook,但其本質原理十分接近。

GOT/PLT Hook 實踐

GOT/PLT Hook 看似簡單,可是實現起來也是有一些坑的,須要考慮兼容性的狀況。通常來講,推薦使用以下業界的成熟方案。

微信 Matrix 開源庫的ELF Hook,它使用的是 GOT Hook,主要使用它來作性能監控。
愛奇藝開源的的xHook,它使用的也是 GOT Hook。
Facebook 的PLT Hook。

若是不想深刻它內部的原理,咱們只須要直接使用這些開源的優秀方案就能夠了。由於這種 Hook 方式很是成熟穩定,除了 Hook 線程的建立,咱們還有不少其餘的使用範例。

  • 「I/O 優化」中使用matrix-io-canary Hook 文件的操做。

  • 「網絡優化」中使用 Hook 了 Socket 的相關操做。

這種 Hook 方法也不是萬能的,由於它只能替換導入函數的方式。有時候咱們不必定能夠找到這樣的外部調用函數。若是想 Hook 函數的內部調用,這個時候就須要用到咱們的 Trap Hook 或者 Inline Hook 了。

2. Trap Hook

對於函數內部的 Hook,你能夠先從頭想一下,會發現調試器就具有一切 Hook 框架具備的能力,能夠在目標函數前斷住程序,修改內存、程序段,繼續執行。相信不少同窗都會使用調試器,可是對調試器如何工做卻知之甚少。下面讓咱們先了解一下軟件調試器是如何工做的。

ptrace

通常軟件調試器都是經過 ptrace 系統調用和 SIGTRAP 配合來進行斷點調試,首先咱們來了解一下什麼是 ptrace,它又是如何斷住程序運行,而後修改相關執行步驟的。

所謂合格的底層程序員,對於未知知識的瞭解,第一步就是使用man命令來查看系統文檔。

The ptrace() system call provides a means by which one process (the 
tracer」) may observe and control the execution of another process (the
tracee」), and examine and change the tracees memory and registers. It
is primarily used to implement breakpoint debugging and system call
tracing.

這段話直譯過來就是,ptrace 提供了一種讓一個程序(tracer)觀察或者控制另外一個程序(tracee)執行流程,以及修改被控制程序內存和寄存器的方法,主要用於實現調試斷點和系統調用跟蹤。
咱們再來簡單瞭解一下調試器(GDB/LLDB)是如何使用 ptrace 的。首先調試器會基於要調試進程是否已啓動,來決定是使用 fork 或者 attach 到目標進程。當調試器與目標程序綁定後,目標程序的任何 signal(除 SIGKILL)都會被調試器作先攔截,調試器會有機會對相關信號進行處理,而後再把執行權限交由目標程序繼續執行。能夠你已經想到了,這其實已經達到了 Hook 的目的。

如何 Hook

但更進一步思考,若是咱們不須要修改內存或者作相似調試器同樣複雜的交互,咱們徹底能夠不依賴 ptrace,只須要接收相關 signal 便可。這時咱們就想到了句柄(signal handler)。對!咱們徹底能夠主動 raise signal,而後使用 signal handler 來實現相似的 Hook 效果。

業界也有很多人將 Trap Hook 叫做斷點 Hook,它的原理就是在須要 Hook 的地方想辦法觸發斷點,並捕獲異常。通常咱們會利用 SIGTRAP 或者 SIGKILL(非法指令異常)這兩種信號。下面以 SIGTRAP 信號爲例,具體的實現步驟以下。

  • 註冊信號接收句柄(signal handler),不一樣的體系結構可能會選取不一樣的信號,咱們這裏用 SIGTRAP。

  • 在咱們須要 Hook 得部分插入 Trap 指令。

  • 系統調用 Trap 指令,進入內核模式,調用咱們已經在開始註冊好的信號接收句柄(signal handler)。

  • 執行咱們信號接收句柄(signal handler),這裏須要注意,全部在信號接收句柄(signal handler)執行的代碼須要保證async-signal-safe。這裏咱們能夠簡單的只把信號接收句柄看成蹦牀,使用 logjmp 跳出這個須要 async-signal-safe(正如我在「崩潰分析」所說的,部分函數在 signal 回調中使用並不安全)的環境,而後再執行咱們 Hook 的代碼。

  • 在執行完 Hook 的函數後,咱們須要恢復現場。這裏若是咱們想繼續調用原來的函數 A,那直接回寫函數 A 的原始指令並恢復寄存器狀態。

Trap Hook 實踐

Trap Hook 兼容性很是好,它也能夠在生產環境中大規模使用。可是它最大的問題是效率比較低,不適合 Hook 很是頻繁調用的函數。
對於 Trap Hook 的實踐方案,可使用 Facebook 的Profilo,它就是經過按期發送 SIGPROF 信號來實現卡頓監控的。

3. Inline Hook

跟 Trap Hook 同樣,Inline Hook 也是函數內部調用的 Hook。它直接將函數開始(Prologue)處的指令更替爲跳轉指令,使得原函數直接跳轉到 Hook 的目標函數函數,並保留原函數的調用接口以完成後續再調用回來的目的。

與 GOT/PLT Hook 相比,Inline Hook 能夠不受 GOT/PLT 表的限制,幾乎能夠 Hook 任何函數。不過其實現十分複雜,我至今沒有見過能夠用在生產環境的實現。而且在 ARM 體系結構下,沒法對葉子函數和很短的函數進行 Hook。

在深刻「邪惡的」細節前,咱們須要先對 Inline Hook 的大致流程有一個簡單的瞭解。

如圖所示,Inline Hook 的基本思路就是在已有的代碼段中插入跳轉指令,把代碼的執行流程轉向咱們實現的 Hook 函數中,而後再進行指令修復,並跳轉回原函數繼續執行。這段描述看起來是否是十分簡單並且清晰?

對於 Trap Hook,咱們只須要在目標地址前插入特殊指令,而且在執行結束後把原始指令寫回去就能夠了。可是對 Inline Hook 來講,它是直接進行指令級的複寫與修復。怎麼理解呢?就至關於咱們在運行過程當中要去作 ASM 的字節碼修改。

固然 Inline Hook 遠遠比 ASM 操做更加複雜,由於它還涉及不一樣 CPU 架構帶來的指令集適配問題,咱們須要根據不一樣指令集來分別進行指令複寫與跳轉。下面我先來簡單說明一下 Android 常見的 CPU 架構和指令集:

  • x86 和 MIPS 架構。這兩個架構已經基本沒有多少用戶了,咱們能夠直接忽視。通常來講咱們只關心主流的 ARM 體系架構就能夠了。

  • ARMv5 和 ARMv7 架構。它的指令集分爲 4 字節對齊的定長的 ARM 指令集和 2 字節對齊的變長 Thumb/Thumb-2 指令集。Thumb-2 指令集雖爲 2 字節對齊,但指令集自己有 16 位也有 32 位。其中 ARMv5 使用的是 16 位的 Thumb16,在 ARMv7 使用的是 32 位的 Thumb32。不過目前 ARMv5 也基本沒有多少用戶了,咱們也能夠放棄 Thumb16 指令集的適配。

  • ARMv8 架構。64 位的 ARMv8 架構能夠兼容運行 32 位,因此它在 ARM32 和 Thumb32 指令集的基礎上,增長了 ARM64 指令集。關於它們具體差別,你能夠查看ARM 的官方文檔。

ARM64 目前我尚未適配,不過 Google Play 要求全部應用在 2019 年 8 月 1 日以前須要支持 64 位。但它們的原理基本相似,下面我以最主流的 ARMv7 架構爲例,爲你庖丁解牛 Inline Hook。

ARM32 指令集

ARMv7 中有一種廣爲流傳的 PC+8的說法。這是指 ARMv7 中的三級流水線(取指、解碼、執行),換句話說 PC寄存器的值老是比當前指令地址要大 8。


是否是感受有些複雜,其實這是爲了引出 ARM 指令集的經常使用跳轉方法:


LDR PC, [PC, #-4] ;0xE51FF004
$TRAMPOLIN_ADDR

在瞭解了三級流水線之後,就不會對這個 PC-4 有什麼疑惑了。

按照咱們前面描述的 Inline Hook 的基本步驟,首先插入跳轉指令,跳入咱們的蹦牀(Trampoline),執行咱們實現的 Hook 後函數。這裏還有一個「邪惡的」細節,因爲指令執行是依賴當前運行環境的,即全部寄存器的值,而咱們插入新的指令是有可能更改寄存器的狀態的,因此咱們要保存當前所有的寄存器狀態到棧中,使用 BLX 指令跳轉執行 Hook 後函數,執行完成後,再從棧中恢復全部的寄存器,最後才能像未 Hook 同樣繼續執行原先函數。


在執行完 Hook 後的函數後,咱們須要跳轉回原先的函數繼續執行。這裏不要忘記咱們在一開始覆蓋的 LDR 指令,咱們須要先執行被咱們複寫的指令,而後再使用以下指令,繼續執行原先函數。


LDR PC, [PC, #-4]
HOOKED_ADDR+8

是否是有一種大功告成的感受?其實這裏還有一個巨大的坑在等着咱們,那就是指令修復。前面我提到保存並恢復了寄存器原有的狀態,已達到能夠繼續像原有程序同樣的繼續執行。但僅僅是恢復寄存器就足夠麼?顯然答案是否認的,雖然寄存器被咱們完美恢復了,可是 2 條備份的指令被移動到了新的地址。當執行它們的時候, PC的值,那麼它們將會執行出徹底不一樣的結果。

Inline Hook 實踐

對於 Inline Hook,雖然它功能很是強大,並且執行效率也很高,可是業界目前尚未一套徹底穩定可靠的開源方案。Inline Hook 通常會使用在自動化測試或者線上疑難問題的定位,例如「UI 優化」中說到 libhwui.so 崩潰問題的定位,咱們就是利用 Inline Hook 去收集系統信息。

業界也有一些不錯的參考方案:
Cydia Substrate。在Chapter3中,咱們就使用它來 Hook 系統的內存分配函數。
adbi。支付寶在GC 抑制中使用的 Hook 框架,不過已經好幾年沒有更新了。

各個流派優缺點總結

最後咱們再來總結一下不一樣的 Hook 方式的優缺點:

  1. GOT/PLT Hook 是一個比較中庸的方案,有較好的性能,中等的實現難度,但其只能 Hook 動態庫之間的調用的函數,而且沒法Hook 未導出的私有函數,並且只存在安裝與卸載 2 種狀態,一旦安裝就會 Hook 全部函數調用。

  2. Trap Hook 最爲穩定,但因爲須要切換運行模式(R0/R3),且依賴內核的信號機制,致使性能不好。

  3. Inline Hook 是一個很是激進的方案,有很好的性能,而且也沒有 PLT 做用域的限制,能夠說是一個很是靈活、完美的方案。但其實現難度極高,我至今也沒有看到能夠部署在生產環境的 Inline Hook 方案,由於涉及指令修復,須要編譯器的各類優化。

可是須要注意,不管是哪種 Hook 都只能 Hook 到應用自身的進程,咱們沒法替換系統或其餘應用進程的函數執行。

總結

總的來講 Native Hook 是一門很是底層的技術,它會涉及庫文件編譯、加載、連接等方方面面的知識,並且不少底層知識是與 Android 甚至移動平臺無關的。

在這一領域,作安全的同窗可能會更有發言權,我來說可能班門弄斧了。不過但願經過這篇文章,讓你對看似黑科技的 Hook 有一個大致的瞭解,但願能夠在本身的平時的工做中使用 Hook 來完成一些看似不可能的任務,好比修復系統 Bug、線上監控 Native 內存分配等。

Native Hook 技術的確很是複雜,即便咱們不懂得它的內部原理,咱們也應該學會使用成熟的開源框架去實現一些功能。固然對於想進一步深刻研究的同窗,推薦你學習下面這些資料。

  • 連接程序和庫

  • 指南程序員的自我修養:連接、裝載與庫

  • 連接器和加載器 Linkers and LoadersLinux

  • 二進制分析 Learning Linux Binary Analysis

若是你對調試器的研究也很是有興趣,強烈推薦Eli Bendersky寫的博客,裏面有一系列很是優秀的底層知識文章。其中一些關於 debugger 的,感興趣的同窗能夠去閱讀,並親手實現一個簡單的調試器。

  • how-debuggers-work-part-1

  • how-debuggers-work-part-2-breakpoints

  • how-debuggers-work-part-3-debugging-information

---END---



推薦閱讀:
JVM史上最最最完整深刻解析!萬字長文!
解決CoordinatorLayout的動畫抖動以及回彈問題
Java14新特性速覽!
2020 年編程語言盤點展望:Java 老兵不死,Kotlin 蓄勢待發


每個「在看」,我都當成真的喜歡

本文分享自微信公衆號 - 技術最TOP(Tech-Android)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索