LD_PRELOAD 劫持专题——从原理到绕过 disable_functions
前言
LD_PRELOAD 是 Linux 动态链接器提供的一个环境变量,允许用户在程序运行前优先加载指定的共享库(.so 文件)。这个特性本是用于调试和性能分析(如 malloc 调试、内存检测),但在攻击场景中,它可以劫持程序调用的库函数,注入恶意代码。
在 CTF 和渗透测试中,LD_PRELOAD 最常见的用途是**绕过 PHP 的 disable_functions**——当 system()、exec() 等危险函数被禁用时,通过劫持 mail() 等函数底层调用的 C 库函数来执行任意命令。
本文从动态链接基础讲起,逐步深入到劫持自定义程序、劫持系统命令、绕过 disable_functions 的多种姿势,以及检测和防御措施。
一、LD_PRELOAD 基础
1.1 什么是动态链接
Linux 程序在运行时需要调用各种库函数(如 printf、rand、strncmp)。这些函数有两种链接方式:
| 链接方式 | 说明 | 特点 |
|---|---|---|
| 静态链接 | 编译时将库代码直接嵌入可执行文件 | 文件大、不依赖外部库、无法劫持 |
| 动态链接 | 运行时从 .so 文件中加载库函数 |
文件小、共享内存、可通过 LD_PRELOAD 劫持 |
绝大多数 Linux 程序使用动态链接。
1.2 LD_PRELOAD 原理
动态链接器在加载程序时,会按以下优先级顺序查找函数符号:
1 | LD_PRELOAD 指定的 .so 文件 ← 最高优先级 |
因此,只要将恶意 .so 文件写入 LD_PRELOAD,其中同名函数就会覆盖系统库中的原始函数,实现劫持。
核心前提:
- 恶意
.so中的函数名必须与目标程序调用的函数名完全一致 - 函数的参数类型和返回值类型也需要匹配(可从编译报错或
man手册中获取) LD_PRELOAD只在进程启动时加载一次——已运行的进程不受影响
1.3 基本操作
1 | # 设置 LD_PRELOAD |
注意:
export设置的环境变量只对当前 shell 及其子进程生效,新开终端不会继承。
二、劫持自定义程序
2.1 编译目标程序
先从最简单的场景开始——劫持自己编译的程序。
1 | # 假设有一个 random.c,内部调用了 rand() 函数 |
2.2 编写恶意共享库
写一个 evil.c,实现同名函数 rand():
1 |
|
编译为共享库:
1 | gcc -shared -fPIC evil.c -o evil.so |
| 编译选项 | 含义 |
|---|---|
-shared |
生成共享库(.so)而非可执行文件 |
-fPIC |
生成位置无关代码(Position Independent Code),使 .so 可被加载到任意内存地址 |
2.3 验证劫持
1 | export LD_PRELOAD=./evil.so |
验证加载顺序:
1 | ldd random |
注意: 恶意
.so中不能有main函数——它是库,不是独立程序。
三、劫持系统命令
3.1 分析目标命令
以 ls 为例,先查看它调用了哪些库函数:
1 | readelf -Ws /usr/bin/ls |
回显示例:
1 | fflush_unlocked@GLIBC_2.2.5 |
选择一个可劫持的函数(如 strncmp),确认其函数签名:
1 | man strncmp |
技巧: 如果不确定参数类型,直接编译一个声明了该函数的
.c文件,GCC 的报错信息会显示系统头文件中该函数的正确签名。
3.2 编写劫持代码
1 |
|
1 | gcc -shared -fPIC evil.c -o evil.so |
为什么要
unsetenv("LD_PRELOAD")? 被劫持的命令可能启动子进程,子进程会再次加载LD_PRELOAD指向的.so,导致payload()被反复执行甚至死循环。unsetenv确保只在当前进程中生效一次。
四、绕过 PHP disable_functions
这是 LD_PRELOAD 在 CTF 中最核心的应用场景。当 PHP 配置中禁用了 system、exec、passthru 等危险函数时,可以利用 mail() 或 error_log() 函数启动新进程,再通过 LD_PRELOAD 劫持来执行命令。
4.1 基本原理与前提
核心思路:
1 | PHP putenv("LD_PRELOAD=evil.so") |
前提条件:
- PHP 的
putenv()未被禁用 mail()或error_log()至少有一个可用- 能够将
.so文件上传到目标服务器(/tmp或/var/tmp通常可写)
4.2 利用 mail() 劫持
Step 1:跟踪 mail() 的系统调用
1 |
|
1 | strace -f php mail.php 2>&1 | grep -A2 -B2 execve |
从输出中找到 mail() 调用的 sendmail 进程所加载的库函数(例如 getuid)。
Step 2:编写劫持 .so
1 |
|
1 | gcc -shared -fPIC evil.c -o getuid.so |
Step 3:PHP 中加载
1 |
|
访问该 PHP 页面,VPS 上即可收到反弹 shell。
error_log()同理:error_log("", 1, "", "")也会触发 sendmail,用法与mail()完全一致。
4.3 利用 __attribute__((constructor)) —— 更优雅的方式
C 语言提供了 __attribute__((constructor)) 修饰符,被修饰的函数会在 main() 之前自动执行。一旦 .so 被加载,无需等待程序调用特定函数,构造函数中的代码立即运行。
这意味着:不需要分析 sendmail 具体调用了哪个库函数,只要 .so 被加载,payload 就会触发。
1 |
|
优势对比:
| 方式 | 需要分析目标函数 | 通用性 | 隐蔽性 |
|---|---|---|---|
劫持具体函数(如 getuid) |
需要 | 低(每个程序调用的函数不同) | 较高 |
__attribute__((constructor)) |
不需要 | 高(加载即触发) | 一般 |
4.4 完整版:环境变量传参 + 命令回显
以下是一个生产级别的 bypass 方案,通过环境变量传递命令,并将结果写入文件供 PHP 读取:
evil.c:
1 |
|
1 | gcc -shared -fPIC evil.c -o bypass_disablefunc.so |
PHP 端:
1 |
|
执行流程:
1 | HTTP 请求(cmd=id&outpath=/tmp/out&sopath=/tmp/evil.so) |
4.5 为什么要找”能启动新进程”的函数
LD_PRELOAD 只在进程启动时被动态链接器读取。在 PHP-FPM 或 mod_php 模式下,PHP 进程已经在运行,设置 LD_PRELOAD 不会影响它自身。
因此必须找到 PHP 中能够fork/exec 新进程的函数,常见的有:
| 函数 | 底层调用 | 是否受 disable_functions 影响 |
|---|---|---|
mail() |
调用 sendmail | 否(mail 函数本身通常不被禁用) |
error_log() |
调用 sendmail | 否 |
mb_send_mail() |
调用 sendmail | 否 |
imap_open() |
调用 imap 客户端 | 否 |
五、自动化工具
手动编写 .so 和 PHP 比较繁琐,社区已有成熟的自动化工具:
5.1 Chankro
1 | # 生成 bypass disable_functions 的 so + php |
5.2 bypass_disablefunc.php(蚁剑/AntSword 插件]
蚁剑的「绕过 disable_functions」插件内置了多种模式:LD_PRELOAD、FastCGI/PHP-FPM、Apache Mod CGI、ImageMagick 等,自动探测可用方法并生成 payload。
六、检测与排查
6.1 检查当前 LD_PRELOAD
1 | # 查看环境变量 |
6.2 检查可疑 .so 文件
1 | # 查找非系统路径下的 .so 文件 |
6.3 审计 PHP 配置
1 | # 检查 putenv 是否被禁用 |
七、防御措施
| 层面 | 措施 | 说明 |
|---|---|---|
| PHP 配置 | disable_functions 中加入 putenv |
阻断 LD_PRELOAD 的设置入口 |
| PHP 配置 | 使用 open_basedir |
限制 PHP 只能访问指定目录,防止读写 /tmp 下的 .so |
| 系统层面 | /tmp 挂载 noexec |
即使 .so 落地也无法被加载执行 |
| SELinux / AppArmor | 限制 sendmail 的可加载库路径 | 只允许从 /usr/lib 等系统路径加载 |
| 内核参数 | kernel.yama.ptrace_scope |
限制 strace 等工具被用于分析进程 |
| 监控 | 告警非系统路径下的 .so 文件 |
/tmp、/dev/shm 等目录出现 .so 通常是攻击迹象 |
| 最小权限 | PHP 进程以低权限用户运行 | 即使拿到 shell,权限也受限 |
防御示例:/tmp noexec 挂载
1 | # /etc/fstab |
防御示例:SELinux 策略
1 | # 限制 httpd 子进程的库加载路径 |
八、小结
LD_PRELOAD 劫持的核心链:
1 | 上传 .so → putenv 设置 LD_PRELOAD → 触发新进程 → .so 被加载 → 恶意代码执行 |
关键记忆点:
LD_PRELOAD只在进程启动时生效——必须找到能启动新进程的 PHP 函数__attribute__((constructor))让 payload 在.so加载时自动执行,无需分析程序调用了哪个函数unsetenv("LD_PRELOAD")或手动置空environ数组,防止子进程反复触发- 通过环境变量(
EVIL_CMDLINE)传参是更灵活的方式,避免硬编码命令