In this article, I will go through the virtual machine architecture issues that Code Virtualizer exposes that i have seen, show you different ways to attack a VM to extract important information for yourself, and show you how to apply this to different applications where different types of information are being checked.
For this article i used Code Virtualizer 3.1.2.0, 3.1.4.0 and the latest version, but only its DEMO version.
Testing was carried out on virtual machines:
Let’s start right away with the fact that in terms of the architecture of Oreans virtual machines, everything turned out to be quite trivial: absolutely any VM from Oreans is easier to analyze than the current version of VMProtect, since during the analysis more and more places are revealed that can potentially be used to extract the information you need.
Let’s take a superficial look at what we’re dealing with
First of all, it is worth understanding the basic structure of the virtual machine as presented in Oreans. The VM uses rbp as a base register to access its data structures. Most operations somehow access a memory area via rbp + offset.
One of the key techniques used in VM Oreans is complex and sometimes useless stack manipulation. Let’s look at a specific example from the trace:
00 | 00007FF7129C1E | 9C | pushfq
01 | 00007FF7129C1E | 4C:89 04 24 | mov qword ptr ss:[rsp],r8
02 | 00007FF7129C1E | 4D:89 C0 | mov r8,r8
03 | 00007FF7129C1E | 48:81 EC 08 00 00 00 | sub rsp,8
04 | 00007FF7129C1E | 48:89 34 24 | mov qword ptr ss:[rsp],rsi
05 | 00007FF7129C1E | 48:BE 3C 02 00 00 00 00 00 00 | mov rsi,23C
06 | 00007FF7129C1E | 56 | push rsi
07 | 00007FF7129C1E | 8F 44 24 08 | pop qword ptr ss:[rsp+8]
08 | 00007FF7129C1E | 5E | pop rsi
Let’s break down what’s going on here:
pushfq - savesRFLAGS on the stack. This is important because further operations may affect their value.
mov qword ptr ss:[rsp],r8- retains valuer8 to the top of the stack
mov r8, r8 - rubbish instructions, doesn’t affect anything
sub rsp, 8 - allocates space on the stack for a local variable
mov qword ptr ss:[rsp],rsi- retains value rsi, equivalent to instruction push rsi, because there is a blank in the form of a dedicated space in the stack and loading a register there RSI.
mov rsi, 23C - loads a constant0x23C in rsi
push rsi - pushes a value onto the stack
pop qword ptr ss:[rsp+8] - extracts the value, but not into a register, but by offset from rsp
pop rsi - restores the original valuersi
Basically, if there wasn’t all this junk, it would look like this:
mov rsi , 0x23C
Yes, all this is actually garbage, and after following the instructionspop rsi, everything will be look like only the 0x23C constant was pushed onto the stack.
This sequence demonstrates the key principle of obfuscation: even a simple operation (in this case, loading a constant) turns into a complex chain of stack manipulations. The effect is achieved by:
Let’s look at a couple more examples.
15 | push rsp ; Saving RSP
16 | pop rbx ; RSP --> RBX
17 | add rbx, 8 ;+8 to RBX
18 | sub rbx, 8 ;-8 from RBX (neutralises add)
19 | xchg qword ptr ss:[rsp],rbx ; Swap values
1A | pop rsp ; RSP Recovery
A simplified version would look like this:
mov rbx, rsp ; Direct Copying
1D | mov qword ptr ss:[rsp], rdx
1E | pop qword ptr ss:[rsp]
1F | pop qword ptr ss;[rsp]
20 | sub rsp, 8
21 | sub rsp, 8
22 | mov qword ptr ss:[rsp], rbx
23 | pop qword ptr ss:[rsp]
24 | pop qword ptr ss:[rsp]
A simplified version would look like this:
mov rdx, [rsp] ; Direct value loading
MBA transformations in VM are used to mask simple arithmetic operations. Let’s consider an illustrative fragment:
67 | 00007FF7126DEF | 49:81 ED 90 00 00 00 | sub r13,90
68 | 00007FF7126DEF | 48:25 3F 00 00 00 | and rax,3F
69 | 00007FF7126DEF | 48:35 00 00 00 80 | xor rax,80000000
70 | 00007FF7126DEF | 48:89 E8 | mov rax,rbp
71 | 00007FF7126DEF | 4D:89 ED | mov r13,r13
72 | 00007FF7126DEF | 49:81 C1 FF FF FF 7F | add r9,7FFFFFFF
73 | 00007FF7126DEF | 49:81 E1 88 00 00 00 | and r9,88
This sequence implements a transformation of the form:
x = (x - 0x90) & 0x3F
x = x ^ 0x80000000
x = x + 0x7FFFFFFF
x = x & 0x88
In fact, this complex transformation can be reduced to a much simpler operation, but the VM uses a chain of operations to mask the real computation.
But, frankly speaking, I haven’t noticed any particularly striking examples of MBA transformations for arithmetic operations. Basically, there are basic operations with some additional manipulations.
Simple multiplication from native code in virtualization looks like this:
mov r9d, r8d ; Copy the first operand
xor r9, rsi ; XOR with second operand
imul r12, r9 ; Multiplication with intermediate result
And all other similar instructions related to arithmetic do not change in any way, executing the original.
One technique is mutation of basic instructions. Let’s look at an example:
24 | 00007FF7126DEE | 48:89 EA | mov rdx,rbp
25 | 00007FF7126DEE | 48:81 C2 12 00 00 00 | add rdx,12
26 | 00007FF7126DEE | 48:8B 12 | mov rdx,qword ptr ds:[rdx]
27 | 00007FF7126DEE | 48:81 C2 06 00 00 00 | add rdx,6
28 | 00007FF7126DEE | 4C:0F B7 22 | movzx r12,word ptr ds:[rdx]
29 | 00007FF7126DEE | 49:01 EC | add r12,rbp
30 | 00007FF7126DEE | 4D:8B 24 24 | mov r12,qword ptr ds:[r12]
This sequence implements what in normal code would be a simple operation of loading a value from memory.
Original operation:
mov r12, [rpb + offset] ; Direct memory access
Transforms into:
Loading the base address that was taken from the VM context into rdx
Adding the first offset (12)
Dereferencing a pointer
Adding a second offset (6)
Loading a 16-bit value
Adding a base address
Final dereference
The following example shows a mutation of a simple addition operation:
61 | movzx r14, word ptr [r11] ; Loading operand
62 | sub r12, 4 ; Offset correction
63 | mov rsi, 0 ; Reset
64 | sub rdi, r12 ; Subtraction
65 | add r14, rbp ; Adding a base
And as you might have guessed, the equivalent operation would simply be:
add rdi, [r11] ; Direct addition
The key element of the VM is the handler dispatch system. Let’s consider a typical sequence:
81 | 00007FF7126DEF | mov rcx, qword ptr ds:[rsi] ; Move value from memory to rcx
82 | 00007FF7126DEF | and r13, 80 ; Bitwise AND operation
83 | 00007FF7126DEF | mov r12, 12 ; Move 12 to r12
84 | 00007FF7126DEF | add rcx, r11 ; Add r11 to rcx
85 | 00007FF7126DEF | mov rdi, qword ptr ds:[rcx] ; Move value from memory to rdi
86 | 00007FF7126DEF | jmp rdi ; Jump to address in rdi
This is a classic example of a dispatch system in VM:
mov rcx,qword ptr ds:[rsi] - loading a pointer to the handler table
and r13,80 - preparing a mask for the index
mov r12,12 - loding base offset
add rcx,r11 - Calculating the address a specific handler
mov rdi,qword ptr ds:[rcx] - loading handler address
jmp rdi- transition to handler
The system is built in such a way that the real addresses of the handlers are calculated dynamically, but there is one point related to the array of handlers, where the developers really screwed up.
Let’s look at how VM virtualizes basic operations. Let’s take an example of working with registers:
41|00007FF7126DEE|49:81C612000000 | add r14,12
42|00007FF7126DEE|4D:8B36 | mov r14,qword ptr ds:[r14]
43|00007FF7126DEE|49:81C602000000 | add r14,2
44|00007FF7126DEE|6641:8B36 | mov si,word ptr ds:[r14]
This sequence implements register access virtualization via:
Calculating the base address of a context
Loading a pointer to a virtual register
Accessing a value via an additional offset
Loading a value into a physical register
In native code, a conditional jump usually looks simple:
cmp eax, ebx ; Comparison
je target ; Jump if equal
Let’s look at one check that was under virtualization, namely:
if (inp == 0xdeadc0de)
The user-entered code is compared to a local value, but how does it look inside the VM? Well, it’s pretty bad:
07C | 8B 09 | mov ecx,dword ptr ds:[rcx] ; ECX = DEADC0DE (loading from memory)
07D | 49:29 FB | sub r11,rdi ; Masking through subtraction
07E | 48:81 F2 FF FF FF 7F | xor rdx,7FFFFFFF ; Handler index masking
07F | 48:81 C2 20 00 00 00 | add rdx,20
080 | 4C:29 EF | sub rdi,r13 ; Offset for the index (another disguise)
081 | 49:01 FA | add r10,rdi ; Calculating the intermediate address
082 | 48:C7 C2 01 00 00 00 | mov rdx,1 ; Preparing the flag
083 | 4D:8B 00 | mov r8,qword ptr ds:[r8] ; Loading a value for comparison
084 | 41:39 08 | cmp dword ptr ds:[r8],ecx ; Comparison with DEADC0DE
085 | 9C | pushfq ; Saving flags for later use
086 | 48:89 EF | mov rdi,rbp ; Saving the base pointer
087 | 48:81 C7 12 00 00 00 | add rdi,12 ; Offset for accessing the handler table
088 | 48:8B 3F | mov rdi,qword ptr ds:[rdi] ; Loading a table pointer
089 | 48:81 C7 09 00 00 00 | add rdi,9 ; Additional offset
08A | 4C:0F B7 0F | movzx r9,word ptr ds:[rdi] ; Loading the Handler Index
08B | 49:01 E9 | add r9,rbp ; Calculating the absolute address
08C | 4D:8B 09 | mov r9,qword ptr ds:[r9] ; Handler address upload
08D | 4C:29 C8 | sub rax,r9 ; Address correction
08E | 9C | pushfq ; Saving flags again
The developers really screwed up by adding the CMP instruction.
Let’s look at the mechanism:
mov ecx, dword ptr ds;[r8],ecx ; Loading DEADC0DE
mov r8, qword ptr ds;[r8] ; Loading value for comparsion
cmp dword ptr ds;[r8], ecx ; Loading DEADC0DE
pushfq ; Loading value for comparsion
mov rdi, rbp ; Base pointer
add rdi, 12 ; Offset to table
mov rdi, qword ptr ds:[rdi] ; Get pointer
add rdi, 9 ; Offset within table
movzx r9, word ptr ds:[rdi] ; Load index
add r9, rbp ; Address calculation
mov r9, qword ptr ds:[r9] ; Getting the final address
sub rax, r9 ; Correction of jump address
pushfq ; Saving flags
This is a standard token/signature checking mechanism in VM, where
In Fish64 Black, it turned out to be essentially the same. Let’s look at this case:
if (0xDEADBEEF == 0xDEADC0DE)
First, the VM prepares the first operand (DEADBEEF):
41:51 | push r9 ; Saving entered value (DEADBEEF)
41:5E | pop r14 ; Move value to R14 (r14 = DEADBEEF)
41:59 | pop r9 ; Recover value in R9 (r9 = DEADBEEF)
49:F7D1 | not r9 ; Inversion (r9 = FFFFFFFFBB60B998)
4D:87CE | xchg r14, r9 ; Exchange values (r9 = DEADBEEF)
The VM then performs a series of transformations to mask the process:
49:C1E6 08 | shl r14, 8 | r14 = FFFFFFBB60B99800 ; Left shift by 8 bits
48:B8 006A469F44000000 | mov rax, 449F466A00 ; Loading a constant
49:81EE 1E13D75B | sub r14, 5BD7131E ; Subtraction constant
49:81C6 AC84EF67 | add r14, 67EF84AC ; Adding a constant
49:01C6 | add r14, rax ; Adding a value from RAX
The reference value (DEADC0DE) is obtained by subtracting:
49:2B13 | sub rdx, qword ptr ds:[r11] | rdx: FFE8A66960560089 -> DEADC0DE
VM starts to compare values:
41:80FF 01 | cmp r15b, 1
0F85 07000000 | jne target1
41:80FF 02 | cmp r15b, 2
0F85 4D810000 | jne target2
41:80FF 03 | cmp r15b, 3
0F85 7CAFFFFF | jne target3
41:39D1 | cmp r9d, edx ; Compare r9d with edx
9C | pushfq ; Save flags on stack
By the way, in the Lion64 Black and Fish64 Red-CustomA1 virtual machines, a similar condition checking mechanism remains:
41:80FC 03 | cmp r12b, 3
0F85 04000000 | jne crackme_protected+0x7FF6107153FE
45:39FD | cmp r13d, r15d
9C | pushfq
BTW, it is not entirely clear what the checks for values 1, 2, and 3 might be connected with.
But maybe it’s not all that bad, and the densest VM - Eagle64 Black - has something better? Oh, no, everything is exactly the same:
mov r11, qword ptr ds:[r8]
and r15, r11
xor r15, rax
sub r11, 6FC2DEC
mov r14, rbp
xor r15, 0xFFFF
add r14, 67
and r15, rax
or r15, 10
mov r9b, byte ptr ds:[r14]
cmp r9b, 63
jne 7FF6B5396B1F
cmp r10d, r11d
pushfq
Sadly
Unlike Code Virtualizer, VMProtect (tests were performed on v3.8.7 Lite)
uses a fundamentally different approach to implementing conditional jumps. Let’s consider
a similar check with 0xDEADC0DE + 0xDEADBEEF.
The cleaned check looks like this:
mov ecx, dword ptr ds:[rcx] ; Load DEADBEEF
not ecx ; Invert DEADBEEF
not r11d ; Invert DEADC0DE
and ecx, r11d ; Compare using AND
mov dword ptr ds:[r10+rax-1FFFE000], ecx ; Store the result
mov r11d, dword ptr ds:[rdx+r10] ; Read the result
mov esi, dword ptr ds:[rdx+r10+4] ; Read DEADC0DE
adc r11d, esi ; Add with carry
mov dword ptr ds:[r10+rdx*2], r11d ; Store the final result
This approach is based on a mathematical principle:
Example for equal values:
A = DEADC0DE
NOT(A) = 21524121
B = DEADC0DE
NOT(B) = 21524121
AND = 21524121 (exact match)
Example for different values:
A = DEADC0DE
NOT(A) = 21524121
B = DEADBEEF
NOT(B) = 21524110
AND = value will be different
As I understand it, VMProtect implements the check using the logical operation “NOR”.
NOR : p ↓ q = ¬(p ∨ q) = ¬p ∧ ¬q.
In our case:
The key role is played by the ADC (Add with Carry) instruction, which takes into account the CF flag from previous operations, sets the OF, SF, ZF, AF, PF, CF flags and also implicitly passes the comparison result.
So yes, in this regard, VMProtect, unlike Code Virtualizer, complicates the analysis of conditional branches several times, since there is no direct comparison of values.
Let’s move on to initialization.
In Oreans virtual machines, the entire context with data is stored either in the .vlizer section, or in the last section (in x64, this is usually the .reloc section, which it expands, changes the rights and embeds its code), here it depends on your settings before virtualizing the file. The context remains encrypted, this is easy to notice, but if you suddenly did not catch what we are talking about, then here is an example when, conditionally, your program has such a check:
if (inp = 0xdeadc0de)
You will see that imm32 will be decrypted via 32-bit xor key (used as one of the constant decryption options), and will be placed at RVA (in the case of the example, it is 0x41D155), then the virtual machine reads the value at this address, and in the future it will be overwritten with some other number:
Write to main module section .vlizer at address 0x7FF61808D155, size: 4, Data: DE C0 AD DE
Read from main module section .vlizer at address 0x7FF61808D155, size: 4, Data: DE C0 AD DE
Write to main module section .vlizer at address 0x7FF61808D155, size: 4, Data: 00 00 00 00
By the way, the decrypted local value is not always stored in .vlizer, it happens that it is used only once for some computational action, and then VM overwrites the register in which it was decrypted.
I told you that the context always remains encrypted, but there are places where the developer has made a mistake. In addition to the context, the section also stores an array with the bare RVAs of all the virtual machine handlers:

