Одним жуть каким прохладным январским утром от знакомого прилетел вопрос — как на C# определить, не запущена ли программа в ОС (оконное приложение в ОС Windows 7 или новее) на виртуальной машине.


Требования к такому детектору были достаточно жёсткими:

  1. Должен быть полностью в исходных кодах,
  2. Должен собираться с помощью Visual Studio,
  3. Должен работать из-под аккаунта непривилегированного пользователя (нельзя использовать методы, требующие, к примеру, установки драйверов устройств, или иных операций, для которых нужны права администратора),
  4. Разрешено использовать .NET Framework 4.5 и никаких лишних зависимостей (типа Visual C++ Redistributable Package).


Под катом описание реализованного детектора на C# (в следующей части — с некоторыми элементами C++) и приличным количеством неприличного кода с использованием Visual Studio 2015 Community.


Структура публикации


  • 1 уровень. Изучение матчасти и простейших существующих решений:

    • немного теории касательно виртуализации,
    • реализация проверки ВМ с помощью данных из Windows Management Instrumentation (WMI).

  • 2 уровень. Поиск статей и публикаций про детектирование запуска в виртуальных машинах:

    • допиливаем реализацию с WMI,
    • работа с инструкцией CPUID.

  • 3 уровень. Поиск материалов с хакерских конференций:

    • допиливаем работу с CPUID,
    • делаем сводную таблицу параметров и результатов тестирования.

1 уровень. Изучение матчасти и существующих решений


Немного теории касательно виртуализации


Прежде, чем пытаться писать детектор сферического коня в вакууме виртуальной машины, следует кратко обозначить, как в рамках задачи мы понимаем термин «виртуальная машина».


Понятие виртуализации можно поделить на две категории (1):

  • Первая — виртуализация ресурсов. Рассмотрим на живом примере, почему принцип работы сервиса хранения файлов Dropbox можно трактовать как виртуализацию ресурсов:

    1. Мы точно знаем, как загружать/выгружать файлы, и как взаимодействовать с сервисом, но как это всё работает внутри мы с уверенностью сказать не можем => инкапсуляция.
    2. У каждого пользователя есть свой уникальный аккаунт, у каждого аккаунта установлена квота на размер сохраняемых файлов, для каждого аккаунта можно настраивать разрешения на доступ индивидуально (хотя по факту данные разных пользователей вполне могут храниться на одном и том же носителе информации) => разделение ресурсов.
    3. Скорее всего, под капотом Dropbox находится не один и не два компьютера, а как минимум пара сотен серверов, функционирующих и обрабатывающих команды от клиентов в рамках системы Dropbox как единое целое => кластеризация.

  • Вторая — виртуализация платформ — создание программных систем на основе существующих аппаратно-программных комплексов, зависящих или независящих от них (1).


Во второй категории сразу введём два термина: система, предоставляющая аппаратные ресурсы и ПО для виртуализации (хостовая система, host) и эмулируемая система (гостевая система, guest).


При этом в реальности в роли «гостевой системы» могут выступать:

  1. Абсолютно всё аппаратное и программное обеспечение эмулируемой системы — такой тип виртуализации называется полной эмуляцией или симуляцией. Примеры программ, обеспечивающих такой тип виртуализации: Bochs, QEMU.

  2. Всё программное обеспечение и только часть аппаратного (часть достаточная для обеспечения изоляции гостевой системы) — такой тип виртуализации назовём частичной эмуляцией или нативной виртуализацией. Примеры программ, обеспечивающих такой тип виртуализации: VMWare Workstation, VMWare ESXi, Microsoft Hyper-V, Oracle VirtualBox.

  3. Также существуют частичная виртуализация, паравиртуализация, виртуализация уровня операционной системы и виртуализация уровня приложений. Во всех трёх случаях физически ОС у нас одна, а гостевыми системами считаются либо отдельные процессы, либо отдельные группы процессов (например, user-mode процессы).


Итог сего экскурса: в рамках статьи и создания детектора виртуальной машины нас будут интересовать только нативная виртуализация платформ (то есть проверять мы будем только запуск в окружении Hyper-V, VirtualBox или других программ, использующих нативную виртуализацию). При этом далее термин «виртуальная машина» мы будем трактовать согласно определению с сайта VMWare: «это строго изолированный контейнер ПО, содержащий операционную систему и приложения» (2).


Реализация проверки ВМ с помощью данных из Windows Management Instrumentation (WMI)


