1. 背景:ELF、动态链接与 .dynamic
表项
在 Linux/UNIX 系统中,ELF 格式是可执行文件和共享库(.so
文件)的标准文件格式。动态链接是现代程序设计的核心特性,允许程序在运行时加载所需的共享库,从而实现模块化、代码复用和高效的资源管理。动态链接器(如 glibc 的 ld-linux.so
或 ld.so
)在程序启动时负责解析 ELF 文件中的依赖关系(NEEDED
条目),并按照特定顺序查找和加载所需的共享库。
ELF 文件的 .dynamic
段包含了与动态链接相关的元数据,其中包括 DT_RPATH
和 DT_RUNPATH
表项,分别对应 RPATH
和 RUNPATH
。这两个表项允许开发者在链接阶段嵌入额外的库查找路径,动态链接器在运行时会参考这些路径来定位共享库。需要明确的是,RPATH
和 RUNPATH
并非任意运行时路径,而是由链接器(如 ld
)在编译或链接阶段写入 ELF 文件的静态元数据。
关键点:
RPATH
和RUNPATH
的作用是为动态链接器提供额外的搜索路径,影响程序运行时加载共享库的行为。错误的配置可能导致安全漏洞,尤其是在高权限或多用户环境中。
2. RPATH
与 RUNPATH
:定义与查找顺序
2.1 定义与区别
RPATH
(DT_RPATH
)和 RUNPATH
(DT_RUNPATH
)都是 ELF 文件中用于指定共享库搜索路径的字段,但它们在查找优先级以及与环境变量 LD_LIBRARY_PATH
的交互方式上存在显著差异:
- RPATH(DT_RPATH):这是较早期的实现方式,其优先级高于环境变量
LD_LIBRARY_PATH
。当 ELF 文件中存在DT_RPATH
且没有DT_RUNPATH
时,动态链接器会优先查找RPATH
中指定的路径。 - RUNPATH(DT_RUNPATH):在 GNU 链接器引入
--enable-new-dtags
选项后,RUNPATH
成为更现代的替代方案。它的查找优先级低于LD_LIBRARY_PATH
,意味着用户可以通过设置LD_LIBRARY_PATH
覆盖RUNPATH
中指定的路径。
2.2 动态链接器的查找顺序
动态链接器(如 glibc 的 ld.so
)在解析共享库路径时的查找顺序可以简化为以下步骤:
- 检查
DT_RPATH
:如果 ELF 文件中存在DT_RPATH
且没有DT_RUNPATH
,则优先查找RPATH
中的路径。 - 检查
LD_LIBRARY_PATH
:如果程序不是 setuid 或 setgid 程序,动态链接器会参考环境变量LD_LIBRARY_PATH
中指定的路径。 - 检查
DT_RUNPATH
:如果存在DT_RUNPATH
,则查找RUNPATH
中的路径。 - 检查系统缓存:动态链接器会查询
/etc/ld.so.cache
,这是由ldconfig
工具生成的标准库路径缓存。 - 检查默认路径:最后,链接器会查找系统默认路径,如
/lib
、/usr/lib
等。
注意:在 setuid 或 setgid 程序中,出于安全考虑,
LD_LIBRARY_PATH
会被忽略,但RPATH
和RUNPATH
仍然生效,这使得它们的配置尤为关键。
2.3 为什么优先级重要?
RPATH
和 RUNPATH
的优先级差异直接影响了攻击者利用路径劫持的可能性。例如,如果 RPATH
中包含用户可控的路径(如当前工作目录 .
),攻击者可能通过放置恶意共享库实现代码执行或权限提升。因此,理解查找顺序是识别潜在安全风险的基础。
3. .
与 $ORIGIN
的语义差异
在配置 RPATH
或 RUNPATH
时,开发者常使用相对路径(如 .
或 $ORIGIN
)来简化库定位。然而,这两者在运行时的语义完全不同,误用可能导致严重的安全问题。
3.1 .
(当前工作目录)
当 RPATH
或 RUNPATH
中包含 .
,动态链接器会将其解析为进程的当前工作目录(CWD),即 getcwd()
返回的路径。例如:
- 假设有一个可执行文件
/usr/local/bin/app
,其RUNPATH
设置为.
。 - 如果用户在
/home/user
目录下运行/usr/local/bin/app
,动态链接器会优先在/home/user
查找共享库。
这意味着攻击者可以在用户可写的目录(如 /home/user
)中放置与程序所需库同名的恶意 .so
文件,从而导致程序加载恶意代码。这种风险在多用户系统或高权限程序(如 setuid 程序)中尤为严重。
3.2 $ORIGIN
(可执行文件所在目录)
$ORIGIN
是一个由动态链接器支持的特殊占位符,表示可执行文件或共享库本身所在的目录。与 .
不同,$ORIGIN
的解析结果与当前工作目录无关。例如:
- 对于
/usr/local/bin/app
,如果RUNPATH
设置为$ORIGIN
,动态链接器会在/usr/local/bin
查找共享库,无论程序从哪个目录启动。
$ORIGIN
的设计初衷是为了支持可移植性,允许开发者将共享库放置在可执行文件旁边,而无需依赖固定的绝对路径。然而,历史上 glibc 在解析 $ORIGIN
时存在漏洞(例如将空 token 错误解析为 .
),这可能导致意外的安全问题。
3.3 使用建议
- 推荐使用
$ORIGIN
:如果需要在可执行文件所在目录加载共享库,$ORIGIN
是更安全的选择,因为它不依赖于运行时的当前工作目录。 - 避免使用
.
:将.
写入RPATH
或RUNPATH
会引入不可控的路径依赖,尤其是在多用户或高权限场景中,应完全避免。
4. 可被劫持的场景
基于 RPATH
和 RUNPATH
的工作机制,以下是两种常见的可被攻击者利用的场景:
4.1 场景 A:RUNPATH
或 RPATH
中包含 .
- 描述:当 ELF 文件的
RUNPATH
或RPATH
中包含.
,动态链接器会在进程的当前工作目录查找共享库。如果用户在可写目录(如/home/user
或/tmp
)运行程序,攻击者可以在该目录放置恶意.so
文件,导致程序加载恶意代码。 - 风险:此场景的风险极高,尤其是在程序以高权限运行或具有 setuid/setgid 属性时。即使
LD_LIBRARY_PATH
在 setuid 程序中被忽略,不当的RPATH
或RUNPATH
配置仍然可能导致漏洞。 - 示例:假设
/usr/bin/suid_app
的RPATH
包含.
,用户在/tmp
运行该程序,攻击者在/tmp
放置一个恶意libfoo.so
,程序可能加载该恶意库,导致代码执行或权限提升。
4.2 场景 B:$ORIGIN
的历史漏洞
- 描述:虽然
$ORIGIN
设计上是安全的,但在 glibc 的早期版本中,解析$ORIGIN
的逻辑存在缺陷。例如,某些版本可能将空 token 或特殊路径错误解析为.
,导致动态链接器意外在当前工作目录查找库。这种问题在 setuid 程序中尤其危险,可能被用于本地提权攻击。 - 风险:风险程度取决于系统使用的 glibc 版本。现代版本已修复大部分相关漏洞,但老旧系统或未更新的环境仍可能受到影响。
- 示例:历史上的 CVE-2011-1659 漏洞展示了 glibc 在处理
$ORIGIN
时的错误解析,可能导致 setuid 程序加载意外的共享库。
5. 安全问题与实际案例
RPATH
和 RUNPATH
的不当使用可能导致以下安全问题:
- 共享库劫持:攻击者通过放置恶意
.so
文件,诱导程序加载非预期的代码,可能导致任意代码执行或权限提升。 - 权限提升:在 setuid 或 setgid 程序中,不安全的路径配置可能允许普通用户加载恶意库,获取高权限。
- 可移植性问题:不正确的
$ORIGIN
使用可能导致程序在不同环境中加载错误的库,引发运行时错误或安全漏洞。
5.1 历史案例
- CVE-2011-1659:glibc 在解析
$ORIGIN
时存在漏洞,可能导致 setuid 程序错误加载当前工作目录中的库。 - 其他相关漏洞:国家漏洞数据库(NVD)中记录了多个与动态链接器路径解析相关的漏洞,涉及 glibc 和其他组件。
这些案例表明,RPATH
和 RUNPATH
的配置需要格外小心,尤其是在安全敏感的场景中。
6. 检测与审计:识别不安全的配置
为了防范 RPATH
和 RUNPATH
相关的安全风险,安全团队可以采取以下审计和检测措施:
6.1 使用 readelf
或 objdump
检查
- 命令:
此命令可以列出 ELF 文件中的readelf -d /path/to/binary | grep -i 'runpath\|rpath'
RUNPATH
和RPATH
条目。 - 用途:检查是否存在
.
、相对路径或指向可写目录的路径。 - 补充:结合
readelf -d | grep NEEDED
查看依赖库,判断哪些库可能受路径配置影响。
6.2 自动化扫描
- 工具:开发脚本或使用现有工具(如
checksec
)批量扫描二进制文件,检测包含.
、相对路径或可写目录的RPATH
/RUNPATH
。 - CI 集成:将扫描工具集成到持续集成(CI)流程中,防止不安全的配置进入生产环境。
6.3 关注 setuid/setgid 程序
- 方法:使用
find / -perm /6000
查找系统中具有 setuid/setgid 属性的可执行文件,重点检查其RPATH
/RUNPATH
配置。 - 建议:setuid 程序不应包含
RPATH
或RUNPATH
,或仅使用指向只读受信目录的绝对路径。
6.4 动态测试
- 方法:在隔离环境中设置
LD_DEBUG=libs
运行程序,观察动态链接器的库查找过程。 - 注意:
LD_DEBUG
在 setuid 程序中无效,需在非特权环境中测试。
7. 最佳实践与加固措施
为最大程度降低 RPATH
和 RUNPATH
相关的安全风险,建议遵循以下最佳实践:
- 避免使用
.
:在RPATH
或RUNPATH
中禁止使用.
或其他相对路径,防止动态链接器在可写目录查找库。 - 优先使用
$ORIGIN
:如果需要将共享库放置在可执行文件附近,使用$ORIGIN
并确保可执行文件目录不可被非管理员写入。 - 禁用 setuid 程序的路径配置:setuid 或 setgid 程序应避免使用
RPATH
或RUNPATH
,或仅使用受信任的绝对路径。 - 定期更新 glibc:确保系统使用最新版本的 glibc,修补已知的解析漏洞。
- 构建流程加固:在编译和链接阶段使用
--enable-new-dtags
启用RUNPATH
,并通过自动化工具检查 ELF 文件的配置。 - 运行时监控:在生产环境中监控程序的库加载行为,及时发现异常。
总结
RPATH
和 RUNPATH
是 ELF 文件中动态链接的重要机制,但其配置不当可能导致严重的共享库劫持和权限提升风险。通过理解动态链接器的查找顺序、避免使用不安全的路径(如 .
)、合理使用 $ORIGIN
、定期审计和更新系统,开发者与安全团队可以有效降低相关风险。
最佳实践清单:
- 禁止在
RPATH
或RUNPATH
中使用.
或可写目录。 - 在 setuid/setgid 程序中避免使用
RPATH
或RUNPATH
,或仅使用受信任的绝对路径。 - 在构建流程中集成 ELF 文件检查,防止不安全配置发布。
- 定期更新 glibc 和相关组件,关注与动态链接相关的 CVE。
通过遵循这些实践,开发者可以在确保功能的同时最大程度提升系统的安全性。