In this series we’ll document a novel and as-yet-undocumented Virtual Machine detection trick for each month of 2021. These detection tricks will be focused on 64-bit Windows 10 or Windows Server 2019 guests, targeting a variety of VM platforms.
Physical memory resource maps
By far the most ubiquitous source of discrepancies between VMs and real hardware is the virtualized hardware façade that is presented by the VM platform. To kick off this series, we’re going to take a look at system resource maps. These lists are available to unprivileged users via the Windows registry, in the following paths:
HKLM\Hardware\ResourceMap\System Resources\Loader Reserved\
HKLM\Hardware\ResourceMap\System Resources\Physical Memory\
HKLM\Hardware\ResourceMap\System Resources\Reserved\
The DACLs on these keys are permissive, allowing read access by everyone, including low integrity processes.
If you take a look at the values in these keys, in regedit, you might notice they are of an unfamiliar type.
The REG_RESOURCE_LIST
key type isn’t a general-purpose data type. It is specifically for device driver resource lists.
A convenient way to query these, without needing to know what the value of the REG_RESOURCE_LIST
constant is on your particular target system, is to call RegQueryValueEx on the registry value with the lpData
parameter set to NULL
. This causes the type constant and length to be returned in the lpType
and lpcbData
parameters respectively:
HKEY hKey = NULL; LPTSTR pszSubKey = "Hardware\\ResourceMap\\System Resources\\Physical Memory"; LPTSTR pszValueName = ".Raw"; DWORD dwLength = 0; DWORD dwType = 0; RegOpenKey(HKEY_LOCAL_MACHINE, pszSubKey, &hKey); RegQueryValueEx(hKey, pszValueName, 0, &dwType, NULL, &dwLength);
This tells you what the REG_RESOURCE_LIST
type constant is, and how much space you need to allocate when reading the value.
The structure of this data type is defined in CM_RESOURCE_LIST:
typedef struct _CM_RESOURCE_LIST { ULONG Count; CM_FULL_RESOURCE_DESCRIPTOR List[1]; } CM_RESOURCE_LIST, *PCM_RESOURCE_LIST;
Digging down a few levels of nested structures, we eventually find that the meat of the information is stored in an array of CM_PARTIAL_RESOURCE_DESCRIPTOR structures.
typedef struct _CM_PARTIAL_RESOURCE_DESCRIPTOR { UCHAR Type; UCHAR ShareDisposition; USHORT Flags; union { struct { PHYSICAL_ADDRESS Start; ULONG Length; } Generic; struct { PHYSICAL_ADDRESS Start; ULONG Length; } Port; struct { #if ... USHORT Level; /* ... */
This structure describes general-purpose descriptor that may contain a number of different fields, depending on the specific type of resource being described. The type, and by extension the union member of the struct we should look at, is specified by the Type field at the start of the structure.
The type of descriptor we are interested in is CmResourceTypeMemory
(numeric value 3). This is used to describe physical memory resource regions. The structure is simple:
struct { PHYSICAL_ADDRESS Start; ULONG Length; } Memory;
By iterating through all of the descriptors, we can extract all of the memory resource map entries.
At this point we can extract and compare the memory resource maps from various physical hosts and VMs. The initial results are as follows.
Type | OS/Platform | Assigned RAM | Physical Memory Translated | Reserved Translated | Loader Reserved Raw |
Host 1 | Win10 x64 | N/A | 00001000 – 0003e000
5acf6000 – 66e71000 |
00001000 – 0003e000
00203000 – 00207000 00600000 – 00800000 |
00000000 – 00101000
002f3000 – 002f8000 02600000 – 02800000 5a8cb000 – 5acf6000 fd000000 – fe800000 |
Host 2 | Win10 x64 | N/A | 00001000 – 0009d000
cb52d000 – cb98c000 |
00001000 – 0009d000 | 00000000 – 000a0000
cb526000 – cb52d000 fec00000 – fec01000 |
Host 3 | Win10 x64 (w. Hyper-V) |
N/A | 00001000 – 0009d000 | 00001000 – 0009d000
001f5000 – 001fe000 002fe000 – 003fe000 |
00000000 – 000a0000
001f5000 – 001fe000 00293000 – 00297000 00875000 – 0095a000 0364c000 – 03675000 6929d000 – 7fa00000 |
Host 4 | Win10 x64 | N/A | 00001000 – 00058000
b83ab000 – c8bed000 |
00001000 – 00058000 | 00000000 – 00100000
00207000 – 0020b000 f0000000 – f8000000 |
VM1 | Win10 x64 (Hyper-V) |
Dynamic | 00001000 – 000a0000
f7fff000 – f8000000 |
00001000 – 0001a000 | 00000000 – 0001a000
f6ecc000 – f6f1b000 |
VM2 | Win10 x64 (Hyper-V) |
Dynamic | 00001000 – 000a0000 | 00001000 – 000a0000 | 00000000 – 000a0000
7eee9000 – 7ef1b000 |
VM3 | Win10 x64 (Hyper-V) |
2GB | 00001000 – 000a0000 | 00001000 – 000a0000 | 00000000 – 000a0000
7eee9000 – 7ef1b000 |
VM4 | Win10 x64 (VirtualBox) |
4GB | 00001000 – 0009f000 | 00001000 – 00018000 | 00000000 – 00018000
00102000 – 00103000 |
VM5 | Win10 x64 (VirtualBox) |
4GB | 00001000 – 0009f000 | 00001000 – 00017000 | 00000000 – 00017000
00102000 – 00103000 |
VM6 | Win8.1 x64 (VirtualBox) |
2GB | 00001000 – 0009f000 | 00001000 – 0000e000 | 00000000 – 0000e000
000f0000 – 00100000 |
VM7 | Win8.1 x64 (VirtualBox) |
10GB | 00001000 – 0009f000 | 00001000 – 0000e000 | 00000000 – 0000e000
000f0000 – 00130000 |
VM8 | Win7 x64 (VirtualBox) |
4GB | 00001000 – 0009f000 | 00001000 – 00008000 | 00000000 – 00008000
00110000 – 00140000 |
VM9 | Win7 x86* (VirtualBox) |
2GB | 00001000 – 0009f000
00100000 – 7fff0000 |
00001000 – 00005000
00030000 – 00040000 |
00000000 – 00005000
00030000 – 00040000 0009f000 – 000a0000 000f0000 – 00100000 7fff0000 – 80000000 fec00000 – fec01000 fee00000 – fee01000 fffc0000 – 100000000 |
The first clear finding is that VMs tend to have fewer map entries, particularly in the loader reserved key. The clear exception is the Win7 x86 VM – the only x86 entry in the table – which has many more regions than the rest in the rightmost column. This count distinction is unfortunately insufficient as a detection technique, as there is sufficient variation between hardware and platform to make this prone to false positives.
Looking a little more carefully, we can start to notice some more useful patterns:
- All Hyper-V VMs have a Physical Memory Translated region matching 00001000 – 000a0000.
- All VirtualBox VMs have a Physical Memory Translated region matching 00001000 – 009f000.
- None of the host machines have Physical Memory Translated regions matching these addresses.
- On both VirtualBox and Hyper-V, the lowest Reserved Translated memory region always perfectly overlaps the lowest Loader Reserved Raw memory region (e.g. 00001000 – 0000e000 becomes 00000000 – 0000e000).
By combining the overlapped region test with the known fixed region test for Hyper-V and VirtualBox, we can determine the status of the current system with confidence.
This check can be implemented in C as follows:
// system resources physical memory map VM detection trick // written by Graham Sutherland (@gsuberland) for Nettitude // based on prior work done as part of the al-khaser project // https://github.com/LordNoteworthy/al-khaser/ // ref: https://blog.xpnsec.com/total-meltdown-cve-2018-1038/ // ref: https://gist.github.com/xpn/3792ec34d712425a5c47caf5677de5fe // compile: // cl.exe -DUNICODE -D_UNICODE vm_resource_check.c kernel32.lib advapi32.lib /W4 /link /out:vm_resource_check.exe #include <stdio.h> #include <Windows.h> typedef LARGE_INTEGER PHYSICAL_ADDRESS, *PPHYSICAL_ADDRESS; #pragma pack(push,4) typedef struct _CM_PARTIAL_RESOURCE_DESCRIPTOR { UCHAR Type; UCHAR ShareDisposition; USHORT Flags; union { struct { PHYSICAL_ADDRESS Start; ULONG Length; } Generic; struct { PHYSICAL_ADDRESS Start; ULONG Length; } Port; struct { #if defined(NT_PROCESSOR_GROUPS) USHORT Level; USHORT Group; #else ULONG Level; #endif ULONG Vector; KAFFINITY Affinity; } Interrupt; struct { union { struct { #if defined(NT_PROCESSOR_GROUPS) USHORT Group; #else USHORT Reserved; #endif USHORT MessageCount; ULONG Vector; KAFFINITY Affinity; } Raw; struct { #if defined(NT_PROCESSOR_GROUPS) USHORT Level; USHORT Group; #else ULONG Level; #endif ULONG Vector; KAFFINITY Affinity; } Translated; } DUMMYUNIONNAME; } MessageInterrupt; struct { PHYSICAL_ADDRESS Start; ULONG Length; } Memory; struct { ULONG Channel; ULONG Port; ULONG Reserved1; } Dma; struct { ULONG Channel; ULONG RequestLine; UCHAR TransferWidth; UCHAR Reserved1; UCHAR Reserved2; UCHAR Reserved3; } DmaV3; struct { ULONG Data[3]; } DevicePrivate; struct { ULONG Start; ULONG Length; ULONG Reserved; } BusNumber; struct { ULONG DataSize; ULONG Reserved1; ULONG Reserved2; } DeviceSpecificData; struct { PHYSICAL_ADDRESS Start; ULONG Length40; } Memory40; struct { PHYSICAL_ADDRESS Start; ULONG Length48; } Memory48; struct { PHYSICAL_ADDRESS Start; ULONG Length64; } Memory64; struct { UCHAR Class; UCHAR Type; UCHAR Reserved1; UCHAR Reserved2; ULONG IdLowPart; ULONG IdHighPart; } Connection; } u; } CM_PARTIAL_RESOURCE_DESCRIPTOR, *PCM_PARTIAL_RESOURCE_DESCRIPTOR; #pragma pack(pop,4) typedef enum _INTERFACE_TYPE { InterfaceTypeUndefined, Internal, Isa, Eisa, MicroChannel, TurboChannel, PCIBus, VMEBus, NuBus, PCMCIABus, CBus, MPIBus, MPSABus, ProcessorInternal, InternalPowerBus, PNPISABus, PNPBus, Vmcs, ACPIBus, MaximumInterfaceType } INTERFACE_TYPE, *PINTERFACE_TYPE; typedef struct _CM_PARTIAL_RESOURCE_LIST { USHORT Version; USHORT Revision; ULONG Count; CM_PARTIAL_RESOURCE_DESCRIPTOR PartialDescriptors[1]; } CM_PARTIAL_RESOURCE_LIST, *PCM_PARTIAL_RESOURCE_LIST; typedef struct _CM_FULL_RESOURCE_DESCRIPTOR { INTERFACE_TYPE InterfaceType; ULONG BusNumber; CM_PARTIAL_RESOURCE_LIST PartialResourceList; } *PCM_FULL_RESOURCE_DESCRIPTOR, CM_FULL_RESOURCE_DESCRIPTOR; typedef struct _CM_RESOURCE_LIST { ULONG Count; CM_FULL_RESOURCE_DESCRIPTOR List[1]; } *PCM_RESOURCE_LIST, CM_RESOURCE_LIST; struct memory_region { ULONG64 size; ULONG64 address; }; struct map_key { LPTSTR KeyPath; LPTSTR ValueName; }; /* registry keys for resource maps */ #define VM_RESOURCE_CHECK_REGKEY_PHYSICAL 0 #define VM_RESOURCE_CHECK_REGKEY_RESERVED 1 #define VM_RESOURCE_CHECK_REGKEY_LOADER_RESERVED 2 #define ResourceRegistryKeysLength 3 const struct map_key ResourceRegistryKeys[ResourceRegistryKeysLength] = { { L"Hardware\\ResourceMap\\System Resources\\Physical Memory", L".Translated" }, { L"Hardware\\ResourceMap\\System Resources\\Reserved", L".Translated" }, { L"Hardware\\ResourceMap\\System Resources\\Loader Reserved", L".Raw" } }; /* parse a REG_RESOURCE_LIST value for memory descriptors */ DWORD parse_memory_map(struct memory_region *regions, struct map_key key) { HKEY hKey = NULL; LPTSTR pszSubKey = key.KeyPath; LPTSTR pszValueName = key.ValueName; LPBYTE lpData = NULL; DWORD dwLength = 0, count = 0, type = 0;; DWORD result; if ((result = RegOpenKeyW(HKEY_LOCAL_MACHINE, pszSubKey, &hKey)) != ERROR_SUCCESS) { printf("[X] Could not get reg key: %d / %d\n", result, GetLastError()); return 0; } if ((result = RegQueryValueExW(hKey, pszValueName, 0, &type, NULL, &dwLength)) != ERROR_SUCCESS) { printf("[X] Could not query hardware key: %d / %d\n", result, GetLastError()); return 0; } lpData = (LPBYTE)malloc(dwLength); RegQueryValueEx(hKey, pszValueName, 0, &type, lpData, &dwLength); CM_RESOURCE_LIST *resource_list = (CM_RESOURCE_LIST *)lpData; for (DWORD i = 0; i < resource_list->Count; i++) { for (DWORD j = 0; j < resource_list->List[0].PartialResourceList.Count; j++) { if (resource_list->List[i].PartialResourceList.PartialDescriptors[j].Type == 3) { if (regions != NULL) { regions->address = resource_list->List[i].PartialResourceList.PartialDescriptors[j].u.Memory.Start.QuadPart; regions->size = resource_list->List[i].PartialResourceList.PartialDescriptors[j].u.Memory.Length; regions++; } count++; } } } return count; } #define VM_RESOURCE_CHECK_ERROR -1 #define VM_RESOURCE_CHECK_NO_VM 0 #define VM_RESOURCE_CHECK_HYPERV 1 #define VM_RESOURCE_CHECK_VBOX 2 #define VM_RESOURCE_CHECK_UNKNOWN_PLATFORM 99 int vm_resource_check( struct memory_region *phys, int phys_count, struct memory_region *reserved, int reserved_count, struct memory_region *loader_reserved, int loader_reserved_count) { const ULONG64 VBOX_PHYS_LO = 0x0000000000001000ULL; const ULONG64 VBOX_PHYS_HI = 0x000000000009f000ULL; const ULONG64 HYPERV_PHYS_LO = 0x0000000000001000ULL; const ULONG64 HYPERV_PHYS_HI = 0x00000000000a0000ULL; const ULONG64 RESERVED_ADDR_LOW = 0x0000000000001000ULL; const ULONG64 LOADER_RESERVED_ADDR_LOW = 0x0000000000000000ULL; if (phys_count <= 0 || reserved_count <= 0 || loader_reserved_count <= 0) { return VM_RESOURCE_CHECK_ERROR; } if (phys == NULL || reserved == NULL || loader_reserved == NULL) { return VM_RESOURCE_CHECK_ERROR; } /* find the reserved address range starting RESERVED_ADDR_LOW, and record its end address */ ULONG64 lowestReservedAddrRangeEnd = 0; for (int i = 0; i < reserved_count; i++) { if (reserved[i].address == RESERVED_ADDR_LOW) { lowestReservedAddrRangeEnd = reserved[i].address + reserved[i].size; break; } } if (lowestReservedAddrRangeEnd == 0) { /* every system tested had a range starting at RESERVED_ADDR_LOW */ /* this is an outlier. error. */ return VM_RESOURCE_CHECK_ERROR; } /* find the loader reserved address range starting LOADER_RESERVED_ADDR_LOW, and record its end address */ ULONG64 lowestLoaderReservedAddrRangeEnd = 0; for (int i = 0; i < loader_reserved_count; i++) { if (loader_reserved[i].address == LOADER_RESERVED_ADDR_LOW) { lowestLoaderReservedAddrRangeEnd = loader_reserved[i].address + loader_reserved[i].size; break; } } if (lowestLoaderReservedAddrRangeEnd == 0) { /* every system tested had a range starting at LOADER_RESERVED_ADDR_LOW */ /* this is an outlier. error. */ return VM_RESOURCE_CHECK_ERROR; } /* check if the end addresses are equal. if not, we haven't detected a VM */ if (lowestReservedAddrRangeEnd != lowestLoaderReservedAddrRangeEnd) { return VM_RESOURCE_CHECK_NO_VM; } /* now find the type of VM by its known physical memory range */ for (int i = 0; i < phys_count; i++) { if (phys[i].address == HYPERV_PHYS_LO && (phys[i].address + phys[i].size) == HYPERV_PHYS_HI) { /* hyper-v */ return VM_RESOURCE_CHECK_HYPERV; } if (phys[i].address == VBOX_PHYS_LO && (phys[i].address + phys[i].size) == VBOX_PHYS_HI) { /* vbox */ return VM_RESOURCE_CHECK_VBOX; } } /* pretty sure it's a VM, but we don't know what type */ return VM_RESOURCE_CHECK_UNKNOWN_PLATFORM; } int main() { DWORD count; printf("[*] Getting physical memory regions from registry\n"); struct memory_region *regions[ResourceRegistryKeysLength]; int region_counts[ResourceRegistryKeysLength]; for (int i = 0; i < ResourceRegistryKeysLength; i++) { printf("[*] Reading data from %ws\\%ws\n", ResourceRegistryKeys[i].KeyPath, ResourceRegistryKeys[i].ValueName); count = parse_memory_map(NULL, ResourceRegistryKeys[i]); if (count == 0) { printf("[X] Could not find memory region, exiting.\n"); return -1; } regions[i] = (struct memory_region *)malloc(sizeof(struct memory_region) * count); count = parse_memory_map(regions[i], ResourceRegistryKeys[i]); region_counts[i] = count; for (DWORD r = 0; r < count; r++) { printf("[*] --> Memory region found: %.16llx - %.16llx\n", regions[i][r].address, regions[i][r].address + regions[i][r].size); } } int check_result = vm_resource_check( regions[VM_RESOURCE_CHECK_REGKEY_PHYSICAL], region_counts[VM_RESOURCE_CHECK_REGKEY_PHYSICAL], regions[VM_RESOURCE_CHECK_REGKEY_RESERVED], region_counts[VM_RESOURCE_CHECK_REGKEY_RESERVED], regions[VM_RESOURCE_CHECK_REGKEY_LOADER_RESERVED], region_counts[VM_RESOURCE_CHECK_REGKEY_LOADER_RESERVED] ); switch (check_result) { case VM_RESOURCE_CHECK_ERROR: printf("[X] Error occurred during VM check.\n"); break; case VM_RESOURCE_CHECK_NO_VM: printf("[-] No VM detected.\n"); break; case VM_RESOURCE_CHECK_HYPERV: printf("[+] Detected Hyper-V.\n"); break; case VM_RESOURCE_CHECK_VBOX: printf("[+] Detected VirtualBox.\n"); break; case VM_RESOURCE_CHECK_UNKNOWN_PLATFORM: printf("[+] Likely VM detected, but cannot identify platform. \n"); break; default: printf("[X] VM check returned unexpected value.\n"); break; } printf("\nDone.\n"); return 0; }
That’s it for this month’s instalment of VM Detection Tricks. Stay tuned for part two in February.