赞
踩
昨晚看完了爱奇艺出品的开源框架xhook中的《Android PLT hook 概述》,内容比较深。主要用到了ELF和动态链接这些Linux的知识点,但是总体上还算是理解了基本原理,这篇文章主要记录下现阶段对PLT hook的学习和理解。感觉写原文的作者很牛逼,内容看的我热血沸腾,需要看原文的读者可以翻到文章最后点开原文链接。
最后希望感谢你花时间阅读本文,下面开始学习下面的内容。
“Hook”(钩子)是计算机编程中的一种技术,它允许开发者拦截、修改或扩展软件或系统的行为。通过使用钩子,开发者可以在特定事件发生时注入自定义代码,以便修改程序的行为或响应程序的特定事件。
钩子的使用场景大概有这些:
钩子技术可以提供很大的灵活性和功能扩展性,但也需要谨慎使用,因为不正确的使用钩子可能会导致程序崩溃、安全漏洞或不稳定的行为。
当然,我们本文要讨论的场景属于函数调用场景。那么PLT hook又是什么意思?
PLT (Procedure Linkage Table) hook 是一种在动态链接库(DLL)或共享对象(SO)中实现的技术,用于在运行时修改或拦截函数调用。这种技术通常用于在不修改原始代码的情况下,对函数的行为进行修改或监视。
PLT hook 的基本原理是利用了动态链接库的符号解析机制。
在程序加载动态链接库时,系统会创建一个 PLT 表来保存对动态链接函数的调用。这个表中的每个条目实际上是一个跳转指令,将控制权转移到动态链接库中的实际函数实现。
PLT hook 就是通过修改 PLT 表中的条目,将原始函数调用指向自定义的函数或者跳转到其他代码段,从而实现对函数行为的修改或拦截。
定位目标函数:确定要 hook 的目标函数,获取其在动态链接库中的地址。
修改 PLT 表:通过修改 PLT 表中目标函数对应的条目,将其指向自定义的函数或者其他代码段。
处理原始函数调用:在自定义函数中可以执行一些额外的操作,然后再调用原始的目标函数,或者完全替换原始函数的行为。
恢复原始调用:有时候需要在自定义函数中调用原始的目标函数,以保持程序的正常行为。
有了上述的基本了解,再来看看给出的例子,就会容易理解很多。
下面通过给出一个存在明显内存泄漏的代码,把它们编译为动态库libtest.so。看看最后能不能通过PLT hook把泄漏给解决了。
头文件 test.h
#ifndef TEST_H
#define TEST_H 1
#ifdef __cplusplus
extern "C" {
#endif
void 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(Executable and Linkable Format)是一种行业标准的二进制数据封装格式,主要用于封装可执行文件、动态库、object 文件和 core dumps 文件。
对于PLT hook,最重要的是了解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 header数据结构如图:
ELF 以 section 为单位来组织和管理各种信息。ELF 使用 SHT 来记录所有 section 的基本信息。主要包括:section 的类型、在文件中的偏移量、大小、加载到内存后的虚拟内存相对地址、内存中字节的对齐方式等。
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)
下面用一张图描述相关的关系,这也是PLT hook的核心原理:
而hook操作关系的是动态形式的内存操作,所以关心的是执行视图,也就是上面的右图。
动态链接的大体步骤如下:
这里是全文最关键地地方,分析重定位操作可以推理出:
只要从这些 .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 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个问题:
上述第一个问题可以通过基地址解决。
第二个问题通过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.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的前提下,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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。