2026 UoFCTF - caculator
Author: 堇姬 Naup
前言
前幾天 @consolebreak 跑來問我這題,看了一下好像蠻有趣的就來寫了一下,沒想到我就這樣卡住了兩天,最後還是解出來了,其中主要卡在最後拿到任意寫後,unordered map 內的 std::string 拿到了非法位置的 pointer,最後解構要去釋放時候會觸發 glibc 檢查導致 crash,以至於沒辦法打 IO_FILE,如果有人知道怎麼打 IO_FILE 在這題可以私訊我XD
總之好玩,最後找到可以用一個酷酷的地方來 get shell
Analyze source code
https://github.com/UofTCTF/uoftctf-2026-chals-public/tree/main/calculator
這是一個 C++ 的 binary pwn,有一個很神秘的通過 unordered map 來記憶 expression 的結果並去計算
其中的問題是傳入的 vector 是一個 pointer,但如果在過程中, vector 發生擴容,那就會用到舊的 address,導致 UAF
1 |
|
challenge
基本上就是通過 UAF 先去通過 result leak 資料,情境是 memo_s = eval(L, op, R, memo_vec[getMemo(L)], memo_vec[getMemo(R)]);,需要讓左邊剛好擴容時候,去算右邊,這樣右邊就會用到舊的,並且他要是第一個才能 leak fd/bk 上的 heap pointer
另外一邊則是先將 heap vector 一路往上加到 free 後會進入 unsorted bin,剩下的就是一樣的方法
要 UAF 寫入 free chunk 則需要剛好 memo_s 是舊的 pointer,排一下就行了,我們希望寫入一個 pointer 來打 tcache poinsoning,我選擇將他 allocate 到 tcache_pthread_struct 上,來控制更多 entry 下一個會 allocate 的 chunk (上面的需要去平衡一下 cnt,需要預先讓 std::string allocate 一些之後 reset 讓這些都被 free 到 tcache entry)
我們可以通過這樣做出兩次任意寫,但我們拿到一個非法位置 chunk 後,如果想打 IO_FILE,當我 exit,__run_exit_handler 會去從 initial 內拿出解構子來去釋放一些 pointer,其中就有非法 pointer 會導致 free 失敗
因此我選擇直接控制 fs_base + 0x30 上的 key,跟 initial 結構
https://elixir.bootlin.com/glibc/glibc-2.41/source/stdlib/exit.c#L108
1 | case ef_cxa: |
這邊將 fs_base 控制成都是 0,他會將 exit_function 上的 function pointer (0x10) 拿出來 xor fs:[0x30],之後右移 0x11,另外 arg 是 0x18,控制成 /bin/sh address 就可以在解構時候成功開 shell
上述的結構其實就是這兩個結構
https://elixir.bootlin.com/glibc/glibc-2.41/source/stdlib/exit.h#L55
1 | struct exit_function_list |
https://elixir.bootlin.com/glibc/glibc-2.41/source/stdlib/exit.h#L34
1 | struct exit_function |
這部分就是 assembly 那段 exit 的部分,rax 會指向 initial,最後解密 pointer 後去跳對應 function
1 | 0x76af98294240 <__run_exit_handlers+448>: mov rcx,QWORD PTR [rax+0x18] |
exploit
1 | from pwn import * |
ref
https://www.kn0sky.com/?p=d400e21d-6a46-4fa0-bce8-81a5a7328c83