---
title: "Native hook技术原理及应用"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/native-hook-techniques-overview/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/02-编程语言与运行时/Native hook技术原理及应用.md"
content_hash: 0e77623d72cf9f073c73feb20afe72101f0100555f584556da15869905700174
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
Hook 的核心原理是：
1. **劫持控制流**：通过修改代码或数据结构，使程序的执行跳转到自定义代码。
2. **原始功能保留**：部分情况下，需要保留原始函数功能，便于继续调用。

Hook 的实现方式可以分为：
- **函数跳转修改**：修改函数入口地址或 PLT/GOT 表。
- **动态库劫持**：加载时优先链接自定义库或重定向符号。

相关知识：
[ELF文件](https://www.pystone.net/notes/elf-file-format/)

# Native hook技术原理及应用
## **1. 基于 PLT/GOT 的 Hook**

##### **PLT/GOT 表概念**

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

### **原理与代码**
- 查找到目标函数的 **GOT 表地址**。
- 将 GOT 中存储的目标函数地址替换为自定义函数地址。
```c
#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`。


### **相关接口说明**
```c
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. 将目标函数开头的几条指令替换为跳转指令（通常是 `jmp` 或 `call`）。
3. 在自定义函数中完成拦截逻辑后，跳回目标函数。

需要的额外操作:
- **保存原始头部指令（跳过修改后的部分）。**
    - Inline Hook 的实现通常会拷贝目标函数被覆盖的指令（通常是 5~15 字节）到一个缓冲区中。
- **在 Hook 函数中调用时，执行保存的指令。**
    - 自定义的 Hook 函数会调用这些保存的指令，然后跳回目标函数的剩余部分。


### 示例说明

原始指令：
```asm
0x1000: 55                   ; push %rbp
0x1001: 48 89 E5             ; mov %rsp, %rbp
0x1004: E8 XX XX XX XX       ; call next_func
```

Hook后的置零:
```asm
0x1000: E9 YY YY YY YY       ; jmp hook_func
```

保存原始头部：
被覆盖的指令（`push %rbp` 和 `mov %rsp, %rbp`）被保存到缓冲区：
```asm
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 头部，执行剩余部分)
```

执行逻辑:
```bash
[调用 printf] --> [jmp hook_func] --> [hooked_printf] --> [缓冲区中的指令] --> [目标函数剩余部分]
```

示例:
```c
#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](/media/ed90bdc582a0d0809e26.png)


## **1. Hook 技术适用的函数类型**
### **1.1 可以被 Hook 的函数**

- **动态链接的函数：**
    - 动态链接函数通过 PLT/GOT 表管理其调用地址（如大多数 C 库函数，例如 `printf`、`malloc`）。
    - Hook 方法：`PLT/GOT Hook` 或者 `xhook`。
    - 适用场景：重定向共享库中函数的实现，如拦截 `open`、`read` 等系统调用。
- **主程序或共享库中的全局函数：**
    - 主程序或通过 `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` 指令。
- **解决方案：**
    - 使用 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
