esDynamic
Manage your attack workflows in a powerful and collaborative platform.
Expertise Modules
Executable catalog of attacks and techniques.
Infrastructure
Integrate your lab equipment and remotely manage your bench.
Lab equipments
Upgrade your lab with the latest hardware technologies.
Side Channel Attacks
Evaluate cryptography algorithms from data acquitition to result visualisation.
Fault Injection Attacks
Laser, Electromagnetic or Glitch to exploit a physical disruption.
Photoemission Analysis
Detect photon emissions from your IC to observe its behavior during operation.
Evaluation Lab
Our team is ready to provide expert analysis of your hardware.
Starter Kits
Build know-how via built-in use cases developed on modern chips.
Cybersecurity Training
Grow expertise with hands-on training modules guided by a coach.
esReverse
Static, dynamic and stress testing in a powerful and collaborative platform.
Extension: Intel x86, x64
Dynamic analyses for x86/x64 binaries with dedicated emulation frameworks.
Extension: ARM 32, 64
Dynamic analyses for ARM binaries with dedicated emulation frameworks.
Penetration Testing
Identify and exploit system vulnerabilities in a single platform.
Vulnerability Research
Uncover and address security gaps faster and more efficiently.
Code Audit & Verification
Effectively detect and neutralise harmful software.
Digital Forensics
Collaboratively analyse data to ensure thorough investigation.
Software Assessment
Our team is ready to provide expert analysis of your binary code.
Cybersecurity training
Grow expertise with hands-on training modules guided by a coach.
Semiconductor
Automotive
Security Lab
Gov. Agencies
Academics
Defense
Healthcare
Energy
Why eShard?
Our team
Careers
Youtube
Gitlab
Github
Emulation and virtualization are methods that enable software to run in a controlled environment, allowing analysts to safely study system behavior. They also provide tools for monitoring the target system, such as network captures, debuggers, and other forensic instruments.
However, to prevent such analysis, attackers often implement anti-emulation techniques designed to detect emulated environments, thereby complicating dynamic analysis by modifying the executable's behavior.
In a Windows system, these techniques cover a wide range of implementations, ranging from Windows API checks, assembly instructions, timing-based methods, and more.
This blog is the result of my internship project at eShard, where I explored how malware detects virtual machines and avoids analysis. Alongside my research, I created a YouTube series to explain each technique step by step.
So let’s get started — first with emulation in QEMU, before moving on to detection techniques and how to bypass them.
A few key terms before we dive deeper:
QEMU (Quick EMUlator) is an open-source software designed to emulate various architectures and systems.
It supports two modes of emulation:
QEMU can also integrate a hypervisor such as KVM to provide hardware-assisted virtualization.
In full system emulation, QEMU uses dynamic binary translation to convert guest instructions into an intermediate representation (IR). Then, the IR is converted into host instructions by the Tiny Code Generator (TCG).
TCG divides the guest code into translation blocks (TBs), considered atomic from the emulator's perspective. This contrasts with a real CPU, where execution can be interrupted between two instructions.
To illustrate this, let’s take the following ARM code:
@ Compilation : @ arm-linux-gnueabi-as -o add.o add.s @ arm-linux-gnueabi-ld -o add add.o .section .text .global _start _start: mov r0, #5 mov r1, #7 add r0, r0, r1 mov r7, #1 svc #0
This code computes 5 + 7
and then exits. Let’s see how TCG translates it.
To observe the full translation from ARM to x86-64, we use an ARM binary in QEMU’s user mode with qemu-arm
. To capture each translation step, we enable the -d
option and specify a log file with -D
. Here is the command:
qemu-arm -d in_asm,op,out_asm -D add.log -singlestep ./add
For simplification, we only focus on the translation of the first instruction mov r0, #5
. The logs look like this:
IN: 0x00010054: OBJD-T: 0500a0e3 OP: ld_i32 loc3,env,$0xfffffffffffffff0 brcond_i32 loc3,$0x0,lt,$L0 st8_i32 $0x1,env,$0xfffffffffffffff4 ---- 0000000000010054 0000000000000000 0000000000000000 mov_i32 loc6,$0x5 mov_i32 r0,loc6 mov_i32 pc,$0x10058 call lookup_tb_ptr,$0x6,$1,tmp9,env goto_ptr tmp9 set_label $L0 exit_tb $0x736b64000043 OUT: [size=64] -- guest addr 0x0000000000010054 + tb prologue 0x736b64000100: OBJD-H: 8b5df085db0f8c1d000000c645f401c7450005000000c7453c58000100488bfd OBJD-H: ff1512000000ffe0488d0514ffffffe9e4feffff -- tb slow paths + alignment 0x736b64000134: OBJD-H: 90909090 data: [size=8] 0x736b64000138: .quad 0x000059c6dad02c20
For brevity, here’s a summary of the results:
Guest assembly code (ARM): mov r0, #5
(opcode: 0500a0e3
)
Intermediate Representation (IR) of QEMU:
mov_i32 loc6,$0x5 mov_i32 r0,loc6
Translated host code (x86-64): mov dword [rbp+0x0], 0x5
With translation covered, let’s now look at the components QEMU emulates, such as CPU, RAM, and storage.
In system emulation, QEMU emulates every component: storage, RAM, CPU, and more. The capabilities of these components are limited by the specifications of the host machine’s physical hardware. For example, you cannot create a virtual machine with 64 GB of RAM if the host only has 16 GB. The same limitations apply to other components, as we’ll see in later examples.
Even though these components are natively limited, concepts like memory overcommitment and thin provisioning can make a virtual machine appear to have more resources than the host physically provides. This doesn’t add real hardware — and if the guest actually uses the overcommitted resources, it can cause severe performance degradation or even instability.
These inherent limitations are the basis for many anti-VM detection techniques.
Before we can think about bypassing anti-emulation techniques, we need to understand how they work. The best way to do this is to re-implement them ourselves and study their behavior.
Anti-emulation techniques can be grouped by how they are implemented or by the part of the environment they target. In this study, we focus on techniques that rely on Windows API functions and assembly instructions, along with two examples targeting QEMU’s Tiny Code Generator (TCG).
The Windows API can be used to query system features that reveal whether the environment is real hardware or virtualized. Two common checks focus on thermal control management and hard drive characteristics.
On physical machines, the system maps hardware components into thermal zones to manage cooling and prevent overheating.
A virtual machine doesn’t need this feature — its hardware is emulated and cannot overheat. Instead, it’s the host system’s job to manage real temperatures.
On Windows, the GetPwrCapabilities
function in powerbase.h
retrieves the system’s power management features:
BOOLEAN GetPwrCapabilities( [out] PSYSTEM_POWER_CAPABILITIES lpspc );
This function fills a SYSTEM_POWER_CAPABILITIES
structure. Within it, the ThermalControl
field indicates whether thermal zones are supported.
ThermalControl = TRUE
, the system supports thermal management, which usually means a physical machine.ThermalControl = FALSE
, it may suggest the system is running in a VM.Malware can use this flag to decide whether to alter its behavior. For example:
#include <windows.h> #include <powrprof.h> #include <stdio.h> int main() { SYSTEM_POWER_CAPABILITIES power_caps; if (GetPwrCapabilities(&power_caps)) { if (!power_caps.ThermalControl) { printf("Detected\n"); } else { printf("Not Detected\n"); } } return 0; }
Another widely used anti-VM check relies on analyzing the system’s hard drive size and available free space.
As explained earlier, the emulated hardware of a virtual machine is limited by the hardware of the host system. For example, the size of a virtual hard drive cannot exceed the size of the host’s physical drive. In practice, VMs are usually configured with smaller disks for convenience.
Modern desktop hard drives typically have 512 GB or more, while laptops often provide at least 256 GB. Virtual disks, however, are constrained by the host’s capacity, so the available free space inside a VM is usually smaller. A common anti-emulation method is to retrieve these values and compare them against standard thresholds.
To do this, malware often uses the GetDiskFreeSpaceExA
function. It retrieves information about the amount of space on a disk volume, including total size, total free space, and free space available to the current user:
BOOL GetDiskFreeSpaceExA( [in, optional] LPCSTR lpDirectoryName, [out, optional] PULARGE_INTEGER lpFreeBytesAvailableToCaller, [out, optional] PULARGE_INTEGER lpTotalNumberOfBytes, [out, optional] PULARGE_INTEGER lpTotalNumberOfFreeBytes );
By comparing lpTotalNumberOfBytes
and lpTotalNumberOfFreeBytes
with predefined thresholds — for instance, 20 GB total and 10 GB free — malware can decide whether the system looks like a VM. Example implementation:
#include <stdbool.h> #include <stdio.h> #include <windows.h> int main() { ULARGE_INTEGER CfreeBytesAvailable, CtotalBytes, CtotalFreeBytes; ULARGE_INTEGER DfreeBytesAvailable, DtotalBytes, DtotalFreeBytes; BOOL success_C = GetDiskFreeSpaceExA("C:\\", &CfreeBytesAvailable, &CtotalBytes, &CtotalFreeBytes); BOOL success_D = GetDiskFreeSpaceExA("D:\\", &DfreeBytesAvailable, &DtotalBytes, &DtotalFreeBytes); ULONGLONG cTotal = success_C ? CtotalBytes.QuadPart : 0; ULONGLONG dTotal = success_D ? DtotalBytes.QuadPart : 0; ULONGLONG cFree = success_C ? CtotalFreeBytes.QuadPart : 0; ULONGLONG dFree = success_D ? DtotalFreeBytes.QuadPart : 0; if (!cTotal && !dTotal && !cFree && !dFree) { printf("Detected !\n"); return 0; } BOOL result = false; if (cFree < 10737418240) result = true; if (dFree < 10737418240) result = true; if (cTotal < 21474836480) result = true; if (dTotal < 21474836480) result = true; printf("%s\n", result ? "Detected !" : "Not detected !"); return 0; }
While using the Windows API to detect VMs is effective, its main limitation is that it applies only to Windows. An alternative is to use assembly instructions that query hardware information directly, without relying on the operating system’s API.
Here we focus on the x86-64 instruction set, and more specifically on two instructions often used to detect virtual machines: cpuid
and sidt
.
The CPUID
instruction is used to retrieve information about the CPU. It takes the value of the eax
register (and in some cases ecx
) as input and returns processor information in the eax
, ebx
, ecx
, and edx
registers.
When cpuid
is called with eax = 0x1
, it returns processor information and feature flags in ecx
and edx
. In particular, bit 31 of ecx
— known as the hypervisor bit — indicates whether the processor is running under a hypervisor.
This bit is always
0
on physical CPUs, and1
on virtualized or emulated CPUs.
A simple check looks like this:
xor eax, eax inc eax cpuid bt ecx, 31
If cpuid
is called with eax = 0x40000000
, it returns additional information about the hypervisor. Specifically, this leaf returns a 12-byte ID string through ebx
, ecx
, and edx
. Examples include:
Microsoft Hv
for Microsoft Hyper-VKVMKVMKVM\0\0\0
or Linux KVM Hv
for KVMTCGTCGTCGTCG
for QEMU’s TCG (even though it is not a hypervisor, QEMU sets this leaf)VBoxVBoxVBox
for VirtualBoxA straightforward detection method is to retrieve this hypervisor ID and compare it with known values. Example implementation:
#include <stdio.h> #include <string.h> int main() { unsigned int eax, ebx, ecx, edx; char hypervisor_id[13]; __asm__ volatile("movl $0x40000000, %%eax;" "cpuid;" : "=b"(ebx), "=c"(ecx), "=d"(edx) : : "%eax"); *(unsigned int *)&hypervisor_id[0] = ebx; *(unsigned int *)&hypervisor_id[4] = ecx; *(unsigned int *)&hypervisor_id[8] = edx; hypervisor_id[12] = '\0'; for (int i = 0; i < NB_ID_STRING; i++) { if (strstr(hypervisor_id, known_hypervisor_id[i])) { printf("Detected !\n"); } } return 1; }
Next, we look at the sidt
instruction. It retrieves the address of the Interrupt Descriptor Table (IDT) stored in the IDTR (IDT Register).
What is the IDT?
The IDT is a data structure specific to the x86 architecture. It holds the addresses of handlers executed on interrupts or exceptions. Each processor has its own IDTR and IDT; on multiprocessor systems, each core may have its own table.
How is this useful for anti-VM?
To avoid conflicts with the host’s IDT, virtual machines often place their IDT at significantly higher memory addresses than a physical host. Based on this, the classic Red Pill technique by Joanna Rutkowska uses sidt
to read the IDT address and compare it against a threshold to decide if the program is running inside a VM.
The original write-up is available via the Wayback Machine. A detailed explanation is also provided in the SANS paper: On the Cutting Edge: Thwarting Virtual Machine Detection.
Example implementation:
int main() { struct { uint64_t base; } __attribute__((packed)) idtr; __asm__("sidt %0" : "=m"(idtr)); uint8_t third_byte = (idtr.base >> 16) & 0xFF; printf("%s", third_byte != 0xFF ? "Detected\n" : "Not detected\n"); return EXIT_SUCCESS; }
Beyond instruction-level checks, some anti-VM techniques target the emulator itself, especially behavior specific to QEMU’s TCG.
In this part, we build on what was introduced in the TCG section. The two techniques discussed here come from Rethinking anti-emulation techniques for large-scale software deployment (paper): context switching–based detection and unaligned vectorization–based detection.
As explained earlier, TCG translation blocks are atomic; QEMU does not handle external interruptions inside them. This limitation can be used for detection through a race condition, which occurs when concurrent processes access shared resources unsafely: Navigating the Concurrency Landscape: A Survey of Race Condition Vulnerability Detectors.
Without locks or synchronization, two threads modifying the same variable can interfere with each other. On a physical CPU, one thread might be interrupted mid-operation, leading to inconsistent results. In contrast, in QEMU’s TCG, the translation block executes fully, so the result is always consistent.
lock = lock + 1; lock = lock - 1;
With lock
as a shared variable:
lock
may be incremented twice before being decremented once, because of interrupts.Below is a Windows implementation inspired by the article:
#include <stdio.h> #include <windows.h> volatile unsigned int lock = 0; void noppp() { for (int i = 0; i < 100000; i++) { __asm__ __volatile__("nop"); } } void *race(void *p) { while (1) { __asm__ volatile("mov %0, %%eax\n" "inc %%eax\n" "mov %%eax, %1\n" : "=m"(lock), "=m"(lock)); noppp(); __asm__ volatile("mov %0, %%eax\n" "dec %%eax\n" "mov %%eax, %1\n" : "=m"(lock), "=m"(lock)); } return 0; } int main() { HANDLE h1 = CreateThread(NULL, 0, race, NULL, 0, NULL); HANDLE h2 = CreateThread(NULL, 0, race, NULL, 0, NULL); if (!h1 || !h2) { printf("Error creating threads\n"); return 1; } while (lock < 2) { printf("Detected !\r"); Sleep(1); } printf("Not detected !\n"); WaitForSingleObject(h1, INFINITE); WaitForSingleObject(h2, INFINITE); CloseHandle(h1); CloseHandle(h2); return 0; }
Although simple, this detection is not very accurate. The next one is more reliable.
This technique relies on unaligned vectorized instructions. These are SIMD (Single Instruction, Multiple Data) instructions that perform the same operation simultaneously on multiple elements within a vector.
“Unaligned” refers to memory addresses that do not satisfy the CPU’s alignment requirements. On real hardware, using an unaligned SIMD instruction such as movntps
can trigger an exception (SIGBUS
/SIGSEGV
) handled by the CPU. In contrast, emulators often execute unaligned accesses without errors, because the host manages them transparently.
The idea is to deliberately execute movntps
on an unaligned address. On real hardware, this raises an exception that can be caught with an exception handler. On an emulator, no exception occurs — revealing virtualization.
Below is a Windows implementation:
#include <stdio.h> #include <stdlib.h> #include <windows.h> LONG WINAPI AlignTrapHandler(PEXCEPTION_POINTERS ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) { printf("Not detected !\n"); ExitProcess(0); } return EXCEPTION_CONTINUE_SEARCH; } int main() { PVOID h = AddVectoredExceptionHandler(1, AlignTrapHandler); __asm__ volatile("mov %rsp, %rax\n" "inc %rax\n" // misalign the stack pointer "movntps %xmm0, (%rax)\n"); printf("Detected !\n"); return 0; }
Now that we have seen different ways malware can detect emulation and virtualization, let’s look at how these techniques can be bypassed.
The simplest approach is to configure the virtual machine so that it closely resembles a physical system. This can mean adjusting parameters such as network adapters, storage, CPU, and RAM to mimic typical hardware. With this setup, most anti-VM checks that rely on system information about virtualized components can be countered.
However, this strategy only works for hardware characteristics and some CPU features. More advanced methods, such as timing checks or instruction-level tricks, cannot be fully bypassed through configuration changes alone.
Another strategy is to intercept and modify the behavior of Windows API functions commonly used in anti-VM checks. By hooking these functions, it is possible to replace the real output with controlled values that look like a physical environment.
For example, the GetPwrCapabilities
function from the Windows API retrieves system power information. By hooking it, we can always force certain fields — such as ThermalControl
— to appear enabled:
BOOL WINAPI HookedGetPwrCapabilities(PSYSTEM_POWER_CAPABILITIES lpspc) { if (!lpspc) return FALSE; BOOL result = TrueGetPwrCapabilities(lpspc); if (!result) return FALSE; lpspc->ThermalControl = TRUE; return result; }
The most robust approach is to use a debugger to patch functions or inspect memory for suspicious instructions. This process can even be automated with scripts.
While flexible, patching is often less stable than VM configuration changes and generally less stealthy than API hooking. Some Windows API functions are explicitly designed to detect the presence of debuggers, making this method more complex and not always foolproof.
Anti-VM techniques cover a wide range of methods for detecting emulated and virtualized environments. Many are relatively simple to bypass. API-based checks can be neutralized through function hooking or debugger patching, while instructions like SIDT
or CPUID
can be intercepted or altered, making them straightforward to overcome.
TCG-focused techniques, however, are more challenging — particularly the unaligned vectorization method. This relies on a limitation inherent to QEMU, making it more difficult to address with patches. One possible workaround is to modify the executable with a debugger, replacing the instruction with one that QEMU will mishandle (such as rdtsc
). Still, if the program also includes anti-debugging protections, the process becomes much harder.
Overall, while anti-VM methods are useful, most are relatively easy to counter and are less common today compared to anti-debugging strategies. A longitudinal study of malware evasive techniques analyzing over 180,000 Windows samples showed that only around 20% used VM detection, while 80% relied on anti-debugging. This suggests that VM detection is losing ground to more robust anti-analysis techniques.
All the detection and bypass strategies discussed here can be explored and tested in esReverse, which provides a full platform for binary analysis with advanced features such as Time Travel Analysis.