Chip Security Testing 
Binary Security Analysis 
Resources 
Blog
Contact us
Back to all articles
Binary Analysis

Analyzing and countering Windows anti-VM techniques

15 min read
Edit by Elouan Charbonnier • Aug 29, 2025
Share

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.

 

Definitions

A few key terms before we dive deeper:

  • Host: Physical machine that runs the emulated operating system
  • Guest: Emulated operating system
  • Basic block: A sequence of instructions with a single entry point and a single exit, without any branches
  • TCG: QEMU's Tiny Code Generator, which translates guest CPU instructions into host instructions
  • Translation Block (TB): A sequence of guest instructions (typically a basic block) that TCG translates as a single object into host code
  • Granularity: The frequency at which the emulator checks for interrupts (events); checks are performed only between TBs, not between every single instruction

 


 

Emulation basics with QEMU

What is QEMU?

QEMU (Quick EMUlator) is an open-source software designed to emulate various architectures and systems.

It supports two modes of emulation:

  • System emulation: Reproduces a complete system with all necessary components (storage, RAM, CPU, etc.), allowing analysts to run an entire operating system in isolation.
  • User emulation: Executes binaries without running a full system. This is particularly useful when executing binaries from a different architecture.

QEMU can also integrate a hypervisor such as KVM to provide hardware-assisted virtualization.

 

Translation with the Tiny Code Generator (TCG)

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:

Example:

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

Willem_Dafoe_Looking_Up.jpg

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

translate_arm.png

With translation covered, let’s now look at the components QEMU emulates, such as CPU, RAM, and storage.

 

Emulated components (CPU, RAM, storage, etc.)

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.

 

Implementation

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).

 

Windows API–based techniques

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.

Thermal control management

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.

  • If ThermalControl = TRUE, the system supports thermal management, which usually means a physical machine.
  • If 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; }

🎥 Watch our video on thermal control detection

Another widely used anti-VM check relies on analyzing the system’s hard drive size and available free space.

 

Hard drive size and remaining 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; }

🎥 Watch our video on storage size detection

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.

 

Assembly instructions

Here we focus on the x86-64 instruction set, and more specifically on two instructions often used to detect virtual machines: cpuid and sidt.

CPUID

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, and 1 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-V
  • KVMKVMKVM\0\0\0 or Linux KVM Hv for KVM
  • TCGTCGTCGTCG for QEMU’s TCG (even though it is not a hypervisor, QEMU sets this leaf)
  • VBoxVBoxVBox for VirtualBox

A 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; }

🎥 Watch our CPUID detection demo

 

SIDT

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.

 

TCG-focused

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.

 

Context switching–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:

  • On real hardware, lock may be incremented twice before being decremented once, because of interrupts.
  • In TCG, the sequence is always increment followed by decrement.

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.

 

Unaligned vectorization–based detection

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; }

 

Bypass strategies

Now that we have seen different ways malware can detect emulation and virtualization, let’s look at how these techniques can be bypassed.

  • Configuration modification

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.

  • API hooking

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; }
  • Applying patches

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.

 

Conclusion

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.

esReverse Release-02.png

Share

Categories

All articles
(102)
Binary Analysis
(57)
Chip Security
(40)
Corporate News
(15)
Expert Review
(5)
Time Travel Analysis
(13)

you might also be interested in

Chip Security
Binary Analysis

"Shifting left" secures PQC implementations from physical attacks

13 min read
Edit by Hugues Thiebeauld • Jun 20, 2025
CopyRights eShard 2025.
All rights reserved
Privacy policy | Legal Notice
CHIP SECURITY
esDynamicExpertise ModulesInfraestructureLab Equipments