VMEntry gets the current base address where our sample was loaded, and in a loop it starts initializing the array by adding the base address to the RVA:
call 7FF7A091268D ; Call the next instruction, pushing the return address
mov rcx, qword ptr ss:[rsp] ; Get the return address
sub rcx, 5
sub rcx, 732688 ; Compute the actual base address by subtracting the RVA
test rbx, rbx
je 7FF7A0913137
add r14, rax
mov rbp, 5BFF9ED8 ; Start decrypting the address for the next handler
mov r10, 2420BA4C
xor r10, rbp
add r10, FFFFFFFF8020DB6C
add r10, 7BF742FF
add r10, r14
sub r10, 7BF742FF ; Address decrypted
add qword ptr ds:[r10], rcx ; Add the actual base to the RVA in the decrypted address
mov qword ptr ss:[rsp], r10 ; Store in stack
mov r10d, 1
sub ebx, r10d ; Decrement iteration count
jmp 7FF7A09130C5
After the loop ends, the handlers will be initialized:

Which is an architectural screw-up on the part of the developers, because in fact, nothing prevents you from parsing these addresses and intercepting them all, and viewing the incoming registers in the hook.
If you have a virtualized function that takes a pointer as input, by which you need to write some important data, then this will be noticeable in the intercepted handler.
What is also important is that all handlers can be found by the pattern “E9 ?? ?? ?? ?? E9 ?? ?? ?? ??”

