MalwareTech vm1 challenge

write up for vm1 challenge

What is this about?

This is an unoficial write up of the vm1 challenge, it is part of the malware reversing challenges by MalwareTech, the list of challenges can be found here

Solving vm1 challenge:

Note: Dynamic execution isn’t allowed for this challenge. We will have to build our own vm interepreter.

let’s go ahead and download vm1.zip and decompress it, we should get vm1.exe which is the main executable:

shasum -a 256 vm1.exe d66f0b939819183614b1c64da9a99cb714639326415d1a6c066ca306d6d25b96

and ram.bin which is the virtual machine memory dump:

shasum -a 256 ram.bin 1ad4c6324b35a0090a6bcb23a31fbe3d3cbf80390292eb609cbacc8f54e06740

Opening vm1, in IDAPro we can see a regular 32 bits PE file, having the following disassembly at the entry point:

runit-disasembly

This code allocates 0x1FB of data into the heap and copy in it data located at 0x404040, calls a function sub_4022EO, After that it calls MD5::digestString to calculate the MD5 hash of the copied data, and print it into a windows MessageBoxA.

let’s look at the function sub_4022EO:

vm-parser

This function is basically a virtual machine bytecode disptacher, it will fetch the vm bytcode instructions and then calls the function at sub_402270 which is the virtual machine handler.

the vm handler at sub_402270 function, will basically execute the byte code instructions, in other words, it will decode the vm bycode into something meaningful, same as as CPU processor will do (this can be anything, arithmetic operations, some variable affectation, etc.. )

vm-interpreter

Ok! Now that we have a basic understanding of what is going on, let’s dig deeper into both the vm dispatcher and the vm handler.

First lets have a look at what is has been copied in memory, we can find the content of this data at 0x404040: vm-bytecode

Nothing interesting a bunch of meaningless vm bytecode.. of course IDA pro cannot disassemble it, we will have to write our own “disassembler” for this bytecode.

Note: The content of the data at 0x404040 and the memory dump file: ram.bin are the same. We can solve this challenge either by interpreting ram.bin vm memory or by interpreting 0x404040 data, the result will be the same. We will the choose the latter, and we will write an IDAPython script to solve this challenge and dump the flag directly into IDA console.

Looking at the vm dispatcher, we can see it reads 3 consecutives bytes, using a counter incremented after each read, and calls the vm handler with the 3 bytes passing them as arguments:

vm_dispatcher

The 3 bytes are vm data that actually represent the following : opcode operand1 operand2

We concluded that by looking at the vm handler sub_4022EO. The vm handler will usually have a switch case and will branch based on the opcode value that can be : 0x1, 0x2 or 0x3

vm-interpreter

as seen about this vm have only 3 instructions (or opcodes), here is what each opcode does, based on the disassembly above:

  • opcode 0x1: This will instruct the CPU to copy the value operand2, into an address pointed by the operand1. In x86 assembly this could be the following :

     mov eax, operand1
     mov [eax], operand2

  • opcode 0x2: This will instruct the CPU to copy the value pointed by operand1, into an address in the heap (like a global variable for example) :

     mov eax, operand1
     mov byte_global, [eax]

  • opcode 0x3: This will instruct the CPU to XOR the value pointed by operand1, with the previously stored global variable value, and store the result again into the address pointed by operand1:

     mov eax, operand1
     xor byte_global, [eax]
     mov [operand1], byte_global

And that’s pretty much it. We can easily write this in IDAPython as following:

# Author : taha@tephracore.com
# copyright 2019 - All rights reserved

from idaapi import *

xor_key = 0 # initializing xor key with zero will have no effect on the result

def one(operand1, operand2, vm_offset):
	PatchByte(vm_offset+operand1, operand2)
	print "mov eax, %#x" % (vm_offset+operand1)
	print "mov [eax], %#x" % operand2
	return "opcode: 1"

def two(operand1, operand2, vm_offset):
	global xor_key
	xor_key = Byte(vm_offset + operand1)
	print "mov byte_global, %#x" % xor_key	
	return "opcode: 2"
	
def three(operand1, operand2, vm_offset):
	print "mov eax, %#x" % (vm_offset+operand1)
	print "xor %#x, [eax]" % xor_key
	res = xor_key ^ Byte(vm_offset + operand1)
	print "mov [eax], %#x" % res
	PatchByte(vm_offset+operand1, res)
	return "opcode: 3"

def vm_interpreter(code, operand1 , operand2, vm_offset):
	
	opcode = {
		1: one,
		2: two,
		3: three
	}
	
	callback = opcode.get(code, lambda operand1, operand2, vm_offset: "invalid opcode")

	return callback(operand1, operand2, vm_offset)

def parse_vm_bytecode(vm_offset, size):
	jmp = 0xff # vm virutal address bytecode offset 
	vm_code = vm_offset + 0xff
	counter = 0
	
	while(counter <= size - 3 - 0xff):
			opcode = Byte(vm_code+counter)
			counter += 1
			operand1 = Byte(vm_code+counter)
			counter += 1
			operand2 = Byte(vm_code+counter)
			vm_interpreter(opcode, operand1, operand2,vm_offset)
			counter += 1

def print_flag(vm_offset):
	print GetString(vm_offset, -1, ASCSTR_C)

def decode_vm(vm_offset, size):
	parse_vm_bytecode(vm_offset, size)
	print_flag(vm_offset)

decode_vm(0x404040, 0x1fb) #call decode_vm (start of vm, size of vm)

This IDAPython script can be found in our github here.