После того, как цель (определение факта работы программы в окружении с частичной эмуляцией) была более-менее уточнена, найдём самые известные виртуальные машины этого типа (далее для краткости ВМ) и способы отличить запуск ОС на реальном железе от запуска в окружении этих ВМ.


Прочитав замечательно свёрстанные рекламные страницы разработчиков популярных программ виртуализации, в голове вырисовывается некая общая схема их работы (разумеется схема работы программ, а не разработчиков):


  • Есть хостовая ОС. Не ограничивая общности будем считать, что она одна.
  • На хостовой ОС установлена программа для обеспечения виртуализации (далее гипервизор).
  • Гипервизор предоставляет интерфейс для установки и последующей работы гостевой ОС.

Примечание: имеют место случаи, когда хостовая ОС и гипервизор есть единое тонкое целое, что позволяет уменьшить расходование ресурсов компьютера по сравнению с использованием хостовой ОС и гипервизора по отдельности (примеры: VMWare ESXi или Windows Hyper-V Server).


Вот только на практике почти в каждом гипервизоре имеется возможность установить «гостевые дополнения» (guest additions) — специальный набор программ и драйверов, дающих гипервизору
расширенный контроль за функциями гостевой ОС (проверка, а не зависла ли гостевая ОС, динамическое изменение доступной ОС оперативной памяти, "общая" мышка для хостовой и гостевой ОС). Однако, как же реализуют такое действо, если, согласно рекламе, «ВМ — это строго изолированный контейнер ПО»?


Получается, что гостевые дополнения, устанавливаемые на гостевую ОС, каким-то строго определённым образом взаимодействуют напрямую с гипервизором, запущенным в хостовой ОС. То есть если программа определения ВМ сможет воспользоваться таким взаимодействием — она докажет, что ОС запущена на ВМ! Правда, по условиям задачи доказательство надо проводить из-под User-Mode без использования собственных драйверов...


Сразу вырисовываются следующие места для проверки:

  • Поиск определённого оборудования в системе, которого на физической машине просто не может быть.
  • Поиск аппаратных интерфейсов, которые реализованы только в ВМ (или наоборот, только на физической машине).

Собственно, если ввести в поисковой строке "detect hyper-v C#" или "detect vmware C#", примерно на это и натыкаешься, осталось только обобщить.


Наиболее полное описание различных критериев проверки было найдено в статье 2013 года в журнале Хакер (3),— возьмём статью за основу. А для получения соответствующих данных об оборудовании и процессах ОС воспользуемся механизмом Windows Management Instrumentation (WMI) — дословно "инструментарием управления Windows". В частности, через WMI можно несложно, быстро и без прав администратора получить большое количество информации об оборудовании, которое видит ОС.


Для получения данных через WMI нам понадобится построить запрос на языке WQL (WMI Query Language), который по сути является сильно упрощённым SQL. Например, если мы хотим получить через WMI имеющуюся в ОС информацию о процессорах, требуется выполнить следующий запрос:


SELECT * FROM Win32_Processor

Ответ на этот запрос — набор объектов типа Win32_Processor с заранее известными названиями полей (подробный список доступных полей и классов см. в 4). Разумеется, если нам не требуются все-все поля, вместо * можно перечислить через запятую только необходимые. В WQL-операторе SELECT, по аналогии с SQL, также поддерживается условие WHERE, позволяющее делать выборку только по объектам, значения в полях которых удовлетворяют указанным условиям.


Для "затравки" научимся получать следующие данные из WMI-объектов следующих типов (данные и ожидаемые в ВМ значения взяты из 3):

WMI-объект и его свойства Условие на WQL-запрос объектов Как использовать
Win32_Processor  
     Manufacturer   В случае VirtualBox равен 'VBoxVBoxVBox', в случае VMWare — 'VMwareVMware', в случае Parallels — 'prl hyperv '.
Win32_BaseBoard  
     Manufacturer   В случае Hyper-V равен 'Microsoft Corporation' при том, что Microsoft материнские платы не выпускает (интересно, а что показывает этот параметр на планшетах Microsoft Surface?).
Win32_DiskDrive  
     PNPDeviceID   В случае VirtualBox содержит 'VBOX_HARDDISK', в случае VMWare содержит 'VEN_VMWARE'.
Win32_NetworkAdapter  
     MACAddress PhysicalAdapter=1 Известно, что по трём старшим байтам MAC-адреса можно определить производителя — и производители виртуальных машин не исключение (то есть если адаптер с признаком PhysicalAdapter=1 но имеет MAC-адрес из пула VMWare — то с высокой вероятностью программа была запущена на ВМ).
