Развитие технологий аппаратной виртуализации (Intel VT-x и AMD-V) открывает широкие возможности по контролю выполнения кода на самом низком уровне.
Привычные всем гипервизоры (Hyper-V, KVM, VMware или VirtualBox) позволяют запускать операционные системы в изолированном окружении: это становится возможным, благодаря способности процессоров работать в специальном режиме, в котором они контролируют доступ к ресурсам и обрабатывают выполнение заданных инструкций и событий.
При наступлении заданного события процессор передаёт управление специальному обработчику - монитору виртуальных машин (VMM).
Обработчик особым образом обрабатывает событие (например, эмулирует инструкцию) и отдаёт управление обратно виртуальной машине.
Этот принцип можно использовать не только для виртуализации самостоятельных операционных систем, но и для виртуализации "живой" системы на лету, что позволит произвольным образом контролировать поведение процессора и, как следствие, обманывать механизмы защиты или, наоборот, не дать их обойти.
В этом цикле статей мы пошагово напишем гипервизор, виртуализующий систему на лету, и научимся с его помощью обрабатывать обращения к памяти, отдавая программам на чтение, запись и исполнение разную память незаметно для самих программ, что позволит создавать изолированные анклавы, скрывать перехваты (в том числе в ядре, что позволит обойти PatchGuard) или скрывать исполняемые модули целиком.
Т.к. виртуализация - аппаратная технология, она не зависит от операционной системы (и может работать вообще без неё), но могут быть ограничения на системах, которые сами уже находятся под управлением других гипервизоров.
Так, например, нельзя повторно виртуализовать хост под управлением Hyper-V (однако, можно виртуализовать созданные виртуальные машины, если в них включена поддержка вложенной виртуализации).
Прежде чем начнём, запасёмся необходимой документацией:
- Intel Software Developer Manual
- AMD64 Architecture Programmer’s Manual
Несмотря на то, что Intel VT-x (другое название VMX - Virtual Machine Extensions) и AMD-V (SVM - Secure Virtual Machine) очень сильно различаются в деталях реализации, они работают по схожему и простому принципу:
1. В процессоре включается поддержка виртуализации.
2. Настраивается контекст процессора (состояние регистров), с которым он выполнит первую инструкцию в виртуальном режиме (в режиме гостя).
3. Настраивается гостевое адресное пространство (память, что будет доступна виртуальному процессору).
4. Настраиваются события, которые требуют специальной обработки (доступ к портам, регистрам и памяти, выполнение особых инструкций и т.д.).
5. Настраивается состояние монитора виртуальных машин (VMM), который получит управление при наступлении одного из событий, заданных в пункте 4.
6. Процессор переводится в виртуальный режим выполнением специальных инструкций и начинает работу в настроенном гостевом окружении.
Как только на виртуализованном процессоре происходит событие, заданное при настройке, процессор останавливает выполнение текущего кода и переходит в монитор виртуальных машин (VMM) - это событие называется #VMEXIT.
Процессор сохраняет состояние виртуальной машины, загружает состояние VMM и отдаёт управление заданному обработчику. Тот обрабатывает событие (например, меняет значение регистров или содержимое памяти) и возвращает управление виртуальной машине.
Основываясь на этих принципах, можно перевести все логические процессоры в виртуальный режим прозрачно для работающей системы - таким образом, что виртуальная система продолжит выполнять код хоста, находясь в общем с хостом адресном пространстве.
Это потребует минимальной настройки гипервизора:
- Физическая память гостевого режима соотносится с физической памятью хоста один к одному (т.е., гостевой физический адрес 0x12345 будет соответствовать такому же хостовому физическому адресу 0x12345).
- Состояние регистров гостя на момент входа в виртуальный режим задаётся идентичным регистрам хоста.
С заданными начальными условиями виртуальный процессор "продолжит" выполнять за хостом его код - но уже под виртуализацией. Таким образом, будет виртуализована вся операционная система, т.к. виртуализуется не поток, а процессор, который сохраняет состояние виртуализованности независимо от переключения потоков.
А начнём мы разработку с гипервизора под Intel VT-x: он более гибок, чем AMD-V, а также, из-за всё ещё более высокой популярности интеловской платформы, нежели AMD.
Писать будем в Visual Studio на C++, используя последний на данный момент комплект WDK.
Создаём проект Empty WDM Driver, создаём пустой Main.cpp и пишем стандартную для всех драйверов заготовку с регистрацией в системе (используем принятый в ядре стиль DrvCamelCase):
Итак, мы научили наш драйвер загружаться и выгружаться.Код (C):
#include <wdm.h> namespace { UNICODE_STRING DeviceName = RTL_CONSTANT_STRING(L"\\Device\\Hypervisor"); UNICODE_STRING DeviceLink = RTL_CONSTANT_STRING(L"\\??\\Hypervisor"); PDEVICE_OBJECT DeviceInstance = NULL; } EXTERN_C_START DRIVER_INITIALIZE DriverEntry; _Function_class_(DRIVER_DISPATCH) _Dispatch_type_(IRP_MJ_DEVICE_CONTROL) _IRQL_requires_max_(DISPATCH_LEVEL) _IRQL_requires_same_ static NTSTATUS NTAPI DriverControl( _In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp ); _Function_class_(DRIVER_DISPATCH) _Dispatch_type_(IRP_MJ_CREATE) _Dispatch_type_(IRP_MJ_CLOSE) _Dispatch_type_(IRP_MJ_CLEANUP) _IRQL_requires_max_(DISPATCH_LEVEL) _IRQL_requires_same_ static NTSTATUS NTAPI DriverStub( _In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp ); _Function_class_(DRIVER_UNLOAD) _IRQL_requires_(PASSIVE_LEVEL) _IRQL_requires_same_ static NTSTATUS NTAPI DriverUnload( _In_ PDRIVER_OBJECT DriverObject ); EXTERN_C_END extern "C" NTSTATUS NTAPI DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { UNREFERENCED_PARAMETER(RegistryPath); DriverObject->DriverUnload = reinterpret_cast<PDRIVER_UNLOAD>(DriverUnload); DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverStub; DriverObject->MajorFunction[IRP_MJ_CLEANUP] = DriverStub; DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverStub; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverControl; NTSTATUS Status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceInstance); if (!NT_SUCCESS(Status)) { DbgPrint("IoCreateDevice error: 0x%X!\r\n", Status); return Status; } Status = IoCreateSymbolicLink(&DeviceLink, &DeviceName); if (!NT_SUCCESS(Status)) { DbgPrint("IoCreateSymbolicLink error: 0x%X!\r\n", Status); IoDeleteDevice(DeviceInstance); return Status; } return STATUS_SUCCESS; } _Function_class_(DRIVER_DISPATCH) _Dispatch_type_(IRP_MJ_DEVICE_CONTROL) _IRQL_requires_max_(DISPATCH_LEVEL) _IRQL_requires_same_ static NTSTATUS NTAPI DriverControl(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject); // ... Put your custom code here if you need it ... Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } _Function_class_(DRIVER_DISPATCH) _Dispatch_type_(IRP_MJ_CREATE) _Dispatch_type_(IRP_MJ_CLOSE) _Dispatch_type_(IRP_MJ_CLEANUP) _IRQL_requires_max_(DISPATCH_LEVEL) _IRQL_requires_same_ static NTSTATUS NTAPI DriverStub(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject); Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } _Function_class_(DRIVER_UNLOAD) _IRQL_requires_(PASSIVE_LEVEL) _IRQL_requires_same_ static NTSTATUS NTAPI DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); IoDeleteSymbolicLink(&DeviceLink); IoDeleteDevice(DeviceInstance); DbgPrint("Successfully unloaded!\r\n"); return STATUS_SUCCESS; }
Заходим в cmd с правами администратора и переводим систему в тестовый режим для возможности загружать драйвер без цифровой подписи:
Перезагружаемся и запускаем драйвер:Код (Text):
bcdedit.exe /set loadoptions DISABLE_INTEGRITY_CHECKS bcdedit.exe /set TESTSIGNING ON
Шаблон готов, можно приступать непосредственно к коду гипервизора.Код (Text):
sc create hypervisor type= kernel binPath= "N:\Path\To\Hypervisor.sys" sc start hypervisor ... sc stop hypervisor sc delete hypervisor
Последовательность запуска и настройка достаточно подробно описаны в Intel SDM (Volume 3, Chapter 31) - мы пройдём все эти шаги в рамках этих статей.
Начать следует с определения, поддерживает ли текущий процессор технологию VMX. Сделаем это в три шага:
1. Убедимся, что мы запускаемся на интеловском процессоре.
2. Проверим, поддерживает ли процессор VMX, посмотрев на соответствующий бит в CPUID (см. Volume 3, Chapter 23.6).
3. Проверим, не заблокирован ли VMX в BIOS'е, о чём свидетельствует бит в MSR-регистре IA32_FEATURE_CONTROL (см. Volume 3, Chapter 23.7).
Так как определения структур очень длинные, а самих структур много, публиковать их в тексте статьи нет смысла: все готовые структуры представлены в архиве с проектом в аттаче в соответствующих заголовочных файлах.
Создадим файлы Hypervisor.cpp и Hypervisor.h, в которых будем писать логику гипервизора:
Код (C):
// Hypervisor.h: #pragma once namespace Hypervisor { bool Virtualize(); bool Devirtualize(); } И вызовем Virtualize() и Devirtualize() в DriverEntry() и DriverUnload() соответственно.Код (C):
// Hypervisor.cpp: #include "Hypervisor.h" #include <ntifs.h> #include "MSR.h" #include "CPUID.h" #include <intrin.h> namespace { enum class CPU_VENDOR { cpuIntel, cpuAmd, cpuUnknown }; CPU_VENDOR GetCpuVendor() { static CPU_VENDOR CpuVendor = CPU_VENDOR::cpuUnknown; if (CpuVendor != CPU_VENDOR::cpuUnknown) { return CpuVendor; } CPUID_REGS Regs; __cpuid(Regs.Raw, CPUID::Generic::CPUID_MAXIMUM_FUNCTION_NUMBER_AND_VENDOR_ID); if (Regs.Regs.Ebx == 'uneG' && Regs.Regs.Edx == 'Ieni' && Regs.Regs.Ecx == 'letn') { // GenuineIntel: CpuVendor = CPU_VENDOR::cpuIntel; } else if (Regs.Regs.Ebx == 'htuA' && Regs.Regs.Edx == 'itne' && Regs.Regs.Ecx == 'DMAc') { // AuthenticAMD: CpuVendor = CPU_VENDOR::cpuAmd; } return CpuVendor; } } namespace VMX { using namespace Intel; static bool VirtualizeAllProcessors() { /* We're good to go */ return false; } static bool IsVmxSupported() { CPUID_REGS Regs = {}; // Check the 'GenuineIntel' vendor name: __cpuid(Regs.Raw, CPUID::Generic::CPUID_MAXIMUM_FUNCTION_NUMBER_AND_VENDOR_ID); if (Regs.Regs.Ebx != 'uneG' || Regs.Regs.Edx != 'Ieni' || Regs.Regs.Ecx != 'letn') { return false; } // Support by processor: __cpuid(Regs.Raw, CPUID::Intel::CPUID_FEATURE_INFORMATION); if (!reinterpret_cast<CPUID::FEATURE_INFORMATION*>(&Regs)->Intel.VMX) { return false; } // Check the VMX is locked in BIOS: IA32_FEATURE_CONTROL MsrFeatureControl = {}; MsrFeatureControl.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_FEATURE_CONTROL)); return MsrFeatureControl.Bitmap.LockBit == TRUE; } } namespace Hypervisor { bool Virtualize() { CPU_VENDOR CpuVendor = GetCpuVendor(); if (CpuVendor == CPU_VENDOR::cpuUnknown) return false; bool Status = false; switch (CpuVendor) { case CPU_VENDOR::cpuIntel: { if (!VMX::IsVmxSupported()) return false; Status = VMX::VirtualizeAllProcessors(); break; } case CPU_VENDOR::cpuAmd: { /* Not supported yet */ break; } } return Status; } bool Devirtualize() { /* Not implemented yet */ return false; } }
После проверки доступности VMX можем запускать гипервизор.
Т.к. нам понадобится виртуализовать все логические процессоры, мы создали заготовку VirtualizeAllProcessors(), но об этом - в следующей главе.
На этом подготовительная часть закончена, исходный проект с определениями нужных структур в аттаче.
В следующей части приступим непосредственно к коду настройки гипервизора и гостевой системы.
Виртуализация для самых маленьких #1: гипервизор - что, зачем и почему
Дата публикации 15 июн 2020
| Редактировалось 15 июн 2020