Relro

Reading time: 9 minutes

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks

Relro

RELRO 代表 Relocation Read-Only,这是链接器(ld)实施的一种缓解措施,它在所有重定位应用后将 ELF 的数据段的一个子集设置为 只读。 其目的是阻止攻击者覆盖 GOT(全局偏移表) 或其他在程序执行期间被解引用的与重定位相关的表(例如 __fini_array)中的条目。

现代链接器通过 重新排序 GOT(和其他几个部分)来实现 RELRO,使其位于 .bss 之前,并且最重要的是,通过创建一个专用的 PT_GNU_RELRO 段,该段在动态加载器完成应用重定位后被重新映射为 R–X。 因此,典型的 .bss 中的缓冲区溢出不再能够到达 GOT,任意写入原语无法用于覆盖位于 RELRO 保护页面内的函数指针。

链接器可以发出 两级 保护:

Partial RELRO

  • 使用标志 -Wl,-z,relro(或在直接调用 ld 时仅使用 -z relro)生成。
  • 仅将 GOT非 PLT 部分(用于数据重定位的部分)放入只读段。 需要在运行时修改的部分 – 最重要的是支持 懒绑定.got.plt – 保持可写。
  • 因此,任意写入 原语仍然可以通过覆盖 PLT 条目(或通过执行 ret2dlresolve)来重定向执行流。
  • 性能影响微乎其微,因此 几乎每个发行版多年来都在发布至少具有部分 RELRO 的软件包(自 2016 年起,它是 GCC/Binutils 的默认设置)

Full RELRO

  • 使用 两个 标志 -Wl,-z,relro,-z,now(也称为 -z relro -z now)生成。 -z now 强制动态加载器提前解析 所有 符号(急切绑定),以便 .got.plt 不再需要被写入,并且可以安全地映射为只读。
  • 整个 GOT.got.plt.fini_array.init_array.preinit_array 和一些额外的内部 glibc 表最终位于只读的 PT_GNU_RELRO 段中。
  • 增加可测量的启动开销(所有动态重定位在启动时处理),但 没有运行时开销

自 2023 年以来,几种主流发行版已切换为默认使用 Full RELRO 编译 系统工具链(和大多数软件包) – 例如 Debian 12 “bookworm” (dpkg-buildflags 13.0.0)Fedora 35+。 因此,作为渗透测试人员,您应该预期遇到 每个 GOT 条目都是只读 的二进制文件。


如何检查二进制文件的 RELRO 状态

bash
$ checksec --file ./vuln
[*] '/tmp/vuln'
Arch:     amd64-64-little
RELRO:    Full
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

checksec(是 pwntools 和许多发行版的一部分)解析 ELF 头并打印保护级别。如果您无法使用 checksec,请依赖 readelf

bash
# Partial RELRO → PT_GNU_RELRO is present but BIND_NOW is *absent*
$ readelf -l ./vuln | grep -E "GNU_RELRO|BIND_NOW"
GNU_RELRO      0x0000000000600e20 0x0000000000600e20
bash
# Full RELRO → PT_GNU_RELRO *and* the DF_BIND_NOW flag
$ readelf -d ./vuln | grep BIND_NOW
0x0000000000000010 (FLAGS)              FLAGS: BIND_NOW

如果二进制文件正在运行(例如,一个 set-uid root 助手),你仍然可以通过 /proc/$PID/exe 检查可执行文件:

bash
readelf -l /proc/$(pgrep helper)/exe | grep GNU_RELRO

在编译自己的代码时启用 RELRO

bash
# GCC example – create a PIE with Full RELRO and other common hardenings
$ gcc -fPIE -pie -z relro -z now -Wl,--as-needed -D_FORTIFY_SOURCE=2 main.c -o secure

-z relro -z now 适用于 GCC/clang(在 -Wl, 后传递)和直接使用 ld。 当使用 CMake 3.18+ 时,您可以通过内置预设请求完整的 RELRO:

cmake
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) # LTO
set(CMAKE_ENABLE_EXPORTS OFF)
set(CMAKE_BUILD_RPATH_USE_ORIGIN ON)
set(CMAKE_EXE_LINKER_FLAGS "-Wl,-z,relro,-z,now")

绕过技术

RELRO 级别典型原语可能的利用技术
无 / 部分任意写入1. 重写 .got.plt 条目并转移执行。
2. ret2dlresolve – 在可写段中构造伪造的 Elf64_RelaElf64_Sym 并调用 _dl_runtime_resolve
3. 重写 .fini_array / atexit() 列表中的函数指针。
完全GOT 是只读的1. 寻找 其他可写代码指针(C++ vtables, __malloc_hook < glibc 2.34, __free_hook, 自定义 .data 段中的回调, JIT 页)。
2. 滥用 相对读取 原语泄露 libc 并执行 SROP/ROP 进入 libc
3. 通过 DT_RPATH/LD_PRELOAD 注入恶意共享对象(如果环境由攻击者控制)或 ld_audit
4. 利用 格式字符串 或部分指针重写来转移控制流而不触碰 GOT。

💡 即使是完全 RELRO,加载的共享库(例如 libc 本身)的 GOT 也是 仅部分 RELRO,因为这些对象在加载器应用重定位时已经被映射。如果你获得了一个 任意写入 原语,可以针对另一个共享对象的页面,你仍然可以通过重写 libc 的 GOT 条目或 __rtld_global 栈来转移执行,这是一种在现代 CTF 挑战中经常被利用的技术。

现实世界的绕过示例 (2024 CTF – pwn.college “enlightened”)

该挑战附带了完全 RELRO。利用了一个 越界 来破坏堆块的大小,通过 tcache poisoning 泄露了 libc,最后重写了 __free_hook(在 RELRO 段外)并使用一个单一 gadget 获得代码执行。无需进行 GOT 写入。


最近的研究与漏洞 (2022-2025)

  • glibc 2.40 废弃 __malloc_hook / __free_hook (2025) – 大多数利用这些符号的现代堆漏洞现在必须转向替代向量,如 rtld_global._dl_load_jump 或 C++ 异常表。由于钩子位于 RELRO 之外,它们的移除增加了完全 RELRO 绕过的难度。
  • Binutils 2.41 “最大页面大小” 修复 (2024) – 一个错误允许 RELRO 段的最后几个字节与某些 ARM64 构建中的可写数据共享一个页面,留下一个微小的 RELRO 缺口,可以在 mprotect 之后写入。上游现在将 PT_GNU_RELRO 对齐到页面边界,消除了这个边缘情况。

参考文献

  • Binutils 文档 – -z relro, -z nowPT_GNU_RELRO
  • “RELRO – 完全、部分和绕过技术” – 博客文章 @ wolfslittlered 2023

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks