// 安全研究 · 2025-08-18

二进制程序动态链接风险与检测脚本实践

在现代 Linux 系统中,动态链接机制是共享库复用的基石,但同时也是攻击者实施代码注入、权限提升和持久化的重要攻击面。本文从动态链接器的工作流程出发,系统梳理了从环境变量劫持到 GOT/PLT 覆写的多层风险,并配套提供了一套可落地的 Bash 检测脚本,帮助安全运维人员快速排查二进制程序的依赖安全隐患。


一、动态链接器的工作流程与攻击面

当用户在 Linux 上执行一个动态链接的 ELF 二进制文件时,内核首先加载程序本身,随后将控制权交给动态链接器 ld.so(或 ld-linux.so)。链接器按照特定顺序解析依赖库,其搜索优先级如下:

  1. LD_PRELOAD 环境变量指定的库(除非程序具有 setuid/setgid 位,此时会被 glibc 的 AT_SECURE 机制自动清除)
  2. /etc/ld.so.preload 系统级预加载配置文件(需要 root 权限修改,但一旦写入即可实现系统级持久化)
  3. RPATH 和 RUNPATH 二进制文件中硬编码的路径(RPATH 优先级高于 LD_LIBRARY_PATH,而 RUNPATH 反之)
  4. LD_LIBRARY_PATH 环境变量
  5. 系统标准路径(/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 校验

扩展建议:

  1. 环境变量审计:在关键服务启动脚本中显式 unset LD_PRELOAD LD_LIBRARY_PATH,或使用 env -i 清理环境。
  2. 编译时加固:对所有安全关键型二进制文件启用 -Wl,-z,relro,-z,now(Full RELRO)和 -fPIE -pie(PIE),消除 GOT 覆写和地址预测风险。
  3. 静态链接关键工具:对于必须在受感染环境中运行的取证或审计工具(如 aide、chkrootkit),建议静态编译以彻底消除动态链接攻击面。
  4. 容器加固:在 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