【学习记录】Week5(二):无输出环境突破——Canary 盲爆破与 off-by-null 部分绕过
写在前面在上一篇中我们通过格式化字符串或覆盖截断符\x00成功偷出了 Canary。但如果题目环境极其苛刻没有格式化字符串漏洞没有puts等输出函数甚至连覆盖截断符的条件都没有我们该怎么破防今天我们将介绍两种极端环境下的 Canary 处理技巧利用多进程特性的逐字节盲爆破以及利用\x00设计特性的Partial Canary 绕过off-by-null 技术应用。 目录核心原理为什么 Canary 能够被爆破逐字节爆破实战基于 fork 的盲测推演另类思路Partial Canary 绕过是什么off-by-null 妙用跨越 Canary 的栈劫持总结与避坑指南1. 核心原理为什么 Canary 能够被爆破Canary 是随机的按理说无法猜测。但在 Linux 中如果程序使用fork创建子进程来处理请求子进程会完全继承父进程的内存空间包括那份相同的 Canary 值即使子进程因为猜错 Canary 而崩溃触发__stack_chk_fail父进程依然存活并可以继续fork新的子进程。这就给了我们“试错”的机会。更重要的是Canary 的检查是逐字节进行的或一旦字节不匹配即崩溃。我们可以利用程序是否崩溃作为“侧信道”每次只猜一个字节猜对了程序不崩猜错了程序崩溃。2. 逐字节爆破实战基于 fork 的盲测推演假设性场景程序是一个经典的fork循环服务端存在栈溢出。Canary 为 8 字节最低位必定是\x00所以我们只需要爆破高 7 字节。爆破逻辑推演第 1 轮发送[Padding] \x00 [猜测的第1个字节]。如果程序没崩说明第 1 个字节猜对了。第 2 轮发送[Padding] \x00 [正确的第1字节] [猜测的第2个字节]。循环 7 次即可还原完整的 Canary。Pwntools 模拟脚本推演from pwn import * context.log_level error # 已知 Padding 长度为 24 padding bA * 24 canary b\x00 # 最低位固定为 \x00 # 逐字节爆破前 7 个字节 for i in range(7): for byte in range(256): try: p process(./vuln) # 接收初始提示语假设为 Welcome!\n p.recvuntil(bWelcome!\n) # 构造 Payload填充 已知 Canary 猜测的字节 payload padding canary bytes([byte]) p.send(payload) # 尝试接收下一轮的提示语 # 如果没崩说明猜对了程序会继续执行并打印 Welcome! response p.recvuntil(bWelcome!\n, timeout1) if bWelcome! in response: canary bytes([byte]) print(f[] Found byte {i1}: {hex(byte)}) p.close() break else: p.close() except EOFError: # 程序崩溃会直接关闭连接触发 EOFError p.close() continue log.success(fFinal Canary: {hex(u64(canary))})假设性终端输出[] Found byte 1: 0x3a [] Found byte 2: 0x7b ... [] Found byte 7: 0x9f [] Final Canary: 0x9f7b3a...00爆破成功拿到 Canary 后我们就可以在同一个父进程派生的子进程中发送带有正确 Canary 的 ROP 链了。3. 另类思路Partial Canary 绕过是什么如果程序不是多进程爆破走不通怎么办回顾 Canary 的设计最低位是\x00。这个设计的初衷是为了防止puts等函数泄露。但这个\x00恰恰成了我们“不破坏 Canary 就能利用漏洞”的跳板。Partial Canary 绕过的核心思想如果漏洞是off-by-one差一溢出或off-by-null单字节空字符溢出我们只能刚好覆盖到 Canary 的第一个字节。由于 Canary 的第一个字节本身就是\x00如果我们用\x00去覆盖它Canary 的值根本没有改变程序在返回时检查 Canary发现一模一样不会触发崩溃。这就意味着我们虽然没改 Canary但我们越过了 Canary成功污染了 Canary 后面的数据如 RBP。4. off-by-null 妙用跨越 Canary 的栈劫持场景推演假设程序存在off-by-null漏洞可以向buf末尾多写一个\x00。栈结构如下高地址 | 返回地址 (RIP) | | 保存的 RBP | | Canary (末尾\x00)| | buf (0x18字节) | 低地址如果buf是 24 字节0x18我们输入 24 个A后再补一个\x00这个\x00就会覆盖 Canary 的最低位。但因为原本就是\x00Canary 完好无损。但这有什么用呢单次溢出似乎没用但如果我们能控制 RBP 的低位就能实现“栈劫持”。进阶攻击模型我们在 BSS 段或堆上布置好一段伪造的栈帧包含伪造的 RBP 和返回地址。利用off-by-null覆盖当前栈帧 RBP 的最低位使其变为\x00将 RBP 指向我们伪造的栈帧附近。当函数执行leave; ret时leave等价于mov rsp, rbp; pop rbp。由于 RBP 被篡改RSP被劫持到了我们伪造的栈帧接下来的ret指令就会从我们伪造的栈帧中取地址执行。通过这种方式我们完全无视了 Canary 的存在直接劫持了执行流。这也是高级 CTF 中针对 Canary 保护最常用的“Partial Bypass”手段。5. 总结与避坑指南爆破的局限性爆破 Canary 必须依赖fork机制。如果程序是单进程且崩溃后直接退出爆破无法进行。网络延迟爆破需要频繁连接进程在远程题目中可能会因为网络延迟导致recv超时误判。建议适当增加timeout或采用多线程爆破。off-by-null 的条件利用 Partial Canary 绕过需要精确计算 RBP 与伪造栈帧的偏移通常需要结合地址泄露或部分覆盖来实现精准定位。Canary 并不是不可战胜的。它为了防泄露设计的\x00反而成了我们爆破的突破口和栈劫持的跳板。下一篇我们将面对另一个令人头疼的保护机制——PIE地址随机化。我们将学习如何利用ret2puts泄露 libc 地址并推导出程序自身的基址。如果本文对你有帮助请点赞收藏支持