inject_hook 深度技术解析:从 ELF 到 GOT/PLT Hook 的完整实现
目录
- 引言
- [ELF 文件格式深度剖析](#ELF 文件格式深度剖析)
- 动态链接原理
- [PLT 与 GOT 机制详解](#PLT 与 GOT 机制详解)
- [inject_hook 实现原理](#inject_hook 实现原理)
- 代码逐行解析
- 实战案例与调试技巧
- 总结
引言
在 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 的实际地址
...
延迟绑定的优势
- 启动速度快: 不需要在程序启动时解析所有符号
- 内存节省: 未使用的函数不会被解析
- 按需加载: 只解析实际调用的函数
inject_hook 实现原理
核心思想
inject_hook.cpp 通过修改 GOT 表中的函数地址,将对 malloc、free 等函数的调用重定向到我们的 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 安装完成
关键技术点
- dl_iterate_phdr: 遍历所有已加载的共享库
- 内存权限修改 : 使用
mprotect临时修改 GOT 表的写权限 - 符号匹配: 通过符号表和字符串表定位目标函数
- 地址计算: 处理 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_Dyn或Elf32_DynELF_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→ 安装 Hookdata != 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 条目
- 符号名不匹配(如
__mallocvsmalloc)
解决方案:
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_tracking 为 true,track_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(直接修改函数指令)更安全、更稳定的原因。