LD_PRELOAD 劫持(LD_PRELOAD Hooking)是 Linux/Unix 系统中一种非常著名且强大的技术。它利用了动态链接器(Dynamic Linker/Loader)的工作机制,允许用户在程序加载前加载自定义的共享库,从而覆盖(Override)系统标准库中的函数。

简单来说,就是“狸猫换太子”

以下是关于 LD_PRELOAD 劫持的详细讲解,包括原理、实战示例、用途以及防御方法。


核心原理

在 Linux 中,运行动态链接的程序时,系统需要由动态链接器(通常是 /lib64/ld-linux-x86-64.so.2)将程序所需的共享库(如 libc.so)加载到内存中。

加载库的顺序通常遵循以下规则:

  1. LD_PRELOAD 环境变量指定的路径(优先级最高)。
  2. LD_LIBRARY_PATH 环境变量指定的路径。
  3. /etc/ld.so.conf 配置文件中的路径。
  4. 默认系统路径(如 /lib, /usr/lib)。

劫持的关键点:
由于 LD_PRELOAD 的优先级最高,如果你编写了一个与标准库函数(例如 printf, strcmp, malloc)同名的函数,并将其编译为共享库(.so 文件),然后在运行目标程序前设置 LD_PRELOAD,链接器就会优先使用你的函数,而不是系统原来的函数。

实战演示:破解密码验证

为了演示,我们写一个简单的 C 程序,它要求用户输入密码,并使用 strcmp 进行比对。

第一步:受害者程序 (victim.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv) {
if (argc < 2) {
printf("Usage: %s <password>\n", argv[0]);
return 1;
}

// 假设正确密码是 "secret123"
if (strcmp(argv[1], "secret123") == 0) {
printf("Access Granted! (密码正确)\n");
} else {
printf("Access Denied! (密码错误)\n");
}
return 0;
}

编译并运行:

1
2
3
gcc victim.c -o victim
./victim hello # 输出: Access Denied!
./victim secret123 # 输出: Access Granted!

第二步:攻击者代码 (hack.c)

我们要劫持 strcmp 函数,让它永远返回 0(表示两个字符串相等),无论输入什么。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>

// 定义与标准库一样的函数签名
int strcmp(const char *s1, const char *s2) {
// 可以在这里打印日志,或者做坏事
printf("[+] strcmp 被劫持了! 你的输入: %s, 目标: %s\n", s1, s2);

// 永远返回 0,欺骗程序说密码匹配
return 0;
}

第三步:编译攻击库

我们需要将其编译为共享对象(Shared Object, .so):

1
gcc -shared -fPIC hack.c -o hack.so
  • -shared: 生成共享库。
  • -fPIC: 生成位置无关代码(Position Independent Code)。

第四步:实施劫持

使用 LD_PRELOAD 加载我们的 hack.so 运行受害者程序:

1
LD_PRELOAD=./hack.so ./victim wrongpassword

输出结果:

1
2
[+] strcmp 被劫持了! 你的输入: wrongpassword, 目标: secret123
Access Granted! (密码正确)

即使输入了错误的密码,程序也认为密码正确,因为我们将判断逻辑劫持了。

如何调用原函数?

在很多场景下(如编写外挂、监控软件),我们不仅想拦截函数,还想在做完自己的操作后,继续调用系统的原始函数。这需要使用 dlsym

例如,劫持 puts 但仍让它输出内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h> // 包含 dlsym

// 函数指针类型
typedef int (*orig_puts_f_type)(const char *s);

int puts(const char *s) {
// 1. 做我们的坏事
printf("[HOOK] 拦截到了输出内容: %s\n", s);

// 2. 获取原本的 puts 函数地址
// RTLD_NEXT 告诉链接器:去"下一个"库里找这个符号,不要找我
orig_puts_f_type orig_puts;
orig_puts = (orig_puts_f_type)dlsym(RTLD_NEXT, "puts");

// 3. 调用原函数
return orig_puts(s);
}

编译时需要链接 dl 库:gcc -shared -fPIC hook.c -o hook.so -ldl

LD_PRELOAD 的应用场景

A. 正面用途 (White Hat / Ops)

  1. Hotfix (热修复): 线上服务某个库函数有 Bug,但源码丢失或无法立即重编译,可以写一个 LD_PRELOAD 补丁临时替换该函数。
  2. Mocking/Testing: 在单元测试中模拟系统调用(如模拟 time 返回固定时间,模拟 read 返回特定数据)。
  3. 性能分析: 替换 mallocfree 来追踪内存泄漏(例如 TCMallocjemalloc 有时就是这样注入的)。
  4. Fakeroot: Linux 下的 fakeroot 工具就是利用这个原理,劫持文件操作函数,让程序以为自己是 root 权限(常用于打包)。
  5. 加速器: 一些网络加速库通过劫持 socket 相关函数,将 TCP 调用改为用户态协议栈。

B. 负面用途 (Black Hat)

  1. 用户态 Rootkit:
    • 劫持 readdir:过滤掉含有特定名字的文件,隐藏木马文件。
    • 劫持 open:阻止查看特定目录(如 /proc 下的恶意进程信息),隐藏进程。
  1. 后门 (Backdoor):
    • 劫持 SSH 服务的加解密函数或验证函数,记录管理员密码。
  1. 破解软件:
    • 劫持验证 License 的函数(如检测加密狗或本地时间的函数),绕过商业软件验证。

局限性与防御

虽然 LD_PRELOAD 很强大,但它不是万能的。

局限性

  1. 静态链接程序无效: 如果程序编译时使用了 -static(静态链接),所有库函数都打包进了一个二进制文件,没有动态链接的过程,LD_PRELOAD 自然失效。
  2. Go 语言程序: Go 程序(默认配置下)通常不使用 libc,而是直接调用 syscall,或者使用自己的链接机制,导致很难通过这种方式劫持。
  3. SUID/SGID 程序限制:
    • 这是 Linux 的安全机制。如果一个二进制文件设置了 SUID 位(如 sudo, passwd),为了防止普通用户通过 LD_PRELOAD 劫持 root 权限的程序来提权,动态链接器会忽略 LD_PRELOAD 变量,除非预加载的库位于系统信任的目录(如 /lib)且有相应的权限。

防御与检测

  1. 检查环境变量: 在服务器排查入侵时,检查 env 中是否有异常的 LD_PRELOAD
  2. 检查 /etc/ld.so.preload: 这是一个全局配置文件,里面列出的库会被所有程序预加载。这是 Rootkit 常驻的地点。
  3. 查看进程内存映射:
1
cat /proc/<PID>/maps | grep .so

如果在输出中看到了可疑路径的 .so 文件,可能就是被劫持了。

  1. 静态链接关键工具: 对于安全敏感的工具(如入侵检测 agent),尽可能采用静态链接,防止被轻易 Hook。

总结

LD_PRELOAD 是 Linux 下一把双刃剑。它既是开发者调试、修补、扩展功能的利器,也是黑客隐藏踪迹、窃取数据的常用手段。理解它的原理对于深入掌握 Linux 系统编程和安全攻防至关重要。