从0做一个小型内存泄露检测器(2): elf文件的动态链接

inject_hook 深度技术解析:从 ELF 到 GOT/PLT Hook 的完整实现

目录

  1. 引言
  2. [ELF 文件格式深度剖析](#ELF 文件格式深度剖析)
  3. 动态链接原理
  4. [PLT 与 GOT 机制详解](#PLT 与 GOT 机制详解)
  5. [inject_hook 实现原理](#inject_hook 实现原理)
  6. 代码逐行解析
  7. 实战案例与调试技巧
  8. 总结

引言

在 Linux 系统中,动态库注入和函数 Hook 是一项强大的技术,广泛应用于内存泄漏检测、性能分析、安全审计等场景。本文将深入剖析 inject_hook.cpp 的实现,从 ELF 文件格式讲起,逐步深入到动态链接、PLT/GOT 表的工作原理,最后结合代码详细解析如何实现运行时函数劫持。

本文适合对 Linux 系统编程有一定了解,希望深入理解动态链接和 Hook 技术的开发者。


ELF 文件格式深度剖析

ELF 文件结构概览

ELF (Executable and Linkable Format) 是 Linux 系统中可执行文件、目标文件、共享库的标准格式。一个 ELF 文件由以下几个部分组成:
ELF File
ELF Header
Program Header Table
Sections / Section Data
Section Header Table
.text
.rodata
.data
.bss
.dynamic
.dynsym / .dynstr
.rela.*
.plt / .got
.symtab / .strtab
.debug_*

ELF Header

ELF Header 位于文件开头,包含文件的基本信息:

c 复制代码
typedef struct {
    unsigned char e_ident[16];  // 魔数和文件类型
    Elf64_Half    e_type;       // 文件类型 (ET_EXEC, ET_DYN, ET_REL)
    Elf64_Half    e_machine;    // 目标架构
    Elf64_Word    e_version;    // 版本
    Elf64_Addr    e_entry;      // 程序入口地址
    Elf64_Off     e_phoff;      // Program Header 偏移
    Elf64_Off     e_shoff;      // Section Header 偏移
    // ...
} Elf64_Ehdr;

Program Headers (程序头表)

Program Headers 描述了程序运行时的内存布局,每个 Program Header 描述一个段 (Segment):

c 复制代码
typedef struct {
    Elf64_Word  p_type;    // 段类型 (PT_LOAD, PT_DYNAMIC, PT_INTERP...)
    Elf64_Word  p_flags;   // 段权限 (PF_R, PF_W, PF_X)
    Elf64_Off   p_offset;  // 文件偏移
    Elf64_Addr  p_vaddr;   // 虚拟地址
    Elf64_Addr  p_paddr;   // 物理地址
    Elf64_Xword p_filesz;  // 文件中的大小
    Elf64_Xword p_memsz;   // 内存中的大小
    Elf64_Xword p_align;   // 对齐
} Elf64_Phdr;
bash 复制代码
Elf file type is EXEC (Executable file)
Entry point 0x4010b0
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000400318 0x0000000000400318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000005a8 0x00000000000005a8  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x0000000000000321 0x0000000000000321  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
                 0x00000000000001cc 0x00000000000001cc  R      0x1000
  LOAD           0x0000000000002e10 0x0000000000403e10 0x0000000000403e10
                 0x0000000000000268 0x00000000000002f0  RW     0x1000
  DYNAMIC        0x0000000000002e20 0x0000000000403e20 0x0000000000403e20
                 0x00000000000001d0 0x00000000000001d0  RW     0x8
  NOTE           0x0000000000000338 0x0000000000400338 0x0000000000400338
                 0x0000000000000030 0x0000000000000030  R      0x8
  NOTE           0x0000000000000368 0x0000000000400368 0x0000000000400368
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000400338 0x0000000000400338
                 0x0000000000000030 0x0000000000000030  R      0x8
  GNU_EH_FRAME   0x0000000000002078 0x0000000000402078 0x0000000000402078
                 0x000000000000004c 0x000000000000004c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002e10 0x0000000000403e10 0x0000000000403e10
                 0x00000000000001f0 0x00000000000001f0  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash                                                                                                      .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .plt.sec .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.gnu.build-id .note.ABI-tag
   09     .note.gnu.property
   10     .eh_frame_hdr
   11
   12     .init_array .fini_array .dynamic .got

关键段类型:

  • PT_LOAD: 可加载段,映射到内存
  • PT_DYNAMIC: 动态链接信息段(本文重点)
  • PT_INTERP: 动态链接器路径
  • PT_GNU_RELRO: 重定位只读段
    大概是这样的分布:

ELF 加载后的虚拟地址空间(示意,不按严格比例)
0x0000000000400000

LOAD #1 R

内容:ELF头 / Program Headers / .interp / note / 动态链接基础信息

文件范围:0x0000 - 0x05a8
0x0000000000401000

LOAD #2 R E

内容:.init / .plt / .plt.sec / .text / .fini

文件范围:0x1000 - 0x1321
0x0000000000402000

LOAD #3 R

内容:.rodata / .eh_frame_hdr / .eh_frame

文件范围:0x2000 - 0x21cc
0x0000000000403e10

LOAD #4 RW

内容:.init_array / .fini_array / .dynamic / .got / .got.plt / .data / .bss

文件范围:0x2e10 - 0x3078

内存大小比文件大:包含 .bss
入口点 Entry

0x00000000004010b0

(位于 .text ,属于 LOAD #2)

段和节的对应关系:
Segment 00

PHDR
无 section 映射
Segment 01

INTERP
.interp
Segment 02

LOAD R

0x400000
.interp

.note.gnu.property

.note.gnu.build-id

.note.ABI-tag

.gnu.hash

.dynsym

.dynstr

.gnu.version

.gnu.version_r

.rela.dyn

.rela.plt
Segment 03

LOAD R E

0x401000
.init

.plt

.plt.sec

.text

.fini
Segment 04

LOAD R

0x402000
.rodata

.eh_frame_hdr

.eh_frame
Segment 05

LOAD RW

0x403e10
.init_array

.fini_array

.dynamic

.got

.got.plt

.data

.bss
Segment 06

DYNAMIC
.dynamic
Segment 07

NOTE
.note.gnu.property
Segment 08

NOTE
.note.gnu.build-id

.note.ABI-tag
Segment 09

GNU_PROPERTY
.note.gnu.property
Segment 10

GNU_EH_FRAME
.eh_frame_hdr
Segment 11

GNU_STACK
无文件内容

仅说明栈权限:RW
Segment 12

GNU_RELRO
.init_array

.fini_array

.dynamic

.got

只有一部分会加载到内存中:

看最后的映射:

Segment 02 (LOAD, R)

包含:

.interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt

Segment 03 (LOAD, R E)

包含:

.init .plt .plt.sec .text .fini

Segment 04 (LOAD, R)

包含:

.rodata .eh_frame_hdr .eh_frame

Segment 05 (LOAD, RW)

包含:

.init_array .fini_array .dynamic .got .got.plt .data .bss

所以这些 section 对应的内容,会随着所属 LOAD 段一起映射进内存。

在elf头和程序头表之后的就是节表,节表是链接时组织代码的方式,段表是运行时组织代码的方式,两者有细微的区别,我们写一段C代码,来看看节表到底是怎么样的:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ── .rodata 段 ──────────────────────────────────────────
// 字符串字面量和 const 全局变量都住在这里,只读
const char* greeting = "Hello, ELF!";
const int   magic    = 0xDEADBEEF;

// ── .data 段 ────────────────────────────────────────────
// 已初始化的全局/静态变量
int   global_counter = 42;
char  global_buf[16] = "init_data";

// ── .bss 段 ─────────────────────────────────────────────
// 未初始化(或初始化为0)的全局变量,不占文件空间
int   uninitialized_var;
char  zero_buf[64];

// ── .text 段 ────────────────────────────────────────────
// 普通函数,编译进代码段

// 演示栈上局部变量
static int add(int a, int b) {
    return a + b;
}

// 演示动态内存分配(会产生 malloc/free 的 PLT 条目)
static void heap_demo() {
    void* p = malloc(128);
    if (p) {
        memset(p, 0xAB, 128);
        free(p);
    }
}

// 演示函数指针(会在 .data 或栈上产生指针条目)
typedef int (*op_fn)(int, int);

static int multiply(int a, int b) { return a * b; }

int main(void) {
    // 局部变量 → 栈
    int local_a = 10, local_b = 20;

    // 调用本地函数(相对跳转,无 PLT)
    int sum = add(local_a, local_b);

    // 调用外部库函数(通过 PLT → GOT)
    printf("sum = %d\n", sum);
    printf("greeting = %s\n", greeting);
    printf("magic = 0x%X\n", magic);
    printf("global_counter = %d\n", global_counter);

    // 修改 .data 段变量
    global_counter++;

    // 堆操作
    heap_demo();

    // 函数指针
    op_fn fn = multiply;
    printf("multiply = %d\n", fn(local_a, local_b));

    // 演示 .bss 段确实是零
    printf("uninitialized_var = %d\n", uninitialized_var);

    return 0;
}

用readelf -h命令可以看到elf文件的程序节表:

bash 复制代码
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400318  00000318
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.pr[...] NOTE             0000000000400338  00000338
       0000000000000030  0000000000000000   A       0     0     8
  [ 3] .note.gnu.bu[...] NOTE             0000000000400368  00000368
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .note.ABI-tag     NOTE             000000000040038c  0000038c
       0000000000000020  0000000000000000   A       0     0     4
  [ 5] .gnu.hash         GNU_HASH         00000000004003b0  000003b0
       000000000000001c  0000000000000000   A       6     0     8
  [ 6] .dynsym           DYNSYM           00000000004003d0  000003d0
       00000000000000a8  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           0000000000400478  00000478
       000000000000005d  0000000000000000   A       0     0     1
  [ 8] .gnu.version      VERSYM           00000000004004d6  000004d6
       000000000000000e  0000000000000002   A       6     0     2
  [ 9] .gnu.version_r    VERNEED          00000000004004e8  000004e8
       0000000000000030  0000000000000000   A       7     1     8
  [10] .rela.dyn         RELA             0000000000400518  00000518
       0000000000000030  0000000000000018   A       6     0     8
  [11] .rela.plt         RELA             0000000000400548  00000548
       0000000000000060  0000000000000018  AI       6    24     8
  [12] .init             PROGBITS         0000000000401000  00001000
       000000000000001b  0000000000000000  AX       0     0     4
  [13] .plt              PROGBITS         0000000000401020  00001020
       0000000000000050  0000000000000010  AX       0     0     16
  [14] .plt.sec          PROGBITS         0000000000401070  00001070
       0000000000000040  0000000000000010  AX       0     0     16
  [15] .text             PROGBITS         00000000004010b0  000010b0
       0000000000000262  0000000000000000  AX       0     0     16
  [16] .fini             PROGBITS         0000000000401314  00001314
       000000000000000d  0000000000000000  AX       0     0     4
  [17] .rodata           PROGBITS         0000000000402000  00002000
       0000000000000077  0000000000000000   A       0     0     4
  [18] .eh_frame_hdr     PROGBITS         0000000000402078  00002078
       000000000000004c  0000000000000000   A       0     0     4
  [19] .eh_frame         PROGBITS         00000000004020c8  000020c8
       0000000000000104  0000000000000000   A       0     0     8
  [20] .init_array       INIT_ARRAY       0000000000403e10  00002e10
       0000000000000008  0000000000000008  WA       0     0     8
  [21] .fini_array       FINI_ARRAY       0000000000403e18  00002e18
       0000000000000008  0000000000000008  WA       0     0     8
  [22] .dynamic          DYNAMIC          0000000000403e20  00002e20
       00000000000001d0  0000000000000010  WA       7     0     8
  [23] .got              PROGBITS         0000000000403ff0  00002ff0
       0000000000000010  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000404000  00003000
       0000000000000038  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000404040  00003040
       0000000000000038  0000000000000000  WA       0     0     16
  [26] .bss              NOBITS           0000000000404080  00003078
       0000000000000080  0000000000000000  WA       0     0     32
  [27] .comment          PROGBITS         0000000000000000  00003078
       000000000000002d  0000000000000001  MS       0     0     1
  [28] .debug_aranges    PROGBITS         0000000000000000  000030a5
       0000000000000030  0000000000000000           0     0     1
  [29] .debug_info       PROGBITS         0000000000000000  000030d5
       00000000000002b3  0000000000000000           0     0     1
  [30] .debug_abbrev     PROGBITS         0000000000000000  00003388
       0000000000000170  0000000000000000           0     0     1
  [31] .debug_line       PROGBITS         0000000000000000  000034f8
       00000000000000b4  0000000000000000           0     0     1
  [32] .debug_str        PROGBITS         0000000000000000  000035ac
       0000000000000171  0000000000000001  MS       0     0     1
  [33] .debug_line_str   PROGBITS         0000000000000000  0000371d
       000000000000007c  0000000000000001  MS       0     0     1
  [34] .symtab           SYMTAB           0000000000000000  000037a0
       0000000000000450  0000000000000018          35    21     8
  [35] .strtab           STRTAB           0000000000000000  00003bf0
       0000000000000233  0000000000000000           0     0     1
  [36] .shstrtab         STRTAB           0000000000000000  00003e23
       000000000000016f  0000000000000000           0     0     1

可以看到内容有很多,我这里直接把内容整理为表格:

节名 作用 运行时是否重要
[0] NULL 占位的空节,ELF 规范里第 0 个节通常留空
.interp 指定动态链接器路径,比如 Linux 下的 ld-linux
.note.gnu.property GNU 属性说明,告诉系统/加载器一些二进制属性 一般
.note.gnu.build-id 构建 ID,方便调试、符号匹配、崩溃分析 一般
.note.ABI-tag 记录 ABI/系统兼容信息 一般
.gnu.hash 动态符号查找用的哈希表,比老式 SysV hash 更快
.dynsym 动态符号表,给动态链接器解析外部符号用
.dynstr 动态符号表对应的字符串表
.gnu.version 动态符号版本表 较重要
.gnu.version_r 依赖的外部符号版本需求 较重要
.rela.dyn 一般动态重定位表,程序加载时要修正地址
.rela.plt PLT 相关重定位表,外部函数调用时常会用到
.init 旧式初始化代码,程序启动早期执行 有时重要
.plt Procedure Linkage Table,外部函数调用跳板
.plt.sec 更细分/更安全的 PLT 代码区
.text 机器指令代码本体,主函数和其他函数基本都在这 最重要
.fini 旧式结束清理代码,程序退出时执行 有时重要
.rodata 只读数据,比如字符串常量、只读查表数据 很重要
.eh_frame_hdr 异常处理/栈回溯信息头 较重要
.eh_frame 异常展开、回溯栈帧信息,C++ 异常/调试常用 较重要
.init_array 新式构造函数数组,启动时依次调用
.fini_array 新式析构函数数组,退出时依次调用
.dynamic 动态链接核心信息表,记录依赖库、重定位、符号表等 最重要
.got Global Offset Table,全局地址表,保存运行时解析出的地址 很重要
.got.plt PLT 专用 GOT 区域,配合延迟绑定调用外部函数 很重要
.data 已初始化的可写全局/静态变量 很重要
.bss 未初始化的可写全局/静态变量;文件里通常不占实体空间,装载时清零 很重要
.comment 编译器注释信息,比如 GCC 版本
.debug_aranges 调试地址范围信息 否,运行时不用
.debug_info DWARF 主调试信息,最核心的调试元数据 否,运行时不用
.debug_abbrev DWARF 缩写表,给 debug_info 解释结构
.debug_line 源码行号映射,反汇编对应源码行常靠它
.debug_str 调试字符串表
.debug_line_str 行号信息相关字符串表
.symtab 完整符号表,给链接/逆向/调试分析看;发布版常会被 strip 掉 否,运行时通常不用
.strtab .symtab 对应的字符串表
.shstrtab 节名字符串表,保存各个 section 的名字

重点关注:.interp,.dynsym,.rela.dyn,.rela.plt,.dynamic,.got,.got.plt,.symtab,.strtab这几个字段。

Dynamic Section (动态段)

PT_DYNAMIC 段包含动态链接器所需的所有信息,由一系列 Elf64_Dyn 结构组成:

c 复制代码
typedef struct {
    Elf64_Sxword d_tag;  // 条目类型
    union {
        Elf64_Xword d_val;  // 整数值
        Elf64_Addr  d_ptr;  // 地址值
    } d_un;
} Elf64_Dyn;

重要的 d_tag 类型:

Tag 含义 用途
DT_SYMTAB 符号表地址 指向 .dynsym 符号表
DT_STRTAB 字符串表地址 指向 .dynstr 字符串表
DT_STRSZ 字符串表大小 字符串表的字节数
DT_RELA 重定位表地址 指向 .rela.dyn
DT_RELASZ 重定位表大小 .rela.dyn 的字节数
DT_JMPREL PLT 重定位表地址 指向 .rela.plt
DT_PLTRELSZ PLT 重定位表大小 .rela.plt 的字节数
DT_SYMENT 符号表条目大小 每个符号的字节数
让我们查看这个代码的动态段:
bash 复制代码
Dynamic section at offset 0x2e20 contains 24 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x401000
 0x000000000000000d (FINI)               0x401314
 0x0000000000000019 (INIT_ARRAY)         0x403e10
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x403e18
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x4003b0
 0x0000000000000005 (STRTAB)             0x400478
 0x0000000000000006 (SYMTAB)             0x4003d0
 0x000000000000000a (STRSZ)              93 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x404000
 0x0000000000000002 (PLTRELSZ)           96 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400548
 0x0000000000000007 (RELA)               0x400518
 0x0000000000000008 (RELASZ)             48 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x4004e8
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x4004d6
 0x0000000000000000 (NULL)               0x0

这是对各个字段的解释:

Tag (十六进制) Tag (名称) Type 含义
0x0000000000000001 NEEDED 依赖库 libc.so.6 需要加载的共享库(C标准库)
0x000000000000000c INIT 地址 0x401000 初始化函数入口地址
0x000000000000000d FINI 地址 0x401314 终止函数入口地址
0x0000000000000019 INIT_ARRAY 地址 0x403e10 初始化函数指针数组地址
0x000000000000001b INIT_ARRAYSZ 数值 8 (bytes) 初始化数组大小
0x000000000000001a FINI_ARRAY 地址 0x403e18 终止函数指针数组地址
0x000000000000001c FINI_ARRAYSZ 数值 8 (bytes) 终止数组大小
0x000000006ffffef5 GNU_HASH 地址 0x4003b0 GNU 哈希表地址(加速符号查找)
0x0000000000000005 STRTAB 地址 0x400478 字符串表地址(符号名称)
0x0000000000000006 SYMTAB 地址 0x4003d0 符号表地址
0x000000000000000a STRSZ 数值 93 (bytes) 字符串表大小
0x000000000000000b SYMENT 数值 24 (bytes) 每个符号表条目大小
0x0000000000000015 DEBUG 地址 0x0 调试信息(未使用)
0x0000000000000003 PLTGOT 地址 0x404000 过程链接表的全局偏移表地址
0x0000000000000002 PLTRELSZ 数值 96 (bytes) PLT 重定位表大小
0x0000000000000014 PLTREL 类型 RELA PLT 使用 RELA 类型重定位
0x0000000000000017 JMPREL 地址 0x400548 PLT 重定位表地址
0x0000000000000007 RELA 地址 0x400518 重定位表地址
0x0000000000000008 RELASZ 数值 48 (bytes) 重定位表总大小
0x0000000000000009 RELAENT 数值 24 (bytes) 每个重定位条目大小
0x000000006ffffffe VERNEED 地址 0x4004e8 版本需求表地址
0x000000006fffffff VERNEEDNUM 数值 1 版本需求条目数
0x000000006ffffff0 VERSYM 地址 0x4004d6 版本符号表地址
0x0000000000000000 NULL 标记 0x0 动态段结束标记

符号表 (Symbol Table)

符号表记录了所有导出和导入的符号信息:

c 复制代码
typedef struct {
    Elf64_Word    st_name;   // 符号名在字符串表中的偏移
    unsigned char st_info;   // 符号类型和绑定属性
    unsigned char st_other;  // 保留
    Elf64_Half    st_shndx;  // 符号所在段索引
    Elf64_Addr    st_value;  // 符号值(地址)
    Elf64_Xword   st_size;   // 符号大小
} Elf64_Sym;

read -s elf命令可以查看完整符号表和动态符号表:

bash 复制代码
Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND free@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _[...]@GLIBC_2.34 (3                                                                                                     )
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND [...]@GLIBC_2.2.5 (2                                                                                                     )
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND [...]@GLIBC_2.2.5 (2                                                                                                     )
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND [...]@GLIBC_2.2.5 (2                                                                                                     )

Symbol table '.symtab' contains 46 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crt1.o
     2: 000000000040038c    32 OBJECT  LOCAL  DEFAULT    4 __abi_tag
     3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
     4: 00000000004010f0     0 FUNC    LOCAL  DEFAULT   15 deregister_tm_clones
     5: 0000000000401120     0 FUNC    LOCAL  DEFAULT   15 register_tm_clones
     6: 0000000000401160     0 FUNC    LOCAL  DEFAULT   15 __do_global_dtors_au                                                                                                     x
     7: 0000000000404080     1 OBJECT  LOCAL  DEFAULT   26 completed.0
     8: 0000000000403e18     0 OBJECT  LOCAL  DEFAULT   21 __do_global_dtor[...                                                                                                     ]
     9: 0000000000401190     0 FUNC    LOCAL  DEFAULT   15 frame_dummy
    10: 0000000000403e10     0 OBJECT  LOCAL  DEFAULT   20 __frame_dummy_in[...                                                                                                     ]
    11: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS elf.c
    12: 0000000000401196    24 FUNC    LOCAL  DEFAULT   15 add
    13: 00000000004011ae    70 FUNC    LOCAL  DEFAULT   15 heap_demo
    14: 00000000004011f4    23 FUNC    LOCAL  DEFAULT   15 multiply
    15: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    16: 00000000004021c8     0 OBJECT  LOCAL  DEFAULT   19 __FRAME_END__
    17: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS
    18: 0000000000403e20     0 OBJECT  LOCAL  DEFAULT   22 _DYNAMIC
    19: 0000000000402078     0 NOTYPE  LOCAL  DEFAULT   18 __GNU_EH_FRAME_HDR
    20: 0000000000404000     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE                                                                                                     _
    21: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND free@GLIBC_2.2.5
    22: 0000000000404060    16 OBJECT  GLOBAL DEFAULT   25 global_buf
    23: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_mai[...                                                                                                     ]
    24: 00000000004040a0     4 OBJECT  GLOBAL DEFAULT   26 uninitialized_var
    25: 0000000000404040     0 NOTYPE  WEAK   DEFAULT   25 data_start
    26: 0000000000402010     4 OBJECT  GLOBAL DEFAULT   17 magic
    27: 0000000000404078     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    28: 0000000000401314     0 FUNC    GLOBAL HIDDEN    16 _fini
    29: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5
    30: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND memset@GLIBC_2.2.5
    31: 0000000000404040     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    32: 0000000000404070     8 OBJECT  GLOBAL DEFAULT   25 greeting
    33: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    34: 0000000000404048     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    35: 0000000000402000     4 OBJECT  GLOBAL DEFAULT   17 _IO_stdin_used
    36: 0000000000404050     4 OBJECT  GLOBAL DEFAULT   25 global_counter
    37: 00000000004040c0    64 OBJECT  GLOBAL DEFAULT   26 zero_buf
    38: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND malloc@GLIBC_2.2.5
    39: 0000000000404100     0 NOTYPE  GLOBAL DEFAULT   26 _end
    40: 00000000004010e0     5 FUNC    GLOBAL HIDDEN    15 _dl_relocate_sta[...                                                                                                     ]
    41: 00000000004010b0    38 FUNC    GLOBAL DEFAULT   15 _start
    42: 0000000000404078     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    43: 000000000040120b   263 FUNC    GLOBAL DEFAULT   15 main
    44: 0000000000404078     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
    45: 0000000000401000     0 FUNC    GLOBAL HIDDEN    12 _init

动态符号表:

Num Value Size Type Bind Vis Ndx Name
0 0x0000000000000000 0 NOTYPE LOCAL DEFAULT UND (空)
1 0x0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5
2 0x0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.34
3 0x0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5
4 0x0000000000000000 0 FUNC GLOBAL DEFAULT UND memset@GLIBC_2.2.5
5 0x0000000000000000 0 NOTYPE WEAK DEFAULT UND gmon_start
6 0x0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc@GLIBC_2.2.5

完整符号表:

Num Value Size Type Bind Vis Ndx Name
1 0x0000000000000000 0 FILE LOCAL DEFAULT ABS crt1.o
2 0x000000000040038c 32 OBJECT LOCAL DEFAULT 4 __abi_tag
4-10 - - - LOCAL - - 各种初始化/清理函数
12 0x0000000000401196 24 FUNC LOCAL DEFAULT 15 add
13 0x00000000004011ae 70 FUNC LOCAL DEFAULT 15 heap_demo
14 0x00000000004011f4 23 FUNC LOCAL DEFAULT 15 multiply
21 0x0000000000000000 0 FUNC GLOBAL DEFAULT UND free
22 0x0000000000404060 16 OBJECT GLOBAL DEFAULT 25 global_buf
23 0x0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main
24 0x00000000004040a0 4 OBJECT GLOBAL DEFAULT 26 uninitialized_var
26 0x0000000000402010 4 OBJECT GLOBAL DEFAULT 17 magic
29 0x0000000000000000 0 FUNC GLOBAL DEFAULT UND printf
32 0x0000000000404070 8 OBJECT GLOBAL DEFAULT 25 greeting
37 0x00000000004040c0 64 OBJECT GLOBAL DEFAULT 26 zero_buf
38 0x0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc
41 0x00000000004010b0 38 FUNC GLOBAL DEFAULT 15 _start
43 0x000000000040120b 263 FUNC GLOBAL DEFAULT 15 main

重定位表 (Relocation Table)

重定位表记录了需要在运行时修正的地址:

c 复制代码
typedef struct {
    Elf64_Addr  r_offset;  // 需要修正的位置(GOT 表项地址)
    Elf64_Xword r_info;    // 重定位类型和符号索引
    Elf64_Sxword r_addend; // 附加值
} Elf64_Rela;

r_info 字段解析:

  • 高 32 位:符号表索引
  • 低 32 位:重定位类型(如 R_X86_64_JUMP_SLOT

动态重定位表 '.rela.dyn'

Offset Info Type Sym. Value Sym. Name + Addend 说明
0x403ff0 0x000200000006 R_X86_64_GLOB_DAT 0x0000000000000000 __libc_start_main@GLIBC_2.34 + 0 __libc_start_main 的实际地址填入内存地址 0x403ff0
0x403ff8 0x000500000006 R_X86_64_GLOB_DAT 0x0000000000000000 gmon_start + 0 __gmon_start__ 的实际地址填入内存地址 0x403ff8

PLT 重定位表 '.rela.plt'

Offset Info Type Sym. Value Sym. Name + Addend 说明
0x404018 0x000100000007 R_X86_64_JUMP_SLOT 0x0000000000000000 free@GLIBC_2.2.5 + 0 free 函数设置跳转槽,地址在 0x404018
0x404020 0x000300000007 R_X86_64_JUMP_SLOT 0x0000000000000000 printf@GLIBC_2.2.5 + 0 printf 函数设置跳转槽,地址在 0x404020
0x404028 0x000400000007 R_X86_64_JUMP_SLOT 0x0000000000000000 memset@GLIBC_2.2.5 + 0 memset 函数设置跳转槽,地址在 0x404028
0x404030 0x000600000007 R_X86_64_JUMP_SLOT 0x0000000000000000 malloc@GLIBC_2.2.5 + 0 malloc 函数设置跳转槽,地址在 0x404030

Offset: 需要重定位的内存地址偏移

Info: 包含符号索引和重定位类型信息

Type: 重定位类型

Sym. Value: 符号的值(链接前为0)

Sym. Name: 要重定位的符号名称

重定位类型:

R_X86_64_GLOB_DAT: 用于全局数据对象的地址重定位

R_X86_64_JUMP_SLOT: 用于 PLT(Procedure Linkage Table)跳转槽的重定位

作用:

.rela.dyn: 程序启动时,动态链接器将根据此表填充全局变量的实际地址

.rela.plt: 当调用外部函数时,通过此表设置的跳转槽来找到实际函数地址

动态链接原理

动态链接的生命周期

libc.so 应用程序 动态链接器 (ld.so) 内核 libc.so 应用程序 动态链接器 (ld.so) 内核 1. 加载程序,启动 ld.so 2. 解析 ELF Header 3. 加载依赖的共享库 4. 处理重定位表 5. 初始化 GOT 表 6. 跳转到程序入口 7. 首次调用 malloc() 8. 通过 PLT/GOT 返回

地址空间布局

当程序运行时,内存布局如下:
进程地址空间
栈 Stack
共享库 libc.so
共享库 其他 .so
堆 Heap
BSS 段
数据段 .data
代码段 .text
动态链接器 ld.so

位置无关代码 (PIC)

共享库必须使用 PIC 编译,因为它们会被加载到不同进程的不同地址。PIC 通过相对寻址和 GOT 表实现:

c 复制代码
// 非 PIC 代码(绝对地址)
mov rax, [0x12345678]  // 硬编码地址

// PIC 代码(相对地址 + GOT)
mov rax, [rip + offset]  // 相对 PC 寻址
mov rax, [rax]           // 通过 GOT 间接访问

PLT 与 GOT 机制详解

什么是 PLT 和 GOT?

  • PLT (Procedure Linkage Table): 过程链接表,存放跳转代码的桩函数
  • GOT (Global Offset Table): 全局偏移表,存放外部函数的实际地址

它们配合实现延迟绑定 (Lazy Binding),即首次调用时才解析函数地址。

PLT/GOT 工作流程

libc.so 动态链接器 GOT 表 PLT 表 应用代码 libc.so 动态链接器 GOT 表 PLT 表 应用代码 首次调用 malloc() 后续调用 malloc() 1. call malloc@plt 2. jmp *malloc@got 3. 返回 PLT 下一条指令(首次未解析) 4. 调用 _dl_runtime_resolve 5. 查找 malloc 真实地址 6. 更新 malloc@got 7. 跳转到真实 malloc 8. 返回结果 9. call malloc@plt 10. jmp *malloc@got 11. 直接跳转到真实 malloc(已解析) 12. 返回结果

PLT 表结构

PLT 表由多个条目组成,每个条目对应一个外部函数:

asm 复制代码
; PLT[0] - 特殊条目,用于调用动态链接器
.plt:
    push   [GOT+8]        ; 压入 link_map
    jmp    [GOT+16]       ; 跳转到 _dl_runtime_resolve

; PLT[1] - malloc 的 PLT 条目
malloc@plt:
    jmp    [malloc@got]   ; 跳转到 GOT 中的地址
    push   0              ; 重定位索引
    jmp    .plt           ; 跳转到 PLT[0]

; PLT[2] - free 的 PLT 条目  
free@plt:
    jmp    [free@got]
    push   1
    jmp    .plt

GOT 表结构

GOT 表存储外部符号的实际地址:

复制代码
GOT[0]: .dynamic 段地址
GOT[1]: link_map 结构指针
GOT[2]: _dl_runtime_resolve 地址
GOT[3]: malloc 的实际地址(初始指向 PLT[1] 的第二条指令)
GOT[4]: free 的实际地址
...

延迟绑定的优势

  1. 启动速度快: 不需要在程序启动时解析所有符号
  2. 内存节省: 未使用的函数不会被解析
  3. 按需加载: 只解析实际调用的函数

inject_hook 实现原理

核心思想

inject_hook.cpp 通过修改 GOT 表中的函数地址,将对 mallocfree 等函数的调用重定向到我们的 Hook 函数:
Hook 后
Hook 前
call malloc@plt
jmp *GOT
原始地址
修改后地址
调用原始
记录追踪
应用代码
PLT 表
GOT 表
libc malloc
GOT 表
hooked_malloc
leak_detector

Hook 流程





程序启动
leak_detector_inject 被调用
dl_iterate_phdr 遍历所有加载的库
查找 PT_DYNAMIC 段
解析 Dynamic Section
获取 SYMTAB, STRTAB, JMPREL
遍历重定位表
符号名匹配?
修改 GOT 表项
下一个符号
遍历完成?
Hook 安装完成

关键技术点

  1. dl_iterate_phdr: 遍历所有已加载的共享库
  2. 内存权限修改 : 使用 mprotect 临时修改 GOT 表的写权限
  3. 符号匹配: 通过符号表和字符串表定位目标函数
  4. 地址计算: 处理 ASLR (地址空间随机化) 的基址偏移

代码逐行解析

1. 平台适配宏定义

cpp 复制代码
#if __SIZEOF_POINTER__ == 8
#define ElfW(type) Elf64_##type
#define ELF_R_SYM(i) ELF64_R_SYM(i)
#else
#define ElfW(type) Elf32_##type
#define ELF_R_SYM(i) ELF32_R_SYM(i)
#endif

解析:

  • 根据指针大小自动选择 32 位或 64 位 ELF 结构
  • ElfW(Dyn) 会展开为 Elf64_DynElf32_Dyn
  • ELF_R_SYM(i) 从重定位信息中提取符号索引

为什么需要这个?

  • 同一份代码可以在 32 位和 64 位系统上编译
  • ELF 格式在不同架构上有细微差异

2. Hook 模板设计

cpp 复制代码
template<typename FuncPtr>
struct Hook {
    const char* name;      // 函数名(如 "malloc")
    FuncPtr original;      // 原始函数指针
    FuncPtr hook_func;     // Hook 函数指针
    
    Hook(const char* n, FuncPtr orig, FuncPtr hook) 
        : name(n), original(orig), hook_func(hook) {}
};

设计亮点:

  • 使用模板支持不同函数签名
  • 封装了 Hook 的三要素:名称、原始函数、Hook 函数
  • 便于统一管理多个 Hook

3. 原始函数指针保存

cpp 复制代码
void* (*real_malloc)(size_t) = ::malloc;
void (*real_free)(void*) = ::free;
void* (*real_calloc)(size_t, size_t) = ::calloc;
void* (*real_realloc)(void*, size_t) = ::realloc;
int (*real_posix_memalign)(void**, size_t, size_t) = ::posix_memalign;

关键点:

  • 在 Hook 安装前,先保存原始函数指针
  • 使用 ::malloc 确保调用全局命名空间的函数
  • 这些指针在 Hook 函数中用于调用真实的内存分配函数

为什么要保存?

  • Hook 函数需要调用原始函数完成实际工作
  • 避免无限递归(Hook 函数调用自己)

4. Hook 函数实现

cpp 复制代码
void* hooked_malloc(size_t size) {
    void* ptr = real_malloc(size);              // 1. 调用真实 malloc
    leak_detector_track_malloc(ptr, size);      // 2. 记录分配
    return ptr;                                  // 3. 返回指针
}

执行流程:
leak_detector_track_malloc real_malloc (libc) hooked_malloc 应用代码 leak_detector_track_malloc real_malloc (libc) hooked_malloc 应用代码 记录 ptr ->> Allocation{size, backtrace} malloc(size) real_malloc(size) ptr track_malloc(ptr, size) ptr

realloc 的特殊处理:

cpp 复制代码
void* hooked_realloc(void* ptr, size_t size) {
    void* new_ptr = real_realloc(ptr, size);
    leak_detector_track_realloc(ptr, new_ptr, size);  // 处理地址变化
    return new_ptr;
}

realloc 可能返回新地址,需要同时删除旧记录、添加新记录。

5. GOT 表修改核心逻辑

cpp 复制代码
template<typename FuncPtr>
bool try_hook_symbol(const char* symname, ElfW(Addr) addr, 
                     Hook<FuncPtr>& hook, bool restore) {
    // 步骤 1: 检查符号名是否匹配
    if (strcmp(symname, hook.name) != 0) {
        return false;
    }
    
    // 步骤 2: 计算 GOT 表项所在内存页
    void* page = reinterpret_cast<void*>(addr & ~(getpagesize() - 1));
    
    // 步骤 3: 修改内存页权限为可写
    if (mprotect(page, getpagesize(), PROT_READ | PROT_WRITE) != 0) {
        return false;
    }
    
    // 步骤 4: 修改 GOT 表项
    FuncPtr* func_ptr = reinterpret_cast<FuncPtr*>(addr);
    if (restore) {
        *func_ptr = hook.original;   // 恢复原始函数
    } else {
        *func_ptr = hook.hook_func;  // 安装 Hook 函数
    }
    
    // 步骤 5: 恢复内存页权限
    mprotect(page, getpagesize(), PROT_READ);
    return true;
}

内存页对齐计算详解:

复制代码
addr = 0x7f1234567890  (GOT 表项地址)
pagesize = 0x1000      (4096 字节)

~(pagesize - 1) = ~0xFFF = 0xFFFFFFFFFFFFF000

page = addr & ~(pagesize - 1)
     = 0x7f1234567890 & 0xFFFFFFFFFFFFF000
     = 0x7f1234567000  (页对齐地址)

为什么需要 mprotect?

GOT 表在程序加载后通常是只读的(RELRO 保护),直接写入会触发段错误。mprotect 临时赋予写权限,修改完成后立即恢复,最小化安全风险。
GOT 初始状态 (PROT_READ)
mprotect(PROT_READ|PROT_WRITE)
写入 Hook 函数地址
mprotect(PROT_READ)
Hook 安装完成
ReadOnly
ReadWrite
Modified

6. 重定位表遍历

cpp 复制代码
template<typename RelaType>
void process_rela_table(const RelaType* rela_start, const RelaType* rela_end,
                       const ElfW(Sym)* sym_start, size_t num_syms,
                       const char* str_start, size_t num_strs,
                       ElfW(Addr) base, bool restore) {
    for (const RelaType* rela = rela_start; rela < rela_end; ++rela) {
        // 从 r_info 提取符号索引
        auto sym_index = ELF_R_SYM(rela->r_info);
        if (sym_index >= num_syms) continue;
        
        // 从符号表获取符号名在字符串表中的偏移
        auto str_index = sym_start[sym_index].st_name;
        if (str_index >= num_strs) continue;
        
        // 获取符号名字符串
        const char* symname = str_start + str_index;
        
        // 计算 GOT 表项的实际地址(考虑 ASLR 基址偏移)
        ElfW(Addr) addr = rela->r_offset + base;
        
        apply_hooks(symname, addr, restore);
    }
}

符号解析链路:
ELF_R_SYM
sym_start索引
st_name 偏移
str_start + offset

  • base
    匹配
    rela->r_info
    sym_index
    Elf64_Sym
    str_index
    符号名字符串
    rela->r_offset
    GOT 表项地址
    strcmp 匹配?
    修改 GOT 表项

ASLR 基址偏移的重要性:

由于 ASLR (Address Space Layout Randomization),每次程序运行时共享库的加载地址都不同。rela->r_offset 是相对于库基址的偏移,必须加上 base(实际加载基址)才能得到真实的 GOT 表项地址。

复制代码
# 没有 ASLR 时(理想情况)
GOT 地址 = r_offset = 0x601020

# 有 ASLR 时(实际情况)
base     = 0x7f1234560000  (随机基址)
r_offset = 0x1020          (相对偏移)
GOT 地址 = base + r_offset = 0x7f1234561020

7. Dynamic Section 解析

cpp 复制代码
void process_dynamic_section(const ElfW(Dyn)* dyn, ElfW(Addr) base, bool restore) {
    const ElfW(Sym)* symtab = nullptr;
    const char* strtab = nullptr;
    const ElfW(Rela)* rela = nullptr;
    const ElfW(Rela)* jmprel = nullptr;
    // ...
    
    // 遍历动态段,提取关键信息
    for (; dyn->d_tag != DT_NULL; ++dyn) {
        switch (dyn->d_tag) {
            case DT_SYMTAB:  symtab = ...; break;  // 符号表
            case DT_STRTAB:  strtab = ...; break;  // 字符串表
            case DT_RELA:    rela   = ...; break;  // 数据重定位表
            case DT_JMPREL:  jmprel = ...; break;  // PLT 重定位表
            // ...
        }
    }
    
    // 处理两种重定位表
    if (rela && rela_size > 0)     process_rela_table(...);  // .rela.dyn
    if (jmprel && jmprel_size > 0) process_rela_table(...);  // .rela.plt
}

为什么要处理两个重定位表?

重定位表 对应 Section 包含内容
DT_RELA .rela.dyn 数据符号重定位(全局变量等)
DT_JMPREL .rela.plt 函数调用重定位(PLT 条目)

malloc 等函数通过 PLT 调用,其 GOT 条目在 .rela.plt 中。但某些情况下(如直接函数指针赋值),也可能出现在 .rela.dyn 中,所以两个都要处理。

8. dl_iterate_phdr 遍历所有库

cpp 复制代码
int iterate_callback(struct dl_phdr_info* info, size_t size, void* data) {
    bool restore = (data != nullptr);
    
    // 跳过自身(避免 Hook 自己的 Hook 函数)
    if (strstr(info->dlpi_name, "leak_detector_inject.so")) {
        return 0;
    }
    
    // 跳过动态链接器本身
    if (strstr(info->dlpi_name, "/ld-linux") || 
        strstr(info->dlpi_name, "linux-vdso")) {
        return 0;
    }
    
    // 遍历程序头,找到 PT_DYNAMIC 段
    for (size_t i = 0; i < info->dlpi_phnum; ++i) {
        if (info->dlpi_phdr[i].p_type == PT_DYNAMIC) {
            const ElfW(Dyn)* dyn = reinterpret_cast<const ElfW(Dyn)*>(
                info->dlpi_phdr[i].p_vaddr + info->dlpi_addr);
            process_dynamic_section(dyn, info->dlpi_addr, restore);
        }
    }
    
    return 0;
}

dl_phdr_info 结构:

c 复制代码
struct dl_phdr_info {
    ElfW(Addr)        dlpi_addr;   // 库的加载基址
    const char*       dlpi_name;   // 库的路径名
    const ElfW(Phdr)* dlpi_phdr;   // 程序头数组
    ElfW(Half)        dlpi_phnum;  // 程序头数量
};

为什么要跳过自身?

如果 Hook 了自身库中的 malloc,当 Hook 函数内部调用 malloc 时会再次触发 Hook,导致无限递归。

为什么要跳过 ld-linux 和 vdso?

  • ld-linux.so: 动态链接器本身,修改它的 GOT 可能破坏链接器功能
  • linux-vdso.so: 内核虚拟动态共享对象,不是真实文件,无法修改

9. 初始化与清理

cpp 复制代码
// 安装 Hook
void install_hooks() {
    dl_iterate_phdr(iterate_callback, nullptr);  // data=null 表示安装
}

// 卸载 Hook
void restore_hooks() {
    void* restore_flag = reinterpret_cast<void*>(1);
    dl_iterate_phdr(iterate_callback, restore_flag);  // data!=null 表示恢复
}

巧妙的 data 参数复用:

  • data == nullptr → 安装 Hook
  • data != nullptr → 恢复原始函数

这避免了为安装/卸载分别写两个回调函数。

10. 两种注入模式

cpp 复制代码
// 模式一:GDB 注入(手动调用)
void leak_detector_inject(const char* output_file) {
    leak_detector_init(output_file ? output_file : "stderr");
    install_hooks();
}

// 模式二:LD_PRELOAD 自动注入
struct InjectInitializer {
    InjectInitializer() {
        const char* output_file = getenv("LEAK_DETECTOR_OUTPUT");
        if (output_file) {
            leak_detector_inject(output_file);
        }
    }
    
    ~InjectInitializer() {
        restore_hooks();
        leak_detector_shutdown();
    }
};

static InjectInitializer g_initializer;  // 全局对象,自动构造/析构

两种模式对比:

特性 GDB 注入 LD_PRELOAD
触发时机 手动调用 程序启动时自动
适用场景 运行中的进程 新启动的进程
需要重启
配置方式 GDB 命令 环境变量

实战案例与调试技巧

完整的 Hook 调用链



应用: malloc(100)
PLT: malloc@plt
GOT: *malloc@got
GOT 被 Hook?
hooked_malloc(100)
libc malloc(100)
real_malloc(100)
返回 ptr
leak_detector_track_malloc(ptr, 100)
记录到 allocations map
返回 ptr 给应用

使用 readelf 验证 GOT 表

bash 复制代码
# 查看 PLT 重定位表
readelf -r test_example | grep JUMP_SLOT

# 输出示例:
# Offset          Info           Type           Sym. Value    Sym. Name + Addend
# 000000601018  000200000007 R_X86_64_JUMP_SLOT 0000000000000000 malloc + 0
# 000000601020  000300000007 R_X86_64_JUMP_SLOT 0000000000000000 free + 0

# 查看 GOT 表内容(运行时用 GDB)
# (gdb) x/8gx 0x601018

使用 GDB 观察 Hook 效果

bash 复制代码
# 启动 GDB
gdb ./test_example

# 在 malloc 处设断点
(gdb) break malloc

# 运行程序
(gdb) run

# 查看 GOT 表中 malloc 的地址
(gdb) x/gx &malloc@got.plt

# 注入 Hook
(gdb) call leak_detector_inject("leak_report.txt")

# 再次查看 GOT 表,地址应该变了
(gdb) x/gx &malloc@got.plt

常见问题排查

问题 1: mprotect 失败

可能原因:

  • Full RELRO 保护(-Wl,-z,relro,-z,now)使 GOT 完全只读
  • 内核安全模块(SELinux/AppArmor)阻止了权限修改

解决方案:

bash 复制代码
# 检查是否启用了 Full RELRO
readelf -l binary | grep GNU_RELRO
checksec --file=binary

问题 2: Hook 未生效

可能原因:

  • 目标函数通过静态链接,没有 GOT 条目
  • 符号名不匹配(如 __malloc vs malloc

解决方案:

bash 复制代码
# 检查是否是动态链接
ldd ./binary

# 查看实际符号名
nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep malloc

问题 3: 递归调用导致栈溢出

leak_detector.cpp 中使用了 thread_local bool in_tracking 来防止递归:

cpp 复制代码
thread_local bool in_tracking = false;

struct RecursionGuard {
    RecursionGuard() : was_tracking(in_tracking) { in_tracking = true; }
    ~RecursionGuard() { in_tracking = was_tracking; }
    bool was_tracking;
};

当 Hook 函数内部调用 malloc 时,in_trackingtruetrack_malloc 直接返回,避免递归。


总结

整体架构回顾

Hook 函数链
inject_hook.cpp 核心流程
GOT 指向
dl_iterate_phdr
遍历所有已加载库
找到 PT_DYNAMIC 段
解析 Dynamic Section
获取 SYMTAB + STRTAB
获取 RELA + JMPREL 重定位表
遍历重定位表
符号名匹配
mprotect 开写权限
修改 GOT 表项
mprotect 恢复只读
hooked_malloc
real_malloc
leak_detector_track_malloc

技术要点总结

技术 作用 关键 API
ELF 解析 定位 GOT 表项 Elf64_Dyn, Elf64_Rela, Elf64_Sym
动态库遍历 找到所有需要 Hook 的库 dl_iterate_phdr
内存权限修改 允许写入只读 GOT mprotect
符号解析 通过名称找到 GOT 地址 字符串表 + 符号表
ASLR 处理 计算真实运行时地址 dlpi_addr + r_offset
递归保护 防止 Hook 函数无限递归 thread_local 标志

这套 GOT Hook 技术的精妙之处在于:它不修改任何代码段,只修改数据段中的函数指针,因此对程序的侵入性极小,且可以完全恢复。这也是为什么它比 inline hook(直接修改函数指令)更安全、更稳定的原因。

相关推荐
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(二十八)——图像格式与编解码:PNG/JPEG全掌握
开发语言·c++·windows·学习·图形渲染·win32
Ricky_Theseus2 小时前
C++静态库
开发语言·c++
洛水水2 小时前
【力扣100题】14.两数相加
c++·算法·leetcode
AlanW2 小时前
# Vcpkg使用总结2
c++
paeamecium2 小时前
【PAT甲级真题】- Insert or Merge (25)
数据结构·c++·算法·排序算法·pat考试·pat
程序员学习随笔3 小时前
C++条件变量(一):从轮询到唤醒 —— 条件变量的设计动机与基础用法
c++·线程并发
是娇娇公主~3 小时前
线程池:缓存线程池CachedThreadPool
c++·线程池
小欣加油3 小时前
leetcode 128 最长连续序列
c++·算法·leetcode·职场和发展
玖釉-3 小时前
图形 API 的前沿试车场:Vulkan 扩展体系深度解析与引擎架构实践
c++·架构·图形渲染