你始終能夠從 這裏 訪問本文的最新版本。html
文中使用的示例代碼能夠從 這裏 獲取。文中提到的 xhook 開源項目能夠從 這裏 獲取。node
咱們有一個新的動態庫:libtest.so。linux
頭文件 test.handroid
#ifndef TEST_H
#define TEST_H 1
#ifdef __cplusplus
extern "C" {
#endif
void say_hello();
#ifdef __cplusplus
}
#endif
#endif
複製代碼
源文件 test.cc++
#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);
}
}
複製代碼
say_hello
的功能是在終端打印出 hello\n
這6個字符(包括結尾的 \n
)。git
咱們須要一個測試程序:main。github
源文件 main.cshell
#include <test.h>
int main() {
say_hello();
return 0;
}
複製代碼
編譯它們分別生成 libtest.so 和 main。運行一下:緩存
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 的代碼雖然看上去有些愚蠢,可是它竟然能夠正確的工做,那還有什麼可抱怨的呢?趕忙在新版 APP 中開始使用它吧!bash
遺憾的是,正如你可能已經發現的,libtest.so 存在嚴重的內存泄露問題,每調用一次 say_hello
函數,就會泄露 1024 字節的內存。新版 APP 上線後崩潰率開始上升,各類詭異的崩潰信息和報障信息跌撞而至。
幸運的是,咱們修復了 libtest.so 的問題。但是之後怎麼辦呢?咱們面臨 2 個問題:
若是咱們能對動態庫中的函數調用作 hook(替換,攔截,竊聽,或者你以爲任何正確的描述方式),那就可以作到不少咱們想作的事情。好比 hook malloc
,calloc
,realloc
和 free
,咱們就能統計出各個動態庫分配了多少內存,哪些內存一直被佔用沒有釋放。
這真的能作到嗎?答案是:hook 咱們本身的進程是徹底能夠的。hook 其餘進程須要 root 權限(對於其餘進程,沒有 root 權限就無法修改它的內存空間,也無法注入代碼)。幸運的是,咱們只要 hook 本身就夠了。
ELF(Executable and Linkable Format)是一種行業標準的二進制數據封裝格式,主要用於封裝可執行文件、動態庫、object 文件和 core dumps 文件。
使用 google NDK 對源代碼進行編譯和連接,生成的動態庫或可執行文件都是 ELF 格式的。用 readelf 能夠查看 ELF 文件的基本信息,用 objdump 能夠查看 ELF 文件的反彙編輸出。
ELF 格式的概述能夠參考 這裏,完整定義能夠參考 這裏。其中最重要的部分是:ELF 文件頭、SHT(section header table)、PHT(program header table)。
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.so
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: ARM Version: 0x1 Entry point address: 0x0 Start of program headers: 52 (bytes into file) Start of section headers: 12744 (bytes into file) Flags: 0x5000200, Version5 EABI, soft-float ABI Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 8 Size of section headers: 40 (bytes) Number of section headers: 25 Section header string table index: 24 複製代碼
ELF 文件頭中包含了 SHT 和 PHT 在當前 ELF 文件中的起始位置和長度。例如,libtest.so 的 SHT 起始位置爲 12744,長度 40 字節;PHT 起始位置爲 52,長度 32字節。
ELF 以 section 爲單位來組織和管理各類信息。ELF 使用 SHT 來記錄全部 section 的基本信息。主要包括:section 的類型、在文件中的偏移量、大小、加載到內存後的虛擬內存相對地址、內存中字節的對齊方式等。
libtest.so 的 SHT:
caikelun@debian:~$ arm-linux-androideabi-readelf -S ./libtest.so
There 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)
複製代碼
比較重要,且和 hook 關係比較大的幾個 section 是:
.dynstr
:保存了全部的字符串常量信息。.dynsym
:保存了符號(symbol)的信息(符號的類型、起始地址、大小、符號名稱在 .dynstr
中的索引編號等)。函數也是一種符號。.text
:程序代碼通過編譯後生成的機器指令。.dynamic
:供動態連接器使用的各項信息,記錄了當前 ELF 的外部依賴,以及其餘各個重要 section 的起始位置等信息。.got
:Global Offset Table。用於記錄外部調用的入口地址。動態連接器(linker)執行重定位(relocate)操做時,這裏會被填入真實的外部調用的絕對地址。.plt
:Procedure Linkage Table。外部調用的跳板,主要用於支持 lazy binding 方式的外部調用重定位。(Android 目前只有 MIPS 架構支持 lazy binding).rel.plt
:對外部函數直接調用的重定位信息。.rel.dyn
:除 .rel.plt
之外的重定位信息。(好比經過全局函數指針來調用外部函數)ELF 被加載到內存時,是以 segment 爲單位的。一個 segment 包含了一個或多個 section。ELF 使用 PHT 來記錄全部 segment 的基本信息。主要包括:segment 的類型、在文件中的偏移量、大小、加載到內存後的虛擬內存相對地址、內存中字節的對齊方式等。
libtest.so 的 PHT:
caikelun@debian:~$ arm-linux-androideabi-readelf -l ./libtest.so
Elf file type is DYN (Shared object file)
Entry point 0x0
There are 8 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00100 0x00100 R 0x4
LOAD 0x000000 0x00000000 0x00000000 0x02604 0x02604 R E 0x1000
LOAD 0x002e3c 0x00003e3c 0x00003e3c 0x001c8 0x001c8 RW 0x1000
DYNAMIC 0x002e48 0x00003e48 0x00003e48 0x00118 0x00118 RW 0x4
NOTE 0x000134 0x00000134 0x00000134 0x000bc 0x000bc R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
EXIDX 0x002504 0x00002504 0x00002504 0x00100 0x00100 R 0x4
GNU_RELRO 0x002e3c 0x00003e3c 0x00003e3c 0x001c4 0x001c4 RW 0x4
Section to Segment mapping:
Segment Sections...
00
01 .note.android.ident .note.gnu.build-id .dynsym .dynstr .hash .gnu.version .gnu.version_d .gnu.version_r .rel.dyn .rel.plt .plt .text .ARM.extab .ARM.exidx
02 .fini_array .init_array .dynamic .got .data
03 .dynamic
04 .note.android.ident .note.gnu.build-id
05
06 .ARM.exidx
07 .fini_array .init_array .dynamic .got
複製代碼
全部類型爲 PT_LOAD
的 segment 都會被動態連接器(linker)映射(mmap)到內存中。
咱們關心的 hook 操做,屬於動態形式的內存操做,所以主要關心的是執行視圖,即 ELF 被加載到內存後,ELF 中的數據是如何組織和存放的。
這是一個十分重要和特殊的 section,其中包含了 ELF 中其餘各個 section 的內存位置等信息。在執行視圖中,老是會存在一個類型爲 PT_DYNAMIC
的 segment,這個 segment 就包含了 .dynamic section 的內容。
不管是執行 hook 操做時,仍是動態連接器執行動態連接時,都須要經過 PT_DYNAMIC
segment 來找到 .dynamic section 的內存位置,再進一步讀取其餘各項 section 的信息。
libtest.so 的 .dynamic section:
caikelun@debian:~$ arm-linux-androideabi-readelf -d ./libtest.so
Dynamic section at offset 0x2e48 contains 30 entries:
Tag Type Name/Value
0x00000003 (PLTGOT) 0x3f7c
0x00000002 (PLTRELSZ) 240 (bytes)
0x00000017 (JMPREL) 0xcb8
0x00000014 (PLTREL) REL
0x00000011 (REL) 0xc78
0x00000012 (RELSZ) 64 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffa (RELCOUNT) 3
0x00000006 (SYMTAB) 0x1f0
0x0000000b (SYMENT) 16 (bytes)
0x00000005 (STRTAB) 0x590
0x0000000a (STRSZ) 1201 (bytes)
0x00000004 (HASH) 0xa44
0x00000001 (NEEDED) Shared library: [libc.so]
0x00000001 (NEEDED) Shared library: [libm.so]
0x00000001 (NEEDED) Shared library: [libstdc++.so]
0x00000001 (NEEDED) Shared library: [libdl.so]
0x0000000e (SONAME) Library soname: [libtest.so]
0x0000001a (FINI_ARRAY) 0x3e3c
0x0000001c (FINI_ARRAYSZ) 8 (bytes)
0x00000019 (INIT_ARRAY) 0x3e44
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001e (FLAGS) BIND_NOW
0x6ffffffb (FLAGS_1) Flags: NOW
0x6ffffff0 (VERSYM) 0xbc8
0x6ffffffc (VERDEF) 0xc3c
0x6ffffffd (VERDEFNUM) 1
0x6ffffffe (VERNEED) 0xc58
0x6fffffff (VERNEEDNUM) 1
0x00000000 (NULL) 0x0
複製代碼
安卓中的動態連接器程序是 linker。源碼在 這裏。
動態連接(好比執行 dlopen)的大體步驟是:
mmap
預留一塊足夠大的內存,用於後續映射 ELF。(MAP_PRIVATE
方式)mmap
把全部類型爲 PT_LOAD
的 segment 依次映射到內存中。.rel.plt
, .rela.plt
, .rel.dyn
, .rela.dyn
, .rel.android
, .rela.android
。動態連接器須要逐個處理這些 .relxxx
section 中的重定位訴求。根據已加載的 ELF 的信息,動態連接器查找所需符號的地址(好比 libtest.so 的符號 malloc
),找到後,將地址值填入 .relxxx
中指明的目標地址中,這些「目標地址」通常存在於.got
或 .data
中。DT_INIT
和 DT_INIT_ARRAY
)。各 ELF 的構造函數是按照依賴關係逐層調用的,先調用被依賴 ELF 的構造函數,最後調用 libtest.so 本身的構造函數。(ELF 也能夠定義本身的析構函數(destructor),在 ELF 被 unload 的時候會被自動調用)等一下!咱們彷佛發現了什麼!再看一遍重定位操做(relocate)的部分。難道咱們只要從這些 .relxxx
中獲取到「目標地址」,而後在「目標地址」中從新填上一個新的函數地址,這樣就完成 hook 了嗎?也許吧。
靜態分析驗證一下仍是很容易的。以 armeabi-v7a 架構的 libtest.so 爲例。先看一下 say_hello 函數對應的彙編代碼吧。
caikelun@debian:~/$ arm-linux-androideabi-readelf -s ./libtest.so
Symbol table '.dynsym' contains 58 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND snprintf@LIBC (2)
3: 00000000 0 FUNC GLOBAL DEFAULT UND malloc@LIBC (2)
4: 00000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND printf@LIBC (2)
6: 00000f61 60 FUNC GLOBAL DEFAULT 12 say_hello
...............
...............
複製代碼
找到了!say_hello
在地址 f61
,對應的彙編指令體積爲 60
(10 進制)字節。用 objdump 查看 say_hello
的反彙編輸出。
caikelun@debian:~$ arm-linux-androideabi-objdump -D ./libtest.so
...............
...............
00000f60 <say_hello@@Base>:
f60: b5b0 push {r4, r5, r7, lr}
f62: af02 add r7, sp, #8
f64: f44f 6080 mov.w r0, #1024 ; 0x400
f68: f7ff ef34 blx dd4 <malloc@plt>
f6c: 4604 mov r4, r0
f6e: b16c cbz r4, f8c <say_hello@@Base+0x2c>
f70: a507 add r5, pc, #28 ; (adr r5, f90 <say_hello@@Base+0x30>)
f72: a308 add r3, pc, #32 ; (adr r3, f94 <say_hello@@Base+0x34>)
f74: 4620 mov r0, r4
f76: f44f 6180 mov.w r1, #1024 ; 0x400
f7a: 462a mov r2, r5
f7c: f7ff ef30 blx de0 <snprintf@plt>
f80: 4628 mov r0, r5
f82: 4621 mov r1, r4
f84: e8bd 40b0 ldmia.w sp!, {r4, r5, r7, lr}
f88: f001 ba96 b.w 24b8 <_Unwind_GetTextRelBase@@Base+0x8>
f8c: bdb0 pop {r4, r5, r7, pc}
f8e: bf00 nop
f90: 7325 strb r5, [r4, #12]
f92: 0000 movs r0, r0
f94: 6568 str r0, [r5, #84] ; 0x54
f96: 6c6c ldr r4, [r5, #68] ; 0x44
f98: 0a6f lsrs r7, r5, #9
f9a: 0000 movs r0, r0
...............
...............
複製代碼
對 malloc
函數的調用對應於指令 blx dd4
。跳轉到了地址 dd4
。看看這個地址裏有什麼吧:
caikelun@debian:~$ arm-linux-androideabi-objdump -D ./libtest.so
...............
...............
00000dd4 <malloc@plt>:
dd4: e28fc600 add ip, pc, #0, 12
dd8: e28cca03 add ip, ip, #12288 ; 0x3000
ddc: e5bcf1b4 ldr pc, [ip, #436]! ; 0x1b4
...............
...............
複製代碼
果真,跳轉到了 .plt
中,通過了幾回地址計算,最後跳轉到了地址 3f90
中的值指向的地址處,3f90
是個函數指針。
稍微解釋一下:由於 arm 處理器使用 3 級流水線,因此第一條指令取到的 pc
的值是當前執行的指令地址 + 8
。 因而:dd4
+ 8
+ 3000
+ 1b4
= 3f90
。
地址 3f90
在哪裏呢:
caikelun@debian:~$ arm-linux-androideabi-objdump -D ./libtest.so
...............
...............
00003f60 <.got>:
...
3f70: 00002604 andeq r2, r0, r4, lsl #12
3f74: 00002504 andeq r2, r0, r4, lsl #10
...
3f88: 00000da8 andeq r0, r0, r8, lsr #27
3f8c: 00000da8 andeq r0, r0, r8, lsr #27
3f90: 00000da8 andeq r0, r0, r8, lsr #27
...............
...............
複製代碼
果真,在 .got
裏。
順便再看一下 .rel.plt
:
caikelun@debian:~$ arm-linux-androideabi-readelf -r ./libtest.so
Relocation section '.rel.plt' at offset 0xcb8 contains 30 entries:
Offset Info Type Sym.Value Sym. Name
00003f88 00000416 R_ARM_JUMP_SLOT 00000000 __cxa_atexit@LIBC
00003f8c 00000116 R_ARM_JUMP_SLOT 00000000 __cxa_finalize@LIBC
00003f90 00000316 R_ARM_JUMP_SLOT 00000000 malloc@LIBC
...............
...............
複製代碼
malloc
的地址竟然正好存放在 3f90
裏,這絕對不是巧合啊!還等什麼,趕忙改代碼吧。咱們的 main.c 應該改爲這樣:
#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 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"
Segmentation fault
caikelun@debian:~$
複製代碼
思路是正確的。但之因此仍是失敗了,是由於這段代碼存在下面的 3 個問題:
3f90
是個相對內存地址,須要把它換算成絕對地址。3f90
對應的絕對地址極可能沒有寫入權限,直接對這個地址賦值會引發段錯誤。my_malloc
也不會被執行,由於處理器有指令緩存(instruction cache)。咱們須要解決這些問題。
在進程的內存空間中,各類 ELF 的加載地址是隨機的,只有在運行時才能拿到加載地址,也就是基地址。咱們須要知道 ELF 的基地址,才能將相對地址換算成絕對地址。
沒有錯,熟悉 Linux 開發的聰明的你必定知道,咱們能夠直接調用 dl_iterate_phdr
。詳細的定義見 這裏。
嗯,先等等,多年的 Android 開發被坑經歷告訴咱們,仍是再看一眼 NDK 裏的 linker.h
頭文件吧:
#if defined(__arm__)
#if __ANDROID_API__ >= 21
int dl_iterate_phdr(int (*__callback)(struct dl_phdr_info*, size_t, void*), void* __data) __INTRODUCED_IN(21);
#endif /* __ANDROID_API__ >= 21 */
#else
int dl_iterate_phdr(int (*__callback)(struct dl_phdr_info*, size_t, void*), void* __data);
#endif
複製代碼
爲何?!ARM 架構的 Android 5.0 如下版本竟然不支持 dl_iterate_phdr
!咱們的 APP 但是要支持 Android 4.0 以上的全部版本啊。特別是 ARM,怎麼能不支持呢?!這還讓不讓人寫代碼啦!
幸運的是,咱們想到了,咱們還能夠解析 /proc/self/maps
:
root@android:/ # ps | grep main
ps | grep main
shell 7884 7882 2616 1016 hrtimer_na b6e83824 S /data/local/tmp/main
root@android:/ # cat /proc/7884/maps
cat /proc/7884/maps
address perms offset dev inode pathname
---------------------------------------------------------------------
...........
...........
b6e42000-b6eb5000 r-xp 00000000 b3:17 57457 /system/lib/libc.so
b6eb5000-b6eb9000 r--p 00072000 b3:17 57457 /system/lib/libc.so
b6eb9000-b6ebc000 rw-p 00076000 b3:17 57457 /system/lib/libc.so
b6ec6000-b6ec9000 r-xp 00000000 b3:19 753708 /data/local/tmp/libtest.so
b6ec9000-b6eca000 r--p 00002000 b3:19 753708 /data/local/tmp/libtest.so
b6eca000-b6ecb000 rw-p 00003000 b3:19 753708 /data/local/tmp/libtest.so
b6f03000-b6f20000 r-xp 00000000 b3:17 32860 /system/bin/linker
b6f20000-b6f21000 r--p 0001c000 b3:17 32860 /system/bin/linker
b6f21000-b6f23000 rw-p 0001d000 b3:17 32860 /system/bin/linker
b6f25000-b6f26000 r-xp 00000000 b3:19 753707 /data/local/tmp/main
b6f26000-b6f27000 r--p 00000000 b3:19 753707 /data/local/tmp/main
becd5000-becf6000 rw-p 00000000 00:00 0 [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
...........
...........
複製代碼
maps 返回的是指定進程的內存空間中 mmap
的映射信息,包括各類動態庫、可執行文件(如:linker),棧空間,堆空間,甚至還包括字體文件。maps 格式的詳細說明見 這裏。
咱們的 libtest.so 在 maps 中有 3 行記錄。offset 爲 0
的第一行的起始地址 b6ec6000
在絕大多數狀況下就是咱們尋找的基地址。
maps 返回的信息中已經包含了權限訪問信息。若是要執行 hook,就須要寫入的權限,可使用 mprotect
來完成:
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
複製代碼
注意修改內存訪問權限時,只能以「頁」爲單位。mprotect
的詳細說明見 這裏。
注意 .got
和 .data
的 section 類型是 PROGBITS
,也就是執行代碼。處理器可能會對這部分數據作緩存。修改內存地址後,咱們須要清除處理器的指令緩存,讓處理器從新從內存中讀取這部分指令。方法是調用 __builtin___clear_cache
:
void __builtin___clear_cache (char *begin, char *end);
複製代碼
注意清除指令緩存時,也只能以「頁」爲單位。__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.so
if(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 address
addr = base_addr + 0x3f90;
//add write permission
mprotect((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 的代碼,甚至沒有從新編譯它。咱們僅僅修改了 main 程序。
libtest.so 和 main 的源碼放在 github 上,能夠從 這裏 獲取到。(根據你使用的編譯器不一樣,或者編譯器的版本不一樣,生成的 libtest.so 中,也許 malloc
對應的地址再也不是 0x3f90
,這時你須要先用 readelf 確認,而後再到 main.c
中修改。)
固然,咱們已經開源了一個叫 xhook 的工具庫。使用 xhook,你能夠更優雅的完成對 libtest.so 的 hook 操做,也沒必要擔憂硬編碼 0x3f90
致使的兼容性問題。
#include <stdlib.h>
#include <stdio.h>
#include <test.h>
#include <xhook.h>
void *my_malloc(size_t size) {
printf("%zu bytes memory are allocated by libtest.so\n", size);
return malloc(size);
}
int main() {
xhook_register(".*/libtest\\.so$", "malloc", my_malloc, NULL);
xhook_refresh(0);
say_hello();
return 0;
}
複製代碼
xhook 支持 armeabi, armeabi-v7a 和 arm64-v8a。支持 Android 4.0 (含) 以上版本 (API level >= 14)。通過了產品級的穩定性和兼容性驗證。能夠在 這裏 獲取 xhook
。
總結一下 xhook 中執行 PLT hook 的流程:
PT_LOAD
且 offset 爲 0
的 segment。計算 ELF 基地址。PT_DYNAMIC
的 segment,從中獲取到 .dynamic
section,從 .dynamic
section中獲取其餘各項 section 對應的內存地址。.dynstr
section 中找到須要 hook 的 symbol 對應的 index 值。.relxxx
section(重定位 section),查找 symbol index 和 symbol type 都匹配的項,對於這項重定位項,執行 hook 操做。hook 流程以下:
mprotect
修改訪問權限爲可讀也可寫。mprotect
修改過內存訪問權限,如今還原到以前的權限。能夠。並且對於格式解析來講,讀文件是最穩妥的方式,由於 ELF 在運行時,原理上有不少 section 不須要一直保留在內存中,能夠在加載完以後就從內存中丟棄,這樣能夠節省少許的內存。可是從實踐的角度出發,各類平臺的動態連接器和加載器,都不會這麼作,可能它們認爲增長的複雜度得不償失。因此咱們從內存中讀取各類 ELF 信息就能夠了,讀文件反而增長了性能損耗。另外,某些系統庫 ELF 文件,APP 也不必定有訪問權限。
正如你已經注意到的,前面介紹 libtest.so 基地址獲取時,爲了簡化概念和編碼方便,用了「絕大多數狀況下」這種不該該出現的描述方式。對於 hook 來講,精確的基地址計算流程是:
0
,且 pathname
爲目標 ELF 的行。保存該行的 start address 爲 p0
。PT_LOAD
且 offset 爲 0
的 segment,保存該 segment 的虛擬內存相對地址(p_vaddr
)爲 p1
。p0
- p1
即爲該 ELF 當前的基地址。絕大多數的 ELF 第一個 PT_LOAD
segment 的 p_vaddr
都是 0
。
另外,之因此要在 maps 裏找 offset 爲 0
的行,是由於咱們在執行 hook 以前,但願對內存中的 ELF 文件頭進行校驗,確保當前操做的是一個有效的 ELF,而這種 ELF 文件頭只能出如今 offset 爲 0
的 mmap 區域。
能夠在 Android linker 的源碼中搜索「load_bias」,能夠找到不少詳細的註釋說明,也能夠參考 linker 中對 load_bias_
變量的賦值程序邏輯。
會有一些影響。
對於外部函數的調用,能夠分爲 3 中狀況:
.got
中。.data
中。通常狀況下,產品級的 ELF 不多會使用 -O0 進行編譯,因此也沒必要太糾結。可是若是你但願你的 ELF 儘可能不被別人 PLT hook,那能夠試試使用 -O0 來編譯,而後儘可能早的將外部函數的指針賦值給局部函數指針變量,以後一直使用這些局部函數指針來訪問外部函數。
總之,查看 C/C++ 的源代碼對這個問題的理解沒有意義,須要查看使用不一樣的編譯選項後,生成的 ELF 的反彙編輸出,比較它們的區別,才能知道哪些狀況因爲什麼緣由致使沒法被 PLT hook。
咱們有時會遇到這樣的問題:
/proc/self/maps
後發現某個內存區域的訪問權限爲可讀,當咱們讀取該區域的內容作 ELF 文件頭校驗時,發生了段錯誤(sig: SIGSEGV, code: SEGV_ACCERR)。mprotect()
修改了某個內存區域的訪問權限爲可寫,mprotect()
返回修改爲功,而後再次讀取 /proc/self/maps
確認對應內存區域的訪問權限確實爲可寫,執行寫入操做(替換函數指針,執行 hook)時發生段錯誤(sig: SIGSEGV, code: SEGV_ACCERR)。.dynamic
section 時發生段錯誤(sig: SIGSEGV, code: SEGV_ACCERR 或 SEGV_MAPERR)。可能的緣由是:
dlclose()
,或者正在用 mprotect()
修改這塊內存區域的訪問權限。/proc/self/maps
的內容中。問題分析:
debuggerd
守護進程通信,debuggerd
使用 ptrace
調試崩潰進程,獲取須要的崩潰現場信息,記錄到 tombstone 文件中,而後 APP 自殺。先明確一個觀點:不要只從應用層程序開發的角度來看待段錯誤,段錯誤不是洪水猛獸,它只是內核與用戶進程的一種正常的交流方式。當用戶進程訪問了無權限或未 mmap 的虛擬內存地址時,內核向用戶進程發送 SIGSEGV 信號,來通知用戶進程,僅此而已。只要段錯誤的發生位置是可控的,咱們就能夠在用戶進程中處理它。
解決方案:
flag
來進行標記,離開危險區域後將 flag
復位。flag
的值,來判斷當前線程邏輯是否在危險區域中。若是是,就用 siglongjmp
跳出 signal handler,直接跳到咱們預先設置好的「危險區域之外的下一行代碼處」;若是不是,就恢復以前加載器向咱們注入的 signal handler,而後直接返回,這時系統會再次向咱們的線程發送段錯誤信號,因爲已經恢復了以前的 signal handler,這時會進入默認的系統 signal handler 中走正常邏輯。具體代碼能夠參考 xhook
中的實現,在源碼中搜索 siglongjmp
和 sigsetjmp
。
咱們這裏介紹的 hook 方式爲 PLT hook,不能作 ELF 內部函數之間調用的 hook。
inline hook 能夠作到,你須要先知道想要 hook 的內部函數符號名(symbol name)或者地址,而後能夠 hook。
有不少開源和非開源的 inline hook 實現,好比:
inline hook 方案強大的同時可能帶來如下的問題:
建議若是 PLT hook 夠用的話,就沒必要嘗試 inline hook 了。
caikelun#qiyi.com (請用 @ 替換 #)
Copyright (c) 2018, 愛奇藝, Inc. All rights reserved.
本文使用 Creative Commons 許可證 受權。