在现代 Linux 系统中,动态链接机制是共享库复用的基石,但同时也是攻击者实施代码注入、权限提升和持久化的重要攻击面。本文从动态链接器的工作流程出发,系统梳理了从环境变量劫持到 GOT/PLT 覆写的多层风险,并配套提供了一套可落地的 Bash 检测脚本,帮助安全运维人员快速排查二进制程序的依赖安全隐患。
一、动态链接器的工作流程与攻击面
当用户在 Linux 上执行一个动态链接的 ELF 二进制文件时,内核首先加载程序本身,随后将控制权交给动态链接器 ld.so(或 ld-linux.so)。链接器按照特定顺序解析依赖库,其搜索优先级如下:
LD_PRELOAD环境变量指定的库(除非程序具有 setuid/setgid 位,此时会被 glibc 的AT_SECURE机制自动清除)/etc/ld.so.preload系统级预加载配置文件(需要 root 权限修改,但一旦写入即可实现系统级持久化)RPATH和RUNPATH二进制文件中硬编码的路径(RPATH优先级高于LD_LIBRARY_PATH,而RUNPATH反之)LD_LIBRARY_PATH环境变量- 系统标准路径(
/lib、/usr/lib、/lib64、/usr/lib64等,由/etc/ld.so.conf定义)
flowchart TD
A[程序启动] --> B[内核加载 ELF]
B --> C[调用 ld.so 动态链接器]
C --> D{检查 /etc/ld.so.preload}
D -->|存在| E[预加载恶意库]
D -->|不存在| F{检查 LD_PRELOAD}
F -->|存在| G[加载攻击者库]
F -->|不存在| H{检查 RPATH/RUNPATH}
H -->|包含可写目录| I[加载恶意 .so]
H -->|安全| J{检查 LD_LIBRARY_PATH}
J -->|被篡改| K[加载伪造库]
J -->|正常| L[按标准路径加载]
L --> M[解析符号 → GOT/PLT]
M --> N[程序执行]
E --> N
G --> N
I --> N
K --> N
style E fill:#f96,stroke:#333
style G fill:#f96,stroke:#333
style I fill:#f96,stroke:#333
style K fill:#f96,stroke:#333
每一个解析节点都可能成为攻击者的突破口。以下分层次展开具体风险。
二、风险分层详解
2.1 环境变量层:LD_PRELOAD 与 LD_LIBRARY_PATH
LD_PRELOAD 允许用户在程序启动前强制加载指定的共享对象,其加载优先级甚至高于程序自身依赖的库。攻击者可以编写一个包含恶意代码的 .so 文件,通过劫持标准库函数(如 connect()、read()、getuid() 等)实现数据窃取、权限提升或后门植入。
一个典型的提权场景是:当 sudo -l 显示某用户可以以 root 身份运行特定命令,且该命令未被配置为清除 LD_PRELOAD 时,攻击者即可注入恶意库获取 root shell。
LD_LIBRARY_PATH 则通过篡改库搜索路径实现劫持。例如,攻击者将伪造的 libssl.so.3 放入 /tmp/evil/,设置 LD_LIBRARY_PATH=/tmp/evil,即可拦截目标程序的 TLS 会话密钥。
缓解措施:
- 对 setuid/setgid 程序,glibc 会自动忽略
LD_PRELOAD(AT_SECURE机制)。 - 通过 SELinux/AppArmor 策略限制不可信库的
mmap(PROT_EXEC)。 - 在容器中使用
readOnlyRootFilesystem: true防止/etc/ld.so.preload被篡改。
2.2 系统配置层:/etc/ld.so.preload
与 LD_PRELOAD 不同,/etc/ld.so.preload 是一个系统级配置文件,写入其中的库路径会被 所有 动态链接进程加载。攻击者在获取 root 权限后,将恶意库路径写入该文件,即可实现跨重启的持久化后门。
由于该文件需要 root 权限才能修改,其检测重点在于:
- 监控文件内容的非预期变更(通过
auditd或 AIDE)。 - 使用
chattr +i将其设为不可变。 - 定期比对文件哈希或内容签名。
2.3 二进制层:RPATH 与 RUNPATH
RPATH 和 RUNPATH 是在编译链接时写入 ELF 文件的动态段(Dynamic Section)的路径信息,用于指定程序查找依赖库的私有路径。两者的关键区别在于优先级:
RPATH:优先级最高,无法 被LD_LIBRARY_PATH覆盖。如果RPATH指向的目录是用户可写的(如/tmp、$ORIGIN/../lib),攻击者只需在该目录放置同名恶意库即可实现劫持。RUNPATH:优先级低于LD_LIBRARY_PATH,管理员可以通过设置环境变量来临时覆盖危险路径,但攻击者同样可以利用这一点注入恶意库。
此外,$ORIGIN 相对路径和空路径(::)会导致链接器搜索当前工作目录,进一步扩大了攻击面。
2.4 文件系统层:库文件权限与目录可写性
即使链接器按正确路径找到了库文件,如果该库文件本身权限过于宽松(如 chmod 777),或者所在目录对非特权用户可写,攻击者仍然可以:
- 直接替换库文件内容(如将
/usr/lib/libcustom.so替换为植入后门的版本)。 - 利用目录写权限创建同名库进行抢占。
对于 root 所有的系统库(如 libc.so.6),如果存在其他用户写权限(o+w),则任何本地用户都可以直接篡改核心系统库,造成全局性的权限提升风险。
2.5 运行时层:GOT/PLT 覆写
动态链接通过 全局偏移表(GOT) 和 过程链接表(PLT) 实现延迟绑定(Lazy Binding)。程序首次调用外部函数时,PLT 跳转到 GOT,由动态链接器解析真实地址并回填 GOT;后续调用直接通过 GOT 跳转。
由于 GOT 在默认情况下是可写的,攻击者若能通过缓冲区溢出、格式化字符串漏洞或任意地址写等手段修改 GOT 中的函数指针,即可将 printf() 的地址替换为 system() 的地址,实现非预期的代码执行。这种攻击被称为 GOT Poisoning。
缓解措施:
- Full RELRO:编译时添加
-Wl,-z,relro,-z,now,强制链接器在程序启动时解析所有符号并将整个 GOT 标记为只读。 - PIE + ASLR:配合地址空间布局随机化,增加攻击者定位 GOT 的难度。
三、检测脚本设计与实现
基于上述风险模型,我们设计了一套 Bash 检测脚本 check_lib.sh,覆盖文件系统层和二进制层的关键检查点。脚本核心逻辑如下:
flowchart LR
subgraph "攻击面层级"
A1[环境变量层<br/>LD_PRELOAD / LD_LIBRARY_PATH]
A2[系统配置层<br/>/etc/ld.so.preload]
A3[二进制层<br/>RPATH / RUNPATH]
A4[文件系统层<br/>库文件权限 / 目录可写性]
A5[运行时层<br/>GOT/PLT 覆写]
end
subgraph "检测脚本覆盖"
D1[✓ 检查库文件权限]
D2[✓ 检查 RPATH/RUNPATH]
D3[✓ 检查 libc 路径]
D4[✗ 环境变量审计]
D5[✗ GOT/PLT 完整性]
end
A1 -.-> D4
A2 -.-> D4
A3 -.-> D2
A4 -.-> D1
A5 -.-> D5
3.1 脚本功能模块
模块一:库文件权限审计
脚本通过 ldd 提取目标程序的所有依赖库路径,逐一检查:
- 权限位是否包含组写(
g+w)或其他用户写(o+w)。 - 对于 root 所有的库,若存在
o+w,则标记为 高危(存在本地提权风险)。 - 库文件是否真实存在(防止 broken dependency 被利用)。
模块二:RPATH / RUNPATH 配置检查
使用 readelf -d 提取 RPATH 和 RUNPATH:
- 检测是否包含相对路径(
./)或空路径(::),这些会导致链接器搜索当前目录。 - 检测路径是否指向用户可写目录(通过
[ -w "$path" ]判断),可写目录意味着攻击者可以放置同名库进行劫持。
模块三:libc 路径安全性验证
libc.so.6 是所有动态链接程序的核心依赖。脚本单独检查:
- 其路径是否位于系统标准目录(
/lib、/lib64、/usr/lib、/usr/lib64)。 - 若 libc 位于非标准路径(如
/opt/、/home/),则警告可能被替换。 - 检查 libc 自身的权限位,防止核心库被篡改。
3.2 完整脚本代码
#!/bin/bash
# 检查是否提供了目标程序路径
if [ $# -ne 1 ]; then
echo "用法: $0 <目标程序路径>"
echo "示例: $0 /usr/bin/tldr-hs"
exit 1
fi
TARGET="$1"
# 检查目标程序是否存在
if [ ! -f "$TARGET" ]; then
echo "错误: 目标程序 $TARGET 不存在!"
exit 1
fi
# 检查是否有ldd命令
if ! command -v ldd &> /dev/null; then
echo "错误: 未找到ldd命令,请安装glibc-utils或类似工具包"
exit 1
fi
# 检查是否有readelf命令
if ! command -v readelf &> /dev/null; then
echo "错误: 未找到readelf命令,请安装binutils工具包"
exit 1
fi
echo "===== 开始检测 $TARGET 的依赖库安全问题 ====="
echo
# 获取所有依赖库列表(排除虚拟动态共享对象vdso)
LIBS=$(ldd "$TARGET" | grep -v 'linux-vdso.so' | awk '{print $3}' | grep -v '^$')
# 检查每个依赖库的权限
echo "----- 1. 检查库文件权限 (RW权限漏洞) -----"
for lib in $LIBS; do
if [ -f "$lib" ]; then
# 检查文件权限是否过于宽松
perm=$(stat -c "%a" "$lib")
# 检查是否有组写或其他写权限
if [ $((perm & 022)) -ne 0 ]; then
echo "警告: 库文件 $lib 权限不安全 ($perm) - 包含组写或其他写权限"
fi
# 检查文件所有者是否为root但有其他写权限
owner=$(stat -c "%U" "$lib")
if [ "$owner" = "root" ] && [ $((perm & 002)) -ne 0 ]; then
echo "高危: root所有的库 $lib 允许其他用户写入 ($perm) - 存在提权风险"
fi
else
echo "警告: 依赖库 $lib 不存在或无法访问"
fi
done
echo
# 检查目标程序的RUNPATH和RPATH设置
echo "----- 2. 检查程序的RUNPATH/RPATH配置 -----"
runpath=$(readelf -d "$TARGET" 2>/dev/null | grep -i 'RUNPATH' | awk -F'[' '{print $2}' | awk -F']' '{print $1}')
rpath=$(readelf -d "$TARGET" 2>/dev/null | grep -i 'RPATH' | awk -F'[' '{print $2}' | awk -F']' '{print $1}')
if [ -n "$runpath" ]; then
echo "发现RUNPATH: $runpath"
# 检查是否包含相对路径或当前目录
if echo "$runpath" | grep -q '\./'; then
echo "警告: RUNPATH包含相对路径 './' - 可能导致库劫持风险"
fi
if echo "$runpath" | grep -q '::'; then
echo "警告: RUNPATH包含空路径 (::) - 可能引用当前目录"
fi
# 检查是否包含用户可写目录
IFS=':' read -ra paths <<< "$runpath"
for path in "${paths[@]}"; do
if [ -n "$path" ] && [ -d "$path" ] && [ -w "$path" ]; then
echo "警告: RUNPATH包含可写目录 $path - 存在库替换风险"
fi
done
fi
if [ -n "$rpath" ]; then
echo "发现RPATH: $rpath"
# 检查是否包含相对路径或当前目录
if echo "$rpath" | grep -q '\./'; then
echo "警告: RPATH包含相对路径 './' - 可能导致库劫持风险"
fi
if echo "$rpath" | grep -q '::'; then
echo "警告: RPATH包含空路径 (::) - 可能引用当前目录"
fi
# 检查是否包含用户可写目录
IFS=':' read -ra paths <<< "$rpath"
for path in "${paths[@]}"; do
if [ -n "$path" ] && [ -d "$path" ] && [ -w "$path" ]; then
echo "警告: RPATH包含可写目录 $path - 存在库替换风险"
fi
done
fi
if [ -z "$runpath" ] && [ -z "$rpath" ]; then
echo "未发现RUNPATH或RPATH设置"
fi
echo
# 检查是否使用了系统标准libc库
echo "----- 3. 检查libc库路径安全性 -----"
libc_path=$(ldd "$TARGET" | grep 'libc.so.6' | awk '{print $3}')
if [ -n "$libc_path" ]; then
echo "libc.so.6 路径: $libc_path"
# 检查是否为系统标准路径
if ! echo "$libc_path" | grep -qE '^/lib/|^/lib64/|^/usr/lib/|^/usr/lib64/'; then
echo "警告: libc库不在标准系统路径中 - 可能被替换"
else
# 检查libc库的权限
if [ -f "$libc_path" ]; then
perm=$(stat -c "%a" "$libc_path")
if [ $((perm & 022)) -ne 0 ]; then
echo "高危: 系统libc库 $libc_path 权限不安全 ($perm) - 包含组写或其他写权限"
fi
fi
fi
else
echo "警告: 未找到libc.so.6依赖"
fi
echo
echo "===== 检测完成 ====="
echo "注意: 以上仅为初步安全检查,发现的问题需要进一步人工确认"
3.3 使用示例
# 赋予执行权限
chmod +x check_lib.sh
# 检测 /usr/bin/ssh 的依赖安全
./check_lib.sh /usr/bin/ssh
# 检测自定义程序
./check_lib.sh /opt/myapp/bin/server
四、检测能力边界与扩展建议
当前脚本主要覆盖 文件系统层 和 二进制层 的静态检查,对于以下攻击面仍需配合其他工具或手段进行补充:
| 攻击面 | 当前脚本覆盖 | 补充检测手段 |
|---|---|---|
LD_PRELOAD / LD_LIBRARY_PATH |
✗ | 审计进程环境变量(ps eww <pid> / /proc/<pid>/environ) |
/etc/ld.so.preload |
✗ | 文件完整性监控(AIDE、Tripwire)、auditd 规则 |
RPATH / RUNPATH 可写目录 |
✓ | 脚本已覆盖 |
| 库文件权限异常 | ✓ | 脚本已覆盖 |
libc.so.6 非标准路径 |
✓ | 脚本已覆盖 |
| GOT/PLT 运行时覆写 | ✗ | 内存取证(Volatility linux_plthook)、Full RELRO 编译检查 |
| 库文件内容篡改 | ✗ | IMA/EVM 完整性度量、dm-verity、RPM/dpkg 校验 |
扩展建议:
- 环境变量审计:在关键服务启动脚本中显式
unset LD_PRELOAD LD_LIBRARY_PATH,或使用env -i清理环境。 - 编译时加固:对所有安全关键型二进制文件启用
-Wl,-z,relro,-z,now(Full RELRO)和-fPIE -pie(PIE),消除 GOT 覆写和地址预测风险。 - 静态链接关键工具:对于必须在受感染环境中运行的取证或审计工具(如
aide、chkrootkit),建议静态编译以彻底消除动态链接攻击面。 - 容器加固:在 Kubernetes 中启用
readOnlyRootFilesystem: true和allowPrivilegeEscalation: false,防止容器内篡改链接器配置。
五、总结
动态链接机制在提升系统资源利用率和软件可维护性的同时,也引入了从环境变量、系统配置、二进制元数据到运行时内存的多层攻击面。理解 ld.so 的库解析优先级(LD_PRELOAD → /etc/ld.so.preload → RPATH → LD_LIBRARY_PATH → 系统路径)是构建防御体系的前提。
本文配套的 check_lib.sh 脚本聚焦于 静态层面的快速排查,能够在日常安全巡检中高效发现库权限异常、RPATH/RUNPATH 配置缺陷和 libc 路径异常等问题。对于更深层的运行时攻击(如 GOT 覆写)和持久化手段(如 /etc/ld.so.preload),则需要结合编译时加固(Full RELRO、PIE)、文件完整性监控和内存取证工具,形成纵深防御体系。
参考来源
- SystemsHardening: Linux Shared Library Security — LD_PRELOAD Attacks, Library Hijacking, and Hardened Linking
- CSDN: ELF 文件中的 RPATH 和 RUNPATH 安全风险分析
- CoderMusings: Dynamic Linking & Relocations — How GOT and PLT Work
- USENIX Security Symposium: PLT/GOT Randomization
- Vickie Li: Attacking Dynamic Linking — GOT Overwrite and RELRO
- GitHub: Library Injection via /etc/ld.so.preload
- dpkg-buildflags(1) — RELRO, BIND_NOW, PIE Hardening
原文 https://blog.csdn.net/2301_79518550/article/details/150486366