These two jumps point to different VM handlers, and both can be intercepted, kekw, which is what I actually did, writing a small code for the emulator to detect these jumps and add a callback for them, which will output the current context of the registers to me:
bool is_valid_section(const MemoryRange& section) const {
return section.start != 0 && section.size != 0;
}
std::vector<jump_t> collect_valid_jumps(const MemoryRange& section) const {
std::vector<jump_t> jumps;
std::vector<uint8_t> buffer(INSTRUCTION_BUFFER_SIZE);
for (auto current_address = section.start; current_address < section.end(); ++current_address) {
read_memory(current_address, buffer);
if (!is_valid_jump_instruction(buffer)) {
continue;
}
auto target_address = get_absolute_target(current_address, buffer);
if (!section.contains(target_address)) {
continue;
}
add_jump_with_hook(jumps, current_address, target_address, buffer);
}
return jumps;
}
void read_memory(std::uint64_t address, std::vector<uint8_t>& buffer) {
m_emulator->mem_read(address, buffer.data(), buffer.size());
}
bool is_valid_jump_instruction(const std::vector<uint8_t>& buffer) const {
return is_relative_jmp(buffer[0]) && !is_invalid_jmp(buffer);
}
void add_jump_with_hook(
std::vector<jump_t>& jumps,
std::uint64_t current_address,
std::uint64_t target_address,
const std::vector<uint8_t>& buffer
) {
jumps.push_back({current_address, target_address, buffer});
uc_hook hook;
m_emulator->hook_add(
&hook,
UC_HOOK_CODE,
reinterpret_cast<void*>(jump_callback),
this,
current_address,
current_address + 1
);
}

