Dozens of virtual machine checks are scattered throughout various open-source projects. You’ll see a handful of the same checks in various applications, from commercial to fully fleshed-out malware. The checks typically involve looking for drivers, devices, processes, registry entries, custom vendor information, timing attacks, etc. Most of these methods are easily mitigated by tweaking the VMs configuration.
With respect to commercial virtualization tools like VMware, VirtualBox, Xen, Hyper-V, Parallels, etc., there are some universal ways to determine if your application is operating in a virtual environment. The methods presented in this article serve as an additional place to check and also provide a few ways to get around the checks. The primary purpose is to show a few attractive alternatives to the very public and well-known checks that most applications employ. This is not a new detection method, it’s just something I was looking into and how it could be mitigated. However, I did notice that large projects with many VM checks don’t utilize this, which is a little surprising.
ACPI Table Entries
I was doing a little poking around trying to determine a simple method to detect if an application was running in a virtual environment and which virtual environment it was. The constraint was that it had to be done from user mode and stay compact in implementation. Assuming the public checks were mitigated, I searched for inconsistencies between a real machine and several commercial virtualization platforms. There are quite a few fun spots to utilize, such as the PCI bus and PnP device information, but they’re a bit out of reach from user mode (unless you go through the registry). I dug through ACPI documentation again and decided to go down the rabbit hole of evading this check. I read about various tables and their purpose when I remembered that the vendor could set the OEM ID and most fields in the ACPI table header. It just so happens that ACPI tables are available for query from user mode using EnumSystemFirmwareTables/GetSystemFirmwareTable or go directly to the source through NtQuerySystemInformation with the SystemFirmwareTableInformation class.
Note: Yes, these could be mitigated through hooks on kernel32, IAT hook on NtQuerySystemInformation, or a syscall hook, DKOM, etc., but we're interested in a more elegant solution.
With this in mind, I slapped together a tool that would dump out all of the ACPI tables and their headers so that I could diff my real machines against a handful of commercial/popular open-source tools like QEMU. The results speak for themselves.
ACPI Table Enumeration (Real Hardware)
10:47:25 26-02-2023 => [ACPI TABLE] DBGP 10:47:25 26-02-2023 => [ACPI TABLE] MCFG 10:47:25 26-02-2023 => [ACPI TABLE] FACP 10:47:25 26-02-2023 => [ACPI TABLE] APIC 10:47:25 26-02-2023 => [ACPI TABLE] HPET 10:47:25 26-02-2023 => [ACPI TABLE] FPDT 10:47:25 26-02-2023 => [ACPI TABLE] SSDT 10:47:25 26-02-2023 => [ACPI TABLE] SSDT 10:47:25 26-02-2023 => [ACPI TABLE] FIDT 10:47:25 26-02-2023 => [ACPI TABLE] SSDT 10:47:25 26-02-2023 => [ACPI TABLE] SSDT 10:47:25 26-02-2023 => [ACPI TABLE] SSDT 10:47:25 26-02-2023 => [ACPI TABLE] SSDT 10:47:25 26-02-2023 => [ACPI TABLE] NHLT 10:47:25 26-02-2023 => [ACPI TABLE] LPIT 10:47:25 26-02-2023 => [ACPI TABLE] WSMT 10:47:25 26-02-2023 => [ACPI TABLE] SSDT 10:47:25 26-02-2023 => [ACPI TABLE] SSDT 10:47:25 26-02-2023 => [ACPI TABLE] DBG2 10:47:25 26-02-2023 => [ACPI TABLE] SSDT 10:47:25 26-02-2023 => [ACPI TABLE] BGRT 10:47:25 26-02-2023 => [ACPI TABLE] WPBT 10:47:25 26-02-2023 => [ACPI OEMID] ALASKA 10:47:25 26-02-2023 => [ACPI OEMID] ALASKA 10:47:25 26-02-2023 => [ACPI OEMID] ALASKA 10:47:25 26-02-2023 => [ACPI OEMID] ALASKA 10:47:25 26-02-2023 => [ACPI OEMID] ALASKA 10:47:25 26-02-2023 => [ACPI OEMID] ALASKA [...]
ACPI Table Enumeration (VMware)
07:54:39 26-02-2023 => [ACPI TABLE] MCFG 07:54:39 26-02-2023 => [ACPI TABLE] FACP 07:54:39 26-02-2023 => [ACPI TABLE] SRAT 07:54:39 26-02-2023 => [ACPI TABLE] WAET 07:54:39 26-02-2023 => [ACPI TABLE] APIC 07:54:39 26-02-2023 => [ACPI TABLE] HPET 07:54:39 26-02-2023 => [ACPI TABLE] WSMT 07:54:39 26-02-2023 => [ACPI OEMID] VMWARE 07:54:39 26-02-2023 => [ACPI OEMID] INTEL 07:54:39 26-02-2023 => [ACPI OEMID] VMWARE 07:54:39 26-02-2023 => [ACPI OEMID] VMWARE 07:54:39 26-02-2023 => [ACPI OEMID] VMWARE 07:54:39 26-02-2023 => [ACPI OEMID] VMWARE 07:54:39 26-02-2023 => [ACPI OEMID] VMWARE
ACPI Table Enumeration (QEMU)
07:58:21 26-02-2023 => [ACPI TABLE] RSDT 07:58:21 26-02-2023 => [ACPI TABLE] FACP 07:58:21 26-02-2023 => [ACPI TABLE] APIC 07:58:21 26-02-2023 => [ACPI TABLE] MCFG 07:58:21 26-02-2023 => [ACPI TABLE] WAET 07:58:21 26-02-2023 => [ACPI OEMID] BOCHS 07:58:21 26-02-2023 => [ACPI OEMID] BOCHS 07:58:21 26-02-2023 => [ACPI OEMID] BOCHS 07:58:21 26-02-2023 => [ACPI OEMID] BOCHS 07:58:21 26-02-2023 => [ACPI OEMID] BOCHS 07:58:21 26-02-2023 => [ACPI OEMID] BOCHS
After installing the most popular virtualization applications (Hyper-V, VirtualBox, Parallels, etc.), It was easy to see they all had specific OEMIDs (as well as OEM Table IDs and Creator IDs) that indicated what the hosting tool was. You may have noticed several things, like the number of tables being significantly less and a specific table appearing on all these platforms — WAET.
After making a note of the differences and the identifiers for each platform, I threw together a quick check given below.
struct acpi_table_header_format { uint32_t signature; uint32_t length; uint8_t revision; uint8_t checksum; uint8_t oem_id[ 6 ]; uint64_t oem_table_id; uint32_t oem_revision; uint32_t creator_id; uint32_t creator_revision; };
std::unordered_set<std::string> disallowed_acpi_entries = {{"WAET"}, [...]}; std::unordered_set<std::string> disallowed_oem_ids = { {"VMWARE"}, {"VBOX"}, {"BOCHS"}, {"VRTUAL"}, {"PRLS"}, }; void check_firmware_tables() { constexpr unsigned long acpi_signature = 'ACPI'; buffer<unsigned long> buf; buf.resize(0x1000); enumerate_fw_tables(acpi_signature, buf.get(), 0x1000); std::vector<std::pair<std::string, unsigned long>> firmware_table_ids{}; for (auto it = 0; buf.get()[it] != 0; it++) { char tid[6] = {0}; memcpy(tid, &buf.get()[it], sizeof(unsigned long)); firmware_table_ids.emplace_back(std::make_pair(tid, buf.get()[it])); if (disallowed_acpi_entries.contains(tid)) elog::critical("acpi entry indicates virtual environment (%s).", tid); } for (const auto &table_id : firmware_table_ids | std::views::values) { buffer<uint8_t> fwt_buffer; fwt_buffer.resize(0x100); if (const auto ret = get_fw_table_info( acpi_signature, table_id, fwt_buffer.get(), fwt_buffer.size()); ret == 0) continue; auto *hdr = reinterpret_cast<acpi_table_header_format *>(fwt_buffer.get()); char oem_id[8] = {0}; memcpy(oem_id, hdr->oem_id, 0x6); if (disallowed_oem_ids.contains(oem_id)) elog::critical("acpi oem id indicates virtual environment (%s).", oem_id); } }
If you were to run an application with the above check implemented in VMware, you’d get the following results:
07:54:39 31-02-2023 => [crit] acpi entry indicates virtual environment (WAET). 07:54:39 31-02-2023 => [crit] acpi oem id is blacklisted (VMWARE).
Of course, this is only useful for tools implementing the WAET or modifying the ACPI table headers. If you’ve read my other posts, you’re likely expecting that I’ll go into detail about the WAET and some other borderline useless information… and you’re correct.
WAET, don’t go.
The purpose of the Windows ACPI Emulated Devices Table (WAET) is pretty straightforward in the documentation provided by Microsoft, but if you don’t want to read that, then know that it’s a table that is required for hypervisors to implement if they are emulating specific devices like the ACPI PM timer or RTC. The idea behind this table is to inform Windows that an emulated RTC/ACPI PM timer is present and functional; “this prevents needless workarounds that can introduce overhead or incorrect behavior when the OS access the device.” (Microsoft WAET Documentation).
It’s worth noting that the table’s presence changes the guest’s behavior. For instance, if the ACPI_WAET_PM_TIMER_GOOD
flag is asserted, the guest will only perform a single read of the ACPI PM timer instead of multiple, reducing unnecessary VM exits and improving performance. VMware, as an example, sets ACPI_WAET_PM_TIMER_GOOD
a flag; QEMU also sets the ACPI_WAET_PM_TIMER_GOOD
. There isn’t an additional trade-off noted or discernible by an observer other than a visible table indicating emulation. The platforms that implement this do it because it’s better for everyone (in terms of performance).
ACPI Table Visibility
You don’t have to stop here either; the ACPI tables are guest visible, which means you can find additional tables and their associated fields that vary from machine to machine. Some only exist on tools like VMware, QEMU, etc., while others exist on real hardware but present with different flags depending on whether it’s a virtual environment. Poke around and see what different tables and field configurations you can find on specific platforms. The inconsistencies are easy to spot but also easy to mitigate; digging into the specifications and locating more specific detections is better for the longevity of any VM check though it may not be “universal.”
That said, you can remove this table from the ACPI namespace without any issues, and the check for this is moot. I assumed you could do this by adding acpi.skiptables="WAET"
to the VMX configuration.
OEM ID, OEM Table IDs, Revision Numbers, etc.
The other part of the check is looking at the OEMID field in the ACPI table header. As their name implies, these fields are set by the OEM to identify themselves. Each virtualization tool I looked at modifies the OEM information to reflect which platform it is. If you’re looking to break this type of check from discovering a hardened VM, you may have to dig a little more into if it’s possible to passthrough or outright modify the OEM IDs at the construction of the environment. With respect to VMware, a setting in the VMX file allows SMBIOS to reflect the host information — SMBIOS.reflectHost = TRUE
. I assumed there might be something similar to that setting for ACPI OEM information, but I couldn’t do it through the typical configuration.
So, what the hell? No elegant solution? Well… I had an idea — why not attempt to inject ACPI tables through configuration options? I’m unsure of alternatives outside of attempting to intercept and modify information during runtime… and personally, I’d opt for a better solution. Anyways, how can we do this? Well, it appears that VMware added support for removing ACPI tables from the guest for EFI and legacy BIOS way back in 2012. We’re able to add tables using acpi.addtable.filename = "path/to/acpi_table.tbl"
and acpi.skiptables = "WAET,MCFG,etc"
. It’s documented that the skiptables
functionality is executed before adding the table, which allows us to use this extension to replace ACPI tables. Injecting ACPI tables into/customizing existing tables for VMware isn’t something I planned on covering in this article, as I intended for it to be brief, but I’ll quickly cover the overall thought process. In a future article, I may dig into more fun stuff related to ACPI hacks and real hardware.
Mitigating ACPI/FIRM Check for VMware
- Determine which ASL compiler was used to build the target table (Intel ASL Compiler/Microsoft ASL Compiler).
- Download the compiler/decompiler from ACPICA (ACPI component architecture) project.
- Decompile and view the target table source (e.g. WAET).
- Dump the ACPI table(s) of interest.
- Modify the ACPI table header and additional characteristics.
- Recompile table and append configuration options to replace the table(s) in VMX file.
- Boot and verify.
We’ll look at the WAET table as an example to modify. After disassembling the table, this is what it looks like:
/* * Intel ACPI Component Architecture * AML/ASL+ Disassembler version 20221020 (32-bit version) * Copyright (c) 2000 - 2022 Intel Corporation * * Disassembly of waet.dat, Fri Mar 10 11:08:30 2023 * * ACPI Data Table [WAET] * * Format: [HexOffset DecimalOffset ByteLength] FieldName : FieldValue (in hex) */ [000h 0000 004h] Signature : "WAET" [Windows ACPI Emulated Devices Table] [004h 0004 004h] Table Length : 00000028 [008h 0008 001h] Revision : 01 [009h 0009 001h] Checksum : 61 [00Ah 0010 006h] Oem ID : "VMWARE" [010h 0016 008h] Oem Table ID : "VMW WAET" [018h 0024 004h] Oem Revision : 06040001 [01Ch 0028 004h] Asl Compiler ID : "VMW " [020h 0032 004h] Asl Compiler Revision : 00000001 [024h 0036 004h] Flags (decoded below) : 00000002 RTC needs no INT ack : 0 PM timer, one read only : 1 Raw Table Data: Length 40 (0x28) 0000: 57 41 45 54 28 00 00 00 01 61 56 4D 57 41 52 45 // WAET(....aVMWARE 0010: 56 4D 57 20 57 41 45 54 01 00 04 06 56 4D 57 20 // VMW WAET....VMW 0020: 01 00 00 00 02 00 00 00 // ........
We can see all the obvious indicators, and we’ll modify them not to be VMWARE
. I took the existing header, modified it, recompiled it, and updated the checksum to reflect the changes. Now, I’ll add the following to our .vmx file.
acpi.skiptables = "WAET" acpi.addtable.filename="waet.dat"
Let’s boot our VM (hope it boots) and see the results…
Excellent. Aside from the table’s signature still being “problematic,” the other checked field will no longer be flagged. We can rinse and repeat the process for the other tables or mimic an actual hardware setup with ACPI entries composed of something equivalent to no-op. The details of ACPI and the ASL/AML interpreter will not be covered here. If you’re interested in learning more about these and poking about yourself, you can find more information in the recommended reading section at the end of the article.
Honorable Mention for QEMU
For those looking to achieve the same results in QEMU, you can count your blessings that the developers provided a convenient way to set the OEM ID and associated fields for ACPI tables using something like this to override default values: qemu -acpidefault oem_id=ALASKA,oem_table_id=AMI. It should propagate to all other tables that use the default values. User-defined ACPI tables are not included in this.
HPET Verification
It’s important to note that even with adjusting the ACPI table header to match a “real hardware” configuration, there are simple checks which are dead giveaways that you’re in a virtual environment. One to look at is the high-precision event timer (HPET). The structure is below, and the values with the machine they were taken from are provided.
struct hpet_acpi_data { uint32_t hardware_block_id; uint8_t space_id; uint8_t bit_width; uint8_t bit_offset; uint8_t encoded_access_width; uint64_t address; uint8_t sequence_number; uint16_t minimum_clock_ticks; uint8_t flags; };
[VMware] Hardware Block ID : 8086AF01 Timer Block Register : [Generic Address Structure] Space ID : 00 [SystemMemory] Bit Width : 00 Bit Offset : 00 Encoded Access Width : 00 [Undefined/Legacy] Address : 00000000FED00000 Sequence Number : 00 Minimum Clock Ticks : 37EE Flags (decoded below) : 01 4K Page Protect : 1 64K Page Protect : 0 [i9-10850k] Hardware Block ID : 8086A201 Timer Block Register : [Generic Address Structure] Space ID : 00 [SystemMemory] Bit Width : 40 Bit Offset : 00 Encoded Access Width : 00 [Undefined/Legacy] Address : 00000000FED00000 Sequence Number : 00 Minimum Clock Ticks : 0080 Flags (decoded below) : 00 4K Page Protect : 0 64K Page Protect : 0 [Ryzen 7 3700X] Hardware Block ID : 10228201 Timer Block Register : [Generic Address Structure] Space ID : 00 [SystemMemory] Bit Width : 40 Bit Offset : 00 Encoded Access Width : 00 [Undefined/Legacy] Address : 00000000FED00000 Sequence Number : 00 Minimum Clock Ticks : 37EE Flags (decoded below) : 00 4K Page Protect : 0 64K Page Protect : 0 [i9-13900k] Hardware Block ID : 8086A201 Timer Block Register : [Generic Address Structure] Space ID : 00 [SystemMemory] Bit Width : 40 Bit Offset : 00 Encoded Access Width : 00 [Undefined/Legacy] Address : 00000000FED00000 Sequence Number : 00 Minimum Clock Ticks : 0080 Flags (decoded below) : 00 4K Page Protect : 0 64K Page Protect : 0 [i7-10710U NUC] Hardware Block ID : 8086A201 Timer Block Register : [Generic Address Structure] Space ID : 00 [SystemMemory] Bit Width : 40 Bit Offset : 00 Encoded Access Width : 00 [Undefined/Legacy] Address : 00000000FED00000 Sequence Number : 00 Minimum Clock Ticks : 0080 Flags (decoded below) : 00 4K Page Protect : 0 64K Page Protect : 0 [VirtualBox] Does Not Exist [QEMU] Does Not Exist
It would be pretty easy to check for the presence of the table, and if it doesn’t exist, then it would be a safe assumption that you’re running in a virtual/emulated environment. However, if it is present, then a few fields in the table can be used to verify whether it is a virtual environment. These fields are the hardware block ID
, bit width
, and flags
. The hardware block ID contents are determined by the following:
So, the value 8086AF01
would yield the following values in this capability field:
[VMware Hardware ID of ETB] PCI Vendor ID = 8086 (Intel) Legacy Replacement IRQ Routing Capable = TRUE Reserved = 0 Counter Size = 1 Number of Comparators = 15 Hardware Revision ID = 1
Nothing stands out immediately but compared against the other systems with standard hardware, you’ll notice the number of comparators is 17 or below—the only hardware with a value higher than VMware HPET reported was an old AMD build. I dumped several other systems and had friends report their values (thank you for the additional data, @mrexodia) and noticed that the number of comparators values is between 2 and 32. The maximum number is 2 for most of these dumps, which ignores the recommendation by the specification, but I’m sure there is a valid reason that I’m not aware of.
EDIT 3/11:
Can this be used to single out VMware (assuming all other checks failed) reliably? Not 100%. From the dump of 8 machines (5 shown above), the maximum number of comparators was never 15 — either significantly less or a bit more. I found it odd that an older AMD machine had 17 comparators referenced for the first-timer block. It’s possible that the documentation I was reviewing doesn’t apply to that HPET, and the fields are off, but I couldn’t find any other form. I would say that the maximum number of comparators, while it might be indicative of a virtual environment and possibly some vHPET, it’s not 100% reliable.*
The other field of interest is the bit width
field, which is actually part of an ACPI address format given below:
struct acpi_address_format { uint8_t address_space_id; // 0 - system memory, 1 - system I/O uint8_t register_bit_width; uint8_t register_bit_offset; uint8_t reserved; uint64_t address; };
The register bit width will never be zero. This is an error in the HPET implementation for VMware, though it probably doesn’t make a difference for any user. Last, the flags field is the setting for page protections set by the OEM. This field is required for an OS that may want to expose the HPET to user space. The value 0 indicates there is no guarantee for page protection. It’s known that the HPET can be accessed from user mode in Windows, so seeing the field be zero on real hardware makes sense. I don’t know for sure why the 4KB page protection bit is set on VMware, though I have a few guesses. My theory is that to emulate the HPET and anything attempting to query it, VMware applies page protections to take advantage of the generated exceptions that occur when accessed from user space. However, from all the data collected, I have only seen the HPET have page protections set by the OEM in a virtualization tool (VMware).
Interesting Problem
Trying to clear this flag and injecting a new HPET table into the guest will cause boot looping. It would require runtime mitigation or some other ACPI hack I’m unaware of to get around a third-party checking the flags for the HPET. I plan on digging into this more since it appears it’s a difficult-to-mitigate problem (at least modifying cleanly.)
As noted earlier, using specific information that either breaks spec or would never occur in a physical environment is ideal for detecting a virtual environment. It’s a good idea to dig deep to find practical anti-VM checks.
Conclusion
I think this method of VM detection is quite useful for any application, given that it can be done from an unprivileged state with no additional privileges assigned (such as SeDebugPrivilege) to the application. Gathering information and discrepancies between a real machine and a virtual environment should be a priority for both offensive/defensive parties. Considering all the differences between commercial platforms and real hardware is vital to hardening your analysis environments against trivial checks like what was covered here. As was seen in this article, changing the OEMID and/or skipping the WAET table through a configuration option was sufficient to pass the environment verification. Of course, additional fields can be checked and validated, such as the OEM Revision, Creator ID, Description, associated fields for each table, and their settings; this was just a quick and dirty write-up because of a previous inquiry.
There are some details not covered in this article, maybe in a future one, I’ll dive into faking it on a real machine or the other platforms. However, I didn’t look into more than VMware or QEMU to override these defaults; if any reader has information on achieving the same results or better in other platforms like VirtualBox, Hyper-V, Xen, Parallels, etc. I’d be more than happy to include them with a mention.
As always, thanks for taking the time to read through the article. Please feel free to leave a comment, feedback, ideas for future posts, or otherwise. Take care.
Recommended Reading
- OpenVM Tools ChangeLog (for interesting features in VMware)
- ACPI Specification – Fields and Configuration
- Windows ACPI Emulated Devices (WAET) Documentation
- QEMU Documentation for ACPI
References
- @mrexodia for additional data to infer from.
- @sixtyvividtails for pointing out an error in the calculation of comparators.