ELF 文件保护机制与 ROP 构造详解
ELF 文件保护机制与 ROP 构造详解
在 CTF(Capture The Flag)竞赛和二进制漏洞利用中,理解 ELF(Executable and Linkable Format)文件的架构信息及其保护机制至关重要。本文将详细分析 ELF 文件的架构特性、常见保护机制、函数调用约定,以及如何通过 ROP(Return-Oriented Programming)技术进行漏洞利用。
ELF 架构信息
ELF 文件是 Linux 系统下常见的可执行文件格式,其架构信息直接影响漏洞利用的策略。以下是关键点:
- 目标程序架构:本文以
amd64-64-little(64 位小端序)为例。 - 作用:
- 函数调用约定:决定了参数传递方式(如寄存器或栈)。
- ROP 构造:需要根据架构选择合适的 gadget。
- 系统调用号:不同架构下系统调用号(如
read、write)存在差异。
保护机制总览
ELF 文件通常启用多种保护机制以增强安全性。以下是常见保护机制及其在 PWN(二进制漏洞利用)中的作用与绕过方式:
| 保护项 | 状态 | 说明 | 在 PWN 中的作用与利用方式 |
|---|---|---|---|
| RELRO | Partial RELRO | GOT 表部分受保护,GOT 可写 | 可通过 ret2got 改写函数指针(如 write → system) |
| RELRO | Full RELRO | GOT 表完全不可写 | 无法改写 GOT,只能泄露 GOT 地址后构造 ROP |
| Stack Canary | No Canary Found | 栈无溢出检测 | 可直接栈溢出控制返回地址 |
| Stack Canary | Canary Found | 启用栈溢出检测机制 | 需先泄露 Canary 值,否则程序崩溃 |
| NX | NX Enabled | 栈不可执行 | 无法执行 Shellcode,需用 ROP 或 ret2libc |
| PIE | No PIE (固定基址,如 0x400000) | 程序基址固定 | 地址可静态分析,ROP 构造更简单 |
| PIE | PIE Enabled | 基址随机化 | 需先泄露模块基址,ROP 构造更复杂 |
CTF 常见题型与保护机制
不同类型的 CTF 题目通常搭配特定的保护机制,影响利用方式:
- ROP / ret2libc 题:通常有 NX 保护,无 PIE 或仅 Partial RELRO,便于构造 ROP 链。
- 格式化字符串题:常搭配 Full RELRO,限制 GOT 重写,需利用格式化字符串漏洞泄露地址。
- 堆题:通常有 Stack Canary,挑战在于绕过 Canary 或利用堆漏洞。
- Shellcode 类题:一般无 NX 保护,允许在栈上直接执行注入的 Shellcode。
函数原型与调用方式
常用函数原型
以下是 PWN 中常用的标准库函数及其参数说明:
-
ssize_t write(int fd, const void *buf, size_t count);fd:文件描述符(如1表示标准输出)。buf:待输出数据的起始地址,可用于泄露内存内容。count:输出字节数,需控制以避免泄露过多数据。
-
ssize_t read(int fd, void *buf, size_t count);fd:文件描述符(如0表示标准输入)。buf:写入数据的目标地址(如栈、堆或 BSS 段)。count:读取字节数,可用于触发溢出或布置 ROP 链。
-
int puts(const char *s);s:字符串起始地址,遇\x00终止。- 常用于泄露 GOT 表中的函数地址(如
puts(puts_got))。
-
int printf(const char *format, ...);format:格式化字符串。- 可利用格式化字符串漏洞泄露任意地址内容。
参数传递方式对比
| 架构 | 参数传递方式 |
|---|---|
| 32 位 (x86) | 所有参数通过栈从右向左压入 |
| 64 位 (x86_64) | 前 6 个参数通过寄存器传递:RDI, RSI, RDX, RCX, R8, R9 |
提示:在 64 位 ROP 构造中,常用 gadget 包括:
pop rdi; retpop rsi; pop r15; retpop rdx; ret
系统调用号(syscall)
系统调用号因架构不同而异,常用函数的调用号如下:
| 函数 | x86 (32 位) | x86_64 (64 位) |
|---|---|---|
read |
3 | 0 |
write |
4 | 1 |
execve |
11 | 59 |
- x86:使用
int 0x80触发系统调用。 - x86_64:使用
syscall指令,通过设置rax寄存器控制系统调用类型。
常见函数 ROP 构造
以下展示如何针对常用函数构造 ROP 链,分别以 32 位和 64 位架构为例。假设使用 pwntools 的 flat 函数来构造 payload。
write 函数
- 32 位构造:
payload = flat(
b'A' * offset, # 填充至返回地址
write_plt, # write 函数 PLT 地址
main_addr, # 返回地址(通常回到 main)
1, # fd = stdout
write_got, # buf = GOT 地址,用于泄露
4 # count = 4 字节
)
- 64 位构造:
payload = flat(
pop_rdi, 1, # RDI = 1 (stdout)
pop_rsi_r15, write_got, 0, # RSI = GOT 地址
pop_rdx, 8, # RDX = 8 字节
write_plt # 调用 write
)
read 函数
- 32 位构造:
payload = flat(
b'A' * offset, # 填充至返回地址
read_plt, # read 函数 PLT 地址
ret_addr, # 返回地址
0, # fd = stdin
buf_addr, # 目标地址(如 BSS 段)
100 # count = 100 字节
)
- 64 位构造:
payload = flat(
pop_rdi, 0, # RDI = 0 (stdin)
pop_rsi_r15, buf_addr, 0, # RSI = 目标地址
pop_rdx, 100, # RDX = 100 字节
read_plt, # 调用 read
main_addr # 返回 main
)
puts 函数
- 32 位构造:
payload = flat(
b'A' * offset, # 填充至返回地址
puts_plt, # puts 函数 PLT 地址
main_addr, # 返回地址
puts_got # 参数:puts GOT 地址
)
- 64 位构造:
payload = flat(
pop_rdi, puts_got, # RDI = puts GOT 地址
puts_plt, # 调用 puts
main_addr # 返回 main
)
printf 函数(格式化字符串泄露)
- 32 位构造:
payload = flat(
b'A' * offset, # 填充至返回地址
printf_plt, # printf 函数 PLT 地址
main_addr, # 返回地址
got_puts # 参数:格式化字符串(如 "%s")
)
- 64 位构造:
payload = flat(
pop_rdi, format_str, # RDI = 格式化字符串地址
pop_rsi_r15, got_read, 0, # RSI = 目标地址
printf_plt, # 调用 printf
main_addr # 返回 main
)
ret2libc 利用流程
ret2libc 是一种经典的漏洞利用技术,绕过 NX 保护,通过调用 libc 中的函数(如 system("/bin/sh"))获取 shell。以下是常规流程:
- 泄露 GOT 表地址:利用
puts或write泄露puts@got的真实地址。 - 计算 libc 基址:通过接收到的地址,结合 libc 的偏移量,计算 libc 基址。
- 获取关键函数地址:利用 libc 基址和偏移,得到
system和"/bin/sh"字符串的地址。 - 构造 ROP 链:调用
system("/bin/sh")执行 shell。
ROP 汇总对比表
| 函数 | 架构 | 参数方式 | 构造说明 |
|---|---|---|---|
| write | 32 位 | 栈上传参 | fd=1, buf=got, count=4 |
| write | 64 位 | RDI, RSI, RDX | ROP 控制 3 个寄存器后调用 |
| read | 32 位 | 栈上传参 | 用于栈迁移或注入后续 ROP 链 |
| read | 64 位 | RDI, RSI, RDX | 常与 leave_ret gadget 搭配使用 |
| puts | 32 位 | 栈上传参 | 调用 puts(got_xxx) 泄露地址 |
| puts | 64 位 | RDI -> GOT 地址 | pop rdi; call puts |
| printf | 32 位 | 栈上传参 | printf(fmt, val) 用于格式化泄露 |
| printf | 64 位 | RDI, RSI | RDI 指向格式化字符串,RSI 指向地址 |
实用工具提示
- ROPgadget:用于查找二进制文件中的 gadget,例如:
ROPgadget --binary a.out --only 'pop|ret' - pwndbg:GDB 插件,方便调试和查找 gadget。
- pwntools:Python 库,简化 ROP 链构造和 payload 生成。
总结
ELF 文件的保护机制和架构特性直接影响漏洞利用的难度和方式。理解 RELRO、Canary、NX 和 PIE 等保护机制的作用,以及如何通过 ROP 和 ret2libc 技术绕过这些保护,是 CTF 和二进制安全研究的核心技能。希望本文的分析和示例代码能为你的 PWN 学习提供帮助!

浙公网安备 33010602011771号