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.