One Punch Man
題目是經典類選單題型,可以new/modify/show/delete:
- debut(new) — 可以分配
0x80–0x400
(small range),然後是用calloc給的 - modiry(rename) — 存在UAF
- show — 沒什麼特別的
- retire — 存在double free
題目保護為:NX + PIE + RELRO,使用libc-2.29!這已經是個我完全沒碰過的版本了…
另外還有個隱藏功能,可以malloc(0x217) 並read讀入,但會判斷tcache_perthread_struct(TPS)
上對應0x20大小的tcache count必須大於6才行.
但由於new功能限制的大小皆為small bin range,無法直接free掉一個fast chunk放進tache進而增加count,這時我腦中出現幾種思路:
- fastbin-dup想辦法要到一塊memory能overwrite TPS count.
- 同上,只是改用tcache-dup
- 同上,只是改用smallbin-dup (House of Lore)
- 用unsorted bin unlink attack把TPS count改成一個大值
- tcache stashing unlink attack把TPS count改成一個大值
首先第一個方法由於我們只能calloc(small chunk size),沒辦法要一塊fast chunk.
而第二個方法一開始我感覺可行,因為只要tcache還沒滿,那麼free掉fast/small chunk後都會先進tcache bin,但搞了很久才發現calloc不會從tcache拿…所以也放棄了這方法
第三個方法(貌似是其他多數人的做法?)後來沒有嘗試.
第四個方法也是一開始就想到,但是試了半天才發現unsorted bin unlink在libc 2.29已經新增了完整性檢查WTF(因為用docker架環境所以不知為何error message都沒收到,是後來把libc 2.27跟libc 2.29的malloc.c拿來diff一下才發現的QQ)。
於是我想到了第五種作法,這大概是去年libc 2.27剛出的時候我在啃code時無聊想到的打法XD不過由於限制很多,再加上他效果跟unsorted bin attack有87%像,所以也沒認真當一回事,場景是這樣的:
TCACHE STASHING UNLINK ATTACK
當要了一塊small chunk後,此時會遍歷跟這塊大小一樣的small bin上看有沒有其他free chunk,如果有就把它擺到tcache bin上(就是最近用到的放在快取上的概念),看似簡單的一句話其實是要符合挺多條件的
- 『當要了一塊small chunk』,換句話說你起碼要有一塊small chunk在這個bin裡面
- 『當要了一塊small chunk』,所以tcache bin必須是空的?否則正常來說tcache優先權是最高的,通常tcache有就會先從那邊拿
(又或是有什麼情況可以讓他跳過tcache來找small bin?) - 『有其他free chunk』,換句話說前面先拿了一個small chunk走後,還要有其他free chunk,代表起碼要有2個。
- 『如果有就把它擺到tcache bin』,也就是說tcache bin必須還有空間。
UNLINKs
而這個情況會發生兩種不同的unlink
- 要了一塊small chunk — 從small bin unlink出去,會做完整性檢查。
2. 把剩餘在small bin的chunk unlink到tcache bin中,不會做檢查。
而上面這個bck->fd = bin
就是關鍵,這個bin是一個libc中的位址,所以會是0x7f??????????
所以如果能控制這個bck,就能在任意位址寫入這個值。
*(bck+0x18) = 0x7f??????????
其實效果真的跟unsorted bin attack差不多,只是要符合挺多條件的,在本題的情況無非就是要改掉TPS count,調整一下讓count欄位被改為0x7f
即可。
然後有發現上面好像有三條很矛盾的狀況嗎?
- 『當要了一塊small chunk』,換句話說你起碼要有一塊small chunk在這個bin裡面
- 『當要了一塊small chunk』,所以tcache bin必須是空的?否則正常來說tcache優先權是最高的,通常tcache有就會先從那邊拿
- 『如果有就把它擺到tcache bin』,也就是說tcache bin必須還有空間。
正常來說free chunk只有在tcache bin滿了之後才會進small bin,第一條情況告訴我們起碼有一個small chunk,那通常代表tcache bin滿了,但第二第三條情況就衝突到了,怎麼辦?
GOOD OLD LAST REMAINDER
這時可以利用unsorted bin中的last remainder,例如以下
[1] A = calloc(1, 0x400); free(A); *7
[2] A = calloc(1, 0x400); free(A);
[3] A = calloc(1, (0x400-0x100));
[4] calloc(1, 0x400);
[1] 塞滿tcache idx_0x400 bin
[2] 此時會進入到unsorted bin之中
[3] 要一塊(0x400–0x100),由於calloc不會從tcache拿,所以直接從unsorted bin拿走,切剩下的那塊last remainder為0x100
[4]此時要一塊比0x100還大的chunk,由於last remainder大小不夠,就會直接被送進small bin裡面。
就只是利用remainder不會進tcache的特性而已。
EXPLOITATION
- put (TCACHE_FILL_COUNT-2) chunks to tcache idx_0x100, reserve 2 chunk space for later use
- fill tcache idx_0x400
- leak heap address from tcache chunk
- leak libc address from unsorted chunk
- put 2 small chunks (remainder) into bin 0x100 (FIFO)
- Use UAF to overwrite victim.bk to &TPS.
- call calloc for 0x100, which would fire up tcache stashing.
- trigger unsafe unlink,
bck->fd
=*(victim+0x18)
=av.bin
, overwriting TPS count by0x7f??????????
- TPS.count > 6 satisfied, we can use malloc(0x217) now.
- Do tcache-dup, overwrite __malloc_hook
- ROP
這邊要注意的是第一個步驟我們必須預留剛好1個空間,這樣victim被unlink進來之後剛好tcache滿了,不再繼續觸發tcache stashing,否則下次bck = tc_victim->bk = TPS->bk
,由於我們無法控制TPS.bk的值,所以多
半會Segmentation Fault.
最後ROP的部分就不多談了,最後選用__malloc_hook是因為stack遠處會有可控buffer,所以第一個gadget只需要add rsp, X ; ret
再來就可以舒服ROPing了。
NOTE
真的太久沒玩大比賽的PWN了,中間踩了很多雷都是看了libc source code之後才找出問題,然後不得不大推gdb套件GEF在HEAP這方面的支援,我都不記得他有這麼好用…
然後事後看了官方的write-up才發現我踩了一個很智障的坑,Ghidra裡面不曉得為什麼把6表示成’\a’,我一個眼殘以為是0xa,想說tcache count最多就到7個而已,算是繞了一個遠路?不過既然都解了那還是來分析一下Tcache Stashing Unlink Attack的優點與特點吧
- 不需要偽造chunk結構(例如House of Lore就需要)
- 只需能夠控制victim.bk即可
- 在無法leak heap位址時也能派上用場,可以透過partial overwrite victim.bk (正常來說上面是某個heap address指向另一個chunk),
例如原本victim.bk為0x????????1520
,透過overwrite使其變為0x????????00XX
,這個位址通常來說有1/16機率位在TPS上(1 byte為ASLR,後3 bytes000
為heap開頭),此時可overwrite TPS.count(如本題例子),或甚至可以改掉某tcache entry指標,當下一次malloc從tcache拿取時即可拿到位址為av.bin的chunk,進而overwrite上面的東西。 - 在新版libc裡面unsorted bin attack的繼承者(?)
當然缺點也很多,主要是必須能精準控制tcache bin chunks與small bin chunks的數量,上面也提了很多限制了就不再贅述。