堆利用在CTF的PWN题目中一直占比较大,而最近的比赛的平台也逐渐从Ubuntu16,Ubuntu18向Ubuntu19甚至更高版本转移,为此学习一下libc-2.29中新的特性对做题还是很有帮助的
libc-2.29新变化
libc-2.29中修改了对tcache_entry的定义,通过注释我们也能看出新增加的key是为了检测double free的,如下
/* 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;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
在tcache_put中,新增了 e->key = tcache;
static __thread tcache_perthread_struct *tcache = NULL;
······
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为tcache_perthread_struct的地址,这里用该地址作为key
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
同时在free中也多了对double free的检测
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))//当key和tcache相等,才会检查是否有double free的可能(有时候可能这里的值正好和tcache相同)
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)//这个循环检测该chunk是否已经进入tcache
if (tmp == e)
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. */
}
if (tcache->counts[tc_idx] < mp_.tcache_count)//tcache没有被装满
{
tcache_put (p, tc_idx);
return;
}
}
绕过策略
在free函数中,只有当key和tcache相等时才会去判断是否double free(所以并不是每一个free的chunk都会被检查是否是double free),而如果程序中正好有漏洞可以把key改了(包括但不限于UAF),就可以直接跳过检查了
libc-2.29中出现的利用方法
libc-2.29出现了一种叫stash的机制,基本原理就是当调用_int_malloc时,如果从smallbin或者fastbin中取出chunk之后,对应大小的tcache没有满,就会把剩下的bin放入tcache中,代码如下:
fastbin
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)//检查tcache是否满了
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
if (SINGLE_THREAD_P)
*fb = tc_victim->fd;
else
{
REMOVE_FB (fb, pp, tc_victim);
if (__glibc_unlikely (tc_victim == NULL))
break;
}
tcache_put (tc_victim, tc_idx);
}
}
#endif
smallbin
bin = bin_at (av, idx);
······
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)//检查tcache是否满了
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
Tcache stash unlink attack
在上面的源代码中,可以发现当smallbin剩余部分被放入tcache中,并没有进行检查,而在把smallbin取出给用户使用的时候,是有检查的,如下:
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;
在把剩余smallbin放入tcache的时候,关键操作如下
bin = bin_at (av, idx);
······
#if USE_TCACHE
······
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
······
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
我们把2个bin放入smallbin(先free的记为smallbin1),6个bin放入对应大小的tcache,如果我们能在不修改smallbin2的fd的情况(为了过第一个检查)下修改其bk,把bk修改为目标地址-0x10,就能在目标地址的位置写入一个不可控的大数(上面代码中的bin)。放入之后tcache数目变为7,就会结束循环
Tcache stash unlink attack+
该操作关键代码如下:
bin = bin_at (av, idx);
······
#if USE_TCACHE
······
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
······
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
如果我们把2个bin放入smallbin(先free的记为smallbin1),5个bin放入对应大小的tcache,如果我们能在不修改smallbin2的fd的情况(为了过第一个检查)下修改其bk,把bk修改为目标地址-0x10,当smallbin1被分配给用户,smallbin2进入tcache之后,smallbin的bk就是目标地址-0x10,此时tcache数目为6,循环不结束。新的循环中,tc_victim就是目标地址-0x10,为了让bck->fd = bin;
能正常运行,我们需要目标地址-0x10+0x18=目标地址+8指向一处可写的内存
Tcache stash unlink attack++
该操作和Tcache stash unlink attack+基本相同。这次我们把smallbin2的bk部分改为目标地址1-0x10,同时在目标地址1+8处写入目标地址2-0x10的地址,这样就能让目标地址1进入tcache,并且在目标地址2处写入一个不可控的大数(bin)