文章目录
- 前言
- 什么是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" {
#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
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.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数据结构如图:
SHT(section header table)
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)
- .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 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个问题:
- 计算出来的地址是个先对内存地址,需要转换为绝对地址
- 地址很可能没有写入权限
- 新的函数地址即使可以赋值成功,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.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