OFF-BY-NULL Tcachebin
该利用链利用了 **Off-By-Null **漏洞来触发 **Backward Consolidation(向后合并)**从而制造出 **Overlapping **。接着,它通过 Unsorted Bin 泄露 libc 的基址,并利用 double-free 进行 Tcache Poisoning,将 __free_hook 覆盖为 one_gadget 从而获取 shell。
hitcon_2018_children_tcache(Off-By-NULL)
main
1 | void __fastcall __noreturn main(const char *p_Invalid_Choice, char **a2, char **a3) |
整体还是一个基本的菜单题,还是从添加开始一步一步来分析:
1 | int __fastcall menu(__int64 p_Invalid_Choice) |
new:
1 | unsigned __int64 new() |
delete
1 | int delete() |
简单的一个删除函数,free 后堆指针置 0,不存在 UAF 漏洞。
show
1 | int __fastcall show(__int64 p_Invalid_Choice) |
打印 dest 的低四字节
EXP 思路:
封装函数
1 | add(0x410,b'a') # Chunk 0: 大小为 0x420 |
- Chunk 0 (0x410): 这是一个大块。被释放时,它会进入 Unsorted Bin。
- Chunk 1 (0x68): 一个较小的块,稍后将用于触发 Off-By-Null 漏洞。
- Chunk 2 (0x4f0): 另一个大块,稍后会被用于触发向后合并。
- Chunk 3 (0x30): 保护块,它的作用是防止 Chunk 2 被释放时直接与堆顶的 Top Chunk合并。
分配 UnSortedbin 和 Tcachebin
1 | free(0) |
1 | pwndbg> heap |
1 | pwndbg> x/60gx 0x561f1cf99670 |
重复利用 Off-By-NULL,篡改 prev_size
1 | for i in range(9): |
脚本在一个循环中不断分配并释放 Chunk 1,且请求大小略微递减。这是 pwn 中常用的技巧,用于清理块内残留的指针(例如 tcache 中的 fd 指针)。通过写入 ‘s’ 覆盖原有数据,可以防止旧指针中的空字节(\x00)在后续调用 show() 尝试泄露数据时截断输出。
这里说一下这一步的思路:
由于 stcrcpy 会被 \x00 截断的特性
- 第 1 次循环 (i=0,大小 0x68):分配 0x68 大小,写入 0x68 个 ‘s’。程序自动在末尾(索引 0x68)处补上一个 \x00。这一步成功清除了下一个 chunk 的 PREV_INUSE 标志!
- 第 2 次循环 (i=1,大小 0x67):分配 0x67 大小,写入 0x67 个 ‘s’。程序在索引 0x67 处补上 \x00。由于只写入了 0x67 字节,上一步在 0x68 处留下的 \x00 完美保留了下来。
- 第 3 次循环 (i=2,大小 0x66):在索引 0x66 处补上 \x00,保留 0x67 和 0x68 的 \x00。
- 依次类推,知道第 9 次循环
- 第 9 次循环 (i=8,大小 0x60):在索引 0x60 处补上 \x00,得到 0x55de8dbfa6e0 处数据全部清零:
1 | pwndbg> x/60gx 0x55de8dbfa670 |
这里补充上不循环利用 off-by-null 的结果:在/x90/x04 之后 strcpy 补上一个/x00,但是其他位并没有修改
1 | pwndbg> x/60gx 0x56448c712670 |
这个时候再去覆盖写 prev_size 就没有问题了:
1 | add(0x68,b'a'*0x60+p64(0x490)) |
1 | pwndbg> x/60gx 0x55de8dbfa670 |
free(2)实现堆向后合并
1 | add(0x68,b'a'*0x60+p64(0x490)) |
- 脚本重新分配了刚才 0x70 大小的块。它用 ‘a’ 填充了前 0x60 个字节,并将 0x490 写在最末尾。
- 在堆内存的物理布局中,Chunk 1 的最后 8 个字节恰好与 Chunk 2 的 size 字段相邻,这里的 0x490 覆盖了 Chunk 2 的 prev_size 字段。
- 当调用 free(2) 时,glibc 检查发现 PREV_INUSE 标志为 0,认为前一个块是空闲的。于是它读取我们伪造的 prev_size (0x490),并精确地向后(向低地址)跳转 0x490 个字节,将它们合并。
- Chunk 0 的大小 (0x420) + Chunk 1 的大小 (0x70) = 0x490。
- 这导致 Chunk 0、Chunk 1 和 Chunk 2 被合并成了一个巨大的空闲块,放入了 Unsorted Bin。****
利用 main_arena 泄露 libc 基地址,计算关键函数
1 | add(0x410,b'a'*1) # 分配后成为 Index 1 |
- 请求分配 0x410 个字节。Glibc 会从刚才那个巨大的合并块的顶部切下一块。
- 切下的部分刚好占用了原始 Chunk 0 的内存。而剩余的空闲内存现在正好从 Index 0 的起始位置开始。
- 因为这块剩余内存仍在 Unsorted Bin 中,glibc 会将其 main_arena 的地址(即双向链表的 fd 和 bk 指针)写入到这块内存的开头。
- 由于程序的 Index 0 依然指向这个内存位置,调用 show(0) 就可以直接把 main_arena 的指针打印出来!
- 脚本获取到泄露的地址后,减去固定的偏移量(在 Ubuntu 18.04 glibc 2.27 中通常是 0x3ebca0),就得到了 libc 的绝对基址。
Overlapping & Tcache Double Free
1 | shell=libc_base + 0x4f322 # 计算 One_Gadget 的地址 |
- add(0x80,b’a’) 继续从 Unsorted Bin 中分配内存。这块内存在物理上与之前的 Index 0 完全重合。此时,Index 2 和 Index 0 指向了同一块内存地址。
- free(2) 将该块放入大小为 0x90 的 tcache 链表中。
- free(0) 再次释放了完全相同的内存。
- 因为 glibc 2.27 的 tcache 完全不检查 double-free,0x90 的 tcache 链表被成功打乱,形成了一个环状结构:
[Chunk] -> [Chunk] -> ...
Tcache Poisoning
1 | add(0x80,p64(free_hook)) |
- **第一次 **
add(0x80)分配出了刚才 double-free 的块,并允许用户向其中写入数据。脚本写入了__free_hook的地址。由于这块内存同时也被 glibc 视为 tcache 链表中的“下一个”空闲块,这一步直接覆盖了 tcache 的fd指针。 - 此时 Tcache 链表变为:
[Chunk] -> [__free_hook] - **第二次 **
add(0x80)再次分配该块,将 tcache 的链表头向后推移一个位置。 - 此时 Tcache 链表变为:
[__free_hook] - **第三次 **
add(0x80)欺骗了 glibc,使其直接在__free_hook的地址处分配内存!脚本顺势将计算好的one_gadget地址(执行后可以直接 get shell 的地址)写入了__free_hook。
触发 One_Gadgets
1 | free(0) |
总 EXP:
1 | from pwn import * |
更新: 2026-04-24 11:30:47
原文: https://www.yuque.com/idcm/wnemg9/koyg91kdmgxrivno