返回「计算机、信息技术与工程」

Native hook技术原理及应用

Hook 的核心原理是: 1. 劫持控制流 :通过修改代码或数据结构,使程序的执行跳转到自定义代码。 2. 原始功能保留 :部分情况下,需要保留原始函数功能,便于继续调用。

更多
Markdown 结构化数据
本文目录 20 个章节

Hook 的核心原理是:

  1. 劫持控制流:通过修改代码或数据结构,使程序的执行跳转到自定义代码。
  2. 原始功能保留:部分情况下,需要保留原始函数功能,便于继续调用。

Hook 的实现方式可以分为:

  • 函数跳转修改:修改函数入口地址或 PLT/GOT 表。
  • 动态库劫持:加载时优先链接自定义库或重定向符号。

相关知识: ELF文件

Native hook技术原理及应用

1. 基于 PLT/GOT 的 Hook

PLT/GOT 表概念

在 Linux 下的 ELF 文件中:

  • PLT (Procedure Linkage Table):
    • 是程序中用于动态链接函数的跳转表。
    • 函数调用时,会先跳到 PLT,然后通过 GOT 获取实际函数地址。
  • GOT (Global Offset Table):
    • 存储动态链接函数的实际地址。
    • 在程序运行时由动态链接器填充。 通过修改 GOT 中函数指针的值,可以将函数调用重定向到自定义的代码。

原理与代码

  • 查找到目标函数的 GOT 表地址
  • 将 GOT 中存储的目标函数地址替换为自定义函数地址。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>

// 定义原始函数指针
static int (*original_printf)(const char *format, ...) = NULL;

// 自定义的 hook 函数
int hooked_printf(const char *format, ...) {
    printf("[HOOKED] ");
    // 调用原始 printf
    va_list args;
    va_start(args, format);
    int result = original_printf(format, args);
    va_end(args);
    return result;
}

void hook_printf() {
    // 获取目标程序的动态符号表
    void *handle = dlopen(NULL, RTLD_LAZY);
    if (!handle) {
        printf("dlopen failed\n");
        return;
    }

    // 查找 printf 的 GOT 地址
    void **got_entry = dlsym(handle, "printf");
    if (!got_entry) {
        printf("dlsym failed\n");
        return;
    }

    // 保存原始函数地址
    original_printf = (int (*)(const char *, ...))(*got_entry);

    // 修改 GOT 表,重定向到 hooked_printf
    *got_entry = (void *)hooked_printf;

    printf("Hook installed\n");
}
  • 调用 printf 时,程序会通过 GOT 获取目标地址。
  • Hook 后,GOT 中地址被修改,跳转到 hooked_printf

相关接口说明

void *handle = dlopen(NULL, RTLD_LAZY);
void *symbol = dlsym(handle, "my_function"); // 获取当前程序中 `my_function` 的地址

使用 dlopen 函数 动态加载程序或库,具体含义如下: dlopen 是 POSIX 标准的动态加载函数,属于动态链接库管理 API,通常用于:

  • 在运行时加载共享库(.so 文件)。
  • 获取库中的符号(函数、变量)地址。
  • 实现插件式架构。

参数: NULL: 传入 NULL 时表示不加载新共享库,而是返回 当前进程已经加载的程序或共享库的句柄。 RTLD_LAZY: 表示符号的解析是延迟的(Lazy Binding)。只有在符号第一次被调用时,才会解析它的地址。

  • RTLD_NOW:在调用 dlopen 时立即解析所有符号。
  • RTLD_GLOBAL:符号对全局可见,其他库可以访问这些符号。
  • RTLD_LOCAL:符号只在当前库可见(默认行为)。

2. Inline Hook

通过修改目标函数的代码(通常是前几条指令),将执行流跳转到自定义代码。

实现步骤

  1. 获取目标函数的起始地址。
  2. 将目标函数开头的几条指令替换为跳转指令(通常是 jmpcall)。
  3. 在自定义函数中完成拦截逻辑后,跳回目标函数。

需要的额外操作:

  • 保存原始头部指令(跳过修改后的部分)。
    • Inline Hook 的实现通常会拷贝目标函数被覆盖的指令(通常是 5~15 字节)到一个缓冲区中。
  • 在 Hook 函数中调用时,执行保存的指令。
    • 自定义的 Hook 函数会调用这些保存的指令,然后跳回目标函数的剩余部分。

示例说明

原始指令:

0x1000: 55                   ; push %rbp
0x1001: 48 89 E5             ; mov %rsp, %rbp
0x1004: E8 XX XX XX XX       ; call next_func

Hook后的置零:

0x1000: E9 YY YY YY YY       ; jmp hook_func

保存原始头部: 被覆盖的指令(push %rbpmov %rsp, %rbp)被保存到缓冲区:

buffer:
0x2000: 55                   ; push %rbp
0x2001: 48 89 E5             ; mov %rsp, %rbp
0x2004: E8 XX XX XX XX       ; call next_func
0x2009: E9 ZZ ZZ ZZ ZZ       ; jmp to 0x1004 (跳过 Hook 头部,执行剩余部分)

执行逻辑:

[调用 printf] --> [jmp hook_func] --> [hooked_printf] --> [缓冲区中的指令] --> [目标函数剩余部分]

示例:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>

void *original_printf; // 保存原始函数地址
unsigned char saved_instructions[8]; // 保存被覆盖的指令

// 自定义 Hook 函数
void hooked_printf(const char *format, ...) {
    printf("[HOOKED]: ");

    // 调用原始 printf
    void (*original)() = (void (*)())saved_instructions;
    original();

    printf("[END HOOK]\n");
}