if we run the vm code, the flag will be written at the start of the VM code at 0x404040 and we will print it into the IDA console directly:

-----------------------------------------------------------------------------------------
Python 2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 20:53:40) [MSC v.1500 64 bit (AMD64)] 
IDAPython v1.7.0 final (serial 0) (c) The IDAPython Team <idapython@googlegroups.com>
-----------------------------------------------------------------------------------------
Using FLIRT signature: SEH for vc7-14
Propagating type information...
Function argument information has been propagated
The initial autoanalysis has been finished.
mov eax, 0x40405d
mov [eax], 0xbd
mov eax, 0x404045
mov [eax], 0x53
mov eax, 0x404052
mov [eax], 0x48
mov eax, 0x404050
mov [eax], 0xe6
mov eax, 0x404053
mov [eax], 0x8a
mov eax, 0x40404d
mov [eax], 0x47
mov eax, 0x404056
mov [eax], 0x13
mov eax, 0x40404a
mov [eax], 0x15
mov eax, 0x404040
mov [eax], 0x98
mov eax, 0x404042
mov [eax], 0x3c
mov eax, 0x404058
mov [eax], 0xd9
mov eax, 0x40405a
mov [eax], 0x57
mov eax, 0x404046
mov [eax], 0xab
mov eax, 0x40405b
mov [eax], 0xc6
mov eax, 0x404041
mov [eax], 0x32
mov eax, 0x404057
mov [eax], 0x20
mov eax, 0x404055
mov [eax], 0x6f
mov eax, 0x404051
mov [eax], 0x2d
mov eax, 0x404048
mov [eax], 0xc9
mov eax, 0x404049
mov [eax], 0xe7
mov eax, 0x404043
mov [eax], 0x12
mov eax, 0x40404c
mov [eax], 0x2f
mov eax, 0x40404e
mov [eax], 0x88
mov eax, 0x404059
mov [eax], 0x6c
mov eax, 0x404044
mov [eax], 0x65
mov eax, 0x40405e
mov [eax], 0xae
mov eax, 0x404054
mov [eax], 0x59
mov eax, 0x40405f
mov [eax], 0x91
mov eax, 0x40405c
mov [eax], 0x5d
mov eax, 0x40404f
mov [eax], 0xae
mov eax, 0x40404b
mov [eax], 0x15
mov eax, 0x404047
mov [eax], 0xcc
mov byte_global, 0xde
mov eax, 0x404040
xor 0xde, [eax]
mov [eax], 0x46
mov byte_global, 0x7e
mov eax, 0x404041
xor 0x7e, [eax]
mov [eax], 0x4c
mov byte_global, 0x7d
mov eax, 0x404042
xor 0x7d, [eax]
mov [eax], 0x41
mov byte_global, 0x55
mov eax, 0x404043
xor 0x55, [eax]
mov [eax], 0x47
mov byte_global, 0x1e
mov eax, 0x404044
xor 0x1e, [eax]
mov [eax], 0x7b
mov byte_global, 0x5
mov eax, 0x404045
xor 0x5, [eax]
mov [eax], 0x56
mov byte_global, 0xe6
mov eax, 0x404046
xor 0xe6, [eax]
mov [eax], 0x4d
mov byte_global, 0x9f
mov eax, 0x404047
xor 0x9f, [eax]
mov [eax], 0x53
mov byte_global, 0xe4
mov eax, 0x404048
xor 0xe4, [eax]
mov [eax], 0x2d
mov byte_global, 0xa6
mov eax, 0x404049
xor 0xa6, [eax]
mov [eax], 0x41
mov byte_global, 0x47
mov eax, 0x40404a
xor 0x47, [eax]
mov [eax], 0x52
mov byte_global, 0x50
mov eax, 0x40404b
xor 0x50, [eax]
mov [eax], 0x45
mov byte_global, 0x2
mov eax, 0x40404c
xor 0x2, [eax]
mov [eax], 0x2d
mov byte_global, 0x1
mov eax, 0x40404d
xor 0x1, [eax]
mov [eax], 0x46
mov byte_global, 0xc7
mov eax, 0x40404e
xor 0xc7, [eax]
mov [eax], 0x4f
mov byte_global, 0xfc
mov eax, 0x40404f
xor 0xfc, [eax]
mov [eax], 0x52
mov byte_global, 0xcb
mov eax, 0x404050
xor 0xcb, [eax]
mov [eax], 0x2d
mov byte_global, 0x60
mov eax, 0x404051
xor 0x60, [eax]
mov [eax], 0x4d
mov byte_global, 0x9
mov eax, 0x404052
xor 0x9, [eax]
mov [eax], 0x41
mov byte_global, 0xc6
mov eax, 0x404053
xor 0xc6, [eax]
mov [eax], 0x4c
mov byte_global, 0xe
mov eax, 0x404054
xor 0xe, [eax]
mov [eax], 0x57
mov byte_global, 0x2e
mov eax, 0x404055
xor 0x2e, [eax]
mov [eax], 0x41
mov byte_global, 0x41
mov eax, 0x404056
xor 0x41, [eax]
mov [eax], 0x52
mov byte_global, 0x65
mov eax, 0x404057
xor 0x65, [eax]
mov [eax], 0x45
mov byte_global, 0xa4
mov eax, 0x404058
xor 0xa4, [eax]
mov [eax], 0x7d
mov eax, 0x404059
mov [eax], 0x0
FLAG{VMS-ARE-FOR-MALWARE}

The flag is indeed: FLAG{VMS-ARE-FOR-MALWARE}


See also