Detailed analysis of Themida's LION VM (Part 1)
Intro
Lion VM (Lion Black, Lion Red and Lion White) is one of the most novel and popular virtual machines available in Themida and Code Virtualizer. According to Themida’s own metrics, it offers the best combination of complexity and performance across all available virtual machines. In the wild, Lion VM is often found in malware and game cheats binaries, hiding malicious code under a heavy layer of obfuscation.
This research aims to document the obfuscation and internal architecture of Themida’s Lion VM.
Disclaimer
This research is published for educational and defensive purposes to document the technology that malware, cheats, and other ill-intended software hide behind, so that anti-cheats, EDRs, and analysts can better understand it. It is not a slight against Oreans, whose engineers clearly built something impressive. Don’t use any of this to infringe anyone’s intellectual property.
Some long assembly snippets were simplified to avoid long code duplicates, obfuscation etc.
Terminology
Virtual Instruction (virtual bytecode) - An encoded instruction decoded and executed (interpreted) by the virtual machine.
VIP - Virtual Instruction Pointer, similar to RIP in AMD64 (EIP in x86), points to the VM instruction currently being executed
VM Context - A structure holding the current state data of the VM.
VM Handler - An interpreter function for a virtual instruction.
VSP (Virtual Stack Pointer, Virtual Stack) - a pointer and a stack space of the virtual machine.
VM Overview
Virtual instructions
Virtual instructions are stored in the VM section. Every virtual instruction has at least two opcodes: a virtual size of the instruction - a value added to the virtual instruction pointer after the instruction completes, and the index of the next VM handler to execute. Additionally, a virtual instruction can contain an operation type opcode, as well as opcodes storing offsets to the VM context fields that are read from or written to during virtual instruction interpretation. Virtual instruction opcodes are often encrypted with rolling keys.
VM handlers
All VM handlers in Lion VM follow the same semantics. First, a handler updates the rolling decryption keys. After that, it reads opcodes from the virtual instruction and interprets them. Opcodes can contain the operation type and other data, such as offsets to VM context fields, global variables, etc. VM handlers end with a dispatcher, reached after the instruction has been interpreted. The dispatcher reads exactly two opcodes from the virtual instruction: the instruction’s virtual size and the next VM handler index. After decrypting those values, the handler adjusts the virtual instruction pointer by the instruction’s virtual size and performs a direct jump to the next VM handler, either via jmp {reg} or a retn instruction.
Virtual Stack
All Themida (Code Virtualizer) virtual machines, including Lion VM, reuse the host’s stack as their own virtual stack. Lion VM implements two core virtual stack operations: VPUSH and VPOP, which push a value onto the stack and retrieve a value from the stack respectively.
VM Context
Lion VM, as well as other VMs provided by Themida and Code Virtualizer, is a synchronous virtual machine with shared VM context. The VM context is a structure that holds the data used by virtual machine internals as well as the data required for context switches. In particular, VM context always stores the following variables: image base, VM handler table base pointer, virtual instruction pointer, stack frame pointer (host’s RBP), a BOOL (dword) value of whether the VM code execution is active (for VM synchronization), 15 fields that store host’s AMD64 (x86_64) registers’ values. Additionally, if VM code is stored in a separate section, VM context saves the address of that section.
Lion VM’s context, in addition to the common fields, stores rolling decryption keys and an RSP value pointing to the top of the stack AFTER all registers are saved prior to VM execution.
In Lion VM, the context fields don’t follow a specific order, and their positions are randomized with every compilation by Themida.
Below is the example of Lion VM context structure.
struct c_vm_ctx {
uint32_t m_is_in_vm = 0; // spinlock var
uint64_t m_vm_section_start = 0; // an address of VM section start
uint64_t m_image_base = 0; // loaded image's base address
void** m_vm_handler_table = nullptr;
uint64_t* m_host_state = nullptr;
uint8_t* m_stack_base_pointer = nullptr; // host's RBP
uint8_t* m_stack_pointer = nullptr; // virtual stack pointer (reuses host’s RSP)
uint8_t* m_virtual_instruction_pointer = nullptr; // VIP
bool m_should_branch = false; // used for VJCC; when true, the condition for jump is met
uint8_t m_operation_type = 0; // used for merged arithmetic handlers
uint8_t m_field_type = 0; // size of data to read/write to a virtual register (8, 16, 32 and 64 bits)
uint8_t m_checksum = 0;
uint64_t vregs_qword[20];
uint32_t vregs_dword[20];
uint16_t vregs_word[20];
uint8_t vregs_byte[20];
}
Rolling keys
Lion VM heavily relies on rolling decryption keys for obfuscation. Decryption keys are dwords stored as virtual registers in the VM context, they change in almost every VM handler and are used to decrypt and encrypt stored values and virtual instruction opcodes. All subtypes of Themida’s Lion VM utilize multiple rolling decryption keys, which can be updated both independently and based on one another. The number of rolling decryption keys can be as high as 8.
VM Flow overview
In the executable section where the original, unobfuscated code was initially stored, the code is replaced with junk bytes, and a jump to vm_entry_setup is placed as the first instruction. As part of VM setup, the very first operation is flag saving via the PUSHFQ instruction, then, an offset to the first virtual instruction’s bytecode, the index of the first VM handler, and a return address are pushed onto the stack. After that, a jump is made to a universal VM entry point, where
- All registers are saved onto the stack
- VM execution is synchronized using a
lock cmpxchgspinlock - The VM context pointer is moved into
RBP - Several VM context constants are initialized: the image base, the start of the Themida section (if applicable), the TEB pointer, and the host’s RBP
- If the VM handler table has not yet been decrypted, it is decrypted entry by entry
- A jump is made to the first VM handler, effectively starting guest code execution/interpretation

