堆利用在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)