Одним жуть каким прохладным январским утром от знакомого прилетел вопрос как на C# определить, не запущена ли программа в ОС (оконное приложение в ОС Windows 7 или новее) на виртуальной машине.
Требования к такому детектору были достаточно жёсткими:
- Должен быть полностью в исходных кодах,
- Должен собираться с помощью Visual Studio,
- Должен работать из-под аккаунта непривилегированного пользователя (нельзя использовать методы, требующие, к примеру, установки драйверов устройств, или иных операций, для которых нужны права администратора),
- Разрешено использовать .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 можно трактовать как виртуализацию ресурсов:
- Мы точно знаем, как загружать/выгружать файлы, и как взаимодействовать с сервисом, но как это всё работает внутри мы с уверенностью сказать не можем => инкапсуляция.
- У каждого пользователя есть свой уникальный аккаунт, у каждого аккаунта установлена квота на размер сохраняемых файлов, для каждого аккаунта можно настраивать разрешения на доступ индивидуально (хотя по факту данные разных пользователей вполне могут храниться на одном и том же носителе информации) => разделение ресурсов.
- Скорее всего, под капотом Dropbox находится не один и не два компьютера, а как минимум пара сотен серверов, функционирующих и обрабатывающих команды от клиентов в рамках системы Dropbox как единое целое => кластеризация.
- Вторая виртуализация платформ создание программных систем на основе существующих аппаратно-программных комплексов, зависящих или независящих от них (1).
Во второй категории сразу введём два термина: система, предоставляющая аппаратные ресурсы и ПО для виртуализации (хостовая система, host) и эмулируемая система (гостевая система, guest).
При этом в реальности в роли «гостевой системы» могут выступать:
- Абсолютно всё аппаратное и программное обеспечение эмулируемой системы такой тип виртуализации называется полной эмуляцией или симуляцией. Примеры программ, обеспечивающих такой тип виртуализации: Bochs, QEMU.
- Всё программное обеспечение и только часть аппаратного (часть достаточная для обеспечения изоляции гостевой системы) такой тип виртуализации назовём частичной эмуляцией или нативной виртуализацией. Примеры программ, обеспечивающих такой тип виртуализации: VMWare Workstation, VMWare ESXi, Microsoft Hyper-V, Oracle VirtualBox.
- Также существуют частичная виртуализация, паравиртуализация, виртуализация уровня операционной системы и виртуализация уровня приложений. Во всех трёх случаях физически ОС у нас одна, а гостевыми системами считаются либо отдельные процессы, либо отдельные группы процессов (например, 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.
Структурируем проект следующим образом:
- Entities объекты с данными (сущности), полученными от WMI.
- Services сервисы; например, служба, инкапсулирующая взаимодействие с WMI-обёрткой .NET.
- Interfaces интерфейсы; например, интерфейс сервиса работы с WMI.
- Queries объекты, содержащие параметры запросов к WMI, с помощью которых извлекаются заданные типы сущностей.
Хочется, чтобы пользователь данной библиотеки мог написать примерно такой код:
var bios = wmiService.QueryFirst<WmiBios>(new WmiBiosQuery());
var processors = wmiService.QueryAll<WmiProcessor>(new WmiProcessorQuery());
и не волновался по поводу механизма взаимодействия с WMI, построения запроса или преобразования ответа в строго типизированный класс языка C#.
Что ж, реализовать такое на самом деле не очень сложно.
Сначала подключим к проекту ссылку на библиотеку System.Management (именно в ней находятся классы .NET для доступа к WMI). Далее опишем интерфейс сервиса IWmiService (реализация этого интерфейса будет извлекать данные и преобразовывать их в строго типизированные объекты):
/// <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:
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-запроса:
/// <summary>
/// Указание, какому свойству сущности соответствует поле объекта WMI.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class WmiResultAttribute : Attribute
{
public WmiResultAttribute(string propertyName)
{
PropertyName = propertyName;
}
/// <summary>
/// Имя поля в объекте WMI.
/// </summary>
public string PropertyName { 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
}
Осталось разобраться с объектом, который будет хранить запрос. Уверен, вы обратили внимание, что в предыдущем примере кода названия полей WQL-результатов запроса вынесены в internal-константы. Это было сделано специально чтобы не дублировать их в классе запроса. Кстати, получился интересный побочный эффект с использованием такой модели вы не сможете прочесть из WMI данные поля некоторого WMI-объекта пока не укажете, в какое свойство какой сущности он должен извлекаться.
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,
})
{
}
}
При такой структуре классов *Query есть только одна неприятность: неудобно формировать параметры WHERE-части WML-запроса внутри класса. Приходится действовать по старинке и ручками формировать строку в зависимости от параметров:
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";
}
}
Хорошо: данные по сущностям раскидали, запросы писать с грехом-пополам научились, осталось только разобраться как будет выглядеть сервис, работающий с указанными классами:
/// <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:
/// <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 попробуем детектировать уже неизвестные ВМ.
Источники
Комментарии (27)
Diaskhan
24.02.2017 11:07-9Эти темы по WMI уже столько изъездили! Думал хоть на хабре этого не будет!!!
Ан нет, WMI это круто, Написали бы лучше прогу инвентаризации и выложили в Opensource !
VitalityM
24.02.2017 13:35Спасибо за статью, очень интересно!
Добавлю от себя, что можно эту информацию также выдернуть из реестра Windows. Кстати малварь в основном так и делает.
molnij
25.02.2017 13:41Первая часть такая обстоятельная :)
ИМХО, постановка должна включать в себя два варианта — детект виртуалки, которая не скрывается и детект виртуалки которая пытается скрыться.
Первая часть очевидно разрешима, что вы и показали.
А вторая — настолько же очевидная разновидность задачи «брони-снаряда» — кому нужнее, тот и победит в краткосрочном периоде.
termsl
28.02.2017 16:22Еще для Hyper-V содержимое секции реестра:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters
namwen
К сожалению, ваш 1 уровень (да и последующие, к сожалению) — совершенно бесполезный детект, без обид. Эти строковые константы у vmWare / VirtualBox (про Hyper-V не встречал) патчатся на ура и есть готовые решения, недавно совсем натыкался на совсем уж киддиса, который в лоб смог забить почти все детекты, вот, почитайте на досуге: https://exelab.ru/f/index.php?action=vthread&forum=5&topic=24207
namwen
И там Клерк объявился с интересным предложением (ядро Win 8+, встроенный механизм детекта виртуальной среды по таймингам), скину сюда, т.к. ссылки не могу нормально вставлять:
Так что, отставить строковые константы оборудования и cpuid (с ним давно уже покончено), надо смотреть в сторону того, что нет эмуляции TLB и всяких хитрых тайминговHonoraryBoT
Ни в одном ядре не нашел. Он сам её так назвал? Дайте пруфов плз.
HonoraryBoT
Нашёл на 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».
HonoraryBoT
P.S. МНК, не дисперсия.
HonoraryBoT
PSPS Таки-да, если в вмваре исправить cpuid, то данный детект не работает ввиду хреновой статистики ;)
namwen
HonoraryBoT
Я лишь говорю о том, что грамотно настроенный гипер детектить достаточно сложно.
SLenik
Приветствую!
Во-первых огромное спасибо за ссылку — одна из целей публикации это сбор дополнительной информации касательно особенностей детектирования ВМ.
А насчёт бесполезности — мне кажется вы рано клеймите этим словом последующие уровни. У первого уровня задача по сути обозначить проблему. На втором уровне добавляется детектирования через инструкцию CPUID (EAX=40000000h) и через бит hypervisor present (EAX = 1, смотрим 31-бит в ECX). Это поведение гипервизоров пропатчить чуть сложнее.
А на 3м уровне рассматриваются уже особенности реализации некоторых гипервизоров для определения.
Приведу конкретный пример: применительно к тому же VMWare — в ВМ чипсет компьютера детектируется как Intel 440BX (это socket 370, уровень Pentium II). Соответственно, увидев в системе указаный чипсет вкупе с процессором Intel Kaby Lake, да ещё и одноядерным — мы можем почти со 100% уверенностью утверждать что это виртуалка.
stickerov
Как по мне, к сожалению все «софтовые» доказательства можно подделать очередным патчем (да и часто просто правкой конфига), если уж сильно надо. Ведь должны понимать, что в ВМ окружение можно настроить таким образом, что детект можно полностью обмануть. Вопрос конечно в целесообразности подготовки такой ВМ. Как по мне, единственный выход — поиск и пользование уязвимостей и бэкдоров самих ВМ, остальное — лишь надежда на ВМ с настройкой по-умолчанию.
SLenik
> поиск и пользование уязвимостей и бэкдоров самих ВМ.
В теории — да. Но на практике для обеспечения приемлемой производительности гипервизор вынужден использовать аппаратные возможности виртуализации (типа Intel VT-x). И, завязавшись на особенности их использования, можно ещё больше увеличить вероятность определения ВМ.
VitalityM
Обсуждали в этой статье на Хабре.
Вкратце. Разработать полностью «прозрачную» виртуализацию с приемлемым уровнем производительности просто невозможно (пруф), а существующие ВМ оставляют нам множество артефактов, которые можно детектить.
Вот интересная статья на тему фаззинга инструкций ВМ (ссылка). Можно все не читать, просто обратите внимание на таблицу 2 на странице 10, они показывают количество инструкций, которые работают в этих виртуальных машинах по-другому, чем на нативной. И это они взяли относительно небольшое количество команд. Самое главное, что разработчики ВМ и не стремятся сделать ВМ «прозрачной» (зачем им тратить на это время и ресурсы?), им достаточно добиться совместимости.
kafeman
Короче, не усложняйте. Не так давно мне надо было вытащить одну строку из Android-приложения. Для защиты авторы написали на Си разделяемую библиотеку и относительно неплохо ее обфусцировали. Они, вероятно, рассчитывали, что я месяцами буду дебажить эту либу в IDA, а я просто взял и запустил ее в нужном окружении (на создание которого ушло минут 10). Так что, как говорится, прочность цепи определяется прочностью ее самого слабого звена (в данном случае — .NET и «управляемый» (managed) код).
SLenik
Думаю, не следует в рамках заметки смешивать задачу определения запуска в окружении ВМ и обфускацию кода.
А насчёт спуска до уровня машинных кодов — не вижу причин не спускаться если это даст дополнительные данные. До инструкции CPUID мы уже спустились (через вызов unmanaged C++ библиотеки, заметка на хабр в процессе).
kafeman
Конечно, не зная конкретной реализации, утверждать что-то очень сложно. Но подумайте еще раз над моими словами о слабом звене. Если библиотека банально возвращает true/false, то библиотеку можно подменить своей реализацией. Даже если проверяется подпись и т.д., то все-равно это делается из managed-кода, который пропатчить значительно легче, чем копаться в скопилированном C++. Тогда нужно выносить сами алгоритмы, которые вы хотите скрыть от посторонних. Но и тут может случиться забавная ситуация, когда вашу библиотеку будут просто использовать в своей программе.
VitalityM
С точки зрения защиты кода Вы 100% правы. Защита должна быть построена таким образом, чтобы её нельзя было бы пропатчить 1 байтом, сам не раз сталкивался с таким. Поэтому предлагаемое в статье решение может (и должно) использоваться только как один из компонентов многослойной защиты. Themida отличный пример (хоть и не managed-code).
Ну и не нужно забывать, что такие методы могут применяться и в малвари, полезно знать какие есть способы обнаружения песочницы.
Goodkat
Вообще, параноикам имеет смысл прописать эти константы вручную в своей ОС — всякая малварь первым делом детектит виртуальную машину, и просто не запускается, чтобы избежать отладки.
А серьёзному софту плевать на виртуализацию.
redmanmale
Отчасти соглашусь, но иногда бывает так, что защита "серьёзного" софта точно так же реагирует на присутствие ВМ и не позволяет ему запускаться.
Goodkat
А что за софт такой серьёзный, что его нельзя запускать в ВМ?
Или вы про игры?
SLenik
Пример серьезного софта, меняющего свое поведение при установке его на ВМ = Windows. Фишка в том, что в MS требуют отдельную лицензию на каждую ВМ (одной на всю физическую машину им мало). Предположу, что именно поэтому на ВМ при клонировании может слететь активация.
redmanmale
Сталкивался с таким поведением различных платных библиотек обработки аудио от крупных производителей.