Win32_Process  
     Name   При установке гостевых дополнений на ВМ в системе появляются дополнительные процессы с известными именами.

Реализуем получение данных об оборудовании через WMI в отдельном проекте в виде библиотеки TTC.Utils.Environment.


Структурируем проект следующим образом:

  1. Entities — объекты с данными (сущности), полученными от WMI.
  2. Services — сервисы; например, служба, инкапсулирующая взаимодействие с WMI-обёрткой .NET.
  3. Interfaces — интерфейсы; например, интерфейс сервиса работы с WMI.
  4. Queries — объекты, содержащие параметры запросов к WMI, с помощью которых извлекаются заданные типы сущностей.


Хочется, чтобы пользователь данной библиотеки мог написать примерно такой код:


var bios = wmiService.QueryFirst<WmiBios>(new WmiBiosQuery());
var processors = wmiService.QueryAll<WmiProcessor>(new WmiProcessorQuery());

и не волновался по поводу механизма взаимодействия с WMI, построения запроса или преобразования ответа в строго типизированный класс языка C#.


Что ж, реализовать такое на самом деле не очень сложно.


Сначала подключим к проекту ссылку на библиотеку System.Management (именно в ней находятся классы .NET для доступа к WMI). Далее опишем интерфейс сервиса IWmiService (реализация этого интерфейса будет извлекать данные и преобразовывать их в строго типизированные объекты):

Код IWmiService.cs
/// <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();
}


Теперь установим как будут выглядеть сущности в нашем проекте. Предположим для детектирования нам потребуются следующие поля из WMI-объектов типа Win32_BaseBoard:

Код WmiBaseBoard.cs - до
public class WmiBaseBoard
{
    public string Manufacturer { get; private set; }

    public string Product { get; private set; }

    public string SerialNumber { get; private set; }
}


В идеале надо писать DTO, чтобы с его помощью транслировать данные из результата WML-запроса в вышеуказанную сущность, но если постулировать, что свойства сущностей в проекте будут 1 к 1 соответствовать полям объектов из результатов WML-запроса, то делать DTO на каждую сущность значит писать достаточно много однообразного кода.


Воспользуемся главным свойством любого программиста (ленью) и вместо создания полноценной DTO просто отметим атрибутом каждое свойство следующим атрибутом, позволяющим связать свойство и поле результата WML-запроса:

Код WmiResultAttribute.cs
/// <summary>
/// Указание, какому свойству сущности соответствует поле объекта WMI.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class WmiResultAttribute : Attribute
{
    public WmiResultAttribute(string propertyName)
    {
        PropertyName = propertyName;
    }

    /// <summary>
    /// Имя поля в объекте WMI.
    /// </summary>
    public string PropertyName { get; }
}


Разметив свойства сущности указанными атрибутами, получим:

Код WmiBaseBoard.cs - после
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
}


Осталось разобраться с объектом, который будет хранить запрос. Уверен, вы обратили внимание, что в предыдущем примере кода названия полей WQL-результатов запроса вынесены в internal-константы. Это было сделано специально чтобы не дублировать их в классе запроса. Кстати, получился интересный побочный эффект — с использованием такой модели вы не сможете прочесть из WMI данные поля некоторого WMI-объекта пока не укажете, в какое свойство какой сущности он должен извлекаться.

Код WmiQueryBase.cs
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; }
    }
}


Код WmiBaseBoardQuery.cs
using TTC.Utils.Environment.Entities;
public class WmiBaseBoardQuery : WmiQueryBase
{
    public WmiBiosQuery()
        : base("Win32_BaseBoard", null, new[]
        {
            WmiBios.MANUFACTURER,
            WmiBios.PRODUCT,
            WmiBios.SERIAL_NUMBER,
        })
    {
    }
}


При такой структуре классов *Query есть только одна неприятность: неудобно формировать параметры WHERE-части WML-запроса внутри класса. Приходится действовать по старинке и ручками формировать строку в зависимости от параметров:

Код WmiNetworkAdapterQuery.cs
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";
    }
}


Хорошо: данные по сущностям раскидали, запросы писать с грехом-пополам научились, осталось только разобраться как будет выглядеть сервис, работающий с указанными классами:

Код WmiService.cs
/// <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();
    }
}


Пара слов касательно метода WmiService.Extract<TResult>.

