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

b3rm1nG
9 min readOct 16, 2019

--

這次這題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必須還有空間。

UNLINKs

而這個情況會發生兩種不同的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滿了,但第二第三條情況就衝突到了,怎麼辦?

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

  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的數量,上面也提了很多限制了就不再贅述。

--

--