Translated ['src/binary-exploitation/common-binary-protections-and-bypas

This commit is contained in:
Translator 2025-07-17 00:13:16 +00:00
parent 0b23c3763c
commit ab49df1398

View File

@ -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` 段,该段在动态加载器完成应用重定位后被重新映射为 `RX`。 因此,典型的 **.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** 条目并转移执行。<br>2. **ret2dlresolve** 在可写段中构造伪造的 `Elf64_Rela``Elf64_Sym` 并调用 `_dl_runtime_resolve`<br>3. 重写 **.fini_array** / **atexit()** 列表中的函数指针。 |
| 完全 | GOT 是只读的 | 1. 寻找 **其他可写代码指针**C++ vtables, `__malloc_hook` < glibc 2.34, `__free_hook`, 自定义 `.data` 段中的回调, JIT )。<br>2. 滥用 *相对读取* 原语泄露 libc 并执行 **SROP/ROP 进入 libc**<br>3. 通过 **DT_RPATH**/`LD_PRELOAD` 注入恶意共享对象(如果环境由攻击者控制)或 **`ld_audit`**。<br>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}}