VM Handlers structure
Deadcode
Every Lion VM handler contains up to 70% deadcode obfuscation (around 50% on average across all handlers). The generated deadcode instructions implement data movement, arithmetic, and bitwise operations between operands, where the operands can be registers, memory (stack reads and writes only), or immediate values.
Constant expansion
Instead of directly dereferencing memory addresses using memory operands, VM handlers resort to two types of constant expansion. The first type is simpler and involves scattering the dynamic memory address computation across multiple instructions. For example, a simple mov rax, qword ptr [rbp+0F4h] becomes mov rdx, rbp -> add rdx, 0F4h -> mov rax, qword ptr [rdx]. The second type of constant expansion relies on storing the constant as the virtual instruction’s encrypted opcode, making it impossible to statically resolve the desired constant unless the virtual instruction corresponding to the handler is known. An example of this expansion would be: mov rax, [[rbp+{vip}]+{opcode_offset}] -> mov rdx, [rbp+rax].
Merged VM handlers
A large portion of VM handlers in Lion VM are universal across multiple operations and interpret the virtual instruction based on the operation type stored in its opcode. These universal handlers not only implement multiple operations within a single interpreter function, but also save or retrieve values from the virtual stack, effectively chaining the actual operation with a virtual push, virtual pop, load or store. Multiple physically distinct copies of the same virtual instruction interpreter exist in the binary, resulting in mirrored handlers. These mirrored handlers perform identical operations semantically, but the virtual instruction opcodes they process differ between them.
Virtual opcode obfuscation
Opcodes of virtual instructions are obfuscated using rolling keys. Since a single instruction can contain multiple opcodes of varying sizes, each one is obfuscated independently.
All rolling key updates, and the resulting virtual instruction opcodes, are generated during the compilation by Themida and vary significantly, not only in the keys themselves, but also in the order and conditions of the algorithmic operations applied to them. As a result, analyzing each VM handler requires understanding the exact decrypted content of its corresponding virtual instruction bytecode.
Dispatcher
The virtual machine’s architecture doesn’t have a central dispatch loop, which is the core of VM bytecode interpretation and execution. Instead, a dispatcher is inlined into every VM handler, so execution is transferred directly from one handler to the next.
Control flow protection
Lion VM handlers update a byte field in the VM context struct immediately before dispatching to the next handler. This value is checked against predetermined magic constants in several handlers, for example, to choose which VM context field a value should be read from. To preserve the predictability of this control-flow checksum, it is not updated on virtual jumps both conditional (VJCC) and unconditional (VJMP), only on natural fall-throughs dispatches.
VM Architecture
VMENTRY
The VM entry algorithm for all of Themida’s virtual machines is split into two parts: initial VM state setup, which provides the VM context with data about the virtual instruction pointer and the first VM handler to execute (vm_entry_setup), and VM context initialization and synchronization, which sets up the context, decrypts the data, and begins executing the VM code (vm_entry).
VM_ENTRY_SETUP
A short prologue executed first. It saves the host’s RFlags value onto the stack, then saves the return address from which the jump to the VM was made, and initializes two key values: an offset to the virtual instruction where execution will begin, and the index of the first VM handler to call. The vm_entry_setup stub can be simplified to:
push {imm_ret_addr}
pushfq
push {imm_vm_handler_index}
push {imm_vip_offset}
jmp vm_entry
VM_ENTRY
vm_entry is shared across all calls to the same VM. It follows vm_entry_setup and is responsible for initializing the VM state. It saves host registers by pushing their values onto the stack, then synchronizes the call using a spinlock. After acquiring the lock, vm_entry saves the following fields in the VM context: the image base address, the start address of the VM section (if applicable), the address of the TEB, the address of a 16-byte storage region for host state, the virtual instruction pointer, and the stack frame pointer. After saving these fields, vm_entry checks whether the VM handler table has already been decrypted; if not, it proceeds to iterate over all VM handler table entries, increasing each qword value by the image base, since they are originally stored as offsets. vm_entry can be simplified to:
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
push rdi
push rsi
push rbp
push rbx
push rbx
push rdx
push rcx
lea rbp, qword ptr ds:[vm_context]
.spin_lock:
xor eax, eax
lock cmpxchg [rbp+{spinlock_offset}], 0x1
jz .start
pause
jmp .spin_lock
.start:
mov qword ptr [rbp+{image_base_offset}], {image_base}
mov qword ptr [rbp+{vm_section_start_offset}], {vm_section_start}
mov qword ptr [rbp+{teb_offset}], {teb_ptr}
mov qword ptr [rbp+{host_state_offset}], {host_state_ptr}
mov qword ptr [rbp+{vip_offset}], qword ptr [rsp+0x88]
add qword ptr [rbp+{vip_offset}], {image_base}
mov qword ptr [rbp+{stack_frame_ptr_offset}], qword ptr [rsp+0x20]
; ... decrypt vm handlers table ...
jmp qword ptr [rax]
VMEXIT
Vmexit is a handler that restores the host’s state, which includes all register values and RFlags; after restoring these values, a jump is made back to the host environment.
In Themida’s, there are two types of vmexit: a jump and a call. The jump vmexit restores the host’s state and returns execution to the host permanently, whereas the call vmexit places the address of a vm_entry_setup on top of the stack before jumping, effectively returning to the VM after the code in the host environment has executed. All vmexit handlers share the same pattern: they restore the RFlags value immediately before returning:
popfq
retn
VSYNC_STACK_POINTER
Lion VM reuses the host’s stack for virtual stack storage. As a result, every block of virtualized code calls the VSYNC_STACK_POINTER instruction in its prologue. This instruction saves the host’s current stack pointer (RSP) in the VM context, the saved RSP value always equals the RSP value before the jump to the VM, minus 152 (the amount of stack space used for host state preservation (15 general-purpose registers and 4 values: VIP, handler index, return offset, and RFlags, all pushed during vm_entry_setup)).
mov rbx, qword ptr [rbp+0x1E3] ; load VIP
movzx rdx, word ptr [rbx+0x4]
add rdx, rbp
mov qword ptr [rdx], rsp
VLOADSTORE
A virtual instruction implemented by a merged VM handler implementing both load and store operations between virtual registers and decoded VM context fields. The operation performed (load or store) and the data width (8/16/32/64 bits) for each operand are independently determined by selector bytes embedded in the virtual instruction’s encrypted opcodes. A single VLOADSTORE handler instance may process up to two separate operand slots, each resolving its own load/store direction and width independently of the other.
mov rax, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx ecx, byte ptr [rax+0x8] ; decode op_type/width selector for operand A
mov rdx, [decoded_vreg_ptr_a] ; resolve decrypted vreg pointer A
cmp {vm_ctx->vreg0}, LOAD
jnz .store_a
mov rsi, [rdx] ; load: width selected via field_type (8/16/32/64)
mov [dest_vreg_a], rsi
jmp .operand_b
.store_a:
mov [rdx], value_a ; store: width selected via field_type, zero-extends on dword
.operand_b:
movzx ecx, byte ptr [rax+0xF]
mov rdx, [decoded_vreg_ptr_b]
cmp {vm_ctx->vreg1}, LOAD
jnz .store_b
mov rdi, [rdx]
mov [dest_vreg_b], rdi
jmp .dispatch
.store_b:
mov [rdx], value_b
.dispatch:
; ...
VPUSHQWORD
Retrieves a QWORD value loaded from a VM context field and pushes it onto the stack. Then adjusts the Virtual Stack Pointer value in the VM context by subtracting 8 (sizeof(QWORD)).
mov rax, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx r9, word ptr [rax+0x2] ; load virtual opcode containing an offset to the field containing the value to push
movzx r10, word ptr [rax+0x4] ; load virtual opcode containing an offset to the VSP field
add r9, rbp
push qword ptr [r9] ; push QWORD onto the stack
sub qword ptr [rbp+r10], 0x8
VPOPQWORD
Retrieves a QWORD from the top of the stack and loads it into the desired field in the VM context. It then adjusts the Virtual Stack Pointer value in the VM context by adding 8 (sizeof(QWORD)).
mov rax, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx r9, word ptr [rax+0x2] ; load virtual opcode containing an offset to the field that the QWORD should be popped to
movzx r10, word ptr [rax+0x4] ; load virtual opcode containing an offset to the VSP field
add r9, rbp
add qword ptr [rbp+r10], 0x8
pop qword ptr [r9] ; pop QWORD from the stack
VPUSH_POP
A merged handler that combines VPUSH and VPOP operations. Themida’s virtual stack has a 2-byte alignment and stack pointer can be adjusted only for 2 or 8 bytes. Regardless of that, values of 1, 2, 4 and 8 bytes can be saved. The operation type and stack adjustment size are encoded as opcodes in the virtual instruction.
mov rdx, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx r13, byte ptr [rdx] ; load operation type: vpush or vpop
movzx r11, byte ptr [rdx+1] ; load stack adjustment type: word (2 bytes) or qword (8 bytes)
mov rcx, word ptr [rdx+2] ; load offset to virtual register to push/pop value from
add rcx, rbp
cmp r13, 0x1
jnz .vpop
; vpush
push qword ptr [rcx]
; adjust SP for push
cmp r11, 2
jnz .push_adjust8
sub qword ptr [rdx], 2 ; adjust stack pointer by 2
jmp .dispatch
.push_adjust8:
sub qword ptr [rdx], 8 ; adjust stack pointer by 8
jmp .dispatch
; vpop
.vpop:
pop qword ptr [rcx]
; adjust SP for pop
cmp r11, 2
jnz .pop_adjust8
add qword ptr [rdx], 2 ; adjust stack pointer by 2
jmp .dispatch
.pop_adjust8:
add qword ptr [rdx], 8 ; adjust stack pointer by 8
.dispatch:
; ...
VALU
The most complex virtual instruction, implemented by the largest VM handler, responsible for all arithmetic, bitwise, and comparison operations, as well as for storing their results in virtual registers or on the stack. Every VALU instruction has a type opcode that determines which operation should be performed, as well as a size operand determining the size of the data the operation is performed on, ranging from 1 byte (8 bits) to 8 bytes (64 bits).
In Lion Black virtual machine, there are three variations of VALU VM handlers, each implementing a different set of arithmetic, bitwise, and comparison operations:
- VALU handler #1. Implements 17 different arithmetic instructions: rcr (8, 16, 32, 64 bits), sub (8, 16, 32, 64 bits), xor (8, 16, 32, 64 bits), imul (64 bits), shl (8, 16, 32, 64 bits), test (8, 16, 32, 64 bits), shr (8, 16, 32, 64 bits), cmp (8, 16, 32, 64 bits), rol (8, 16, 32, 64 bits), inc (8, 16, 32, 64 bits), or (8, 16, 32, 64 bits), add (8, 16, 32, 64 bits), rcl (8, 16, 32, 64 bits), and (8, 16, 32, 64 bits), ror (8, 16, 32, 64 bits), neg (8, 16, 32, 64 bits), dec (8, 16, 32, 64 bits).
- VALU handler #2. Implements 14 different arithmetic instructions: rcr (8, 16, 32, 64 bits), or (8, 16, 32, 64 bits), add (8, 16, 32, 64 bits), rcl (8, 16, 32, 64 bits), and (8, 16, 32, 64 bits), ror (8, 16, 32, 64 bits), xor (8, 16, 32, 64 bits), shl (8, 16, 32, 64 bits), sub (8, 16, 32, 64 bits), imul (64 bits), shr (8, 16, 32, 64 bits), test (8, 16, 32, 64 bits), rol (8, 16, 32, 64 bits), cmp (8, 16, 32, 64 bits).
- VALU handler #3. Implements 5 different arithmetic instructions: cmp (64 bits), inc (8, 16, 32, 64 bits), neg (8, 16, 32, 64 bits), add (64 bits), dec (8, 16, 32, 64 bits).
These instructions share the property that they read and modify flags during the target operation’s execution. To read the flags before the instruction, as well as to save the flags after instruction execution, the handler reads and writes the RFlags value from stack storage using the popfq and pushfq instructions, respectively.
In the prologue, each VALU handler reads two opcode fields from the virtual instruction: the operand size and the operation type. After decoding, both values are stored directly into the VM context structure for use during dispatch.
mov rax, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx r15, byte ptr [rax+2] ; retrieve data size
movzx r14, byte ptr [rax+4] ; retrieve operation type
The operations are performed on virtual registers stored in the VM context. To access these fields, the handler reads two virtual opcodes, each the size of a word (2 bytes), containing the offsets to both the source and target virtual registers. Additionally, the handler reads an offset to the RFlags register.
mov r8, word ptr [rax+8] ; get source virtual register
add r8, rbp
mov r9, word ptr [rax+0x10] ; get target virtual register
add r9, rbp
mov r10, word ptr [rax+0xB8] ; get RFlags virtual register
add r10, rbp
Based on the data size and operation type, an action is taken, for example:
pushfq
ror r14b, cl
popfq
To identify VALU handlers, it’s enough to scan for popfq instruction following bitwise or arithmetic operations.
VXCHG
Exchanges the values of two virtual registers. The values may optionally be re-encrypted as part of the swap.
mov r13, dword ptr [rbp+0x10]
mov rax, dword ptr [rbp+0x14]
mov qword ptr [rbp+0x14], r13
mov qword ptr [rbp+0x10], rax
xor dword ptr [rbp+0x10], 0x75AD183B
VJMP
The virtual unconditional jump is used to transfer execution to a virtual instruction and dispatch its handler unconditionally. This instruction is used to implement loops and jump tables.
The VJMP virtual instruction contains two virtual operands: the next VM handler index, and a virtual jump RVA. The offset is an integer (4 bytes) that can be either positive or negative, with the sign determined by its sign bit, to implement forward and backward jumps respectively.
The VJMP handler increases (or decreases) the virtual instruction pointer by the RVA, then dispatches the next handler using the handler table base pointer and the handler index encoded in the virtual instruction.
mov rax, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx r12, word ptr [rax+0x2] ; load virtual opcode containing next VM handler index
mov r9, [rbp+0xEC]
mov r9, [r9 + r12*8] ; calculate the address of the next vm handler
mov edi, dword ptr [rax+0x4] ; load virtual jump RVA
test edi, 0x80000000 ; check if the jump RVA is negative (backward jump)
jz .fwd
and edi, 0x7FFFFFFF
sub [rbp+0x1E3], rdi ; move VIP back
jmp r9
.fwd:
add [rbp+0x1E3], rdi ; move VIP forward
jmp r9
VJCC
The virtual jump if condition is met instruction implements flag checks and branching decisions based on the flags and the condition type. The virtual instruction contains the following opcodes:
- An offset to the RFlags field in the VM context structure (short, 2 bytes)
- Condition type (byte, 1 byte)
- Virtual jump target RVA (int, 4 bytes)
- Target VM handler index, used when the branch is taken (short, 2 bytes)
- Virtual instruction length (int, 4 bytes)
- Next VM handler index, used when the branch is not taken and execution falls through (short, 2 bytes)
Instead of using the conditional jumps available in the x86 architecture, Themida’s VJCC checks EFLAGS (the only portion of RFlags actually used in amd64) against masks determined by the condition type stored in the virtual instruction. The result of this comparison is then stored in a boolean field branch_taken in the VM context. In the handler epilogue, if branch_taken is true, a jump is made to the virtual RVA by adjusting VIP and jumping to the target VM handler, otherwise, VIP is increased by the virtual size and the jump is made to the fall-through (next) VM handler.
mov rdx, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx rcx, word ptr [rdx+0x0] ; load offset to RFlags field from the virtual instruction
add rcx, rbp
mov esi, dword ptr [rcx] ; esi = RFlags (EFLAGS portion)
movzx eax, byte ptr [rdx+0x2] ; condition type
mov byte ptr [rbp+0xA0], 0 ; reset branch_taken value
; JE/JZ — ZF == 1
cmp al, 0x9E
jz .check_zf
cmp al, 0x41
jnz .check_jne
.check_zf:
test esi, 0x40 ; ZF is bit 6
jz .epilogue
mov byte ptr [rbp+0xA0], 1
jmp .epilogue
.check_jne: ; JNE/JNZ — ZF == 0
cmp al, 0x35
jnz .epilogue
test esi, 0x40
jnz .epilogue
mov byte ptr [rbp+0xA0], 1
; ...remaining 14 condition checks follow the same pattern...
.epilogue:
movzx r9, word ptr [rdx+0x4] ; target handler index (taken path)
movzx r10, word ptr [rdx+0xA] ; next handler index (not-taken path)
mov rcx, [rbp+0xEC]
cmp byte ptr [rbp+0xA0], 0
jz .fallthrough
; jump to target virtual RVA (branch taken)
mov eax, dword ptr [rdx+0x6] ; virtual jump RVA
test eax, 0x80000000
jz .taken_fwd
and eax, 0x7FFFFFFF
sub qword ptr [rbp+0x1E3], rax
jmp .dispatch_taken
.taken_fwd:
add qword ptr [rbp+0x1E3], rax
.dispatch_taken:
mov r9, qword ptr [rcx+r9*8]
jmp r9 ; dispatch to target VM handler
.fallthrough:
movsxd rax, dword ptr [rdx+0xC] ; virtual instruction length
add qword ptr [rbp+0x1E3], rax
mov r10, qword ptr [rcx+r10*8]
jmp r10 ; dispatch to the fallthrough (next) VM handler
All VJMP and VJCC VM handlers can be found via binary search for the “81 ?? 00 00 00 80” pattern. This pattern represents a sign bit check of virtual jump RVA from the virtual instruction:
and {gpr}, 80000000h. This is true not only for Themida’s LION VM but for all Themida’s virtual machines.
VMOVS/VMOVSB/VMOVSW/VMOVSD/VMOVSQ
VMOVS is a family of VM handlers implementing the x86 string move instructions (MOVSB, MOVSW, MOVSD, MOVSQ). Each variant copies a value of a fixed width (1, 2, 4, or 8 bytes) from a source virtual register’s pointed to address to a destination virtual register’s pointed-to address. After the copy, both the source and destination pointers are advanced or decremented by the operand’s width, depending on the Direction Flag (DF). The offsets to the source pointer, destination pointer, and direction-flag vreg are all stored as opcodes in the virtual instruction, only the fixed step size (1/2/4/8) differs between the SB/SW/SD/SQ variants.
mov r10, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx rdx, word ptr [r10+0x2]
add rdx, rbp ; dest virtual register ptr (in the VM context)
movzx rcx, word ptr [r10+0x4]
add rcx, rbp ; source virtual register ptr (in the VM context)
mov rax, qword ptr [rcx] ; dereference source vreg pointer
mov rax, qword ptr [rax] ; load value (width: 1/2/4/8 bytes — sz)
mov rsi, qword ptr [rdx] ; dereference destination vreg pointer
mov qword ptr [rsi], rax ; store value to destination
movzx r9, word ptr [r10+0x9] ; offset to RFlags in the VM context
add r9, rbp
test qword ptr [r9], 0x400 ; check DF in flags vreg
jnz .backward
.forward: ; DF == 0
add qword ptr [rcx], {sz} ; advance source pointer
add qword ptr [rdx], {sz} ; advance destination pointer
jmp .dispatch
.backward:
sub qword ptr [rcx], {sz} ; decrement source pointer
sub qword ptr [rdx], {sz} ; decrement destination pointer
.dispatch:
; ...
VLODS/VLODSB/VLODSW/VLODSD/VLODSQ
VLODS is a family of VM handlers implementing the x86 string load instructions (LODSB, LODSW, LODSD, LODSQ). Each variant reads a value of a fixed width (1, 2, 4, or 8 bytes) through a source virtual register’s pointed to address and writes the result into a destination virtual register. Only the source pointer is advanced or decremented afterward, by the operand’s width, depending on the direction flag (DF), which is read from a dedicated rflags vreg. Unlike the VMOVS family, the destination here is not a pointer: the loaded value is stored directly into the destination vreg slot and the destination is never advanced. The offsets to the source pointer, destination vreg, and direction flag vreg are all stored as opcodes in the virtual instruction, only the fixed step size (1/2/4/8) and load width differ between the SB/SW/SD/SQ variants.
mov r10, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx rdx, word ptr [r10+0x2]
add rdx, rbp ; dest virtual register ptr (in the VM context)
movzx rcx, word ptr [r10+0x4]
add rcx, rbp ; source virtual register ptr (in the VM context)
mov rax, qword ptr [rcx] ; dereference source vreg pointer
mov rax, qword ptr [rax] ; load value (width: 1/2/4/8 bytes - sz)
mov qword ptr [rdx], rax ; store value to destination
movzx r9, word ptr [r10+0x9] ; offset to RFlags in the VM context
add r9, rbp
test qword ptr [r9], 0x400 ; check DF in flags vreg
jnz .backward
.forward: ; DF == 0
add qword ptr [rcx], {sz} ; advance source pointer
jmp .dispatch
.backward:
sub qword ptr [rcx], {sz} ; decrement source pointer
.dispatch:
; ...
VSCAS/VSCASB/VSCASW/VSCASD/VSCASQ
VSCAS is a family of VM handlers implementing the x86 string scan instructions (SCASB, SCASW, SCASD, SCASQ). Each variant compares a fixed accumulator value against a value of a fixed width (1, 2, 4, or 8 bytes) read through a scan virtual register’s pointed to address, and captures the resulting flags. Only the scan pointer is advanced or decremented afterward by the operand’s width, depending on the direction flag (DF), which is read from a dedicated rflags vreg. The captured flags are optionally written back to a destination vreg. The offsets to the accumulator vreg, scan pointer vreg, direction flag vreg, should write rflags flag, and destination vreg are all stored as opcodes in the virtual instruction, only the fixed step size (1/2/4/8) and comparison width differ between the SB/SW/SD/SQ variants.
mov r10, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx rax, word ptr [r10+6h] ; offset to accumulator vreg
add rax, rbp
mov rax, qword ptr [rax] ; accumulator value (width: 1/2/4/8 - sz)
movzx r15, word ptr [r10+0xDh] ; offset to scan vreg pointer
add r15, rbp
mov r15, qword ptr [r15] ; dereference scan vreg pointer
sub rax, qword ptr [r15] ; compare accumulator against scanned value
pushfq ; save current RFlags onto the stack
movzx r12, word ptr [r10+0x4] ; offset to direction-flag vreg
add r12, rbp
test qword ptr [r12], 0x400 ; check DF in flags vreg
jnz .backward
.forward: ; (DF == 0)
add qword ptr [scan_vreg_addr], {sz} ; advance scan pointer
jmp .check_writeback
.backward:
sub qword ptr [scan_vreg_addr], {sz} ; decrement scan pointer
.check_writeback:
movzx r9, byte ptr [r10+8] ; write-enable flag
cmp r9, 0x00
jz .dispatch
movzx r14, word ptr [r10+2] ; offset to destination vreg
add r14, rbp
popfq ; restore captured flags
mov qword ptr [r14], rsp_flags_value ; store flags to destination vreg
.dispatch:
; ...
VSETFLAGS
Implements multiple flag control operations from the x86 ISA: CLC (clear carry), CLD (clear direction), CLI (clear interrupt), CMC (complement carry), STC (set carry), STD (set direction), and STI (set interrupt). The exact flag control operation, as well as the offset to the RFlags value in the VM context, are stored as opcodes in the virtual instruction.
mov r8, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx rdx, word ptr [r8+0x5]
add rdx, rbp ; address of RFlags in the VM context
mov al, byte ptr [r8+0] ; decoded selector byte
cmp al, 0x16
jnz .check_cld
and dword ptr [rdx], ~0x1 ; CLC — clear CF
.check_cld:
cmp al, 0x19
jnz .check_cli
and dword ptr [rdx], ~0x400 ; CLD — clear DF
and dword ptr [rbp+0x10], ~0x400
.check_cli:
cmp al, 0xF2
jnz .check_cmc
and dword ptr [rdx], ~0x200 ; CLI — clear IF
and dword ptr [rbp+0x10], ~0x200
.check_cmc:
cmp al, 0x79
jnz .check_stc
test dword ptr [rdx], 0x1
jz .cmc_set
and dword ptr [rdx], ~0x1 ; CMC — complement CF
jmp .check_stc
.cmc_set:
or dword ptr [rdx], 0x1
.check_stc:
cmp al, 0x36
jnz .check_std
or dword ptr [rdx], 0x1 ; STC — set CF
.check_std:
cmp al, 0x25
jnz .check_sti
or dword ptr [rdx], 0x400 ; STD — set DF
or dword ptr [rbp+0x10], 0x400
.check_sti:
cmp al, 0x85
jnz .dispatch
or dword ptr [rdx], 0x200 ; STI — set IF
or dword ptr [rbp+0x10], 0x200
.dispatch:
; ...
VLOAD_VLIZER_SEC_BASE
Loads the address of Themida’s section start to a virtual register in the VM context structure. The offset to the virtual register is stored as an opcode in the virtual instruction.
mov rcx, qword ptr [rbp+0x1E3] ; load virtual instruction pointer
movzx rdx, word ptr [rcx+0x1] ; retrieve offset to vm_ctx's field to add section base to
add rdx, rbp
mov r11, qword ptr [rbp+0x62] ; load section base address
mov qword ptr [rdx], r11
VADD_IMAGE_BASE
Instead of storing full addresses (pointers) or RIP-relative addresses, Themida saves the absolute offsets of pointers. Because of this approach, in order to access valid addresses from inside the VM, those offsets must be added to the image base of the mapped binary. The VADD_IMAGE_BASE instruction adds the image base value to a field of the VM context containing an absolute offset loaded earlier. The offset of this field is stored as an opcode in the virtual instruction.
mov rbx, qword ptr [rbp+0x1E3] ; mov rbx, vm_ctx->vip
movzx rdx, word ptr [rbx+0x2] ; retrieve offset to vm_ctx's field containing RVA loaded earlier
add rdx, rbp
mov rax, qword ptr [rbp+0xA8] ; mov rax, vm_ctx->image_base
add [rdx], rax
Unknown (non-translated) instructions
When the host’s instruction can’t be converted to a virtual instruction (or a set of virtual instructions) by Themida’s compiler, it’s treated as an unknown instruction. To execute such instructions, a context switch is performed, in which the guest (VM-obfuscated code) switches back to the host to execute the instruction, then returns to the guest.
Every unknown instruction is stored in an RWX section created by Themida, and each such instruction is followed by a jump back to vm_entry.