У объектов WMI обычно достаточно большое количество свойств (причем многие поля могут иметь значение NULL). В предположении, что в рамках задачи выгружать из WMI мы будем только небольшое количество свойств объектов, логично начать маппинг данных с перебора свойств результирующей сущности. Далее, при наличии у свойства атрибута WmiResultAttribute мы считываем из объекта результата запроса значение свойства с указанным в атрибуте именем и выполняем преобразование типов. При этом, если свойство сущности имеет тип, с которым стандартный метод Convert.ChangeType не справится или преобразует тип не так, как нам хочется, мы легко можем передать управление на своё преобразование (как это сделано для типов System.DateTime и System.Guid).


Кстати, было бы ещё лучше разделить Extract на два метода: первый извлекает информацию из типа класса, второй заполняет экземпляры (иначе метод QueryAll для второго и последующих элементов выходной коллекции делает ненужную работу по повторному изучению структуры его типа). Но конкретно для целей детектирования виртуалки мы вряд ли будет ожидать более 10 объектов за один запрос, поэтому предлагаю списать эту задачу с пометкой "не реализовано, ибо природная лень". Но если у кого-то дойдут руки до такой модификации — с радостью приму вашу доработку.


Послесловие


Чтобы не заканчивать эту часть статьи только библиотекой, сделаем самое простое приложение, использующее возможности данной библиотеки для детектирования нескольких самых популярных виртуальных машин фирм VMWare, Microsoft, Parallels и Oracle на осове вышеизложенных критериев.


Создадим отдельный проект — консольное приложение TTC.Utils.VMDetect и создадим в нём такой класс DemoTrivialVmDetector:

Код WmiService.cs
/// <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;
    }
}


Весь код, включая библиотеку и простейшее тестовое приложение, выложен в репозитории на github, отзывы и комментарии приветствуются.


В следующей части мы чутка структурируем работу с известными ВМ и с помощью ассемблерной инструкции CPUID попробуем детектировать уже неизвестные ВМ.


Источники


  1. Виртуализация: новый подход к построению IT-инфраструктуры
  2. Виртуализация при помощи VMWare
  3. Детектим виртуалки (xakep.ru)
  4. WMI: Win32 Provider (MSDN)
Поделиться с друзьями
-->

