From ab49df1398bde924ac87ce803263fcf4c01958fa Mon Sep 17 00:00:00 2001 From: Translator Date: Thu, 17 Jul 2025 00:13:16 +0000 Subject: [PATCH] Translated ['src/binary-exploitation/common-binary-protections-and-bypas --- .../relro.md | 92 ++++++++++++++++--- 1 file changed, 79 insertions(+), 13 deletions(-) diff --git a/src/binary-exploitation/common-binary-protections-and-bypasses/relro.md b/src/binary-exploitation/common-binary-protections-and-bypasses/relro.md index 8f47064a2..908f2a982 100644 --- a/src/binary-exploitation/common-binary-protections-and-bypasses/relro.md +++ b/src/binary-exploitation/common-binary-protections-and-bypasses/relro.md @@ -4,30 +4,96 @@ ## Relro -**RELRO** 代表 **Relocation Read-Only**,它是用于二进制文件的安全特性,旨在减轻与 **GOT (Global Offset Table)** 重写相关的风险。**RELRO** 保护有两种类型:(1)**Partial RELRO** 和(2)**Full RELRO**。它们都重新排列了 ELF 文件中的 **GOT** 和 **BSS**,但结果和影响不同。具体来说,它们将 **GOT** 部分放在 **BSS** 之前。也就是说,**GOT** 的地址低于 **BSS**,因此通过溢出 **BSS** 中的变量来重写 **GOT** 条目是不可能的(记住,写入内存是从低地址向高地址进行的)。 +**RELRO** 代表 **Relocation Read-Only**,这是链接器(`ld`)实施的一种缓解措施,它在所有重定位应用后将 ELF 的数据段的一个子集设置为 **只读**。 其目的是阻止攻击者覆盖 **GOT(全局偏移表)** 或其他在程序执行期间被解引用的与重定位相关的表(例如 `__fini_array`)中的条目。 -让我们将这个概念分解为两个不同的类型以便于理解。 +现代链接器通过 **重新排序** **GOT**(和其他几个部分)来实现 RELRO,使其位于 **.bss** 之前,并且最重要的是,通过创建一个专用的 `PT_GNU_RELRO` 段,该段在动态加载器完成应用重定位后被重新映射为 `R–X`。 因此,典型的 **.bss** 中的缓冲区溢出不再能够到达 GOT,任意写入原语无法用于覆盖位于 RELRO 保护页面内的函数指针。 -### **Partial RELRO** +链接器可以发出 **两级** 保护: -**Partial RELRO** 采取更简单的方法来增强安全性,而不会显著影响二进制文件的性能。Partial RELRO 使 **.got 只读(GOT 部分的非 PLT 部分)**。请记住,部分区域(如 .got.plt)仍然是可写的,因此仍然容易受到攻击。这 **并不防止 GOT** 被 **任意写入** 漏洞滥用。 +### Partial RELRO -注意:默认情况下,GCC 编译的二进制文件使用 Partial RELRO。 +* 使用标志 `-Wl,-z,relro`(或在直接调用 `ld` 时仅使用 `-z relro`)生成。 +* 仅将 **GOT** 的 **非 PLT** 部分(用于数据重定位的部分)放入只读段。 需要在运行时修改的部分 – 最重要的是支持 **懒绑定** 的 **.got.plt** – 保持可写。 +* 因此,**任意写入** 原语仍然可以通过覆盖 PLT 条目(或通过执行 **ret2dlresolve**)来重定向执行流。 +* 性能影响微乎其微,因此 **几乎每个发行版多年来都在发布至少具有部分 RELRO 的软件包(自 2016 年起,它是 GCC/Binutils 的默认设置)**。 -### **Full RELRO** +### Full RELRO -**Full RELRO** 通过 **使整个 GOT(包括 .got 和 .got.plt)和 .fini_array** 部分完全 **只读** 来加强保护。一旦二进制文件启动,所有函数地址都会在 GOT 中解析并加载,然后,GOT 被标记为只读,有效地防止在运行时对其进行任何修改。 +* 使用 **两个** 标志 `-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` 段中。 +* 增加可测量的启动开销(所有动态重定位在启动时处理),但 **没有运行时开销**。 -然而,Full RELRO 的权衡在于性能和启动时间。因为它需要在启动时解析所有动态符号,然后再将 GOT 标记为只读,**启用 Full RELRO 的二进制文件可能会经历更长的加载时间**。这种额外的启动开销就是为什么并非所有二进制文件默认启用 Full RELRO。 +自 2023 年以来,几种主流发行版已切换为默认使用 **Full RELRO** 编译 **系统工具链**(和大多数软件包) – 例如 **Debian 12 “bookworm” (dpkg-buildflags 13.0.0)** 和 **Fedora 35+**。 因此,作为渗透测试人员,您应该预期遇到 **每个 GOT 条目都是只读** 的二进制文件。 -可以通过以下方式查看二进制文件是否 **启用** Full RELRO: +--- + +## 如何检查二进制文件的 RELRO 状态 ```bash -readelf -l /proc/ID_PROC/exe | grep BIND_NOW +$ checksec --file ./vuln +[*] '/tmp/vuln' +Arch: amd64-64-little +RELRO: Full +Stack: Canary found +NX: NX enabled +PIE: No PIE (0x400000) +``` +`checksec`(是 [pwntools](https://github.com/pwncollege/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 ``` -## 绕过 -如果启用了完整的 RELRO,绕过它的唯一方法是找到另一种不需要在 GOT 表中写入以获得任意执行的方法。 +```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 +``` +--- -请注意,**LIBC 的 GOT 通常是部分 RELRO**,因此可以通过任意写入进行修改。更多信息请参见 [Targetting libc GOT entries](https://github.com/nobodyisnobody/docs/blob/main/code.execution.on.last.libc/README.md#1---targetting-libc-got-entries)**.** +## 在编译自己的代码时启用 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_Rela` 和 `Elf64_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 now` 和 `PT_GNU_RELRO`* +* *“RELRO – 完全、部分和绕过技术”* – 博客文章 @ wolfslittlered 2023 {{#include ../../banners/hacktricks-training.md}}