第46章:ELF文件修改实战:修改符号表、PLT/GOT Hook、LD_PRELOAD注入
说实话,ELF文件格式是逆向工程的必修课。你想想看,Linux下的可执行文件、动态库,哪个不是ELF?我最早接触ELF修改,是为了给一个闭源程序打补丁——那程序有个严重的内存泄漏,但厂商已经跑路了。没办法,只能自己动手。
这一章,咱们就聊聊三种常见的ELF修改技术:符号表修改、PLT/GOT Hook、以及LD_PRELOAD注入。这三种手段,说白了就是让你能在不修改源代码的情况下,改变程序的行为。
核心思路:ELF文件本质上是一堆结构体的组合。你只要搞懂了它的布局,修改起来就跟改JSON一样简单——当然,得小心别把结构搞坏了。
46.1 符号表修改:直接动刀子
符号表里存的是啥?函数名、变量名、以及它们在文件中的位置。我习惯把符号表想象成一本电话簿——你想找谁,翻到对应页码就行。
修改符号表,最常见的场景是:你想让程序调用你的函数,而不是原来的函数。比如,把malloc改成my_malloc。
实操步骤
- 找到符号表段:用
readelf -S查看ELF的段信息,找到.symtab或.dynsym段。 - 定位目标符号:用
readelf -s列出所有符号,找到你要改的那个。 - 修改符号名或地址:用十六进制编辑器(比如
hexedit)直接改。
# 查看符号表
readelf -s ./target_binary | grep malloc
# 输出示例
# 123: 0000000000404b20 16 FUNC GLOBAL DEFAULT 13 malloc
# 用hexedit修改符号名
hexedit ./target_binary
# 找到偏移量,把"malloc"改成"my_malloc"(注意长度要一致)
小技巧:改符号名时,新名字长度必须和原名字一样。如果不一样,你得调整字符串表,那工作量就大了。我一般用patchelf这个工具,省事不少。
46.2 PLT/GOT Hook:运行时拦截
PLT(过程链接表)和GOT(全局偏移表)是动态链接的核心机制。说白了,PLT是一层跳板,GOT是函数指针表。程序调用外部函数时,先跳到PLT,PLT再跳到GOT里存的地址。
Hook的原理很简单:把GOT里的地址改成你的函数地址。这样,程序一调用原函数,实际上跑的是你的代码。
注意:GOT Hook只对动态链接的函数有效。静态链接的函数,地址是写死的,你改GOT没用。
我常用的Hook方法
用LD_PRELOAD配合dlsym,是最优雅的方式。但如果你想直接改GOT,那就得用ptrace或者直接写内存。
// 一个简单的GOT Hook示例
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
// 原函数的函数指针
static int (*real_puts)(const char*) = NULL;
// 我们的Hook函数
int my_puts(const char* str) {
// 先调用原函数
if (!real_puts) {
real_puts = dlsym(RTLD_NEXT, "puts");
}
// 在输出前加点料
fprintf(stderr, "[Hook] puts called with: %s\n", str);
return real_puts(str);
}
// 编译成动态库
// gcc -shared -fPIC -o hook.so hook.c -ldl
嗯,这里要注意:RTLD_NEXT是GNU扩展,不是POSIX标准。但Linux下基本都能用。
46.3 LD_PRELOAD注入:最省事的Hook
LD_PRELOAD是Linux环境变量,它告诉动态链接器:加载目标程序之前,先加载我指定的动态库。这样,你的库里的符号会覆盖掉原来的符号。
说白了,这就是个"狸猫换太子"的把戏。你写个动态库,里面定义和原函数同名的函数,然后设置LD_PRELOAD指向你的库。程序一跑,调的就是你的函数。
实战案例:拦截文件操作
我曾经用这个技术给一个老旧程序加日志功能。那程序没有日志,出问题根本不知道它在干啥。我写了个库,Hook了open、read、write,把所有文件操作都记录下来。
// hook_file.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdarg.h>
typedef int (*orig_open_t)(const char*, int, ...);
static orig_open_t real_open = NULL;
int open(const char* pathname, int flags, ...) {
if (!real_open) {
real_open = (orig_open_t)dlsym(RTLD_NEXT, "open");
}
// 记录文件打开操作
fprintf(stderr, "[FileHook] open: %s\n", pathname);
// 处理可变参数
mode_t mode = 0;
if (flags & O_CREAT) {
va_list args;
va_start(args, flags);
mode = va_arg(args, mode_t);
va_end(args);
}
return real_open(pathname, flags, mode);
}
// 编译
// gcc -shared -fPIC -o hook_file.so hook_file.c -ldl
// 使用
// LD_PRELOAD=./hook_file.so ./target_program
避坑指南:我曾经在Hookfopen时踩过坑——fopen内部会调用open,如果你同时Hook了这两个函数,就会造成递归调用。解决办法是:在Hook函数里用dlsym(RTLD_NEXT, ...)获取原函数地址,而不是直接调用原函数名。
46.4 三种技术的对比
| 技术 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 符号表修改 | 直接改ELF文件中的符号名或地址 | 永久生效,不需要额外库 | 需要修改文件,签名会失效 |
| PLT/GOT Hook | 修改GOT表中的函数指针 | 运行时生效,灵活 | 只对动态链接有效,实现稍复杂 |
| LD_PRELOAD注入 | 环境变量指定预加载库 | 最简单,不需要改文件 | 容易被检测,环境变量可被清除 |
46.5 知识体系图
下面这张图,帮你理清这三种技术的关系和适用场景。
46.6 实战中的坑与经验
我做了这么多年逆向,总结了几条血泪教训:
- 签名校验:很多商业程序会校验ELF文件的完整性。你改了符号表或GOT,程序可能直接崩溃。我遇到过一款游戏,它用CRC校验整个
.text段,我改了一个字节就闪退。解决办法是:找到校验函数,把它NOP掉。 - ASLR的影响:地址空间随机化会让GOT Hook变得麻烦。你写死的地址,下次运行可能就变了。我习惯用
/proc/<pid>/maps动态获取地址。 - 多线程安全:Hook函数里如果有全局变量,记得加锁。我早期写的Hook库,在多线程程序里经常死锁,后来加了
pthread_mutex才搞定。
重要提醒:修改ELF文件可能违反软件许可协议。我只建议在你有合法权限的情况下使用这些技术,比如调试自己的程序、或者分析开源软件。
好了,这一章的内容就这些。三种技术各有千秋,你根据实际场景选就行。我个人最常用的是LD_PRELOAD,因为它快、简单、不用改文件。但如果你需要永久修改,那就得动符号表或GOT了。