技術討論 | Largebin attack漏洞利用分析
<div>
把以前學過的東西再複習總結一下,溫故而知新。由於相較其他heap,largebin 中的機制相對複雜,所以largebin attack 一直是CTF的堆漏洞利用常規而少見的一種,但是卻一直活着人們的心中。
背景知識
我們以64位,libc 2.27爲例,其實與2.23差不多,只不過2.27中,0×420以下堆塊會進入tcache,但是2.23中0×400就能進入largebin了
開始介紹large bin的相關機制。在此之前,請學習堆的申請與釋放等相關知識。
largebin 從哪裏來
在堆的機制中,free掉的chunk會進入bin,如果此chunk的大小大於fastbin,free後就會先進unsorted bin,之後,再次申請更大堆塊,unsorted bin無法滿足,heap就會進入large bin,那麼large bin的範圍是多少呢? 源碼告訴我們一切:源碼中可以看到,MIN_LARGE_SIZE是(64 – 0)*0×10 = 0×400,也就是最小的large bin。這裏的MALLOC_ALIGNMENT其實等於SIZE_SZ,即系統位數乘以2,64位系統即 0×8乘以2=0×10,32位爲0×4乘以2=0×8。 也就是說我們可以申請0×400的chunk,然後free掉進unsorted bin ,之後申請更大chunk,就可以將0×400的堆放入largebin。 注意:此時,我們使用的是libc 2.27,所以,其實0×400到不了largebin,而是tcache的範圍,所以,在libc 2.27中,一般使用0×420,但是如果可以將0×400的tcache填滿,也是可以申請到largebin的 那麼large bin 的index如何確定呢,這裏,系統採取了相對於fastbin和smallbin更加粗獷的方式:
large bin採取了分段的存儲,比如第一個範圍就是0×400到(48<<6),即0×400到0xc00,而其中,每個鏈表之間大小差爲(1<<6)=0×40,結果如下表:
index | size範圍 |
---|---|
64 | [0x400,0x440) 相差0x40 |
65 | [0x440,0x480)相差0x40 |
...... | ......相差0x40 |
96 | [0xc00,0xc40)相差0x40 |
97 | [0xc40,0xe00)相差0x1c0 |
98 | [0xe00,0x1000)相差0x200 |
...... | ......相差0x200 |
...... | ...... |
largebin 的內部
因爲largebin,一個bin內部並不是一個size,所以需要fd_nextsize與bk_nextsize將其串起來。 那麼largebin內部是如何佈置的呢: 首先fd_nextsize指向比他小的最大heap,而bk_nextsize指向比他大的最小的heap,最後將兩條鏈條首尾相連。而fd和bk和其原來的任務一樣,都是指向和其大小相同的堆塊。那麼,畫個草圖:那麼如果bin裏的大小都一樣的話,那麼第一個釋放的堆塊作爲此bin鏈的鏈首(這個和fastbin和tcache都不一樣),fd_nextsize與bk_nextsize都指向自己,其餘的大小相同的堆塊free的時候,fd_nextsize與bk_nextsize就都爲0了。 而fd和bk與原本作用一樣,指向上一個釋放的堆塊,但是,這裏的鏈頭始終爲第一個釋放的chunk。 做個小實驗,申請4個堆塊ABCD,AB大小爲0x450,CD大小爲0x460,如下圖:
其堆塊如下:
將其釋放至unsorted bin,釋放順序爲A B D C:
之後我們申請0x470堆塊,此時unsorted bin無法滿足,將ABCD放入large bin:
此時largebin內由fd和bk組成的鏈爲:
此時的fd_nextsize與bk_nextsize:
最後的示意圖即:
那麼large bin 是如何申請到的呢? 源碼中顯示: 首先通過申請的size找到index,進而找到指定的bin鏈 在找到bin鏈後,從最小的heap開始通過bk_nextsize找到第一個大於等於size的heap 在找到heap後,利用fd判斷下一個heap的size是否於當前heap的size,如果相等,將下一個heap返回,這樣可以減少設置鏈頭的操作 利用unlink取下得到的heap 判斷heap的size減去申請的size是否大於MINSIZE,如果大於就將其放入unsorted bin,如果小於,那麼直接分配給用戶 這就是largebin的分配過程,那麼largebin只有這一種被申請的方式嗎? 如果在申請堆塊的時候沒有找到合適heap的時候,會從topchunk開始申請,在此之前,程序會遍歷比當前index大的索引,如果在大於fastbin的範圍內,存在heap,就會從中切割申請的大小,當前heap的size減去申請的size是否大於MINSIZE,如果大於就將其放入unsorted bin,如果小於,那麼直接分配給用戶。 即,如果在上述情況下申請0x60,就會從B中分配0x60,餘下0x3e0歸入unsorted bin
這就是largebin在ptmalloc中的機制了。
large bin attack
那麼large bin attack又是怎麼進行的呢? 現在有兩種常見的利用方式: 在申請largebin的過程中,僞造largebin的bk_nextsize,實現非預期內存申請。 在largebin插入的過程中,僞造largebin的bk_nextsize以及bk,實現任意地址寫堆地址。申請時利用
在申請largebin的時候,從最小的heap開始,利用bk_nextsize尋找合適的heap,如果找到了合適的heap,就將其取出,那麼如果我們將bk_nextsize指向其他地方,就可以申請到其他地方。看下源碼:if ((victim = first (bin)) != bin && (unsigned long) chunksize_nomask (victim) >= (unsigned long) (nb)) { victim = victim->bk_nextsize; while (((unsigned long) (size = chunksize (victim)) < (unsigned long) (nb))) victim = victim->bk_nextsize; //尋找堆塊 ...... remainder_size = size - nb; unlink (av, victim, bck, fwd); //unlink所以只需要在目標地方符合unlink,那麼就可以通過檢查了。舉個例子:申請一個largebin,之後free,再申請一個大堆塊使之前堆塊進入large bin: 申請A和底下分割的0x20,之後freeA,申請B和C。
之後,我們開始利用,這裏提供兩種思路吧,但是其本質都是修改bk_nextsize。 直接將bk_nextsize指向B的頭 將bk_nextsize指向B的內容 第一種和第二種本質上一樣,但是第二種可以順便利用unlink,向bss段上寫一個bss段地址,這裏假設bss段上存着堆地址,如果PIE開啓了,就不好操作了,這裏我們關掉PIE,演示第二種漏洞利用。修改A的bk_nextsize,假設此時有UAF:也就是將
bk_nextsize = B+0x10
,而B+0x10一般存在bss段上,所以,此時B上僞造以一個的 fd = bss - 0x18
, bk= bss - 0x10
的堆塊,這都屬於unlink常規操作。注意僞造堆塊的fd_nextsize要等於0,因爲unlink有檢查。
之後注意對於僞造堆塊檢查,即檢查下一個堆塊的pre_size。最後得到:
此時申請large bin ,就會得到fake B的內存區域。
插入時利用
在申請大堆塊後,unsorted bin不滿足size要求,會將其放入large bin,此時,我們可以通過一些辦法,達到一次任意地址寫堆地址的目的,看代碼:victim_index = largebin_index (size); bck = bin_at (av, victim_index); //這個是main_arena的地址 fwd = bck->fd;//這是最大size的鏈首 /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size |= PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck->bk)); if ((unsigned long) (size) //bck->bk是最小size的鏈首 < (unsigned long) chunksize_nomask (bck->bk)) //如果當前申請的size小於最小szie { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;//這裏不好整 } else //如果當前申請的size不是最小的 { assert (chunk_main_arena (fwd)); while ((unsigned long) size < chunksize_nomask (fwd)) //從最大塊開始尋找一個小於szie的鏈 { fwd = fwd->fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) // 如果找到的鏈和申請的size相同 /* Always insert in the second position. */ fwd = fwd->fd; else//如果不同,則說明應該插在這個鏈前面 { victim->fd_nextsize = fwd;//小的鏈在victim上 victim->bk_nextsize = fwd->bk_nextsize;//這裏如果可以控制 fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim;//這裏能寫一個victim } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim; } mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;//最後這裏,還可以有一次寫,如果fwd->bk可控 可以看到,這段代碼中有兩次可以寫入victim的地方,由於其比較簡單,沒啥檢查,所以可以直接搞起: 搞到一塊large bin 然後搞到一塊更大的large bin,且其index一樣。在得到這個large bin之前,先改寫原本就在large bin中的堆塊的bk或bk_nextsize。 舉個例子: 首先申請3個chunk,free A堆塊,之後申請更大的chunk
此時A在large bin內,之後修改A的bk或bk_nextsize:
修改上述的bk和bk_nextsize,後續就可以達到在target1和target2中寫堆B地址的目的。 之後,free chunk B,然後申請大於等於0x480的堆塊,比如申請malloc(0x470),就可以達到漏洞利用目的了。