tl;dr
- Giving custom array size of NaN, passes checks while allowing OOB r/w
- Use OOB r/w to get libc, stack (environ) addresses
- Craft fake chunk on array and overwrite fastbin fd
- Reset machine to allocate register context on fake chunk
- Overwrite VM sp with real stack
- Push ropchain onto stack and halt VM to execute ropchain
Challenge Points: 996
No. of solves: 4
Challenge Author: k1R4
Challenge Description
The VM is only kawaii from the outside T_T
Handout has the kawaii_vm binary & ld, libc and libseccomp inside the lib folder
Initial Analysis
The binary is stripped when checked with file
command. Typical for a VM challenge.
Here is the checksec output:
1 | [k1r4@zg15 handout]$ checksec kawaii_vm |
All mitigations are enabled. Since libseccomp is included checking the binary with seccomp-tools reveals:
1 | line CODE JT JF K |
Only syscalls allowed that are relevant are open,read,write. So the goal would be to perform orw on flag file.
Running the binary it is seen that it intially checks if a custom array size is needed, if so it takes input in unit pages. Then it reads bytecode to be executed.
Source Code
Full src can be found here
I will be including only the important parts here. It is recommended to have the full src open while going through this writeup.
This VM has:
- Some general purpose registers
- A program counter
- A stack pointer
- An array pointer
Bytecode, stack are allocated a fixed size of 0x10000 bytes, whereas array can have maximum size of 0x10000 bytes.
1 |
|
The register context contains 4 general purpose registers, PC, SP and an AR (array pointer), and is allocated on the heap.
The instruction set can be found in full src. It doesn’t have any useful bugs, nor does the bytecode sanity checker.
The general instruction format for this VM goes something like:
1 | <INSTRUCTION> <DEST_REG> <SRC_REG1> <SRC_REG2> (for arithmetic instructions) |
Bug
The bug here is hidden in plain sight since its a niche one. My goal with this challenge was to demonstrate this cool trick.
1 | void get_kawaii_map() |
The scanf is taking in a float and the bounds are checked, however passing in NaN
as input bypasses these checks. I learnt this cool trick from Histogram - ACSC 2021 authored by ptr-yudai. The value of NaN
in memory is 0x8000000000000
. So when multiplied with 0x1000, array_size
would be 0x8000000000000000
which is clearly out of bounds.
Exploit Strategy
Now the get
and set
instructions can be used to achieve OOB read/write on the mmapped region which is meant to hold the bytecode, stack and array of the VM. Since mmapped regions are adjacent to libc, the primary attack vector is libc rw region which holds main_arena
, file structures and critical function pointers.
Only open,read,write is viable in this challenge, so overwriting function pointers or hijacking file structures wouldn’t be useful. This is where the reset
instruction comes into play. It updates the register context allocation without freeing the current one. So messing with the main_arena
could potentially let us control the register context after which we would have arbitrary r/w.
Strategy:
- Read libc base from ld rw using
get
- Read stack address from
environ
in libc rw usingget
- Store both values in array for later use, with
set
- Craft fake fastbin chunk on array by setting
size
andfd
- Since this challenge uses libc 2.36,
fd
has to be set to mangled ptr to the fake chunk - Use
reset
to allocate register context on array - Overwrite
sp
to point to the real stack - Craft orw ropchain with libc gadgets using previously saved libc base in array
- Use
push
instructions to put ropchain on the stack - Use
halt
to execute ropchain
Conclusion
Since this was my first time writing a VM challenge, I learnt a lot. Hope it was fun to solve this :D
You can find the full exploit here
Flag: bi0sctf{kawaii_vm_n0t_s0_k4wa1i_4ft3r_4ll_f97cf315ea3a}