引言
在 Android 开发中,函数拦截(Hook)技术是性能监控、热修复、行为分析的核心手段之一。PLT Hook 凭借其稳定性与兼容性,成为动态库函数拦截的首选方案。本文从动态链接机制出发,解析 PLT Hook 的核心原理,并实战演示如何拦截系统生成 ANR Trace 文件的写入操作,实现监控与定制化处理。
一、ELF 与动态链接机制:PLT/GOT 的协作原理
要理解 PLT Hook,需先掌握 ELF 文件格式和动态链接的工作流程。
1. ELF 文件的核心结构
ELF(Executable and Linkable Format)是 Linux/Android 系统的可执行文件标准,包含以下关键节区:
-
.text:存储代码逻辑。 -
.plt(Procedure Linkage Table):跳转到动态库函数的入口表。 -
.got.plt(Global Offset Table):存储动态库函数的实际地址。 -
.dynsym:动态符号表,记录函数名与地址的映射关系。
2. 延迟绑定(Lazy Binding)机制
当程序首次调用动态库函数(如 libc.so 的 printf)时,流程如下:
- 跳转至
.plt表项。 -
.plt通过.got.plt中的地址(初始指向动态链接器ld.so)触发解析。 - 动态链接器解析函数地址并回填至
.got.plt。 - 后续调用直接通过
.got.plt跳转,避免重复解析。
PLT/GOT 的协作 实现了高效的动态链接,但也为 Hook 提供了切入点——修改 GOT 表项。
二、PLT Hook 的核心原理
PLT Hook 的核心思路是 劫持 GOT 表中的函数地址,将其替换为自定义函数。以下是实现步骤:
1. 定位目标函数的 GOT 表项
-
解析 ELF 结构:通过
.dynsym(符号表)和.rel.plt(重定位表)确定目标函数在 GOT 中的偏移。 -
计算内存地址:结合动态库的加载基址(通过
dlopen或/proc/self/maps获取),得到 GOT 表项的实际内存地址。
2. 替换 GOT 表项
-
修改内存权限:使用
mprotect将内存页设为可写(PROT_READ | PROT_WRITE)。 - 替换地址:将 GOT 中的原函数地址替换为 Hook 函数地址。
-
恢复权限:还原内存页的原始保护属性(如
PROT_READ | PROT_EXEC)。
3. 保留原始函数
备份原函数地址,便于在 Hook 函数中调用其逻辑(如统计耗时后执行原函数)。
三、PLT Hook 的实现细节与挑战
1. 解析 ELF 的关键节区
以下代码片段展示了如何遍历 ELF 的动态段(.dynamic),定位符号表和重定位表:
Elf64_Dyn *dyn = (Elf64_Dyn *)dynamic_addr;
for (; dyn->d_tag != DT_NULL; dyn++) {
if (dyn->d_tag == DT_SYMTAB) { // 符号表地址
symtab = (Elf64_Sym *)dyn->d_un.d_ptr;
} else if (dyn->d_tag == DT_REL) { // 重定位表地址
reltab = (Elf64_Rel *)dyn->d_un.d_ptr;
}
}
2. 绕过 Android 高版本的 RELRO 保护
Android 7.0 后默认启用 RELRO(Relocation Read-Only),将 GOT 表设为只读。解决方案:
- 使用
dlopen加载库时指定RTLD_NOW,强制立即绑定。 - 通过
mprotect临时修改内存权限。
3. 处理 ARM/Thumb 指令集
ARM 架构中,函数地址的最低位为 1 表示 Thumb 模式。替换地址时需保留该标志位:
// 保留 Thumb 模式标志位
void *original_addr = (void *)((uintptr_t)*got_entry & ~1UL);
四、实战:拦截 ANR Trace 文件的写入操作
当发生 ANR(Application Not Responding)时,系统会生成 traces.txt 文件记录堆栈信息。通过 Hook 文件写入函数,可监控或修改 Trace 内容。
1. 选择 Hook 的目标函数
ANR Trace 的写入通常通过以下函数实现:
-
write:底层系统调用,操作文件描述符。 -
fwrite:C 标准库的缓冲写入函数。 -
open:拦截文件路径,识别 ANR 文件。
本文以 write 函数为例,演示拦截逻辑。
2. 完整代码实现
#include <dlfcn.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <android/log.h>
#define TAG "ANR_HOOK"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
// 原始函数指针
static ssize_t (*original_write)(int, const void *, size_t);
// 自定义 Hook 函数
ssize_t hooked_write(int fd, const void *buf, size_t count) {
// 通过 /proc/self/fd/<fd> 获取文件路径
char path[256] = {0};
char proc_fd_path[64];
snprintf(proc_fd_path, sizeof(proc_fd_path), "/proc/self/fd/%d", fd);
ssize_t len = readlink(proc_fd_path, path, sizeof(path) - 1);
if (len != -1) {
path[len] = '\0';
// 判断是否为 ANR Trace 文件
if (strstr(path, "/data/anr/") != NULL) {
LOGD("Detected ANR Trace write to: %s", path);
// 示例:在文件头部插入标记
const char *marker = "[Hooked by PLT]\n";
original_write(fd, marker, strlen(marker));
}
}
// 调用原始 write 函数
return original_write(fd, buf, count);
}
// 初始化 Hook
void hook_anr_write() {
void *handle = dlopen("libc.so", RTLD_NOW);
if (!handle) return;
// 获取 write 的 GOT 地址(需解析 ELF,此处为伪代码)
uintptr_t *got_entry = find_got_entry(handle, "write");
if (!got_entry) return;
// 修改内存权限
uintptr_t page = (uintptr_t)got_entry & ~(PAGE_SIZE - 1);
mprotect((void *)page, PAGE_SIZE, PROT_READ | PROT_WRITE);
// 替换 GOT 表项
original_write = (ssize_t (*)(int, const void *, size_t))(*got_entry);
*got_entry = (uintptr_t)hooked_write;
// 恢复权限(可选)
mprotect((void *)page, PAGE_SIZE, PROT_READ | PROT_EXEC);
}
3. 代码解析
-
路径检测:通过
/proc/self/fd/<fd>获取文件描述符对应的实际路径,识别/data/anr/目录。 -
内容修改:在检测到 ANR 文件时,插入自定义标记
[Hooked by PLT]。 -
权限管理:使用
mprotect确保 GOT 表可写,完成替换后恢复权限。
4. 扩展场景
-
重定向写入路径:拦截
open函数,替换目标路径。int (*original_open)(const char *, int, ...); int hooked_open(const char *path, int flags, mode_t mode) { if (strstr(path, "/data/anr/") != NULL) { path = "/sdcard/custom_anr/traces.txt"; // 重定向路径 } return original_open(path, flags, mode); } - 上报 ANR 事件:在 Hook 函数中触发网络请求,将日志上传至服务器。
五、PLT Hook 的优缺点与注意事项
优点
- 稳定性高:直接修改动态链接流程,兼容性较好。
- 轻量级:无需修改代码段,仅操作数据段(GOT)。
缺点
- 局限性:仅能 Hook 动态链接函数,静态链接函数无法拦截。
- 高版本限制:需绕过 Android 的 RELRO 保护。
注意事项
-
权限问题:读取
/data/anr/需 root 权限,普通应用需系统级权限。 - 多线程安全:替换 GOT 表项时,需使用锁或原子操作避免竞争。
- 性能影响:频繁的路径检查可能影响性能,建议在非高频调用中使用。
六、总结
PLT Hook 是 Android 平台上一种高效的函数拦截技术,通过劫持 GOT 表项实现动态库函数的监控与替换。本文以拦截 ANR Trace 文件为例,详细演示了从原理到实战的完整流程。关键点包括:
-
精准定位目标函数:选择
write、open等关键函数。 -
合理过滤路径:通过
/proc/self/fd识别 ANR 文件。 - 绕过系统保护:处理 Android 高版本的 RELRO 机制。
对于更高阶需求(如无 Root 权限拦截),可结合 Frida 或 Xposed 等框架,但 PLT Hook 凭借其轻量级和低侵入性,仍是性能敏感场景的首选方案。
扩展阅读