As a result, we have the following output when jumping to a new handler:
Jump to .vlizer detected at 0x7ff617f0aebd
RAX: 0x7ff61808d194, RBX: 0xffffffffffd57c6d, RCX: 0x320, RDX: 0x7ff61808d1a4
RSI: 0x7ff61812b937, RDI: 0x7ff61812b923, RBP: 0x7ff61808d0b3, RSP: 0x558c13f9b0
R8: 0x7ff617f0aebd, R9: 0x7ff61808d0fd, R10: 0x7ff61812b925, R11: 0x7ff61808d1a4
R12: 0x7ff61808d121, R13: 0x7ff617e8358e, R14: 0x492a62, R15: 0x7ff6180c2450
And, in this way you can track all the handlers that should be executed in the file :)
In this section I will focus on catching interesting instructions that can provide useful information to the reverser, this list of instructions includes those that can potentially decrypt some value, access the context of the virtual machine, or write something to it.
Such instructions include CMP and MOV. In our reality, when we deal with some light VM like Fish64 Lite, we can add logging and ADD, SUB, XOR and other instructions, but this will not be of much use, and if instead of it there is a heavier VM, you will simply get lost in the logs.
In the intercepted MOV we can observe calls to the VM context, or watch how the write goes there. In CMP, as you already understood, you can watch what exactly is being checked, especially when we entered some number and try to understand what the program will compare it with.
Plus, useful CMP instructions in our case carry an important pattern, which you probably noticed, namely pushfq. Whenever important kind of checks occur, then each time after the execution of CMP there is a saving of flags.
As a result, I wrote myself the following code, adding only CMP logging for the test:
if (count > 1) {
cs_detail* detail = insn[0].detail;
cs_x86* x86 = &(detail->x86);
if (insn[0].id == X86_INS_CMP && insn[1].id == X86_INS_PUSHFQ) {
if (x86->op_count == 2) {
std::string op1_str, op2_str;
uint64_t val1 = 0, val2 = 0;
if (x86->operands[0].type == X86_OP_MEM) {
x86_op_mem mem = x86->operands[0].mem;
uint64_t base_val = 0, index_val = 0;
if (mem.base != X86_REG_INVALID)
uc_reg_read(uc, mem.base, &base_val);
if (mem.index != X86_REG_INVALID)
uc_reg_read(uc, mem.index, &index_val);
uint64_t effective_addr = base_val + (index_val * mem.scale) + mem.disp;
uint32_t operand_size = x86->operands[0].size;
uc_mem_read(uc, effective_addr, &val1, operand_size);
op1_str = "memory[0x" + std::to_string(effective_addr) + "]";
} else if (x86->operands[0].type == X86_OP_REG) {
uc_reg_read(uc, x86->operands[0].reg, &val1);
op1_str = cs_reg_name(handle, x86->operands[0].reg);
}
if (x86->operands[1].type == X86_OP_MEM) {
x86_op_mem mem = x86->operands[1].mem;
uint64_t base_val = 0, index_val = 0;
if (mem.base != X86_REG_INVALID)
uc_reg_read(uc, mem.base, &base_val);
if (mem.index != X86_REG_INVALID)
uc_reg_read(uc, mem.index, &index_val);
uint64_t effective_addr = base_val + (index_val * mem.scale) + mem.disp;
uint32_t operand_size = x86->operands[1].size;
uc_mem_read(uc, effective_addr, &val2, operand_size);
op2_str = "memory[0x" + std::to_string(effective_addr) + "]";
} else if (x86->operands[1].type == X86_OP_REG) {
uc_reg_read(uc, x86->operands[1].reg, &val2);
op2_str = cs_reg_name(handle, x86->operands[1].reg);
} else if (x86->operands[1].type == X86_OP_IMM) {
val2 = x86->operands[1].imm;
op2_str = "0x" + std::to_string(val2);
}
printf("CMP %s (0x%llx), %s (0x%llx) at 0x%llx [Followed by PUSHFQ]\n",
op1_str.c_str(), val1, op2_str.c_str(), val2, address);
cs_free(insn, count);
}
cs_close(&handle);
}
}
Result:

