LD_PRELOAD是Linux动态链接器的环境变量,用于优先加载指定共享库以拦截动态链接函数;它可拦截malloc、open、read、printf等PLT调用的libc函数,但无法拦截内联函数(如优化后的strlen)或启动期函数(如__libc_start_main)。

LD_PRELOAD 是什么,它能拦截哪些函数
LD_PRELOAD 是 Linux 动态链接器(ld-linux.so)的一个环境变量,它让系统在加载主程序依赖的共享库之前,先加载你指定的共享库。只要这个预加载的库导出了与目标函数同名、同签名的符号,运行时就会优先使用你的实现——这就是“劫持”的本质。
它只能拦截通过 PLT(Procedure Linkage Table)调用的动态链接函数,也就是那些在 libc 或其他 .so 中定义、且未被编译器内联或静态链接的函数。像 malloc、open、read、printf 这类标准 C 库函数基本都符合;但 strlen 在某些优化级别下可能被内联,__libc_start_main 这类启动期函数则无法用 LD_PRELOAD 直接覆盖。
- 必须是动态链接的可执行文件(
file ./a.out输出含 “dynamically linked”) - 不能拦截 static inline 函数或编译时已确定地址的调用
- 不能劫持自身所在库中定义的函数(除非用 dlsym(RTLD_NEXT, ...) 手动跳转)
写一个能被 LD_PRELOAD 加载的拦截库
关键不是“重写所有逻辑”,而是“导出同名函数 + 正确调用原函数”。不调用原实现就容易导致程序崩溃(比如拦截 malloc 却不分配内存)。
最安全的方式是用 dlsym(RTLD_NEXT, "func_name") 获取原始函数地址。RTLD_NEXT 表示“在当前库之后的下一个匹配符号”,这能避开自循环调用。
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
// 声明原函数类型,必须和 libc 一致
static ssize_t (*real_read)(int fd, void *buf, size_t count) = NULL;
ssize_t read(int fd, void *buf, size_t count) {
if (!real_read) {
real_read = (ssize_t (*)(int, void*, size_t)) dlsym(RTLD_NEXT, "read");
}
// 可加日志、过滤、修改参数等
fprintf(stderr, "[intercept] read(%d, %p, %zu)\n", fd, buf, count);
return real_read(fd, buf, count);
}</stdio.h></dlfcn.h>
- 必须加
#define _GNU_SOURCE,否则RTLD_NEXT不可见 - 函数签名要完全一致(包括 const/volatile、返回值、参数类型),否则 ABI 错误会导致段错误
- 不要在 init 函数里提前调用
dlsym,因为此时其他库可能还没加载完;延迟到第一次调用时解析更稳妥
编译、加载与验证的完整流程
编译时不能加 -pie 或 -fPIE(除非你明确处理位置无关代码),但现代发行版默认启用 PIE,所以最好显式关掉:gcc -shared -fPIC -o libintercept.so intercept.c -ldl。
加载方式很简单:
LD_PRELOAD=./libintercept.so ./your_program
验证是否生效,看输出或用 strace -e trace=read ./your_program 对比前后行为;也可以在拦截函数里写入日志到文件(注意避免递归调用 write 或 fputs 导致死锁)。
- 如果提示
cannot open shared object file,检查路径是否正确、是否漏了./(LD_PRELOAD 不搜索当前目录) - 如果程序直接 segfault,大概率是函数签名不匹配,或在
dlsym返回 NULL 后没判空就调用 - 用
ldd ./your_program确认它是动态链接的;用objdump -T ./your_program | grep read看是否真有 PLT 条目
绕过拦截、防止被劫持的常见手段
不是所有程序都欢迎被 LD_PRELOAD 劫持。一些敏感程序(如 sudo、ssh)会主动清空环境变量或调用 prctl(PR_SET_NO_NEW_PRIVS) 后禁用 LD_PRELOAD。
更隐蔽的对抗方式包括:用 setuid 程序在启动时自动 unset LD_PRELOAD;或者用 AT_SECURE 标志(由内核设置)让动态链接器忽略该变量。
如果你控制目标程序源码,可用以下方式规避被劫持:
- 编译时加
-Wl,-z,noexecstack,-z,relro,-z,now并启用hardening,虽不直接禁用 LD_PRELOAD,但提高利用门槛 - 对关键函数用
__attribute__((visibility("hidden")))或链接脚本隐藏符号(仅影响你自己写的库) - 用
syscall(SYS_read, ...)绕过 libc 封装(但失去 portability 和缓冲区优化)
真正难搞的是那些既没 setuid 又没加固、还重度依赖 libc 的服务进程——它们最容易成为 LD_PRELOAD 劫持的目标,也最容易因拦截逻辑缺陷而崩掉。写拦截库时,别只想着“我能改什么”,得多想“我改错后它会怎么挂”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











