This one is a little side quest I went on when trying to evade defender. After spending some time using Windows as my host I decided I wanted to go back to linux. Now I had a lab setup in HyperV but nothing of real importance so I decided to just start fresh this time (Using arch btw).
Now something that had always attracted my attention is VM detection. I previously had a VFIO gaming setup which meant I was able to pass through a GPU and some other components directly to my VM giving it near native performance. This setup was great however the main downside was that you still couldn’t play majority of competitive games because they would detect the VM and stop it all there. Interestingly enough there were always people who claimed to have bypassed this but it was always kept pretty hush hush as the methods woulds be patched.
While I have no intentions of actually setting up VFIO again as it was a monumental pain with the hardware I had, I am still interested in what detection methods exist, and what can be done to cloak my windows VM.
So thats where this quest begins. I’m going to setup a VM and do a little research into what VM detection can be done, and what I can do to hide from VM detection.
Broadly speaking from my research (I love how you can replace googling with research and instantly sound more credible) detection methods can be broken down into 2 main categories.
Hardware based detection, software based detection, and detection through memory and timing analysis.
While I will concede perhaps timing and memory analysis will be more interesting, Im going to do hardware based first as it seems like a good introduction.
Hardware based detection
Hardware detection involves looking at the hardware configuration and ensuring the features enabled or disabled are all representative of real hardware.
You might be wondering why? Well it turns out Windows and OS’s are big complicated things and will behave differently based on the hardware features enabled. By being transparent about what features are present and whether or not windows is running as a VM Windows can work with the hypervisor to ensure everything behaves as expected.
Unfortunately these same features can be queries by malware or Anti-cheats to do the same thing. Therefore bypassing requires a balance of cloaking enough of the features to evade detection while leaving enough enabled that stability and performance isn’t sacrificed.
Realistically, some of these features will be enabled or disabled based on hardware features, bios/eufi settings, and your star sign. This does leave us wiggle room to look like real hardware while still hopefully maintaining some level of stability and performance.
All about CPUID instructions
Bare with me here. The documentation about this stuff is pretty dry (https://www.felixcloutier.com/x86/cpuid).
I’m going to do my best to relay the exciting bits that are relevant to detection but I suppose if you wanted to, you could build a huge table of hardware configurations and their associated features and status and use that to detect anomalies but that’s far beyond my pay grade. If they want it that bad they can have it.
The CPUID instruction is the assembly instruction that returns CPU features. Most general purpose architectures have some form of this instruction to allow software to determine which features the CPU has enabled.
When we call the CPUID instruction we will pass it the leaf we want to access. Each leaf is generally organised into related features.
When we query a leaf, it will then return the features as a bitfield with each bit identifying whether a feature is enabled or disabled.
Theres a BUNCH of stuff written up for this if you care about specifics: https://www.geoffchappell.com/studies/windows/km/cpu/cpuid/index.htm?tx=236 https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/feature-discovery https://en.wikipedia.org/wiki/CPUID
For the sake of simplicity we’re only going to look at 1 leaf for now (However if this goes on we will certainly find ourselves revisiting CPUID bits!)
Hypervisor Presence Bit
As the name suggests the most straightforward bit to check is simply the one that says “I am running under a hypervisor”.
There is something interesting to consider however. When you enable HyperV in windows, your host actually will run under HyperV as a VM itself. This means that technically your Host OS becomes a VM when using HyperV. This leaves some decisions for anti-cheat and malware to make. Some Anti Cheats will refuse to run under HyperV because of this while others have made exceptions.
Moving on from that fun fact how do we actually query this bit? How do we query any of these bits?
Well we need to know what leaf we want first which is largely a matter of checking the documentation but because heres a neat SVG that explains it for x86 and x86-64 (At a high level anyways)
At its lowest level this process can be broken down into 2 steps
- Call the CPUID instruction with the desired leaf to return
mov eax, 1 ; Load leaf number into EAX
cpuid ; Execute CPUID instruction
;;; This will then return the results in the following registers
; EAX = CPU signature (family, model, stepping)
; EBX = Brand index, cache info, logical processor count
; ECX = Feature flags including HYPERVISOR BIT
; EDX = Feature flags (MMX, SSE, SSE2, etc.)
Since the HYPERVISOR bit is in the ECX register, we will check that.
- Check bit 31 of ECX using AND masking
In zig this is implemented using the following code:
const std = @import("std");
fn cpuid(leaf: u32) [4]u32 {
var eax: u32 = undefined;
var ebx: u32 = undefined;
var ecx: u32 = undefined;
var edx: u32 = undefined;
// Inline assembly for CPUID
asm volatile ("cpuid"
: [eax] "={eax}" (eax),
[ebx] "={ebx}" (ebx),
[ecx] "={ecx}" (ecx),
[edx] "={edx}" (edx),
: [leaf] "{eax}" (leaf),
);
return [4]u32{ eax, ebx, ecx, edx };
}
fn checkHypervisor() bool {
const regs = cpuid(1);
const ecx = regs[2];
// Check bit 31 in ECX
return (ecx & 0x80000000) != 0;
}
pub fn main() !void {
const is_virtualized = checkHypervisor();
std.debug.print("Hypervisor detected: {}\n", .{is_virtualized});
}
Which outputs the following:
Hypervisor detected: true
Now of course there are loads of other flags we could check here but I’ll leave that as an exercise to the reader (I’m now understanding this is code word for ‘The author is too lazy to implement this himself’)