VMEXIT (Virtual Machine Exit) is a mechanism used by virtual machines to transfer control from virtualized code back to native code, or to transfer control to another region within the virtualized code.
If you need to know what is being called inside the virtualized code, what external APIs, calls to the .text section, and so on, then the intercepted VMEXIT comes to the rescue.
Let’s imagine a case where virtualized code tries to call user32::MessageBoxA, to do this it restores the registers, and through the “ret 0” instruction transfers control to the export:
pop r8 ; Restore registers
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
popfq ; Restore flags
ret 0 ; Exit VM
sub rsp, 38 ; MessageBoxA prologue
In some Oreans VMs, the VM exit is slightly modified, and this linearity is no longer visible, but there are still two instructions by which VMEXIT can be tracked:
popfq
ret 0
A year ago I wrote code that intercepted all VMEXITs by finding them by pattern, and I only managed to find this piece of code:
std::vector<std::uint8_t> vecShellcode = {
0x48, 0x8B, 0xCC, 0x48, 0x81, 0xEC, 0x00, 0x02, 0x00, 0x00, 0x48, 0x83, 0xE4,
0xF0, 0x48, 0xB8,
0x41, 0x58, 0x41,
0x5F, 0x5E, 0x5D,
0xDE, 0xC0, 0xAD, 0xDE, 0xDE, 0xC0, 0xAD, 0xDE, 0xFF, 0xD0, 0x48, 0x8B, 0xE0,
0x59, 0x41, 0x5A, 0x41, 0x5B, 0x41, 0x5C, 0x41, 0x5D, 0x41, 0x5E, 0x41, 0x5F,
0x5B, 0x5A, 0x59, 0x58, 0x9D, 0xC2, 0x00, 0x00
};
int iVmExitCount = 0;
do {
printf("Searching vmexit..\n");
auto pVmExit = Utils::FindSignature(
GetModuleHandleA("OreansSolutions_protected.exe"),
"41 58 41 59 41 5A 41 5B"
);
if (!pVmExit) {
printf("The handlers are over! Stop searching..\n");
break;
}
iVmExitCount++;
printf("[%d] VMEXIT Found! 0x%p\n", iVmExitCount, pVmExit);
auto lpMemShellcode = VirtualAlloc(
0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
);
printf("Allocated memory: 0x%p\n", lpMemShellcode);
printf("Patching shellcode..\n");
*(DWORD64*)(vecShellcode.data() + 16) = (DWORD64)Callback;
memcpy(lpMemShellcode, vecShellcode.data(), vecShellcode.size());
std::vector<std::uint8_t> vecTrampolineShellcode = {
0x48, 0xB8, 0xDE, 0xC0, 0xAD, 0xDE, 0xDE, 0xC0, 0xAD, 0xDE,
0xFF, 0xE0
};
printf("Patching VMEXIT to JMP..\n");
*(DWORD64*)(vecTrampolineShellcode.data() + 2) = (DWORD64)lpMemShellcode;
memcpy(pVmExit, vecTrampolineShellcode.data(), vecTrampolineShellcode.size());
} while (pVmExit);
printf("Everything is patched successfully!\n\n");
And the screenshot itself, which demonstrates the work of this code. As we can see from the screenshot, we intercepted 82 such patterns, and intercepted the API call:

