返回「计算机、信息技术与工程」
Native hook技术原理及应用
Hook 的核心原理是: 1. 劫持控制流 :通过修改代码或数据结构,使程序的执行跳转到自定义代码。 2. 原始功能保留 :部分情况下,需要保留原始函数功能,便于继续调用。
本文目录 20 个章节
Hook 的核心原理是:
- 劫持控制流:通过修改代码或数据结构,使程序的执行跳转到自定义代码。
- 原始功能保留:部分情况下,需要保留原始函数功能,便于继续调用。
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
通过修改目标函数的代码(通常是前几条指令),将执行流跳转到自定义代码。
实现步骤
- 获取目标函数的起始地址。
- 将目标函数开头的几条指令替换为跳转指令(通常是
jmp或call)。 - 在自定义函数中完成拦截逻辑后,跳回目标函数。
需要的额外操作:
- 保存原始头部指令(跳过修改后的部分)。
- 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 %rbp 和 mov %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));
}
适用范围

1. Hook 技术适用的函数类型
1.1 可以被 Hook 的函数
- 动态链接的函数:
- 动态链接函数通过 PLT/GOT 表管理其调用地址(如大多数 C 库函数,例如
printf、malloc)。 - Hook 方法:
PLT/GOT Hook或者xhook。 - 适用场景:重定向共享库中函数的实现,如拦截
open、read等系统调用。
- 动态链接函数通过 PLT/GOT 表管理其调用地址(如大多数 C 库函数,例如
- 主程序或共享库中的全局函数:
- 主程序或通过
RTLD_GLOBAL加载的共享库中全局可见的符号。 - Hook 方法:基于
dlsym或 Inline Hook。 - 适用场景:拦截主程序调用的函数(如自定义实现某个库函数行为)。
- 主程序或通过
- Native 层的系统调用(syscall):
- 系统调用通过内核提供,例如
write、mmap。 - 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,记录内存分配日志。 - 在网络请求中拦截函数如
send、recv,用于数据分析或防护。
- 修改已有函数的逻辑,例如拦截
调试与监控:
- 捕获函数调用栈,用于性能分析或错误诊断。
- 拦截
open、close等系统调用,分析文件操作行为。
兼容性补丁:
- 修改第三方库行为,例如修复 API 的兼容性问题。
- 替换废弃的函数调用为新的实现。
2.2 Hook 技术的限制与挑战
(1) 不可见符号
- 动态库中未导出的符号(
static修饰的函数)无法通过 PLT/GOT Hook。 - 解决方案:
- 使用 Inline Hook,直接修改函数体指令。
- 或者在库加载时通过符号表或调试工具定位私有函数。
(2) ASLR(地址空间随机化)
- ASLR 导致每次运行时共享库的加载地址随机变化,无法通过固定地址定位函数。
- 解决方案:
- 使用
dlsym或类似方法获取符号地址。 - 对于内核级函数,通过
proc文件系统或调试接口解析加载地址。
- 使用
(3) 函数体覆盖问题
- 如果被 Hook 的函数体过短,修改后可能破坏函数逻辑。
- 例如,一个函数只有 4 字节机器码,无法完整插入
jmp指令。
- 例如,一个函数只有 4 字节机器码,无法完整插入
- 解决方案:
- 使用 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