【学习记录】Week15(二):栈防卫的突破与堆结构的精妙手术——fmtstr+Canary 与 Off-by-one+Tcache
写在前面:在上一篇中,我们通过 ret2win 和 ret2libc 进行了综合实战的热身。今天,我们将面对 CTF 中最常见的两种防御机制与漏洞的组合拳:栈保护与堆指针加密。当栈溢出遇到 Canary,当堆溢出只有单字节(Off-by-one)且面临 Tcache 的 Safe-Linking 机制时,单纯的暴力覆盖已不再可行。我们需要利用格式化字符串进行“精准外科手术”,并利用 Off-by-one 制造堆块重叠,最终完成控制流劫持。
📑 目录
- 突破栈防卫:fmtstr 泄露 Canary + ROP 组合
- 堆的精妙手术:Off-by-one 原理与重叠构造
- 绕过指针加密:Tcache Poisoning 实战
- 综合实战:从漏洞触发到 Getshell 的闭环
- 总结与下篇预告
1. 突破栈防卫:fmtstr 泄露 Canary + ROP 组合
Canary(栈金丝雀)是位于栈帧中局部变量与返回地址之间的随机值。程序在返回前会检查该值是否被篡改,一旦改变则触发__stack_chk_fail直接终止。传统的栈溢出因为连续覆盖,必定会破坏 Canary。
1.1 组合策略:信息泄露 + 精确溢出
如果程序同时存在格式化字符串漏洞(如printf(buf);)和栈溢出漏洞(如read(0, buf, 0x100);),我们可以分两步走:
- 利用 fmtstr 泄露栈上的 Canary 值。
- 在栈溢出 payload 中,将原位置的 Canary 原封不动地填回,实现“无损穿越”,随后覆盖返回地址执行 ROP。
1.2 实战利用流程
第一步:定位 Canary
在 64 位系统中,Canary 通常在栈上的rbp - 0x8位置。由于printf的参数是通过寄存器和栈传递的,我们可以通过发送%p序列(如%p.%p.%p...)或利用%N$p定位到 Canary。
Canary 的特征是最低位通常为\x00(用于截断字符串防止泄露)。
假设通过调试发现,第 7 个参数输出的是类似于0x7fffa1234500的值,且末尾为00,则判定该位置为 Canary。
# 伪代码:泄露 Canary p.sendline(b'%7$p') canary = int(p.recvuntil(b'00', drop=True), 16) log.success(f'Canary leaked: {hex(canary)}')第二步:构造 ROP Payload
假设栈溢出的偏移量是 24 个字节(其中前 16 字节为普通变量,后 8 字节为 Canary)。
# 伪代码:Canary + ROP payload = b'A' * 24 # 填充到 Canary 位置 payload += p64(canary) # 填回真实的 Canary,骗过检查 payload += p64(0) # 填充 8 字节的 Saved RBP (旧栈基址) payload += p64(pop_rdi_ret) # ROP 链开始 payload += p64(bin_sh_addr) payload += p64(system_addr) p.sendline(payload)通过这种组合,Canary 形同虚设。
2. 堆的精妙手术:Off-by-one 原理与重叠构造
在堆利用中,off-by-one(差一字节溢出)是指向堆块写入数据时,多写了一个字节(通常是字符串末尾的\x00)。这一个字节往往溢出到了下一个堆块的size字段的最低位。
2.1 核心危害:Poison Null Byte(毒化空字节)
在 glibc 中,size字段的最低位是PREV_INUSE标志位(表示前一个堆块是否在使用中)。
如果我们多写了一个\x00,就会将下一个堆块的size最低位由1变为0,欺骗 glibc 认为前一个堆块是空闲的。
2.2 构造堆块重叠
结合 Off-by-one,我们可以构造经典的 A-B-C 堆块模型,制造堆块重叠,进而实现任意地址写。
布局策略:
- 分配堆块 A (size: 0x18, 实际分配 0x20),B (size: 0x108, 实际分配 0x110),C (size: 0x108, 实际分配 0x110)。
- 释放 B,B 进入 Unsorted Bin。
- 利用 A 的 Off-by-one 漏洞,向 A 写入 0x19 字节数据。最后一个
\x00溢出到 B 的 size 字段,将 B 的 size 从0x111修改为0x100。- 注意:这同时也清除了 C 的
PREV_INUSE位。 - 同时,我们需要在 B 的
prev_size位置(即 A 的最后 8 字节)伪造 B 的前一个堆块大小为 0x20(即 A 的大小)。
- 注意:这同时也清除了 C 的
- 分配一个比 B 小的块 B1 (size: 0xf8),从 B 中切割走 0x100 的空间。此时 B 剩余的 0x10 空间因为太小,不会被单独分配。
- 关键操作:释放 C。由于 C 的
PREV_INUSE为 0,glibc 会检查 C 的prev_size(即 0x100),认为前一个堆块(B)是空闲的,于是触发Backward Consolidation(向后合并)。 - glibc 将“想象中空闲的 B 块”与 C 合并,放入 Unsorted Bin。但实际上 B1 仍在被使用!
- 此时,我们再次分配一个大小为 0x100 的块 D,它会从合并后的大块中切出。由于 B1 仍在这个范围内,D 和 B1 发生了内存重叠!
分配 A, B, C
释放 B 进 Unsorted Bin
A 触发 Off-by-one
修改 B size 低位为 00
清除 C 的 PREV_INUSE
分配 B1 从 B 中切割
B1 仍在使用中
释放 C
触发 Backward Consolidation
glibc 误以为 B 是空闲的
将 B 和 C 合并
分配大块 D
D 与 B1 内存重叠!
通过修改 D 的内容
即可篡改 B1 的结构
实现任意地址写
3. 绕过指针加密:Tcache Poisoning 实战
在 glibc 2.32+ 中,Tcache 引入了Safe-Linking机制。Tcache 链表中 chunk 的fd指针不再直接指向下一个 chunk 的地址,而是:fd_encrypted = (chunk_addr >> 12) ^ next_chunk_addr
3.1 利用前提
通过上述的 Off-by-one 制造堆重叠后,我们可以控制一个在 Tcache 链表中的 chunk 的fd指针。
3.2 构造 Poisoning
假设我们通过重叠块 D,可以修改处于 Tcache 链表中 B1 的fd指针。我们想将下一个分配指向target_addr。
- 泄露堆基址:由于 B1 的
fd当前是加密状态,我们可以读取它。如果 B1 是链表尾部,next_chunk_addr为 0,则fd_encrypted = (chunk_addr >> 12) ^ 0 = chunk_addr >> 12。我们读取这个值,即可获得堆基址的高位(堆地址右移 12 位)。 - 计算伪造密钥:得知堆基址后,我们计算
key = B1_addr >> 12。 - 加密目标地址:我们想分配到
target_addr,则计算fake_fd = key ^ target_addr。 - 写入伪造指针:通过重叠块 D,将
B1->fd修改为fake_fd。 - 连续调用两次
malloc,第二次malloc返回的便是target_addr,实现任意地址写。
4. 综合实战:从漏洞触发到 Getshell 的闭环
将上述技术组合起来,面对一道高版本的综合 PWN 题,我们的完整思维链路如下:
- 漏洞识别:发现程序存在 Off-by-one(或单字节溢出)。
- 堆风水布局:分配 A-B-C,通过 Off-by-one 修改 B 的 size 和 C 的 prev_inuse。
- 制造重叠:切割 B,释放 C 触发合并,分配 D 与 B1 重叠。
- 获取原语:将 B1 释放进 Tcache。利用重叠块 D 读取 B1 的
fd,泄露堆基址;然后修改 B1 的fd为加密后的__free_hook(glibc ≤2.33) 或_IO_list_all(glibc ≥2.34)。 - 劫持控制流:
- 如果 ≤2.33:分配到
__free_hook,写入system,释放包含/bin/sh的块。 - *如果 ≥2.34*:分配到
_IO_list_all,伪造 House of Apple 链,调用exit触发 Getshell。
- 如果 ≤2.33:分配到
- (如果在栈题中):利用 fmtstr 泄露 Canary -> 精确覆盖 -> ROP 到
system或 ORW 链。
5. 总结与下篇预告
5.1 核心知识点总结
- Canary 绕过:格式化字符串是泄露 Canary 的最佳利器,掌握
%N$p定位和末尾\x00特征识别是关键。 - Off-by-one 破坏力:一个
\x00足以改变堆块的合并逻辑,通过 Poison Null Byte 制造堆块重叠,是现代堆题的核心起手式之一。 - Safe-Linking 破解:利用 Tcache 链表尾节点泄露堆基址右移 12 位的值,是完成高版本 Tcache Poisoning 的必经之路。
5.2 下篇预告
在下一篇中,我们将挑战更复杂的叠加场景:
- UAF + Tcache Poisoning:更直接的堆劫持,结合 IO_FILE (House of Apple) 打通 glibc 2.34+。
- 多漏洞叠加(fmt + UAF + ORW):模拟 2022 年后的高频赛事题型,当程序既有格式化字符串又有 UAF,且开了沙箱时,如何规划最优利用路径?
结语:无论是栈上的 Canary,还是堆上的 Safe-Linking,防护机制的本质都是“引入未知随机性”。而攻破它们的秘诀,就是寻找一切机会“泄露这种随机性”。掌握了 Off-by-one 制造重叠和 fmtstr 泄露,你就拥有了在现代保护机制下撕开裂口的战术组合。