And for the emulator, this check is enough:
if (insn[0].id == X86_INS_POPFQ &&
insn[1].id == X86_INS_RET &&
insn[1].detail->x86.operands[0].type == X86_OP_IMM &&
insn[1].detail->x86.operands[0].imm == 0)
{
uint64_t rsp;
uc_reg_read(uc, UC_X86_REG_RSP, &rsp);
rsp += 8;
uint64_t return_address;
uc_mem_read(uc, rsp, &return_address, sizeof(return_address));
uint64_t regs[5];
uc_reg_read(uc, UC_X86_REG_RAX, ®s[0]);
uc_reg_read(uc, UC_X86_REG_RCX, ®s[1]);
uc_reg_read(uc, UC_X86_REG_RDX, ®s[2]);
uc_reg_read(uc, UC_X86_REG_R8, ®s[3]);
uc_reg_read(uc, UC_X86_REG_R9, ®s[4]);
printf("VMEXIT at 0x%llx -> 0x%llx (%s)\n", address, return_address, region.c_str());
printf("RAX: 0x%llx RCX: 0x%llx RDX: 0x%llx R8: 0x%llx R9: 0x%llx\n",
regs[0], regs[1], regs[2], regs[3], regs[4]);
}
And we watch as Code Virtualizer goes into VMEXIT to call the API:

