深入掌握 Android PLT Hook:从原理到拦截 ANR Trace 的实战

引言

在 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.soprintf)时,流程如下:

  1. 跳转至 .plt 表项。
  2. .plt 通过 .got.plt 中的地址(初始指向动态链接器 ld.so)触发解析。
  3. 动态链接器解析函数地址并回填至 .got.plt
  4. 后续调用直接通过 .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 保护。

注意事项

  1. 权限问题:读取 /data/anr/ 需 root 权限,普通应用需系统级权限。
  2. 多线程安全:替换 GOT 表项时,需使用锁或原子操作避免竞争。
  3. 性能影响:频繁的路径检查可能影响性能,建议在非高频调用中使用。

六、总结

PLT Hook 是 Android 平台上一种高效的函数拦截技术,通过劫持 GOT 表项实现动态库函数的监控与替换。本文以拦截 ANR Trace 文件为例,详细演示了从原理到实战的完整流程。关键点包括:

  • 精准定位目标函数:选择 writeopen 等关键函数。
  • 合理过滤路径:通过 /proc/self/fd 识别 ANR 文件。
  • 绕过系统保护:处理 Android 高版本的 RELRO 机制。

对于更高阶需求(如无 Root 权限拦截),可结合 Frida 或 Xposed 等框架,但 PLT Hook 凭借其轻量级和低侵入性,仍是性能敏感场景的首选方案。


扩展阅读

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 一、ELF(Executable and Linkable Format) 1.1、ELF(Executable ...
    feifei_fly阅读 3,669评论 0 1
  • 最近经常用到PLT hook,接下来几篇文章,给大家介绍一下PLT hook的原理、使用、案例、以及一些注意事项。...
    尹学姐阅读 4,793评论 0 9
  • Preload简介Linux常见Hook技术对比函数调用类型内核模块Hook应用层Inline HookGot表应...
    超哥__阅读 16,562评论 1 3
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 14,519评论 2 59
  • 转载自 https://bbs.pediy.com/thread-255670.htm ELF文件结构详解 链接与...
    LiuJP阅读 5,375评论 0 0

友情链接更多精彩内容