你始终能够从 这里 访问本文的最新版本。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 许可证 受权。