One creepiness of how a cool January morning brought a question from a friend - how in C # to determine whether the program is running on the OS (window application on Windows 7 or later) on the virtual machine.
The requirements for such a detector were quite tough:
Under the description of the description of the implemented detector in C # (in the next part - with some elements of C ++) and a decent amount of indecent code using the Visual Studio 2015 Community.
Before attempting to write a spherical horse detector in a virtual machine vacuum , it is necessary to briefly indicate how, as part of the task, we understand the term “virtual machine”.
The concept of virtualization can be divided into two categories ( 1 ):
In the second category, we immediately introduce two terms: a system that provides hardware resources and software for virtualization (host system, host) and an emulated system (guest system, guest).
At the same time, in reality, the role of a “guest system” can be:
The result of this excursion: in the framework of the article and the creation of a virtual machine detector, we will be interested only in native platform virtualization (that is, we will only check the launch in the Hyper-V, VirtualBox or other programs using native virtualization). Moreover, the term “virtual machine” will be interpreted according to the definition from the VMWare site: “this is a strictly isolated software container containing the operating system and applications” ( 2 ).
After the goal (determination of the fact of the program's work in the environment with partial emulation) was more or less specified, we find the most famous virtual machines of this type (hereinafter referred to as VM for short) and ways to distinguish the launch of the OS on real hardware from the launch surrounded by these VMs.
After reading the remarkably minted advertising pages of the developers of popular virtualization programs, a certain general scheme of their work emerges in my head (of course, the scheme of the programs, and not the developers):
Only in practice, almost every hypervisor has the opportunity to install guest additions (guest additions) - a special set of programs and drivers that give the hypervisor
advanced control of the guest OS functions (checking whether the guest OS is stuck, dynamic change of the available operating system RAM, "common" mouse for the host and guest OS). However, how do they implement such an action, if, according to the advertisement, “VM is a strictly isolated software container”?
It turns out that the guest add-ons installed on the guest OS interact in some strictly defined way directly with the hypervisor running in the host OS. That is, if the VM definition program can take advantage of this interaction, it will prove that the OS is running on the VM! However, under the terms of the problem, the proof must be carried out from under the User-Mode without using its own drivers ...
Immediately loom the following places to check:
The most complete description of the various verification criteria was found in the 2013 article in the Hacker magazine ( 3 ) - take the article as a basis. And to get the relevant data about the hardware and processes of the OS, we will use the Windows Management Instrumentation (WMI) mechanism - literally “Windows management tools”. In particular, using WMI you can easily, quickly and without administrator rights obtain a large amount of information about the hardware that the OS sees.
To get data through WMI, we need to build a query in WQL (WMI Query Language), which is essentially a very simplified SQL. For example, if we want to obtain information about the processors in the OS via WMI, the following query is required:
SELECT * FROM Win32_Processor
The answer to this query is a set of objects of the Win32_Processor type with previously known field names (see 4 for a detailed list of available fields and classes). Of course, if we do not need all-all fields, instead of * you can list only the necessary ones separated by commas. In the WQL SELECT statement, by analogy with SQL, the WHERE condition is also supported, allowing you to select only by objects whose values ​​in the fields satisfy the specified conditions.
For the "seed", let's learn how to get the following data from WMI-objects of the following types (data and expected values ​​in VM are taken from 3 ):
WMI object and its properties | Condition on WQL object query | How to use | |
---|---|---|---|
Win32_Processor | |||
Manufacturer | In the case of VirtualBox, it is equal to 'VBoxVBoxVBox', in the case of VMWare - 'VMwareVMware', in the case of Parallels - 'prl hyperv'. | ||
Win32_BaseBoard | |||
Manufacturer | In the case of Hyper-V, it is equal to 'Microsoft Corporation', despite the fact that Microsoft does not release motherboards (I wonder, what does this parameter show on Microsoft Surface tablets ?). | ||
Win32_DiskDrive | |||
PNPDeviceID | In the case of VirtualBox it contains 'VBOX_HARDDISK', in the case of VMWare it contains 'VEN_VMWARE'. | ||
Win32_NetworkAdapter | |||
MACAddress | PhysicalAdapter = 1 | It is known that a manufacturer can be identified by the three upper bytes of the MAC address — and the virtual machine manufacturers are no exception (that is, if the adapter has the PhysicalAdapter = 1 attribute but has the MAC address from the VMWare pool, then the program was most likely running on the VM). | |
Win32_Process | |||
Name | When installing guest add-ons on a VM, additional processes with known names appear in the system. |
We realize the acquisition of equipment data via WMI in a separate project in the form of the TTC.Utils.Environment library.
We structure the project as follows:
I would like the user of this library to write something like this code:
var bios = wmiService.QueryFirst<WmiBios>(new WmiBiosQuery()); var processors = wmiService.QueryAll<WmiProcessor>(new WmiProcessorQuery());
and did not worry about the mechanism of interacting with WMI, building a query, or transforming the response into a strongly typed C # language class.
Well, to implement this is actually not very difficult.
First, we connect a link to the System.Management library to the project (it contains the .NET classes for accessing WMI). Next, we describe the IWmiService service interface (the implementation of this interface will extract data and convert it into strongly typed objects):
/// <summary> /// Windows Management Instrumentation (WMI). /// </summary> public interface IWmiService { /// <summary> /// WMI. /// </summary> /// <typeparam name="TResult"> , .</typeparam> /// <param name="wmiQuery">, WMI-.</param> /// <returns> .</returns> TResult QueryFirst<TResult>(WmiQueryBase wmiQuery) where TResult : class, new(); /// <summary> /// WMI. /// </summary> /// <typeparam name="TResult"> , .</typeparam> /// <param name="wmiQuery">, WMI-.</param> /// <returns> .</returns> IReadOnlyCollection<TResult> QueryAll<TResult>(WmiQueryBase wmiQuery) where TResult : class, new(); }
public class WmiBaseBoard { public string Manufacturer { get; private set; } public string Product { get; private set; } public string SerialNumber { get; private set; } }
Let's use the main property of any programmer (laziness) and instead of creating a full-fledged DTO, we simply mark each property with the following attribute, allowing us to associate the property and the result field of the WML query:
/// <summary> /// , WMI. /// </summary> [AttributeUsage(AttributeTargets.Property)] public class WmiResultAttribute : Attribute { public WmiResultAttribute(string propertyName) { PropertyName = propertyName; } /// <summary> /// WMI. /// </summary> public string PropertyName { get; } }
Having marked the properties of the entity with the specified attributes, we get:
public class WmiBaseBoard { internal const string MANUFACTURER = "Manufacturer"; internal const string PRODUCT = "Product"; internal const string SERIAL_NUMBER = "SerialNumber"; // ReSharper disable UnusedAutoPropertyAccessor.Local [WmiResult(MANUFACTURER)] public string Manufacturer { get; private set; } [WmiResult(PRODUCT)] public string Product { get; private set; } [WmiResult(SERIAL_NUMBER)] public string SerialNumber { get; private set; } // ReSharper restore UnusedAutoPropertyAccessor.Local }
It remains to deal with the object that will store the request. I'm sure you noticed that in the previous code example, the names of the fields of the WQL query results are in internal-constants. This was done on purpose so as not to duplicate them in the request class. By the way, an interesting side effect has turned out - with the use of such a model, you cannot read the data from the WMI field of a certain WMI object until you specify in which property of which entity it should be extracted.
using System.Management; /// <summary> /// WMI. /// </summary> public class WmiQueryBase { private readonly SelectQuery _selectQuery; /// <summary> /// WMI. /// </summary> /// <param name="className"> , .</param> /// <param name="condition"> .</param> /// <param name="selectedProperties"> .</param> protected WmiQueryBase(string className, string condition = null, string[] selectedProperties = null) { _selectQuery = new SelectQuery(className, condition, selectedProperties); } /// <summary> /// SELECT- WMI. /// </summary> internal SelectQuery SelectQuery { get { return _selectQuery; } } }
using TTC.Utils.Environment.Entities; public class WmiBaseBoardQuery : WmiQueryBase { public WmiBiosQuery() : base("Win32_BaseBoard", null, new[] { WmiBios.MANUFACTURER, WmiBios.PRODUCT, WmiBios.SERIAL_NUMBER, }) { } }
With this class structure * Query, there is only one nuisance: it is inconvenient to form the parameters of the WHERE part of a WML query inside the class. We have to act in the old fashioned way and with pens to form a string depending on the parameters:
using System.Text; using TTC.Utils.Environment.Entities; public class WmiNetworkAdapterQuery : WmiQueryBase { private static readonly string[] COLUMN_NAMES = { WmiNetworkAdapter.GUID, WmiNetworkAdapter.MAC_ADDRESS, WmiNetworkAdapter.PNP_DEVICE_ID, }; public WmiNetworkAdapterQuery(WmiNetworkAdapterType adapterType = WmiNetworkAdapterType.All) : base("Win32_NetworkAdapter", null, COLUMN_NAMES) { if (adapterType == WmiNetworkAdapterType.Physical) SelectQuery.Condition = "PhysicalAdapter=1"; else if (adapterType == WmiNetworkAdapterType.Virtual) SelectQuery.Condition = "PhysicalAdapter=0"; } }
Well: the data on the entities was scattered, they learned to write requests with sin-in half, it remains only to figure out how the service will work, which works with the specified classes:
/// <summary> /// Windows Management Instrumentation (WMI). /// </summary> public class WmiService : IWmiService { /// <summary> /// WMI . /// </summary> /// <typeparam name="TResult"> , .</typeparam> /// <param name="managementObject">, WMI.</param> /// <returns> .</returns> private static TResult Extract<TResult>(ManagementBaseObject managementObject) where TResult : class, new() { var result = new TResult(); foreach (var property in typeof(TResult).GetProperties()) { var wmiAttribute = (WmiResultAttribute)Attribute.GetCustomAttribute(property, typeof(WmiResultAttribute)); if (wmiAttribute != null) { var sourceValue = managementObject.Properties[wmiAttribute.PropertyName].Value; var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; object targetValue; if (sourceValue == null) { targetValue = null; } else if (targetType == typeof(DateTime)) { targetValue = ManagementDateTimeConverter.ToDateTime(sourceValue.ToString()).ToUniversalTime(); } else if (targetType == typeof(Guid)) { targetValue = Guid.Parse(sourceValue.ToString()); } else { targetValue = Convert.ChangeType( managementObject.Properties[wmiAttribute.PropertyName].Value, targetType); } property.SetValue(result, targetValue); } } return result; } /// <summary> /// WMI. /// </summary> /// <param name="selectQuery"> .</param> /// <param name="searcher"> WMI.</param> /// <returns> .</returns> private ManagementObjectCollection QueryAll(SelectQuery selectQuery, ManagementObjectSearcher searcher = null) { searcher = searcher ?? new ManagementObjectSearcher(); searcher.Query = selectQuery; return searcher.Get(); } /// <summary> /// WMI. /// </summary> /// <param name="selectQuery"> .</param> /// <param name="searcher"> WMI.</param> /// <returns> .</returns> private ManagementBaseObject QueryFirst(SelectQuery selectQuery, ManagementObjectSearcher searcher = null) { return QueryAll(selectQuery, searcher).Cast<ManagementBaseObject>().FirstOrDefault(); } public TResult QueryFirst<TResult>(WmiQueryBase wmiQuery) where TResult : class, new() { var managementObject = QueryFirst(wmiQuery.SelectQuery); return managementObject == null ? null : Extract<TResult>(managementObject); } public IReadOnlyCollection<TResult> QueryAll<TResult>(WmiQueryBase wmiQuery) where TResult : class, new() { var managementObjects = QueryAll(wmiQuery.SelectQuery); return managementObjects?.Cast<ManagementBaseObject>() .Select(Extract<TResult>) .ToList(); } }
A few words regarding the WmiService.Extract <TResult> method.
WMI objects usually have a fairly large number of properties (and many fields can be NULL). Assuming that, within the framework of the task, we are going to unload only a small number of object properties from WMI, it is logical to start mapping data by sorting the properties of the resulting entity. Further, if the attribute attribute WmiResultAttribute is present, we read the value of the property with the name specified in the attribute from the query result object and perform type conversion. At the same time, if an entity property has a type with which the standard Convert.ChangeType method does not cope or converts the type not in the way we want, we can easily transfer control to our transformation (as was done for the System.DateTime and System.Guid types) .
By the way, it would be even better to split Extract into two methods: the first extracts information from the class type, the second fills the instances (otherwise the QueryAll method for the second and subsequent elements of the output collection does unnecessary work on re-studying the structure of its type). But specifically for the purpose of detecting a virtual machine, we are unlikely to expect more than 10 objects per request, so I propose to write off this task with the note "not implemented, for natural laziness." But if someone gets their hands on such a modification, I will gladly accept your revision.
In order not to finish this part of the article only with the library, we will make the simplest application that uses the capabilities of this library to detect several of the most popular VMWare, Microsoft, Parallels and Oracle virtual machines based on the above criteria.
Create a separate project - a console application TTC.Utils.VMDetect and create in it the following class DemoTrivialVmDetector:
/// <summary> /// - . /// </summary> class DemoTrivialVmDetector { private readonly IWmiService _wmiService; public DemoTrivialVmDetector(IWmiService wmiService) { _wmiService = wmiService; } public MachineType GetMachineType() { var wmiProcessor = _wmiService.QueryFirst<WmiProcessor>(new WmiProcessorQuery()); if (wmiProcessor.Manufacturer != null) { if (wmiProcessor.Manufacturer.Contains("VBoxVBoxVBox")) return MachineType.VirtualBox; if (wmiProcessor.Manufacturer.Contains("VMwareVMware")) return MachineType.VMWare; if (wmiProcessor.Manufacturer.Contains("prl hyperv")) return MachineType.Parallels; } var wmiBaseBoard = _wmiService.QueryFirst<WmiBaseBoard>(new WmiBaseBoardQuery()); if (wmiBaseBoard.Manufacturer != null) { if (wmiBaseBoard.Manufacturer.Contains("Microsoft Corporation")) return MachineType.HyperV; } var wmiDiskDrives = _wmiService.QueryAll<WmiDiskDrive>(new WmiDiskDriveQuery()); if (wmiDiskDrives != null) foreach (var wmiDiskDrive in wmiDiskDrives) { if (wmiDiskDrive.PnpDeviceId.Contains("VBOX_HARDDISK")) return MachineType.VirtualBox; if (wmiDiskDrive.PnpDeviceId.Contains("VEN_VMWARE")) return MachineType.VMWare; } return MachineType.Unknown; } }
All code, including the library and the simplest test application, is laid out in the repository on github , feedback and comments are welcome.
In the next part, we are sensitively structuring work with known VMs and using the CPUID assembler instructions we will try to detect already unknown VMs.
Source: https://habr.com/ru/post/322486/
All Articles