This post shows how to reverse a niteCTF-22 Rust VM challenge.
There are two files vm (Rust Vitual Machine) and encrpyt (Program File containing instructions). Running the binary, it asks for a input, prints ‘\n’ and exits.
Analyzing Binary in IDA
Loading up the binary in IDA, there is a main function which reads the content of the encrypt file into an array mem, and calls run_loop. 
The run_loop calls process_opcode, and program runs till pc doesn’t become 0.
1
2
3
4
r-> register array (total 8 registers)
mem -> memory
cflag -> jump flag
pc -> program counter

The process_opcode function interprets instructions according to the op_code and returns new pc after executing the instruction. 
I like to remove all the panic and overflow conditions from the decompiled functions, it makes my eyes bleed. So here is what the process_opcode function looks like.
Reversing process_opcode the various OP codes can be summarised as:
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
0x10 - 0x17 // 2 byte instruction
a b
load into register ---->
reg[a & 0xf] = b
eg: 0x11 0xa3 instruction -> reg[1] = 0xa3
0x18 - 0x1f // 3 byte instruction
a b c
reg[a & 0xf] = mem[(b<<8)+c]
0x20 - 0x27 // 3 byte instruction
a b c
load to memory
mem[c+(b<<8)] = register[a & 0xf]
0x80 // 3 byte instruction
a b c
print data from mem
prints from mem[(b<<8)+c] till it encounters a null byte
0x81 // 3 byte instruction
get input and store in mem[(b<<8)+c]
0x90 // 1 byte instruction
nop(no operation)
'0' // 2 byte instruction
'0' 0x(c)(d)
register[c] = register[d]
'1' // 2 byte instruction
'1' 0x(c)(d)
register[c] = ~(register[c] & register[d])
'2' // 2 byte instruction
'2' 0x(c)(d)
if register[c] ==register[d]
cflag = 1
'@' // 2 byte instruction
'@' 0x(c)(d)
reg[c] = reg[c] >> (mem[0xcd]& 0xF)
'A' // 2 byte instruction
'A' 0x(c)(d)
reg[c] = reg[c] << (mem[0xcd]& 0xF)
'P' // 3 byte instruction
'P' b c
if(cflag == 1)
cflag = 0
pc +=3
else:
pc = ((b<<8)+c)
Invalid op_code
print UNRECOGNIZED OPCODE: {opcode}! and exit
Analysing the encrypt file
Printing every instruction in a new line for better visiblity. All the instructions are here
1
2
3
4
5
6
7
8
9
10
17 00 // reg[7] = 0
16 01 // reg[6] = 1
81 b0 00 // reads input from std:io and stroes it into mem[((0xb0)<<8) + 0x00]
10 59 // reg[0] = 'Y' (0x59)
20 c0 00 // mem[((0xc0)<<8) + 0x00] = reg[0]
10 6f // reg[0] = 'o' (0x6f)
20 c0 01 // mem[((0xc0)<<8) + 0x01] = reg[0]
10 75 // so from mem[(0xc0)<<8)] onwards it stores 'You got it right!'
20 c0 02 // which gets printed when we enter the correct flag.(80 C0 00 last instruction in the file)
...
Further instructions loads a character from our input into in reg[0], some number in reg[1] , operates on it and checks whether it is equal or not to some hardcoded value.
1
2
3
4
5
6
7
8
9
10
...
18 b0 00 reg[0] = mem[(0xb0 << 8)+0x00]
11 a3 reg[1] = 0xa3
30 20 reg[2] = reg[0]
30 31 reg[3] = reg[1]
31 01 reg[0] = ~(reg[0] & reg[1])
30 10 reg[1] = reg[0]
31 20 reg[2] = ~(reg[2] & reg[0])
31 31 reg[3] = ~(reg[2] & reg[0])
...
1
2
3
4
...
32 12 if (reg[1]==reg[2]) cflag=1
50 00 00 if (cflag!=1) pc = 0 and the program exits
...
Basically there is a character-by-character comparison and the program terminates as soon as it encounters a wrong character, I used this vulnerability to solve this challenge.
I opened hex editor, edited the 50 00 00 instruction to invalid instruction 88 00 00, now one by one went on correcting the instruction back and forth, side by side brute forcing characters with a simple python script, so whenever I got a correct character program returns UNRECOGNIZED OPCODE and I know that previous entered character is correct.
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
import string
flag = "nite{"
for i in string.printable:
p = process("./vm")
p.sendline(flag+i)
x = p.recv()
if len(x)>3:
print(flag+i)
break
And the flag is nite{n4nd_1s_un1v3rs4l_g4te}.