HITCON CTF 2019 Quals — One Punch Man [PWN 292pts]

berming
berming
Oct 16, 2019 · 9 min read

這次這題One Punch Man是我在HITCON 2019 Quals唯一解出的一題PWN題(竟然連假期間辦比賽!實在是靜不下心來玩XD),由於今年開始工作之後就沒那麼常碰CTF了,如果有玩的話也都是玩Web & Reverse題型,所以一直沒更新我的Heap Exploit知識,不得不一直回去翻libc的source code…總之記錄一下這題write-up.


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,這時我腦中出現幾種思路:

  1. fastbin-dup想辦法要到一塊memory能overwrite TPS count.
  2. 同上,只是改用tcache-dup
  3. 同上,只是改用smallbin-dup (House of Lore)
  4. 用unsorted bin unlink attack把TPS count改成一個大值
  5. 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必須還有空間。

而這個情況會發生兩種不同的unlink

  1. 要了一塊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滿了,但第二第三條情況就衝突到了,怎麼辦?

這時可以利用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

  1. put (TCACHE_FILL_COUNT-2) chunks to tcache idx_0x100, reserve 2 chunk space for later use
  2. fill tcache idx_0x400
  3. leak heap address from tcache chunk
  4. leak libc address from unsorted chunk
  5. put 2 small chunks (remainder) into bin 0x100 (FIFO)
  6. Use UAF to overwrite victim.bk to &TPS.
  7. call calloc for 0x100, which would fire up tcache stashing.
  8. trigger unsafe unlink, bck->fd = *(victim+0x18)=av.bin, overwriting TPS count by 0x7f??????????
  9. TPS.count > 6 satisfied, we can use malloc(0x217) now.
  10. Do tcache-dup, overwrite __malloc_hook
  11. 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的優點與特點吧

  1. 不需要偽造chunk結構(例如House of Lore就需要)
  2. 只需能夠控制victim.bk即可
  3. 在無法leak heap位址時也能派上用場,可以透過partial overwrite victim.bk (正常來說上面是某個heap address指向另一個chunk),
    例如原本victim.bk為0x????????1520,透過overwrite使其變為
    0x????????00XX ,這個位址通常來說有1/16機率位在TPS上(1 byte為ASLR,後3 bytes 000為heap開頭),此時可overwrite TPS.count(如本題例子),或甚至可以改掉某tcache entry指標,當下一次malloc從tcache拿取時即可拿到位址為av.bin的chunk,進而overwrite上面的東西。
  4. 在新版libc裡面unsorted bin attack的繼承者(?)

當然缺點也很多,主要是必須能精準控制tcache bin chunks與small bin chunks的數量,上面也提了很多限制了就不再贅述。

berming

Written by

berming

Security Consultant & CTF lover :)

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade