文章目錄
- 前言
- 什么是hook?
- PLT hook
- 作用
- 基本原理
- PLT hook 總體步驟
- 代碼案例分析
- 方案預研
- 面臨的問題
- 怎么做?
- ELF
- ELF 文件頭
- SHT(section header table)
- 鏈接視圖(Linking View)和執行視圖(Execution View)
- 動態鏈接
- 猜想-解決-驗證
- 第一次驗證
- 方案
- 再次驗證
- 參考資料
前言
昨晚看完了愛奇藝出品的開源框架xhook中的《Android PLT hook 概述》,內容比較深。主要用到了ELF和動態鏈接這些Linux的知識點,但是總體上還算是理解了基本原理,這篇文章主要記錄下現階段對PLT hook的學習和理解。感覺寫原文的作者很牛逼,內容看的我熱血沸騰,需要看原文的讀者可以翻到文章最后點開原文鏈接。
最后希望感謝你花時間閱讀本文,下面開始學習下面的內容。
什么是hook?
“Hook”(鉤子)是計算機編程中的一種技術,它允許開發者攔截、修改或擴展軟件或系統的行為。通過使用鉤子,開發者可以在特定事件發生時注入自定義代碼,以便修改程序的行為或響應程序的特定事件。
鉤子的使用場景大概有這些:
- 鍵盤和鼠標事件:攔截鍵盤和鼠標輸入,用于實現自定義的快捷鍵、鼠標手勢或輸入法。
- 窗口消息:攔截和處理窗口消息,用于實現窗口管理、界面定制等功能。
- 函數調用:攔截特定函數的調用,用于實現調試、性能分析、代碼注入等功能。
- 文件操作和網絡請求:攔截文件操作和網絡請求,用于實現文件監控、安全檢測等功能。
鉤子技術可以提供很大的靈活性和功能擴展性,但也需要謹慎使用,因為不正確的使用鉤子可能會導致程序崩潰、安全漏洞或不穩定的行為。
當然,我們本文要討論的場景屬于函數調用場景。那么PLT hook又是什么意思?
PLT hook
作用
PLT (Procedure Linkage Table) hook 是一種在動態鏈接庫(DLL)或共享對象(SO)中實現的技術,用于在運行時修改或攔截函數調用。這種技術通常用于在不修改原始代碼的情況下,對函數的行為進行修改或監視。
基本原理
PLT hook 的基本原理是利用了動態鏈接庫的符號解析機制。
在程序加載動態鏈接庫時,系統會創建一個 PLT 表來保存對動態鏈接函數的調用。這個表中的每個條目實際上是一個跳轉指令,將控制權轉移到動態鏈接庫中的實際函數實現。
PLT hook 就是通過修改 PLT 表中的條目,將原始函數調用指向自定義的函數或者跳轉到其他代碼段,從而實現對函數行為的修改或攔截。
PLT hook 總體步驟
-
定位目標函數:確定要 hook 的目標函數,獲取其在動態鏈接庫中的地址。
-
修改 PLT 表:通過修改 PLT 表中目標函數對應的條目,將其指向自定義的函數或者其他代碼段。
-
處理原始函數調用:在自定義函數中可以執行一些額外的操作,然后再調用原始的目標函數,或者完全替換原始函數的行為。
-
恢復原始調用:有時候需要在自定義函數中調用原始的目標函數,以保持程序的正常行為。
有了上述的基本了解,再來看看給出的例子,就會容易理解很多。
代碼案例分析
下面通過給出一個存在明顯內存泄漏的代碼,把它們編譯為動態庫libtest.so。看看最后能不能通過PLT hook把泄漏給解決了。
頭文件 test.h
#ifndef TEST_H
#define TEST_H 1#ifdef __cplusplus
extern "C" {
#endifvoid say_hello();#ifdef __cplusplus
}
#endif#endif
源文件 test.c
#include <stdlib.h>
#include <stdio.h>void say_hello()
{char *buf = malloc(1024);if(NULL != buf){snprintf(buf, 1024, "%s", "hello\n");printf("%s", buf);}
}
源文件 main.c
#include <test.h>int main()
{say_hello();return 0;
}
執行:
caikelun@debian:~$ adb push ./libtest.so ./main /data/local/tmp
caikelun@debian:~$ adb shell "chmod +x /data/local/tmp/main"
caikelun@debian:~$ adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"
hello
caikelun@debian:~$
把編譯后的libtest.so 推到Android設備中,給個可執行權限后,執行它。雖然泄漏了,但是還是可以執行的。這正是模擬真實情況的代碼,泄漏了卻不自知。
方案預研
面臨的問題
假如我們發現了泄漏問題,要修復它并不難,問題在于怎么及時發現它們。
問題在于下面2個:
1.假如程序測試覆蓋得不夠的話,怎么及時發現和定位一些已經上線的APP呢?
2.如果libtest.so是第三方庫,而且還是閉源的。我們可以修復它嗎?能否監控它?
怎么做?
這正是hook可以做到的事情,比如 hook malloc,calloc,realloc 和 free,我們就能統計出各個動態庫分配了多少內存,哪些內存一直被占用沒有釋放。
作者提到,hook自己的進程完全是可以的,hook其它的進程需要root權限。因為假如沒有root權限,就無法修改其它進程的內存空間。
好消息是,我們只要hook自己的進程就夠了。
下面本文的主角要出來了。
而要理解PLT hook,首先要了解ELF是什么。
ELF
ELF(Executable and Linkable Format)是一種行業標準的二進制數據封裝格式,主要用于封裝可執行文件、動態庫、object 文件和 core dumps 文件。
ELF概述
ELF定義
對于PLT hook,最重要的是了解ELF 文件頭、SHT(section header table)、PHT(program header table)。
ELF 文件頭
ELF 文件的起始處,有一個固定格式的定長的文件頭(32 位架構為 52 字節,64 位架構為 64 字節)。ELF 文件頭以 magic number 0x7F 0x45 0x4C 0x46 開始(其中后 3 個字節分別對應可見字符 E L F)。
libtest.so 的 ELF 文件頭信息:
caikelun@debian:~$ arm-linux-androideabi-readelf -h ./libtest.soELF Header:Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00Class: ELF32Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: DYN (Shared object file)Machine: ARMVersion: 0x1Entry point address: 0x0Start of program headers: 52 (bytes into file)Start of section headers: 12744 (bytes into file)Flags: 0x5000200, Version5 EABI, soft-float ABISize of this header: 52 (bytes)Size of program headers: 32 (bytes)Number of program headers: 8Size of section headers: 40 (bytes)Number of section headers: 25Section header string table index: 24
ELF 文件頭中包含了 SHT 和 PHT 在當前 ELF 文件中的起始位置和長度。例如,libtest.so 的 SHT 起始位置為 12744,長度 40 字節;PHT 起始位置為 52,長度 32字節。
ELF header數據結構如圖:
SHT(section header table)
ELF 以 section 為單位來組織和管理各種信息。ELF 使用 SHT 來記錄所有 section 的基本信息。主要包括:section 的類型、在文件中的偏移量、大小、加載到內存后的虛擬內存相對地址、內存中字節的對齊方式等。
caikelun@debian:~$ arm-linux-androideabi-readelf -S ./libtest.soThere are 25 section headers, starting at offset 0x31c8:Section Headers:[Nr] Name Type Addr Off Size ES Flg Lk Inf Al[ 0] NULL 00000000 000000 000000 00 0 0 0[ 1] .note.android.ide NOTE 00000134 000134 000098 00 A 0 0 4[ 2] .note.gnu.build-i NOTE 000001cc 0001cc 000024 00 A 0 0 4[ 3] .dynsym DYNSYM 000001f0 0001f0 0003a0 10 A 4 1 4[ 4] .dynstr STRTAB 00000590 000590 0004b1 00 A 0 0 1[ 5] .hash HASH 00000a44 000a44 000184 04 A 3 0 4[ 6] .gnu.version VERSYM 00000bc8 000bc8 000074 02 A 3 0 2[ 7] .gnu.version_d VERDEF 00000c3c 000c3c 00001c 00 A 4 1 4[ 8] .gnu.version_r VERNEED 00000c58 000c58 000020 00 A 4 1 4[ 9] .rel.dyn REL 00000c78 000c78 000040 08 A 3 0 4[10] .rel.plt REL 00000cb8 000cb8 0000f0 08 AI 3 18 4[11] .plt PROGBITS 00000da8 000da8 00017c 00 AX 0 0 4[12] .text PROGBITS 00000f24 000f24 0015a4 00 AX 0 0 4[13] .ARM.extab PROGBITS 000024c8 0024c8 00003c 00 A 0 0 4[14] .ARM.exidx ARM_EXIDX 00002504 002504 000100 08 AL 12 0 4[15] .fini_array FINI_ARRAY 00003e3c 002e3c 000008 04 WA 0 0 4[16] .init_array INIT_ARRAY 00003e44 002e44 000004 04 WA 0 0 1[17] .dynamic DYNAMIC 00003e48 002e48 000118 08 WA 4 0 4[18] .got PROGBITS 00003f60 002f60 0000a0 00 WA 0 0 4[19] .data PROGBITS 00004000 003000 000004 00 WA 0 0 4[20] .bss NOBITS 00004004 003004 000000 00 WA 0 0 1[21] .comment PROGBITS 00000000 003004 000065 01 MS 0 0 1[22] .note.gnu.gold-ve NOTE 00000000 00306c 00001c 00 0 0 4[23] .ARM.attributes ARM_ATTRIBUTES 00000000 003088 00003b 00 0 0 1[24] .shstrtab STRTAB 00000000 0030c3 000102 00 0 0 1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),y (noread), p (processor specific)
- .dynamic:供動態鏈接器使用的各項信息,記錄了當前 ELF 的外部依賴,以及其他各個重要 section 的起始位置等信息。
- .got:Global Offset Table。用于記錄外部調用的入口地址。動態鏈接器(linker)執行重定位(relocate)操作時,這里會被填入真實的外部調用的絕對地址。
- .plt:Procedure Linkage Table。外部調用的跳板,主要用于支持 lazy binding 方式的外部調用重定位。(Android 目前只有 MIPS 架構支持 lazy binding)
下面用一張圖描述相關的關系,這也是PLT hook的核心原理:
鏈接視圖(Linking View)和執行視圖(Execution View)
- 連接視圖:ELF 未被加載到內存執行前,以 section 為單位的數據組織形式。
- 執行視圖:ELF 被加載到內存后,以 segment 為單位的數據組織形式。
而hook操作關系的是動態形式的內存操作,所以關心的是執行視圖,也就是上面的右圖。
動態鏈接
動態鏈接的大體步驟如下:
- 檢查已經加載的ELF列表
- 從libtest.so的.dynamic section 中讀取 libtest.so中外部依賴的ELF列表,從列表中剔除已經加載的ELF,得到本次需要加載的ELF
- 用mmap預留一塊內存,用于后面映射ELF
- 讀取ELF的PHT,用mmap把所有PT_LOAD類型的segment映射到內存中
- 從.dynamic section 中讀取各個section的虛擬內存地址,計算&保存各個section的虛擬內存絕對地址。
- 執行重定位操作(relocate),這是最關鍵的一步。重定位信息可能存在于下面的一個或多個 secion 中:.rel.plt, .rela.plt, .rel.dyn, .rela.dyn, .rel.android, .rela.android。動態鏈接器需要逐個處理這些 .relxxx section 中的重定位訴求。根據已加載的 ELF 的信息,動態鏈接器查找所需符號的地址(比如 libtest.so 的符號 malloc),找到后,將地址值填入 .relxxx 中指明的目標地址中,這些“目標地址”一般存在于.got 或 .data 中。
- ELF引用計數加一
- 逐個調用ELF的構造函數,先調用被依賴ELF的構造函數,再調用libtest.so自己的構造函數。
這里是全文最關鍵地地方,分析重定位操作可以推理出:
只要從這些 .relxxx 中獲取到“目標地址”,然后在“目標地址”中重新填上一個新的函數地址,這樣就完成 hook 了
.dynamic section
這是一個十分重要和特殊的 section,其中包含了 ELF 中其他各個 section 的內存位置等信息。在執行視圖中,總是會存在一個類型為 PT_DYNAMIC 的 segment,這個 segment 就包含了 .dynamic section 的內容。
無論是執行 hook 操作時,還是動態鏈接器執行動態鏈接時,都需要通過 PT_DYNAMIC segment 來找到 .dynamic section 的內存位置,再進一步讀取其他各項 section 的信息。
猜想-解決-驗證
原文這部分就是通過讀取libtest.so的匯編代碼,通過NDK的objdump 查出反匯編輸出。接著通過一些計算得出libtest.so中malloc的地址3f90 。
第一次驗證
#include <test.h>void *my_malloc(size_t size)
{printf("%zu bytes memory are allocated by libtest.so\n", size);return malloc(size);
}int main()
{void **p = (void **)0x3f90;*p = (void *)my_malloc; // do hooksay_hello();return 0;
}
運行結果:
caikelun@debian:~$ adb push ./main /data/local/tmp
caikelun@debian:~$ adb shell "chmod +x /data/local/tmp/main"
caikelun@debian:~$ adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"
Segmentation fault
caikelun@debian:~$
例子驗證了思路是正確的但是需要解決3個問題:
- 計算出來的地址是個先對內存地址,需要轉換為絕對地址
- 地址很可能沒有寫入權限
- 新的函數地址即使可以賦值成功,my_malloc 也不會執行,因為處理器有指令緩存。
方案
上述第一個問題可以通過基地址解決。
第二個問題通過mprotect解決。
第三個問題通過__builtin___clear_cache函數調用解決。
再次驗證
把main.c 修改為:
#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include <test.h>#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE)void *my_malloc(size_t size)
{printf("%zu bytes memory are allocated by libtest.so\n", size);return malloc(size);
}void hook()
{char line[512];FILE *fp;uintptr_t base_addr = 0;uintptr_t addr;//find base address of libtest.soif(NULL == (fp = fopen("/proc/self/maps", "r"))) return;while(fgets(line, sizeof(line), fp)){if(NULL != strstr(line, "libtest.so") &&sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)break;}fclose(fp);if(0 == base_addr) return;//the absolute addressaddr = base_addr + 0x3f90;//add write permissionmprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);//replace the function address*(void **)addr = my_malloc;//clear instruction cache__builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}int main()
{hook();say_hello();return 0;
}
caikelun@debian:~$ adb push ./main /data/local/tmp
caikelun@debian:~$ adb shell "chmod +x /data/local/tmp/main"
caikelun@debian:~$ adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"
1024 bytes memory are allocated by libtest.so
hello
caikelun@debian:~$
上述代碼在沒有重新編譯libtest.so的前提下,libtest.so的函數say_hello的函數地址替換成了my_malloc的函數地址。從而完成了native層面的hook。
至此,PLT hook的整體原理介紹完畢。
原文更加詳細和精彩,適合需要更深入理解和實操的朋友,鏈接在下面。
參考資料
參考原文:https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md
https://en.wikipedia.org/wiki/Hooking