写在前面:在上一篇中,我们通过格式化字符串或覆盖截断符
\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 = b'A' * 24 canary = b'\x00' # 最低位固定为 \x00 # 逐字节爆破前 7 个字节 for i in range(7): for byte in range(256): try: p = process('./vuln') # 接收初始提示语,假设为 "Welcome!\n" p.recvuntil(b'Welcome!\n') # 构造 Payload:填充 + 已知 Canary + 猜测的字节 payload = padding + canary + bytes([byte]) p.send(payload) # 尝试接收下一轮的提示语 # 如果没崩,说明猜对了,程序会继续执行并打印 "Welcome!" response = p.recvuntil(b'Welcome!\n', timeout=1) if b'Welcome!' in response: canary += bytes([byte]) print(f"[+] Found byte {i+1}: {hex(byte)}") p.close() break else: p.close() except EOFError: # 程序崩溃会直接关闭连接,触发 EOFError p.close() continue log.success(f"Final 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 的最低位。但因为原本就是\x00,Canary 完好无损。
但这有什么用呢?
单次溢出似乎没用,但如果我们能控制 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 地址,并推导出程序自身的基址。如果本文对你有帮助,请点赞收藏支持!🙏