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。
    • 系统调用号:不同架构下系统调用号(如 readwrite)存在差异。

保护机制总览

ELF 文件通常启用多种保护机制以增强安全性。以下是常见保护机制及其在 PWN(二进制漏洞利用)中的作用与绕过方式:

保护项 状态 说明 在 PWN 中的作用与利用方式
RELRO Partial RELRO GOT 表部分受保护,GOT 可写 可通过 ret2got 改写函数指针(如 writesystem
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 中常用的标准库函数及其参数说明:

  1. ssize_t write(int fd, const void *buf, size_t count);

    • fd:文件描述符(如 1 表示标准输出)。
    • buf:待输出数据的起始地址,可用于泄露内存内容。
    • count:输出字节数,需控制以避免泄露过多数据。
  2. ssize_t read(int fd, void *buf, size_t count);

    • fd:文件描述符(如 0 表示标准输入)。
    • buf:写入数据的目标地址(如栈、堆或 BSS 段)。
    • count:读取字节数,可用于触发溢出或布置 ROP 链。
  3. int puts(const char *s);

    • s:字符串起始地址,遇 \x00 终止。
    • 常用于泄露 GOT 表中的函数地址(如 puts(puts_got))。
  4. int printf(const char *format, ...);

    • format:格式化字符串。
    • 可利用格式化字符串漏洞泄露任意地址内容。

参数传递方式对比

架构 参数传递方式
32 位 (x86) 所有参数通过栈从右向左压入
64 位 (x86_64) 前 6 个参数通过寄存器传递:RDI, RSI, RDX, RCX, R8, R9

提示:在 64 位 ROP 构造中,常用 gadget 包括:

  • pop rdi; ret
  • pop rsi; pop r15; ret
  • pop 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 位架构为例。假设使用 pwntoolsflat 函数来构造 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。以下是常规流程:

  1. 泄露 GOT 表地址:利用 putswrite 泄露 puts@got 的真实地址。
  2. 计算 libc 基址:通过接收到的地址,结合 libc 的偏移量,计算 libc 基址。
  3. 获取关键函数地址:利用 libc 基址和偏移,得到 system"/bin/sh" 字符串的地址。
  4. 构造 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 学习提供帮助!

posted @ 2025-07-22 20:13  bae-ace  阅读(75)  评论(1)    收藏  举报