Комментарии (27)


  1. namwen
    24.02.2017 09:52
    +1

    К сожалению, ваш 1 уровень (да и последующие, к сожалению) — совершенно бесполезный детект, без обид. Эти строковые константы у vmWare / VirtualBox (про Hyper-V не встречал) патчатся на ура и есть готовые решения, недавно совсем натыкался на совсем уж киддиса, который в лоб смог забить почти все детекты, вот, почитайте на досуге: https://exelab.ru/f/index.php?action=vthread&forum=5&topic=24207


    1. namwen
      24.02.2017 09:57
      +2

      И там Клерк объявился с интересным предложением (ядро Win 8+, встроенный механизм детекта виртуальной среды по таймингам), скину сюда, т.к. ссылки не могу нормально вставлять:

      ExpDetectHypervisorCr3Heuristic()
      NTSTATUS
      NtQueryLicenseValue (
          PUNICODE_STRING "Kernel-VMDetection-Private",
          NULL,
          PBOOL Result,
          ULONG 4,
          PULONG ResultLength);
      
      Так что, отставить строковые константы оборудования и cpuid (с ним давно уже покончено), надо смотреть в сторону того, что нет эмуляции TLB и всяких хитрых таймингов


      1. HonoraryBoT
        28.02.2017 12:26

        Ни в одном ядре не нашел. Он сам её так назвал? Дайте пруфов плз.


        1. HonoraryBoT
          28.02.2017 14:52

          Нашёл на 7 x64.
          Детекты забавные.
          В ExpIsVirtualMachinePrivate() набор детектов:
          ExpDetectHyperVisorCpuid(), ExpIsViridian(), ExpIsViridianGuest() — детекты через cpuid. Вообще это даже в vmware правится через настройки, можно назначить, какой результат будет выдавать cpuid. Кстати, если гость является мсовской вм, то ядро не считает это виртуальной машиной, гы.
          ExpDetectHyperVisorCr3Heuristic() — забавный детект. Сравнивается время выполнения «медленной» и «быстрой» инструкции. Причем за медленную инструкцию берется доступ к CR3. Прикол в том, что если гипервизор поддерживает EPT, то доступ к CR3 не приводит к выходу по умолчанию. Так что детект сработает только если гипервизор не поддерживает EPT или трапает CR3 вручную.
          ExpDetectHypervisorVarianceHeuristic() — вот это да, головная боль. По трем листам cpuid считается статистика по времени выхода, причем время замеряется при помощи perf counter'а, источником коего может быть и tsc, и pm timer, и hpet. Запоминается 1000 значений (если до этого мы не вылезли слишком за таймаут), из которых потом высчитывается разброс во времени выполнения (дисперсия). Далее детект по значению разброса, который не должен превышать 5000. Здесь все упирается в качественную эмуляцию таймеров. Надо будет отпробовать vmware с галочкой «virtualize cpu performance counters».


          1. HonoraryBoT
            28.02.2017 15:05

            P.S. МНК, не дисперсия.


            1. HonoraryBoT
              28.02.2017 15:45

              PSPS Таки-да, если в вмваре исправить cpuid, то данный детект не работает ввиду хреновой статистики ;)


              1. namwen
                01.03.2017 03:07

                данный
                ExpDetectHypervisorVarianceHeuristic? В итоге все MS-варианты оказались провальны, так?


                1. HonoraryBoT
                  01.03.2017 11:54

                  Я лишь говорю о том, что грамотно настроенный гипер детектить достаточно сложно.


    1. SLenik
      24.02.2017 10:04
      +1

      Приветствую!

      Во-первых огромное спасибо за ссылку — одна из целей публикации это сбор дополнительной информации касательно особенностей детектирования ВМ.

      А насчёт бесполезности — мне кажется вы рано клеймите этим словом последующие уровни. У первого уровня задача по сути обозначить проблему. На втором уровне добавляется детектирования через инструкцию CPUID (EAX=40000000h) и через бит hypervisor present (EAX = 1, смотрим 31-бит в ECX). Это поведение гипервизоров пропатчить чуть сложнее.
      А на 3м уровне рассматриваются уже особенности реализации некоторых гипервизоров для определения.
      Приведу конкретный пример: применительно к тому же VMWare — в ВМ чипсет компьютера детектируется как Intel 440BX (это socket 370, уровень Pentium II). Соответственно, увидев в системе указаный чипсет вкупе с процессором Intel Kaby Lake, да ещё и одноядерным — мы можем почти со 100% уверенностью утверждать что это виртуалка.


      1. stickerov
        24.02.2017 11:11
        +3

        Как по мне, к сожалению все «софтовые» доказательства можно подделать очередным патчем (да и часто просто правкой конфига), если уж сильно надо. Ведь должны понимать, что в ВМ окружение можно настроить таким образом, что детект можно полностью обмануть. Вопрос конечно в целесообразности подготовки такой ВМ. Как по мне, единственный выход — поиск и пользование уязвимостей и бэкдоров самих ВМ, остальное — лишь надежда на ВМ с настройкой по-умолчанию.


        1. SLenik
          24.02.2017 11:15

          > поиск и пользование уязвимостей и бэкдоров самих ВМ.

          В теории — да. Но на практике для обеспечения приемлемой производительности гипервизор вынужден использовать аппаратные возможности виртуализации (типа Intel VT-x). И, завязавшись на особенности их использования, можно ещё больше увеличить вероятность определения ВМ.


        1. VitalityM
          24.02.2017 13:31
          +3

          Обсуждали в этой статье на Хабре.
          Вкратце. Разработать полностью «прозрачную» виртуализацию с приемлемым уровнем производительности просто невозможно (пруф), а существующие ВМ оставляют нам множество артефактов, которые можно детектить.

          Вот интересная статья на тему фаззинга инструкций ВМ (ссылка). Можно все не читать, просто обратите внимание на таблицу 2 на странице 10, они показывают количество инструкций, которые работают в этих виртуальных машинах по-другому, чем на нативной. И это они взяли относительно небольшое количество команд. Самое главное, что разработчики ВМ и не стремятся сделать ВМ «прозрачной» (зачем им тратить на это время и ресурсы?), им достаточно добиться совместимости.


          1. kafeman
            25.02.2017 17:35

            Вот интересная статья на тему фаззинга инструкций ВМ
            У автора приложение на .NET'е, вы всерьез полагаете, что кто-то будет опускаться до уровня машинных кодов? Уверен, что если кому-то понадобится взломать эту защиту (привет, неуловимый Джо!), то все закончится максимум патчем .NET-рантайма. Можно взять за основу Mono, например.

            Короче, не усложняйте. Не так давно мне надо было вытащить одну строку из Android-приложения. Для защиты авторы написали на Си разделяемую библиотеку и относительно неплохо ее обфусцировали. Они, вероятно, рассчитывали, что я месяцами буду дебажить эту либу в IDA, а я просто взял и запустил ее в нужном окружении (на создание которого ушло минут 10). Так что, как говорится, прочность цепи определяется прочностью ее самого слабого звена (в данном случае — .NET и «управляемый» (managed) код).


            1. SLenik
              25.02.2017 19:20

              Думаю, не следует в рамках заметки смешивать задачу определения запуска в окружении ВМ и обфускацию кода.

              А насчёт спуска до уровня машинных кодов — не вижу причин не спускаться если это даст дополнительные данные. До инструкции CPUID мы уже спустились (через вызов unmanaged C++ библиотеки, заметка на хабр в процессе).


              1. kafeman
                25.02.2017 20:32

                не следует в рамках заметки смешивать задачу определения запуска в окружении ВМ и обфускацию кода
                С академической точки зрения — да, с практической — нет. Зачем месяцами разрабатывать проверки, которые потом обходятся патчем одного байта в программе?

                через вызов unmanaged C++ библиотеки
                Конечно, не зная конкретной реализации, утверждать что-то очень сложно. Но подумайте еще раз над моими словами о слабом звене. Если библиотека банально возвращает true/false, то библиотеку можно подменить своей реализацией. Даже если проверяется подпись и т.д., то все-равно это делается из managed-кода, который пропатчить значительно легче, чем копаться в скопилированном C++. Тогда нужно выносить сами алгоритмы, которые вы хотите скрыть от посторонних. Но и тут может случиться забавная ситуация, когда вашу библиотеку будут просто использовать в своей программе.


                1. VitalityM
                  26.02.2017 15:28

                  С точки зрения защиты кода Вы 100% правы. Защита должна быть построена таким образом, чтобы её нельзя было бы пропатчить 1 байтом, сам не раз сталкивался с таким. Поэтому предлагаемое в статье решение может (и должно) использоваться только как один из компонентов многослойной защиты. Themida отличный пример (хоть и не managed-code).

                  Ну и не нужно забывать, что такие методы могут применяться и в малвари, полезно знать какие есть способы обнаружения песочницы.


    1. Goodkat
      24.02.2017 10:54
      +8

      Вообще, параноикам имеет смысл прописать эти константы вручную в своей ОС — всякая малварь первым делом детектит виртуальную машину, и просто не запускается, чтобы избежать отладки.


      А серьёзному софту плевать на виртуализацию.


      1. redmanmale
        26.02.2017 00:38

        Отчасти соглашусь, но иногда бывает так, что защита "серьёзного" софта точно так же реагирует на присутствие ВМ и не позволяет ему запускаться.


        1. Goodkat
          26.02.2017 01:15

          А что за софт такой серьёзный, что его нельзя запускать в ВМ?

          Или вы про игры?


          1. SLenik
            26.02.2017 05:50

            Пример серьезного софта, меняющего свое поведение при установке его на ВМ = Windows. Фишка в том, что в MS требуют отдельную лицензию на каждую ВМ (одной на всю физическую машину им мало). Предположу, что именно поэтому на ВМ при клонировании может слететь активация.


          1. redmanmale
            26.02.2017 17:56

            Сталкивался с таким поведением различных платных библиотек обработки аудио от крупных производителей.


  1. Dywar
    24.02.2017 10:16

    Добавлю свои закладки:
    1) Attacks on Virtual Machine Emulators
    2) On the Cutting Edge: Thwarting Virtual Machine Detection


    1. SLenik
      24.02.2017 10:22

      Большое спасибо! Обязательно просмотрю.


  1. Diaskhan
    24.02.2017 11:07
    -9

    Эти темы по WMI уже столько изъездили! Думал хоть на хабре этого не будет!!!
    Ан нет, WMI это круто, Написали бы лучше прогу инвентаризации и выложили в Opensource !


  1. VitalityM
    24.02.2017 13:35

    Спасибо за статью, очень интересно!

    Добавлю от себя, что можно эту информацию также выдернуть из реестра Windows. Кстати малварь в основном так и делает.


  1. molnij
    25.02.2017 13:41

    Первая часть такая обстоятельная :)
    ИМХО, постановка должна включать в себя два варианта — детект виртуалки, которая не скрывается и детект виртуалки которая пытается скрыться.
    Первая часть очевидно разрешима, что вы и показали.
    А вторая — настолько же очевидная разновидность задачи «брони-снаряда» — кому нужнее, тот и победит в краткосрочном периоде.


  1. termsl
    28.02.2017 16:22

    Еще для Hyper-V содержимое секции реестра:

    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters