---
title: "Native hook技术原理及应用-PLTHook"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/native-hook-plt-hook/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/02-编程语言与运行时/Native hook技术原理及应用-PLTHook.md"
content_hash: 10edb44917ace92ea11f5df2253a98e6bd4be1624eb1f8146e9863d6ae0292c6
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# Native hook技术原理及应用-PLTHook

[ELF文件-程序的执行与动态链接](https://www.pystone.net/notes/elf-execution-dynamic-linking/)

动态链接（比如执行 dlopen）的大致步骤是：
1. 检查已加载的 ELF 列表。（如果 libtest.so 已经加载，就不再重复加载了，仅把 libtest.so 的引用计数加一，然后直接返回。）
2. 从 libtest.so 的 .dynamic section 中读取 libtest.so 的外部依赖的 ELF 列表，从此列表中剔除已加载的 ELF，最后得到本次需要加载的 ELF 完整列表（包括 libtest.so 自身）。
3. 逐个加载列表中的 ELF。加载步骤：
    - 用 `mmap` 预留一块足够大的内存，用于后续映射 ELF。（`MAP_PRIVATE` 方式）
    - 读 ELF 的 PHT，用 `mmap` 把所有类型为 `PT_LOAD` 的 segment 依次映射到内存中。
    - 从 .dynamic segment 中读取各信息项，主要是各个 section 的虚拟内存相对地址，然后计算并保存各个 section 的虚拟内存绝对地址。
    - **执行重定位操作（relocate），这是最关键的一步。重定位信息可能存在于下面的一个或多个 secion 中：`.rel.plt`, `.rela.plt`, `.rel.dyn`, `.rela.dyn`, `.rel.android`, `.rela.android`。动态链接器需要逐个处理这些 `.relxxx` section 中的重定位诉求。根据已加载的 ELF 的信息，动态链接器查找所需符号的地址（比如 libtest.so 的符号 `malloc`），找到后，将地址值填入 `.relxxx` 中指明的目标地址中，这些“目标地址”一般存在于`.got` 或 `.data` 中。**
    - ELF 的引用计数加一。
4. 逐个调用列表中 ELF 的构造函数（constructor），这些构造函数的地址是之前从 .dynamic segment 中读取到的（类型为 `DT_INIT` 和 `DT_INIT_ARRAY`）。各 ELF 的构造函数是按照依赖关系逐层调用的，先调用被依赖 ELF 的构造函数，最后调用 libtest.so 自己的构造函数。（ELF 也可以定义自己的析构函数（destructor），在 ELF 被 unload 的时候会被自动调用）


只要从这些 `.relxxx` 中获取到“**目标地址**”，然后在“**目标地址**”中重新填上一个新的函数地址，这样就完成 hook 了


test.h
```c
#ifndef TEST_H
#define TEST_H 1

#ifdef __cplusplus
extern "C" {
#endif

void say_hello();

#ifdef __cplusplus
}
#endif

#endif
```

test.c
```c
#include <stdlib.h>
#include <stdio.h>

void say_hello()
{
    char *buf = malloc(1024);
    if(NULL != buf)
    {
        snprintf(buf, 1024, "%s", "hello\n");
        printf("%s", buf);
    }
}
```

main.c
```c
#include <test.h>

int main()
{
    say_hello();
    return 0;
}
```

需要动态链接的符号  `malloc@LIBC`

```bash
caikelun@debian:~/$ arm-linux-androideabi-readelf -s ./libtest.so

Symbol table '.dynsym' contains 58 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_finalize@LIBC (2)
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND snprintf@LIBC (2)
     3: 00000000     0 FUNC    GLOBAL DEFAULT  UND malloc@LIBC (2)
     4: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_atexit@LIBC (2)
     5: 00000000     0 FUNC    GLOBAL DEFAULT  UND printf@LIBC (2)
     6: 00000f61    60 FUNC    GLOBAL DEFAULT   12 say_hello
...............
...............
```


调用该函数的汇编指令:
```assembly

00000dd4 <malloc@plt>:
 dd4:   e28fc600    add ip, pc, #0, 12
 dd8:   e28cca03    add ip, ip, #12288  ; 0x3000
 ddc:   e5bcf1b4    ldr pc, [ip, #436]! ; 0x1b4


00000f60 <say_hello@@Base>:
     f60:   b5b0        push    {r4, r5, r7, lr}
     f62:   af02        add r7, sp, #8
     f64:   f44f 6080   mov.w   r0, #1024   ; 0x400
     f68:   f7ff ef34   blx dd4 <malloc@plt>
```

计算下来,跳转的目标地址是3f90
> 因为 arm 处理器使用 3 级流水线，所以第一条指令取到的 `pc` 的值是当前执行的指令地址 + `8`。 于是：`dd4` + `8` + `3000` + `1b4` = `3f90`。

这与RELA表malloc项的offset值一致.

```bash
caikelun@debian:~$ arm-linux-androideabi-readelf -r ./libtest.so

Relocation section '.rel.plt' at offset 0xcb8 contains 30 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00003f88  00000416 R_ARM_JUMP_SLOT   00000000   __cxa_atexit@LIBC
00003f8c  00000116 R_ARM_JUMP_SLOT   00000000   __cxa_finalize@LIBC
00003f90  00000316 R_ARM_JUMP_SLOT   00000000   malloc@LIBC
...............
...............
```


那么, “**目标地址**”就是 3f90, 在“**目标地址**”中重新填上一个新的函数地址，这样就完成 hook 了.


3f90是相对于ELF 的基地址的偏移值, 我们还需要将3f90换算成绝对地址.

使用以下代码即可hook成功.

> 根据你使用的编译器不同，或者编译器的版本不同，生成的 libtest.so 中，也许 `malloc` 对应的地址不再是 `0x3f90`，这时你需要先用 readelf 确认，然后再到 `main.c` 中修改。


```c
#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include <test.h>

#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)

void *my_malloc(size_t size)
{
    printf("%zu bytes memory are allocated by libtest.so\n", size);
    return malloc(size);
}

void hook()
{
    char       line[512];
    FILE      *fp;
    uintptr_t  base_addr = 0;
    uintptr_t  addr;

    //find base address of libtest.so
    if(NULL == (fp = fopen("/proc/self/maps", "r"))) return;
    while(fgets(line, sizeof(line), fp))
    {
        if(NULL != strstr(line, "libtest.so") &&
           sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)
            break;
    }
    fclose(fp);
    if(0 == base_addr) return;

    //the absolute address
    addr = base_addr + 0x3f90;

    //add write permission
    mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);

    //replace the function address
    *(void **)addr = my_malloc;

    //clear instruction cache
    __builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}

int main()
{
    hook();

    say_hello();
    return 0;
}
```


## xhook

```c
#include <stdlib.h>
#include <stdio.h>
#include <test.h>
#include <xhook.h>

void *my_malloc(size_t size)
{
    printf("%zu bytes memory are allocated by libtest.so\n", size);
    return malloc(size);
}

int main()
{
    xhook_register(".*/libtest\\.so$", "malloc", my_malloc, NULL);
    xhook_refresh(0);

    say_hello();
    return 0;
}
```

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

仓库： https://github.com/iqiyi/xHook