In this section I will touch on global variables that are read under virtualized code.
Yes, in fact it is not the fault of the Oreans developers, it is the fault of the people who use global variables to detect some anomalies.
And I actually came across this in one paid cheat one year ago (I won’t mention its name here), where because of one global variable that was checked under virtualization, the cheat initialization did not go further.
Let’s imagine this code:
bool g_detected = true;
int main() {
if (g_detected) {
return 0;
}
// Continue the initialization ...
MessageBoxA(0, "dinahu", 0, 0);
return 0;
}
And we’ll send the Main function to Eagle64 Black.
Knowing that global variables are stored in the “.data” section, we go to the debugger and put a hardware breakpoint on it when accessing the section:

We press F9.. and the hardware breakpoint is triggered inside the virtualized code:

We change 1 to 0, and we get our MsgBox:

And that’s it, the conclusion: don’t use global variables for critical checks :)
For testing, I wrote a simple KeygenMe, which is based on obtaining the user’s CPUID:
int main() {
std::string key;
printf("Key: ");
std::getline(std::cin, key);
if (key.length() != 29) { // XXXX-YYYY-ZZZZ-WWWW-VVVV-UUUU
printf("Invalid key length!\n");
return 1;
}
key = to_upper(key);
uint32_t cpu_id = get_cpuid();
uint64_t checksum = 0;
checksum ^= cpu_id;
std::array<uint16_t, 6> sections;
sections[0] = (checksum & 0xFFFF) ^ 0x1234;
sections[1] = ((checksum >> 16) & 0xFFFF) ^ 0x5678;
sections[2] = (sections[0] + sections[1]) ^ 0xABCD;
sections[3] = (sections[0] * sections[1]) & 0xFFFF;
sections[4] = cpu_id & 0xFFFF;
sections[5] = cpu_id >> 16;
std::string expected_key = std::format(
"{:04X}-{:04X}-{:04X}-{:04X}-{:04X}-{:04X}",
sections[0], sections[1], sections[2], sections[3], sections[4], sections[5]
);
bool valid = true;
for (size_t i = 0; i < expected_key.length() && valid; i++) {
if (key[i] != expected_key[i]) {
valid = false;
}
}
if (valid) {
printf("Valid key!\n");
} else {
printf("Invalid key!\n");
}
return 0;
}
I threw the Main function under virtualization and went to check how our interception methods would help us in extracting important information.
With the serial number “44444444444444444444” we will encounter the following check, which will not let us go further:
if (serial.length() != 29)
In the callback, during the execution of such a check, we catch the following log:
CMP rcx (0x18), r12 (0x1d)
at 0x7ff790067e75 [Followed by PUSHFQ]
VMEXIT at 0x7ff790050cb9 -> 0x7ff790001020 (keygenme_protected.exe::.text)
RAX: 0x7ff903d64e10
RCX: 0x7ff79002d8b8
RDX: 0x7ff903d37d18
R8: 0x0
R9: 0x0
We see that we caught exactly this check, since the second operand is a register with the value 0x1d - 29.
VMEXIT indicates that it will transfer control to the .text section, if you look closely at my code, you can see that next we have a printf call:
printf("Invalid serial length!\n");
return 1;
This is exactly what happened, VMEXIT points to the printf function, which is implemented inside the .text section of the main module:

