skip to content
HexPharoah Hexpharoah
Table of Contents

My Experience with VM Obfuscation

The first time I hit a VM-protected binary, I stared at the disassembly for hours and got nowhere. The decompiler showed me one giant loop that made no sense. No clear logic, no recognizable patterns - just a dispatcher reading bytes and jumping to handlers. That was my introduction to VM obfuscation, and it humbled me.

Since then I have spent a lot of time understanding how these protections work and developing my approach to breaking them down. Here is what I learned.


What I Was Looking At

VM obfuscation takes normal x86 code and converts it into custom bytecode. The binary ships with its own interpreter that runs this bytecode at runtime. So instead of reading native instructions, I was looking at:

  1. A dispatch loop fetching custom opcodes from a data section
  2. Handler routines - one for each virtual instruction
  3. A virtual context (fake registers, fake stack, virtual instruction pointer)

The original code was gone. Replaced by something I had to reverse engineer from scratch.

The Structure I Identified

+------------------+
| Bytecode Stream | <- custom opcodes in .rdata
+------------------+
|
v
+------------------+
| VM Dispatcher | <- fetch, decode, execute loop
+------------------+
|
v
+------------------+
| VM Handlers | <- one per virtual opcode
+------------------+
|
v
+------------------+
| Virtual Context | <- virtual registers, vstack, VIP
+------------------+

The Dispatcher Pattern

Once I knew what to look for, I recognized the dispatcher:

// What the dispatcher roughly looks like after cleanup
void vm_run(uint8_t *bytecode, vm_ctx *ctx) {
while (1) {
uint8_t op = bytecode[ctx->vip++];
switch (op) {
case 0x01: handle_add(ctx); break;
case 0x02: handle_sub(ctx); break;
case 0x03: handle_mov(ctx); break;
case 0x04: handle_push(ctx); break;
case 0x05: handle_jmp(ctx); break;
case 0xFF: return;
}
}
}

In practice, commercial protectors obscure this with computed jumps, encrypted handler tables, and junk code between operations.


Protectors I Have Encountered

VMProtect

The most common one I run into. What makes it painful:

  • Every build generates different handler code (mutation)
  • Multiple VM architectures in a single binary
  • Bytecode is encrypted with rolling XOR keys
  • Anti-debug woven into the handler execution itself

Themida

I hit this on a few CTF challenges and some commercial software:

  • Combines VM with anti-dump and anti-attach
  • FISH and TIGER virtual machines
  • Less documentation available compared to VMProtect
  • The anti-analysis layers made even attaching a debugger difficult

Why It Took Me So Long Initially

Static Analysis Failed

  • Decompilers showed me the interpreter, not the actual program logic
  • No recognizable instruction patterns to latch onto
  • Control flow was completely flat (just the dispatch loop)
  • Everything meaningful was data (bytecode), not structure

Dynamic Analysis Was Painful

  • Setting breakpoints on native instructions missed the virtual logic
  • Tracing produced millions of handler hits for simple operations
  • I could not tell which handler executions were important vs junk

How I Approach It Now

After failing multiple times, I developed a process that works for me:

Step 1: Find the VM Boundary

I look for where native code enters the VM:

  • Usually a function that saves all registers, sets up a virtual context, then jumps to the dispatcher
  • The exit point is where the VM restores native registers and returns

Step 2: Map the Handlers

I trace execution and log which addresses get hit:

# I use Frida to log handler hits
handlers = {}
dispatch_addr = 0x401000 # the switch/jump point
def on_dispatch(args):
target = get_jump_target()
if target not in handlers:
handlers[target] = {"count": 0, "name": f"handler_{len(handlers)}"}
handlers[target]["count"] += 1

After running the sample, I have a list of unique handlers and their frequency.

Step 3: Classify Each Handler

I reverse each handler individually. Most fall into categories:

Arithmetic: vadd, vsub, vxor, vnot, vshl
Data: vmov, vpush, vpop, vload, vstore
Control: vjmp, vcjmp, vcall, vret
System: vnative_call, vexit

Each handler is small - usually 10-30 native instructions. I can reverse those one at a time.

Step 4: Build a Disassembler

Once I know the opcode format, I write a custom disassembler:

def disassemble(bytecode):
vip = 0
while vip < len(bytecode):
op = bytecode[vip]
if op == 0x01:
reg = bytecode[vip+1]
imm = struct.unpack('<I', bytecode[vip+2:vip+6])[0]
print(f" vmov vr{reg}, {hex(imm)}")
vip += 6
elif op == 0x02:
dst, src = bytecode[vip+1], bytecode[vip+2]
print(f" vadd vr{dst}, vr{src}")
vip += 3
elif op == 0xFF:
print(f" vexit")
break

Step 5: Simplify

The raw virtual instruction trace is noisy. I apply:

  • Constant folding (replace vmov vr0, 5; vmov vr1, 3; vadd vr0, vr1 with vr0 = 8)
  • Dead store elimination
  • Pattern matching for known sequences (like calling conventions)
  • Reconstruct control flow from virtual jumps

Tools That Helped Me

ToolHow I Use It
x64dbg + traceRecording handler execution order
FridaHooking the dispatcher for logging
TritonSymbolic execution through handlers
VTILTranslating virtual instructions to analyzable IR
NoVmpAutomated VMProtect devirtualization (when it works)
Custom PythonBytecode disassembly and simplification

What I Learned

  • Patience is everything. My first VM took me weeks. Now it takes days. It gets faster.
  • Reverse the VM, not the program first. Once I understand the VM architecture, the actual logic falls out.
  • Handlers are the key. Each one is small and reversible. The intimidation is in the volume, not the complexity.
  • Automation matters. Writing scripts to trace, log, and disassemble saves massive time on the next protected binary.
  • No protection is unbreakable. VM obfuscation raises the cost of analysis significantly, but it does not make it impossible.

My Advice for Getting Started

If you are hitting VM-protected binaries for the first time:

  1. Start with a simple custom VM (write one yourself - best way to understand the concept)
  2. Try crackmes with basic VM protection before touching VMProtect
  3. Get comfortable with dynamic instrumentation (Frida, Pin)
  4. Read published devirtualization research papers
  5. Do not get discouraged - everyone struggles with this at first

The ability to handle VM-protected binaries separates intermediate reversers from advanced ones. It took me time to get here, and I am still learning.


Until Next -- Tata Bye Bye!!
jaswanth

Related Posts