tl;dr
- The VM takes a custom binary as input
- Binary contains function table, code and bss sections
- Code can overlap with bss and be modified at runtime
- The JIT compiler assumes that a function is safe since it ran many times
- Functions modified right before JIT bypass security checks
Challenge Points: 919
No. of solves: 13
Challenge Author: k1R4
Challenge Description
I fear no man, but that thing..., JIT, it scares me
Handout has the binary, dynamic libraries and the source code. The source code includes:
kowaiiVm.h
- The main header filekowaiiVm.cpp
- Implementation of the VM without JITkowaiiJitVm.cpp
- JIT Implementation of the VM inherited from the previous file
Initial Analysis
Mitigations
The binary has all mitigations that are expected:
1 | [k1r4@enderman handout]$ pwn checksec kowaiiVm |
Furthermore it uses seccomp to enable only certain syscalls
1 | line CODE JT JF K |
The flag will have to be obtained through open, read, write.
Implementation
The VM is implemented as a class kowaiiJitVm
which inherits kowaiiVm
, which itself contains a child class kowaiiCtx
as member. kowaiiCtx
has members struct kowaiiBin
and struct kowaiiRegisters
. The source code is quite lengthy, so only the necessary parts will be covered here.
Opcodes
List of opcodes present in the VM:
ADD
,SUB
,MUL
- Perform arithmetic on 2 registers and store result in thirdSHR
,SHL
- Shift a register based on 1 byte immidiate and store result in other registerPUSH
,POP
- push/pop register onto/from stackGET
,SET
- read/write register from/tobss
offset by 4 byte immidiateMOV
- move 4 byte immidiate into register, clearing upper 4 bytesCALL
- call a function based on 2 byte hashRET
- return from functionNOP
- no operation
Structs
1 | typedef struct __attribute__((__packed__)) kowaiiFuncEntry |
- Multiple counts of this struct make up the function table present in
kowaiiBin
- During function calls,
hash
of the callee is looked up in the table andaddr
is put in pc size
is used to know the end of a functioncallCount
is kept track of to know how hot a function is, in order to JIT it
1 | typedef struct __attribute__((__packed__)) kowaiiBin |
- Binary is supposed to have the header “KOWAII”
entry
contains the entrypoint for the binarymagic
is supposed to be0xdeadc0de
to pass verificationbss
contains the start of bssno_funcs
contains no of functions present in the function tablefunct
is an arrray of function entries of sizeno_funcs
1 | typedef struct __attribute__((__packed__)) kowaiiRegisters |
- The VM has 5 registers, [x0-x5] which in JITed code translate to [r10-r15]
- Furthermore it has a program counter, stack pointer and base pointer
1 | public: |
callStack
andcallStackBase
are used to keep track of function calls for JIT-ingjitBase
andjitEnd
are used to keep track of mapping created for JIT-ed code
Important Functions
1 | void *genAddr() |
This private method of kowaiiCtx
is used to generate address for mappings of kowaiiBin
. The seed is initialized properly, so the addresses aren’t guessable. All these addresses are low in memory, so there is no chance of OOB access to other memory regions.
During the initialization of kowaiiBin
, the function table addresses, entry
and bss
are patched so that they are offset from the base of the memory map of kowaiiBin
. This is done in prepareFuncTable()
and prepareCtx()
.
1 | void runVm() |
During execution of the interpreter, there are plenty of checks in checkState()
and then only it proceeds to executeIns()
, where the execution actually occurs. No obvious bugs here
1 | void virtual retFunc() |
kowaiiJitVm
overrides retFunc()
to catch hot functions as they return and JIT them. The condition for JIT-ing is:
- The function has to be called atleast
JIT_CC
times (10 by default) - The function has to be atleast of size
JIT_MS
(10 by default)
After a function is JIT-ed using jitGen()
, the address is updated with the respective JIT region address.
When its called next time through callFunc()
, the constraints for JIT-ing a function is checked again to see if it would’ve been JIT-ed. If so, the function is called using jitCall()
. jitCall()
seems complicated but its essentially doing the following
- Saves [r8-r15],
rbp
,rsp
,rcx
,rdi
,rdx
- Moves [x0-x5] from the register context into [r10-r15]
- Sets
rsp
to the stack from register context - Sets
rdx
to startbss
(used inSET
andGET
) - Calls
fe->addr
- Moves [r10-r15] into [x0-x5] in the register context
- Restores saved registers
Bug
The intended bug here is very subtle. There is no bounds check on pc
anywhere in the interpreter. This allows a function to start in code
section but overlap onto bss
. So the function can modify itself at runtime using GET
opcode.
There were some interesting unintended solutions that leveraged these bugs:
stackBalance
doesn’t work as intended with nested calls and overlapping functions- After JIT-ing
MUL
instruction could clobberrdx
, which was used as start ofbss
Exploit Strategy
- Create the target function at the end of
code
such that most of it overlaps withbss
- The function modifies itself at runtime using
SET
- Modification results in the
RET
at the end, considered as part of immidiate of previous instruction - Place unsafe instructions below the ret which will JIT-ed later, bypassing the checks of interpreter
- The function modifies itself at runtime using
- Create an entry function
- This function calls the target function
JIT_CC
+1 times - Restores the opcodes that were changed at runtime
- This is done to pass checks everytime the function runs until it is JIT-ed
- This function calls the target function
- Place “flag.txt” somewhere in bss
- The unsafe instructions at the end of target function do the following
- Use OOB
GET
to read function table (this->ctx.bin->funct
) to leakkowaiiBin
and JIT region addresses - Include ropgadgets as immidiate values in
MOV
instruction - Use leaks and offset to ropgadgets and “flag.txt”
- Construct ropchain in reverse using
PUSH
instructions - Add a
ret
finally to execute ropchain
- Use OOB
- The ropchain performs open, read, write to leak flag
Conclusion
I spent a lot of time making this challenge and it was a lot of fun. There were a few unintended bugs as mentioned earlier which have arguably cooler solutions that the one I envisioned. The bugs are actual errors I made when writing the code. I noticed the intended bug and decided to keep that and built my exploit around it. After this experience, I really understood how hard it is to write a safe JIT compiler. I find JIT really fascinating now :D
You can find the exploit here
Flag: bi0sctf{4ssump7i0ns_4r3nt_4lw4y5_tru3_811f079e}