VaultVM
Mr. Smith has designed a digital vault for his important files and passwords. Seeing how many ASICs are being built to mine crypto he started questioning the strength of cryptographic hash algorithms so he decided to implement his own way to store the master password. Now he asks you to test it, offering a substantial reward if you can break in.
Author: avlad171
At first I didn’t want to reverse engineer the virtual machine and tried to cheese it with symbolic execution. But my tool could not propagate symbolic expressions for some reason. After that I changed symbolic engine to taint engine with pointer propagation and got some results:
address instruction memory operand tainted register r/w value
0xbaa: movzx eax, byte ptr [rax] 0x9fffff90 rax:64 bv[63..0] 0x58
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff38 rax:64 bv[63..0] 0x58
0xb2b: mov rdx, qword ptr [rsi + rcx*8] 0x9fffff38 rdx:64 bv[63..0] 0x58
0xb32: add qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0x58
0xba0: mov rax, qword ptr [rsi + rax*8] 0x9fffff70 rax:64 bv[63..0] 0x204638
0xbaa: movzx eax, byte ptr [rax] 0x204638 rax:64 bv[63..0] 0xa8
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff28 rax:64 bv[63..0] 0xa8
0xb2b: mov rdx, qword ptr [rsi + rcx*8] 0x9fffff28 rdx:64 bv[63..0] 0xa8
0xb32: add qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0xa8
0xba0: mov rax, qword ptr [rsi + rax*8] 0x9fffff70 rax:64 bv[63..0] 0x212788
0xbaa: movzx eax, byte ptr [rax] 0x212788 rax:64 bv[63..0] 0xa8
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff20 rax:64 bv[63..0] 0xa8
0xa3e: imul rax, qword ptr [rdx] 0x9fffff20 rax:64 bv[63..0] 0xa800
0xa42: mov qword ptr [rdx], rax 0x9fffff20 rax:64 bv[63..0] 0xa800
0xbaa: movzx eax, byte ptr [rax] 0x9fffff91 rax:64 bv[63..0] 0x2d
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff38 rax:64 bv[63..0] 0x2d
0xb2b: mov rdx, qword ptr [rsi + rcx*8] 0x9fffff38 rdx:64 bv[63..0] 0x2d
0xb32: add qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0x2d
0xba0: mov rax, qword ptr [rsi + rax*8] 0x9fffff70 rax:64 bv[63..0] 0x20e80d
0xbaa: movzx eax, byte ptr [rax] 0x20e80d rax:64 bv[63..0] 0xed
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff28 rax:64 bv[63..0] 0xed
0xc37: mov rdx, qword ptr [rsi + rdx*8] 0x9fffff20 rdx:64 bv[63..0] 0x21cee0
0xc3b: mov qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0x21cee0
0xb2b: mov rdx, qword ptr [rsi + rcx*8] 0x9fffff28 rdx:64 bv[63..0] 0xed
0xb32: add qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0xed
0xba0: mov rax, qword ptr [rsi + rax*8] 0x9fffff70 rax:64 bv[63..0] 0x21cfcd
0xbaa: movzx eax, byte ptr [rax] 0x21cfcd rax:64 bv[63..0] 0x95
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff20 rax:64 bv[63..0] 0x95
0xa3e: imul rax, qword ptr [rdx] 0x9fffff20 rax:64 bv[63..0] 0x9500
0xa42: mov qword ptr [rdx], rax 0x9fffff20 rax:64 bv[63..0] 0x9500
0xbaa: movzx eax, byte ptr [rax] 0x9fffff92 rax:64 bv[63..0] 0x4d
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff38 rax:64 bv[63..0] 0x4d
0xb2b: mov rdx, qword ptr [rsi + rcx*8] 0x9fffff38 rdx:64 bv[63..0] 0x4d
0xb32: add qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0x4d
0xba0: mov rax, qword ptr [rsi + rax*8] 0x9fffff70 rax:64 bv[63..0] 0x20582d
0xbaa: movzx eax, byte ptr [rax] 0x20582d rax:64 bv[63..0] 0xbd
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff28 rax:64 bv[63..0] 0xbd
0xc37: mov rdx, qword ptr [rsi + rdx*8] 0x9fffff20 rdx:64 bv[63..0] 0x21bbe0
0xc3b: mov qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0x21bbe0
0xb2b: mov rdx, qword ptr [rsi + rcx*8] 0x9fffff28 rdx:64 bv[63..0] 0xbd
0xb32: add qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0xbd
0xba0: mov rax, qword ptr [rsi + rax*8] 0x9fffff70 rax:64 bv[63..0] 0x21bc9d
0xbaa: movzx eax, byte ptr [rax] 0x21bc9d rax:64 bv[63..0] 0x52
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff20 rax:64 bv[63..0] 0x52
0xa3e: imul rax, qword ptr [rdx] 0x9fffff20 rax:64 bv[63..0] 0x5200
0xa42: mov qword ptr [rdx], rax 0x9fffff20 rax:64 bv[63..0] 0x5200
0xbaa: movzx eax, byte ptr [rax] 0x9fffff93 rax:64 bv[63..0] 0x41
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff38 rax:64 bv[63..0] 0x41
0xb2b: mov rdx, qword ptr [rsi + rcx*8] 0x9fffff38 rdx:64 bv[63..0] 0x41
0xb32: add qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0x41
0xba0: mov rax, qword ptr [rsi + rax*8] 0x9fffff70 rax:64 bv[63..0] 0x210421
0xbaa: movzx eax, byte ptr [rax] 0x210421 rax:64 bv[63..0] 0x1d
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff28 rax:64 bv[63..0] 0x1d
0xc37: mov rdx, qword ptr [rsi + rdx*8] 0x9fffff20 rdx:64 bv[63..0] 0x2178e0
0xc3b: mov qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0x2178e0
0xb2b: mov rdx, qword ptr [rsi + rcx*8] 0x9fffff28 rdx:64 bv[63..0] 0x1d
0xb32: add qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0x1d
0xba0: mov rax, qword ptr [rsi + rax*8] 0x9fffff70 rax:64 bv[63..0] 0x2178fd
0xbaa: movzx eax, byte ptr [rax] 0x2178fd rax:64 bv[63..0] 0x6f
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff20 rax:64 bv[63..0] 0x6f
0xa3e: imul rax, qword ptr [rdx] 0x9fffff20 rax:64 bv[63..0] 0x6f00
0xb32: add qword ptr [rsi + rax*8], rdx 0x9fffff70 rdx:64 bv[63..0] 0x18
0xba0: mov rax, qword ptr [rsi + rax*8] 0x9fffff70 rax:64 bv[63..0] 0x2195f8
0xbaa: movzx eax, byte ptr [rax] 0x2195f8 rax:64 bv[63..0] 0x87
0xbad: mov qword ptr [rsi + rdx*8], rax 0x9fffff20 rax:64 bv[63..0] 0x87
0xa3e: imul rax, qword ptr [rdx] 0x9fffff20 rax:64 bv[63..0] 0x8700
Now it somewhat doable, but after a few hours I gave up on this because it became way more complicated. So let’s start reverse engineering a virtual machine.
The binary cointains a virtual machine with 16 virtual registers and 28 instructions. The registers are decoded as follows:
r = bytecode[pc++];
reg_dst = (r & 0xF0) >> 4;
reg_src = r & 0xF;
The instruction set is rather simple:
VM_MOV
VM_XOR
VM_AND
VM_OR
VM_ADD
VM_SUB
VM_NOT
VM_ADD_IMM_8
VM_ADD_IMM_16
VM_ADD_IMM_32
VM_ADD_IMM_64
VM_MUL
VM_MUL_IMM_8
VM_MUL_IMM_16
VM_MUL_IMM_32
VM_MUL_IMM_64
VM_LOAD_8
VM_LOAD_16
VM_LOAD_32
VM_LOAD_64
VM_STR_8
VM_STR_16
VM_STR_32
VM_STR_64
VM_JNZ
VM_NOP
VM_EXIT
The dispatcher decrypts next handler and jumps to it:
movzx eax, al
movsxd rax, dword ptr [r8+rax*4]
xor eax, 13371337h
not eax
movsxd rax, eax
add rax, r8
jmp rax ; switch jump
Before vm enter
, the registers are intialized with some values.
I used miasm
to lift bytecode into my IR and then into miasm IR
for further optimizations.
lifting to miasm IR
is very simple:
def xor(ir, instr, arg):
e = []
src1, src2 = decode_regs(arg)
result = src1 ^ src2
e += [ExprAssign(src1, result)]
return e, []
def sub(ir, instr, arg):
e = []
dst, src = decode_regs(arg)
e += [ExprAssign(dst, dst - src)]
return e, []
So after all of this, I can now generate IRcfg of virtualized function:
This code even has opaque predicate at loc_4
!
Now we can use miasm standard features to optimize ircfg:
ira = machine.ira(loc_db)
ircfg = ira.new_ircfg_from_asmcfg(asmcfg)
loc = loc_db.get_offset_location(addr)
simp = IRCFGSimplifierCommon(ira)
simp.simplify(ircfg, loc)
save_ircfg(ircfg, "ircfg.dot")
And easily translate it into original non virtualized function:
uint8_t *input;
uint8_t *flag;
uint8_t *sbox;
uint8_t *matrix1;
uint8_t *matrix2;
int main(){
uint8_t check = 1, t1, t2, t3;
for (int i = 0; i < 39; i++) {
uint8_t out = 0;
for (int j = 0; j < 39; j++) {
t1 = sbox[j];
t2 = matrix1[t1][input[j]]
out = matrix2[out][t2];
}
if (out != flag[i])
check = 0;
sbox += 39;
}
}
So now we know why my symbolic tracer failed to propagate expressions. Our input is used as an index in the array. If we look closely at matrix1 and matrix2 it will become clear that matrix1
is a multiplication matrix and matrix2
is a addition matrix. With that in mind, our source code will look like this:
uint8_t *input;
uint8_t *flag;
uint8_t *sbox;
int main(){
uint8_t check = 1;
for (int i = 0; i < 39; i++) {
uint8_t out = 0;
for (int j = 0; j < 39; j++) {
out += sbox[j] * input[j];
}
if (out != flag[i])
check = 0;
sbox += 39;
}
}
Now, it is a system of 39 equations and can be solved with sage in a few minutes. But I used z3 and got a flag in under an hour.
Here is the final solution:
from z3 import *
import sys
known = [ord(i) for i in 'X-MAS{']
def main():
raw = bytearray(open('vault', 'rb').read())
sbox = 0x020E0
flag = 0x020A0
s = Solver()
input = list()
for i in range(39):
c = BitVec('key_%d' % i, 8)
input.append(c)
s.add(c > 32, c < 127)
for i, v in enumerate(known):
s.add(input[i] == v)
s.add(input[-1] == ord('}'))
for j in range(39):
out = 0
for i in range(39):
out += raw[sbox + i] * input[i]
s.add(out & 0xff == raw[flag+j])
sbox += 39
if s.check() == sat:
m = s.model()
flag = ''
for i in range(39):
flag += chr(m[input[i]].as_long())
print(flag)
if __name__ == "__main__":
sys.exit(main())
And we get the flag!
X-MAS{m0dulO_eQuatioN_sYst3ms_arE_cO0L}
The end!