// 修改内存保护
void unprotect_memory(void *addr, size_t size) {
    uintptr_t page_start = (uintptr_t)addr & ~(getpagesize() - 1);
    mprotect((void *)page_start, size, PROT_READ | PROT_WRITE | PROT_EXEC);
}

// 安装 Hook
void hook_printf() {
    original_printf = dlsym(RTLD_NEXT, "printf");

    // 保存原始指令
    memcpy(saved_instructions, original_printf, sizeof(saved_instructions));
    saved_instructions[sizeof(saved_instructions) - 1] = 0xE9; // 跳转指令
    *(int32_t *)(saved_instructions + sizeof(saved_instructions) - 4) =
        (int32_t)((uintptr_t)original_printf + sizeof(saved_instructions) - (uintptr_t)saved_instructions - 5);

    // 修改原始函数
    unprotect_memory(original_printf, 8);
    unsigned char jmp_instruction[5] = {0xE9}; // JMP 指令
    *(int32_t *)(jmp_instruction + 1) = (int32_t)((uintptr_t)hooked_printf - (uintptr_t)original_printf - 5);
    memcpy(original_printf, jmp_instruction, sizeof(jmp_instruction));
}

适用范围

assets/image-20250106200653584.png

1. Hook 技术适用的函数类型

1.1 可以被 Hook 的函数

  • 动态链接的函数:
    • 动态链接函数通过 PLT/GOT 表管理其调用地址(如大多数 C 库函数,例如 printfmalloc)。
    • Hook 方法:PLT/GOT Hook 或者 xhook
    • 适用场景:重定向共享库中函数的实现,如拦截 openread 等系统调用。
  • 主程序或共享库中的全局函数:
    • 主程序或通过 RTLD_GLOBAL 加载的共享库中全局可见的符号。
    • Hook 方法:基于 dlsym 或 Inline Hook。
    • 适用场景:拦截主程序调用的函数(如自定义实现某个库函数行为)。
  • Native 层的系统调用(syscall):
    • 系统调用通过内核提供,例如 writemmap
    • Hook 方法:Inline Hook 或通过 libc 中封装的调用(如 write)实现。
    • 适用场景:修改底层文件操作或网络行为。
  • 动态加载的库中的函数:
    • 动态加载的共享库(通过 dlopen 加载)中的函数可以 Hook,但仅限于全局可见符号。
    • Hook 方法:Inline Hook 或在加载时使用 xhook

1.2 无法直接被 Hook 的函数

  • 静态链接的函数:
    • 静态链接的函数直接嵌入主程序中,调用时无需通过 PLT/GOT 跳转,地址是固定的。
    • Hook 方法:必须使用 Inline Hook 修改函数体。
    • 限制:由于无符号表支持,难以精确定位函数起始地址。
  • 内联函数(Inline Functions):
    • 编译器可能将内联函数直接展开到调用处,导致没有明确的函数地址。
    • Hook 方法:几乎无法针对内联函数 Hook,除非修改编译选项或直接修改机器码。
  • 优化后的匿名符号:
    • 在高优化模式下,编译器可能合并函数、消除未使用的符号,导致符号无法通过常规方法定位。
    • Hook 方法:需要使用反汇编或调试工具精确识别指令位置。

2. Hook 技术的适用场景与限制

2.1 Hook 适用场景

  • 功能拦截与增强:

    • 修改已有函数的逻辑,例如拦截 malloc,记录内存分配日志。
    • 在网络请求中拦截函数如 sendrecv,用于数据分析或防护。
  • 调试与监控:

    • 捕获函数调用栈,用于性能分析或错误诊断。
    • 拦截 openclose 等系统调用,分析文件操作行为。
  • 兼容性补丁:

    • 修改第三方库行为,例如修复 API 的兼容性问题。
    • 替换废弃的函数调用为新的实现。

2.2 Hook 技术的限制与挑战

(1) 不可见符号

  • 动态库中未导出的符号(static 修饰的函数)无法通过 PLT/GOT Hook。
  • 解决方案:
    • 使用 Inline Hook,直接修改函数体指令。
    • 或者在库加载时通过符号表或调试工具定位私有函数。

(2) ASLR(地址空间随机化)

  • ASLR 导致每次运行时共享库的加载地址随机变化,无法通过固定地址定位函数。
  • 解决方案:
    • 使用 dlsym 或类似方法获取符号地址。
    • 对于内核级函数,通过 proc 文件系统或调试接口解析加载地址。

(3) 函数体覆盖问题

  • 如果被 Hook 的函数体过短,修改后可能破坏函数逻辑。
    • 例如,一个函数只有 4 字节机器码,无法完整插入 jmp 指令。
  • 解决方案:
    • 使用 trampoline 技术,将指令复制到安全位置执行后跳回。
    • 或者使用 PLT/GOT Hook 替代 Inline Hook。

(4) 多线程环境

  • 多线程程序中,修改共享数据(如 GOT 表或代码段)可能导致竞争条件或崩溃。
  • 解决方案:
    • 使用线程同步机制,确保 Hook 安装过程的原子性。
    • 或者提前在单线程初始化阶段完成 Hook。

(5) SELinux 和权限限制

  • 在 Android 等系统中,SELinux 会限制对某些进程的内存或文件访问权限。
  • 解决方案:
    • 在 Root 权限下关闭 SELinux。
    • 或者使用非侵入性 Hook 技术(如基于 Frida 的动态注入)。

现有工具

xHook: https://github.com/iqiyi/xHook https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md

ByteHook: 博客: https://github.com/caikelun/caikelun.github.io/blob/master/site/blog/2021-08-19-bytedance-open-source-bytehook.md

开源库: https://github.com/bytedance/bhook