Let’s enter the serial number “XXXX-YYYY-ZZZZ-WWWW-VVVV-UUUU” and get the following log:
VMEXIT at 0x7ff7ae1814c0 -> 0x7ff916e182b0 (ucrtbase.dll::.text)
RAX: 0xccf52ff8d0
RCX: 0x58
RDX: 0xb0006
R8: 0x26124a42530
R9: 0x1
The log indicates that we have reached the call to the function “ucrtbase.dll::toupper”, which was called here:
key = to_upper(key);
Let’s move on. And we get this log:
CMP memory[0x2616249820464] (0x58), r9b (0x31)
at 0x7ff7ae1361b2 [Followed by PUSHFQ]
__stdio_common_vfprintf() output: Invalid key!
We’ve reached the point where our serial number is being checked symbol by symbol. 0x58 is the ‘X’ I entered, and 0x31 is ‘1’.
When you change the first symbol the next time you run it, the logic of the program will tell you to start checking the next symbol:
CMP memory[0x2035654313841] (0x58), r9b (0x44) at 0x7ff7ae1361b2 [Followed by PUSHFQ]
I think you got the gist of it. Here we didn’t take into account the fact that we used std::format, where we openly expose a ready-made serial number, because the goal itself was to show how one can exploit VM vulnerabilities in such applications.
The second example is an integrity check, it checks the CRC32 of sections that have rights to execute code. In our case, these are “.text” and “.vlizer”:
std::vector<section_info> get_executable_sections() {
std::vector<section_info> sections;
HMODULE module_handle = GetModuleHandle(NULL);
if (!module_handle) {
return sections;
}
auto dos_header = reinterpret_cast<PIMAGE_DOS_HEADER>(module_handle);
if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) {
return sections;
}
auto nt_headers = reinterpret_cast<PIMAGE_NT_HEADERS>(
reinterpret_cast<uint8_t*>(module_handle) + dos_header->e_lfanew
);
if (nt_headers->Signature != IMAGE_NT_SIGNATURE) {
return sections;
}
auto section_header = IMAGE_FIRST_SECTION(nt_headers);
for (WORD i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) {
if (section_header[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) {
section_info section = {};
memcpy(section.name, section_header[i].Name, 8);
section.name[8] = '\0';
section.base_address = reinterpret_cast<uintptr_t>(module_handle) + section_header[i].VirtualAddress;
section.size = section_header[i].Misc.VirtualSize;
section.crc32 = calculate_crc32(reinterpret_cast<uint8_t*>(section.base_address), section.size);
sections.push_back(section);
}
}
return sections;
}
void print_section_info(const section_info §ion, const section_info *prev_section = nullptr) {
printf("Section: %s\n"
"Base Address: 0x%zx\n"
"Size: 0x%zx\n"
"CRC32: 0x%08x\n",
section.name, section.base_address, section.size, section.crc32);
if (prev_section && section.crc32 != prev_section->crc32) {
printf("WARNING: Section integrity changed!\n"
"Previous CRC32: 0x%08x\n"
"Current CRC32: 0x%08x\n",
prev_section->crc32, section.crc32);
}
printf("\n");
}
All functions, including these two, will be under the virtual machine.
We see how a real nightmare began in our logs:
CMP memory[0x140700081501139] (0x5cf), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d0), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d1), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d2), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d3), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d4), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d5), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d6), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d7), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d8), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5d9), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5da), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
CMP memory[0x140700081501139] (0x5db), r10 (0x320000)
at 0x7ff74a7295b4 [Followed by PUSHFQ]
At this stage, a cycle occurs where the crc32 section of .vlizer is calculated:
uint32_t calculate_crc32(const uint8_t* data, size_t length) {
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < length; i++) {
crc = crc32_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
}
return ~crc;
}
section.crc32 = calculate_crc32(reinterpret_cast<uint8_t*>(section.base_address), section.size);
But I’m not so crazy as to wait for 3276800 iterations to complete under a virtual machine, so let’s move on:
CMP memory[0x2795828561248] (0x2d00069d), r8d (0x2d00069d) at 0x7ff74a6daaed [Followed by PUSHFQ]
Here we have a check of the .text section hash, which we are not touching yet, because its hash was not violated. But you can’t say the same about the virtual machine section, for some reason, even without my intervention, the section’s crc32 changes, by the way, the check was at the same address:
CMP memory[0x2795828561288] (0x93fb8045), r8d (0xb312e99a) at 0x7ff74a6daaed [Followed by PUSHFQ]
Here, for some reason, we actually have different hashes, so the file itself, the previous log, complains:
Section: .vlizer
Base Address: 0x7ff74a5c9000
Size: 0x320000
CRC32: 0xb312e99a
WARNING: Section integrity changed!
Previous CRC32: 0xc0c6d153
Current CRC32: 0xb312e99a
And so instead of instructions
cmp dword ptr ds:[r11], r8d
We’ll patch it on
cmp al, al
nop
So that ZF (Zero Flag) is always equal to 1
And yes, it worked:
[Before]
Scanning executable sections in memory...
Section: .text
Base Address: 0x7ff74a5c1000
Size: 0x1eb9
CRC32: 0x2d00069d
Section: .vlizer
Base Address: 0x7ff74a5c9000
Size: 0x320000
CRC32: 0x6c061c52
WARNING: Section integrity changed!
Previous CRC32: 0xa08b163e
Current CRC32: 0x6c061c52
[After]
Scanning executable sections in memory...
Section: .text
Base Address: 0x7ff74a5c1000
Size: 0x1eb9
CRC32: 0x2d00069d
Section: .vlizer
Base Address: 0x7ff74a5c9000
Size: 0x320000
CRC32: 0x88f928e4
That’s all I have for now, thank you for reading this article. Again, I hope that you, dear reader, learned something new from this. Perhaps with this article I saved you from using Code Virtualizer on critical sections of code or gave you some ground for an idea when reverse engineering some software.
Best wishes and take care of yourself! :)