黑客級別的文章:把動態(tài)庫的內(nèi)存操作玩出了新花樣!
目錄
-
理論與實踐
-
開始
-
新的動態(tài)庫
-
面臨的問題
-
怎么做?
-
ELF
-
概述
-
ELF 文件頭
-
SHT(section header table)
-
PHT(program header table)
-
連接視圖和運行視圖
-
.dynamic section
-
動態(tài)鏈接器(liker)
-
追蹤
-
內(nèi)存
-
基地址
-
內(nèi)存訪問權(quán)限
-
指令緩存
-
驗證
-
使用 xhook
-
FAQ
-
可以直接從文件中讀取 ELF 信息嗎?
-
計算地址的精確方法是什么?
-
目標 ELF 使用的編譯選項對 hook 有什么影響?
-
hook 時遇到偶發(fā)的段錯誤時什么原因?如何處理?
-
ELF 內(nèi)部函數(shù)之間的調(diào)用能 hook 嗎?
為了便于閱讀,我在原文中比較關(guān)鍵的文字上,添加了字體顏色。
理論與實踐
關(guān)于動態(tài)庫的相關(guān)內(nèi)容,市面上質(zhì)量比較好的書籍可能就是:《程序員的自我修養(yǎng)-鏈接、裝載和庫》這本書了。
開始
新的動態(tài)庫
我們有一個新的動態(tài)庫:libtest.so。
頭文件 test.h#ifndef TEST_H
#define TEST_H 1
#ifdef __cplusplus
extern "C" {
#endif
void say_hello();
#ifdef __cplusplus
}
#endif
#endif
源文件 test.c#include
#include
void say_hello()
{
char *buf = malloc(1024);
if(NULL != buf)
{
snprintf(buf, 1024, "%s", "hello\n");
printf("%s", buf);
}
}
say_hello的功能是在終端打印出hello\n這6個字符(包括結(jié)尾的\n)。
源文件 main.c#include
int main()
{
say_hello();
return 0;
}
編譯它們分別生成libtest.so和main。運行一下:
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的代碼雖然看上去有些愚蠢,但是它居然可以正確的工作,那還有什么可抱怨的呢?
面臨的問題
幸運的是,我們修復(fù)了libtest.so的問題??墒且院笤趺崔k呢?我們面臨2個問題:
- 當(dāng)測試覆蓋不足時,如何及時發(fā)現(xiàn)和準確定位線上 APP 的此類問題?
- 如果 libtest.so 是某些機型的系統(tǒng)庫,或者第三方的閉源庫,我們?nèi)绾涡迯?fù)它?如果監(jiān)控它的行為?
怎么做?
如果我們能對動態(tài)庫中的函數(shù)調(diào)用做hook(替換,攔截,竊聽,或者你覺得任何正確的描述方式),那就能夠做到很多我們想做的事情。
ELF
道哥注解:
概述
ELF(Executable and Linkable Format)是一種行業(yè)標準的二進制數(shù)據(jù)封裝格式,主要用于封裝可執(zhí)行文件、動態(tài)庫、object文件和core dumps文件。
ELF 文件頭
ELF文件的起始處,有一個固定格式的定長的文件頭(32位架構(gòu)為52字節(jié),64位架構(gòu)為64字節(jié))。ELF文件頭以magic number 0x7F 0x45 0x4C 0x46開始(其中后3個字節(jié)分別對應(yīng)可見字符E L F)。
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在當(dāng)前ELF文件中的起始位置和長度。
SHT(section header table)
ELF以section為單位來組織和管理各種信息。
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關(guān)系比較大的幾個section是:
dynstr:保存了所有的字符串常量信息。道哥注解:dynsym:保存了符號(symbol)的信息(符號的類型、起始地址、大小、符號名稱在 .dynstr 中的索引編號等)。函數(shù)也是一種符號。
text:程序代碼經(jīng)過編譯后生成的機器指令。
dynamic:供動態(tài)鏈接器使用的各項信息,記錄了當(dāng)前 ELF 的外部依賴,以及其他各個重要 section 的起始位置等信息。
got:Global Offset Table。用于記錄外部調(diào)用的入口地址。動態(tài)鏈接器(linker)執(zhí)行重定位(relocate)操作時,這里會被填入真實的外部調(diào)用的絕對地址。
plt:Procedure Linkage Table。外部調(diào)用的跳板,主要用于支持 lazy binding 方式的外部調(diào)用重定位。(Android 目前只有 MIPS 架構(gòu)支持 lazy binding)
rel.plt:對外部函數(shù)直接調(diào)用的重定位信息。
rel.dyn:除 .rel.plt 以外的重定位信息。(比如通過全局函數(shù)指針來調(diào)用外部函數(shù))
依賴于其他哪些共享對象;使用指令:readelf -d xxx.so,即可查看一個動態(tài)庫中 .dynamic 的內(nèi)容。動態(tài)鏈接符號表的位置(.dynsym);
動態(tài)鏈接重定位表的位置;
初始化代碼的位置;
...
PHT(program header table)
·ELF被加載到內(nèi)存時,是以segment為單位的。一個segment包含了一個或多個section`。
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都會被動態(tài)鏈接器(linker)映射(mmap)到內(nèi)存中。
連接視圖(Linking View)和執(zhí)行視圖(Execution View)
連接視圖:ELF 未被加載到內(nèi)存執(zhí)行前,以 section 為單位的數(shù)據(jù)組織形式。我們關(guān)心的hook操作,屬于動態(tài)形式的內(nèi)存操作,因此主要關(guān)心的是執(zhí)行視圖,即ELF被加載到內(nèi)存后,ELF中的數(shù)據(jù)是如何組織和存放的。執(zhí)行視圖:ELF 被加載到內(nèi)存后,以 segment 為單位的數(shù)據(jù)組織形式。
.dynamic section
這是一個十分重要和特殊的section,其中包含了ELF中其他各個section的內(nèi)存位置等信息。
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
動態(tài)鏈接器(linker)
安卓中的動態(tài)鏈接器程序是linker。源碼在這里。
等一下!我們似乎發(fā)現(xiàn)了什么!再看一遍重定位操作(relocate)的部分。
- 檢查已加載的 ELF 列表。(如果 libtest.so 已經(jīng)加載,就不再重復(fù)加載了,僅把 libtest.so 的引用計數(shù)加一,然后直接返回。)
- 從 libtest.so 的 .dynamic section 中讀取 libtest.so 的外部依賴的 ELF 列表,從此列表中剔除已加載的 ELF,最后得到本次需要加載的 ELF 完整列表(包括 libtest.so 自身)。
- 逐個加載列表中的 ELF。加載步驟:
(1) 用 mmap 預(yù)留一塊足夠大的內(nèi)存,用于后續(xù)映射 ELF。(MAP_PRIVATE 方式)(2) 讀 ELF 的 PHT,用 mmap 把所有類型為 PT_LOAD 的 segment 依次映射到內(nèi)存中。
(3) 從 .dynamic segment 中讀取各信息項,主要是各個 section 的虛擬內(nèi)存相對地址,然后計算并保存各個 section 的虛擬內(nèi)存絕對地址。
(4) 執(zhí)行重定位操作(relocate),這是最關(guān)鍵的一步。重定位信息可能存在于下面的一個或多個 secion 中:.rel.plt, .rela.plt, .rel.dyn, .rela.dyn, .rel.android, .rela.android。動態(tài)鏈接器需要逐個處理這些 .relxxx section 中的重定位訴求。根據(jù)已加載的 ELF 的信息,動態(tài)鏈接器查找所需符號的地址(比如 libtest.so 的符號 malloc),找到后,將地址值填入 .relxxx 中指明的目標地址中,這些“目標地址”一般存在于.got 或 .data 中。
(5) ELF 的引用計數(shù)加一。
- 逐個調(diào)用列表中 ELF 的構(gòu)造函數(shù)(constructor),這些構(gòu)造函數(shù)的地址是之前從 .dynamic segment 中讀取到的(類型為 DT_INIT 和 DT_INIT_ARRAY)。各 ELF 的構(gòu)造函數(shù)是按照依賴關(guān)系逐層調(diào)用的,先調(diào)用被依賴 ELF 的構(gòu)造函數(shù),最后調(diào)用 libtest.so 自己的構(gòu)造函數(shù)。(ELF 也可以定義自己的析構(gòu)函數(shù)(destructor),在 ELF 被 unload 的時候會被自動調(diào)用)
追蹤
靜態(tài)分析驗證一下還是很容易的。以armeabi-v7a架構(gòu)的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,對應(yīng)的匯編指令體積為60(10 進制)字節(jié)。
...............
...............
00000f60
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
f6c: 4604 mov r4, r0
f6e: b16c cbz r4, f8c
f70: a507 add r5, pc, #28 ; (adr r5, f90
f72: a308 add r3, pc, #32 ; (adr r3, f94
f74: 4620 mov r0, r4
f76: f44f 6180 mov.w r1, #1024 ; 0x400
f7a: 462a mov r2, r5
f7c: f7ff ef30 blx de0
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函數(shù)的調(diào)用對應(yīng)于指令blx dd4。跳轉(zhuǎn)到了地址dd4。
...............
...............
00000dd4
dd4: e28fc600 add ip, pc, #0, 12
dd8: e28cca03 add ip, ip, #12288 ; 0x3000
ddc: e5bcf1b4 ldr pc, [ip, #436]! ; 0x1b4
...............
...............
果然,跳轉(zhuǎn)到了.plt中,經(jīng)過了幾次地址計算,最后跳轉(zhuǎn)到了地址3f90中的值指向的地址處,3f90是個函數(shù)指針。
...............
...............
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里。
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里,這絕對不是巧合?。?
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 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 是個相對內(nèi)存地址,需要把它換算成絕對地址。
- 3f90 對應(yīng)的絕對地址很可能沒有寫入權(quán)限,直接對這個地址賦值會引起段錯誤。
- 新的函數(shù)地址即使賦值成功了,my_malloc 也不會被執(zhí)行,因為處理器有指令緩存(instruction cache)。
內(nèi)存
基地址
在進程的內(nèi)存空間中,各種ELF的加載地址是隨機的,只有在運行時才能拿到加載地址,也就是基地址。
#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架構(gòu)的Android 5.0以下版本居然不支持dl_iterate_phdr!
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返回的是指定進程的內(nèi)存空間中mmap的映射信息,包括各種動態(tài)庫、可執(zhí)行文件(如:linker),??臻g,堆空間,甚至還包括字體文件。
內(nèi)存訪問權(quán)限
maps返回的信息中已經(jīng)包含了權(quán)限訪問信息。
int mprotect(void *addr, size_t len, int prot);
注意修改內(nèi)存訪問權(quán)限時,只能以“頁”為單位。
指令緩存
注意.got和.data的section類型是PROGBITS,也就是執(zhí)行代碼。處理器可能會對這部分數(shù)據(jù)做緩存。
注意清除指令緩存時,也只能以“頁”為單位。__builtin___clear_cache的詳細說明見這里。
驗證
我們把main.c修改為:
#include
#include
#include
#include
#include
#define PAGE_START(addr) ((addr)