Before an unknown instruction is executed, a regular VM exit flow is performed, in which all of the host’s registers are pushed onto the stack via the VPUSH instruction, and vm_exit instruction itself is processed.
Miscellaneous instructions
VROTATE_KEYS
This virtual instruction preserves the virtual machine’s state by saving all registers, then adjusts the rolling decryption keys, and finally restores the state. The VM handler for this instruction not only interprets its corresponding virtual instruction, but can also be called directly via a call $rva instruction from other VM handlers. This is possible because the VROTATE_KEYS handler does not adjust the virtual instruction pointer and never dispatches directly. Instead, it returns to the caller’s return address, taken from the top of the stack. As a result, when the VROTATE_KEYS instruction is interpreted directly from bytecode, it must be preceded by a VPUSH or an equivalent that places the address of the next virtual instruction’s handler on top of the virtual stack.
push rax
push rcx
push rdx
push rbx
push rbp
push rsi
push rdi
push r15
push r14
push r13
push r12
push r11
push r10
push r9
push r8
xor qword ptr [rbp+0x10], 0x5930533
add qword ptr [rbp+0x18], 0x10440000
mov rax, qword ptr [rbp+0x88]
xchg rax, r10
or word ptr [rbp+0x44], r10w
pop r8
pop r9
pop r10
pop r11
pop r12
pop r13
pop r14
pop r15
pop rdi
pop rsi
pop rbp
pop rbx
pop rdx
pop rcx
pop rax
ret
VSETUP
Resets the virtual registers used as rolling decryption keys to their default values, so their behavior is always the same and predictable regardless of how many times the VM code has been executed. VSETUP is always the first or second virtual instruction following vm_entry.
mov dword ptr [rbp+0x11A], 0x0
mov dword ptr [rbp+0x10], 0x13DEAD
mov dword ptr [rbp+0x44], 0x13FFABC
mov byte ptr [rbp+0x8], 0xCA
VRDTSC
Reads host’s clock time by calling rdtsc instruction and saving its results to two dword fields of vm context by offsets specified in virtual instruction’s operands.
rdtsc
; …
mov dword ptr [rbp+rcx], rax ; rcx stores an offset to vm context, read from virtual opcode
mov dword ptr [rbp+rbx], rdx ; rbx stores an offset to vm context, read from virtual opcode
VMCALL
Calls two internal handlers / functions.
mov r14, rbp
add r14, 0x00
mov r11, qword ptr [r14] ; load function pointer from vm context
call r11 ; first native call
add rsp, 0x28 ; clean up marshalled args + padding
push rdx
mov r9, rbp
add r9, 0x00
mov rbx, qword ptr [r9] ; reload function pointer
call rbx ; second native call
Final words
This blogpost was made possible thanks to the devirtualization engine I’ve been working on for quite some time.
The question remains, whether this approach of identifying VM internals and lifting VM bytecode to readable pseudocode is scalable. Some people think it is, some people think it’s not. I guess we’ll find out in THE NEXT article.