Виртуализация для самых маленьких #1: гипервизор - что, зачем и почему

Дата публикации 15 июн 2020 | Редактировалось 15 июн 2020
HvSample.png

Развитие технологий аппаратной виртуализации (Intel VT-x и AMD-V) открывает широкие возможности по контролю выполнения кода на самом низком уровне.
Привычные всем гипервизоры (Hyper-V, KVM, VMware или VirtualBox) позволяют запускать операционные системы в изолированном окружении: это становится возможным, благодаря способности процессоров работать в специальном режиме, в котором они контролируют доступ к ресурсам и обрабатывают выполнение заданных инструкций и событий.

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

В этом цикле статей мы пошагово напишем гипервизор, виртуализующий систему на лету, и научимся с его помощью обрабатывать обращения к памяти, отдавая программам на чтение, запись и исполнение разную память незаметно для самих программ, что позволит создавать изолированные анклавы, скрывать перехваты (в том числе в ядре, что позволит обойти PatchGuard) или скрывать исполняемые модули целиком.

Т.к. виртуализация - аппаратная технология, она не зависит от операционной системы (и может работать вообще без неё), но могут быть ограничения на системах, которые сами уже находятся под управлением других гипервизоров.
Так, например, нельзя повторно виртуализовать хост под управлением Hyper-V (однако, можно виртуализовать созданные виртуальные машины, если в них включена поддержка вложенной виртуализации).

Vmm.gif

Прежде чем начнём, запасёмся необходимой документацией:
- 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):
  1.  
  2. #include <wdm.h>
  3.  
  4. namespace
  5. {
  6.     UNICODE_STRING DeviceName = RTL_CONSTANT_STRING(L"\\Device\\Hypervisor");
  7.     UNICODE_STRING DeviceLink = RTL_CONSTANT_STRING(L"\\??\\Hypervisor");
  8.     PDEVICE_OBJECT DeviceInstance = NULL;
  9. }
  10.  
  11. EXTERN_C_START
  12.  
  13. DRIVER_INITIALIZE DriverEntry;
  14.  
  15. _Function_class_(DRIVER_DISPATCH)
  16. _Dispatch_type_(IRP_MJ_DEVICE_CONTROL)
  17. _IRQL_requires_max_(DISPATCH_LEVEL)
  18. _IRQL_requires_same_
  19. static NTSTATUS NTAPI DriverControl(
  20.     _In_ PDEVICE_OBJECT DeviceObject,
  21.     _In_ PIRP Irp
  22. );
  23.  
  24. _Function_class_(DRIVER_DISPATCH)
  25. _Dispatch_type_(IRP_MJ_CREATE)
  26. _Dispatch_type_(IRP_MJ_CLOSE)
  27. _Dispatch_type_(IRP_MJ_CLEANUP)
  28. _IRQL_requires_max_(DISPATCH_LEVEL)
  29. _IRQL_requires_same_
  30. static NTSTATUS NTAPI DriverStub(
  31.     _In_ PDEVICE_OBJECT DeviceObject,
  32.     _In_ PIRP Irp
  33. );
  34.  
  35. _Function_class_(DRIVER_UNLOAD)
  36. _IRQL_requires_(PASSIVE_LEVEL)
  37. _IRQL_requires_same_
  38. static NTSTATUS NTAPI DriverUnload(
  39.     _In_ PDRIVER_OBJECT DriverObject
  40. );
  41.  
  42. EXTERN_C_END
  43.  
  44. extern "C" NTSTATUS NTAPI DriverEntry(
  45.     _In_ PDRIVER_OBJECT DriverObject,
  46.     _In_ PUNICODE_STRING RegistryPath
  47. ) {
  48.     UNREFERENCED_PARAMETER(RegistryPath);
  49.     DriverObject->DriverUnload = reinterpret_cast<PDRIVER_UNLOAD>(DriverUnload);
  50.     DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverStub;
  51.     DriverObject->MajorFunction[IRP_MJ_CLEANUP] = DriverStub;
  52.     DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverStub;
  53.     DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverControl;
  54.  
  55.     NTSTATUS Status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceInstance);
  56.     if (!NT_SUCCESS(Status))
  57.     {
  58.         DbgPrint("IoCreateDevice error: 0x%X!\r\n", Status);
  59.         return Status;
  60.     }
  61.  
  62.     Status = IoCreateSymbolicLink(&DeviceLink, &DeviceName);
  63.     if (!NT_SUCCESS(Status))
  64.     {
  65.         DbgPrint("IoCreateSymbolicLink error: 0x%X!\r\n", Status);
  66.         IoDeleteDevice(DeviceInstance);
  67.         return Status;
  68.     }
  69.  
  70.     return STATUS_SUCCESS;
  71. }
  72.  
  73. _Function_class_(DRIVER_DISPATCH)
  74. _Dispatch_type_(IRP_MJ_DEVICE_CONTROL)
  75. _IRQL_requires_max_(DISPATCH_LEVEL)
  76. _IRQL_requires_same_
  77. static NTSTATUS NTAPI DriverControl(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp)
  78. {
  79.     UNREFERENCED_PARAMETER(DeviceObject);
  80.     // ... Put your custom code here if you need it ...
  81.     Irp->IoStatus.Status = STATUS_SUCCESS;
  82.     IoCompleteRequest(Irp, IO_NO_INCREMENT);
  83.  
  84.     return STATUS_SUCCESS;
  85. }
  86.  
  87. _Function_class_(DRIVER_DISPATCH)
  88. _Dispatch_type_(IRP_MJ_CREATE)
  89. _Dispatch_type_(IRP_MJ_CLOSE)
  90. _Dispatch_type_(IRP_MJ_CLEANUP)
  91. _IRQL_requires_max_(DISPATCH_LEVEL)
  92. _IRQL_requires_same_
  93. static NTSTATUS NTAPI DriverStub(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp)
  94. {
  95.     UNREFERENCED_PARAMETER(DeviceObject);
  96.  
  97.     Irp->IoStatus.Status = STATUS_SUCCESS;
  98.     Irp->IoStatus.Information = 0;
  99.     IoCompleteRequest(Irp, IO_NO_INCREMENT);
  100.  
  101.     return STATUS_SUCCESS;
  102. }
  103.  
  104. _Function_class_(DRIVER_UNLOAD)
  105. _IRQL_requires_(PASSIVE_LEVEL)
  106. _IRQL_requires_same_
  107. static NTSTATUS NTAPI DriverUnload(_In_ PDRIVER_OBJECT DriverObject)
  108. {
  109.     UNREFERENCED_PARAMETER(DriverObject);
  110.     IoDeleteSymbolicLink(&DeviceLink);
  111.     IoDeleteDevice(DeviceInstance);
  112.     DbgPrint("Successfully unloaded!\r\n");
  113.     return STATUS_SUCCESS;
  114. }
  115.  
Итак, мы научили наш драйвер загружаться и выгружаться.
Заходим в cmd с правами администратора и переводим систему в тестовый режим для возможности загружать драйвер без цифровой подписи:
Код (Text):
  1.  
  2. bcdedit.exe /set loadoptions DISABLE_INTEGRITY_CHECKS
  3. bcdedit.exe /set TESTSIGNING ON
  4.  
Перезагружаемся и запускаем драйвер:
Код (Text):
  1.  
  2. sc create hypervisor type= kernel binPath= "N:\Path\To\Hypervisor.sys"
  3. sc start hypervisor
  4. ...
  5. sc stop hypervisor
  6. sc delete hypervisor
  7.  
Шаблон готов, можно приступать непосредственно к коду гипервизора.

Последовательность запуска и настройка достаточно подробно описаны в 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).

upload_2020-6-15_0-29-10.png

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

Создадим файлы Hypervisor.cpp и Hypervisor.h, в которых будем писать логику гипервизора:
Код (C):
  1.  
  2. // Hypervisor.h:
  3. #pragma once
  4.  
  5. namespace Hypervisor
  6. {
  7.     bool Virtualize();
  8.     bool Devirtualize();
  9. }
  10.  
Код (C):
  1.  
  2. // Hypervisor.cpp:
  3. #include "Hypervisor.h"
  4.  
  5. #include <ntifs.h>
  6.  
  7. #include "MSR.h"
  8. #include "CPUID.h"
  9.  
  10. #include <intrin.h>
  11.  
  12. namespace
  13. {
  14.     enum class CPU_VENDOR
  15.     {
  16.         cpuIntel,
  17.         cpuAmd,
  18.         cpuUnknown
  19.     };
  20.  
  21.     CPU_VENDOR GetCpuVendor()
  22.     {
  23.         static CPU_VENDOR CpuVendor = CPU_VENDOR::cpuUnknown;
  24.         if (CpuVendor != CPU_VENDOR::cpuUnknown)
  25.         {
  26.             return CpuVendor;
  27.         }
  28.  
  29.         CPUID_REGS Regs;
  30.  
  31.         __cpuid(Regs.Raw, CPUID::Generic::CPUID_MAXIMUM_FUNCTION_NUMBER_AND_VENDOR_ID);
  32.         if (Regs.Regs.Ebx == 'uneG' && Regs.Regs.Edx == 'Ieni' && Regs.Regs.Ecx == 'letn')
  33.         {
  34.             // GenuineIntel:
  35.             CpuVendor = CPU_VENDOR::cpuIntel;
  36.         }
  37.         else if (Regs.Regs.Ebx == 'htuA' && Regs.Regs.Edx == 'itne' && Regs.Regs.Ecx == 'DMAc')
  38.         {
  39.             // AuthenticAMD:
  40.             CpuVendor = CPU_VENDOR::cpuAmd;
  41.         }
  42.  
  43.         return CpuVendor;
  44.     }
  45. }
  46.  
  47. namespace VMX
  48. {
  49.     using namespace Intel;
  50.  
  51.     static bool VirtualizeAllProcessors()
  52.     {
  53.         /* We're good to go */
  54.         return false;
  55.     }
  56.  
  57.     static bool IsVmxSupported()
  58.     {
  59.         CPUID_REGS Regs = {};
  60.  
  61.         // Check the 'GenuineIntel' vendor name:
  62.         __cpuid(Regs.Raw, CPUID::Generic::CPUID_MAXIMUM_FUNCTION_NUMBER_AND_VENDOR_ID);
  63.         if (Regs.Regs.Ebx != 'uneG' || Regs.Regs.Edx != 'Ieni' || Regs.Regs.Ecx != 'letn')
  64.         {
  65.             return false;
  66.         }
  67.  
  68.         // Support by processor:
  69.         __cpuid(Regs.Raw, CPUID::Intel::CPUID_FEATURE_INFORMATION);
  70.         if (!reinterpret_cast<CPUID::FEATURE_INFORMATION*>(&Regs)->Intel.VMX)
  71.         {
  72.             return false;
  73.         }
  74.  
  75.         // Check the VMX is locked in BIOS:
  76.         IA32_FEATURE_CONTROL MsrFeatureControl = {};
  77.         MsrFeatureControl.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_FEATURE_CONTROL));
  78.    
  79.         return MsrFeatureControl.Bitmap.LockBit == TRUE;
  80.     }
  81. }
  82.  
  83. namespace Hypervisor
  84. {
  85.     bool Virtualize()
  86.     {
  87.         CPU_VENDOR CpuVendor = GetCpuVendor();
  88.         if (CpuVendor == CPU_VENDOR::cpuUnknown) return false;
  89.    
  90.         bool Status = false;
  91.    
  92.         switch (CpuVendor) {
  93.         case CPU_VENDOR::cpuIntel:
  94.         {
  95.             if (!VMX::IsVmxSupported()) return false;
  96.             Status = VMX::VirtualizeAllProcessors();
  97.             break;
  98.         }
  99.         case CPU_VENDOR::cpuAmd:
  100.         {
  101.             /* Not supported yet */
  102.             break;
  103.         }
  104.         }
  105.  
  106.         return Status;
  107.     }
  108.  
  109.     bool Devirtualize()
  110.     {
  111.         /* Not implemented yet */
  112.         return false;
  113.     }
  114. }
  115.  
И вызовем Virtualize() и Devirtualize() в DriverEntry() и DriverUnload() соответственно.

После проверки доступности VMX можем запускать гипервизор.
Т.к. нам понадобится виртуализовать все логические процессоры, мы создали заготовку VirtualizeAllProcessors(), но об этом - в следующей главе.

На этом подготовительная часть закончена, исходный проект с определениями нужных структур в аттаче.
В следующей части приступим непосредственно к коду настройки гипервизора и гостевой системы.

Вложения:

  • Hypervisor.zip
    Размер файла:
    34,6 КБ
    Просмотров:
    719

18 7.174
HoShiMin

HoShiMin
Well-Known Member

Регистрация:
17 дек 2016
Публикаций:
5

Комментарии


      1. LastNoob 6 июл 2020
        Вот и пищча для ума прибыла, интересная тематика, обязательно буду отслеживать обновления:focus:
      2. UbIvItS 26 июн 2020
        HoShiMin
        мокрые стругают гибридный режим работы == два ядра (линь и энти) будут пахать бок о бок.
        у меня претензий в сущности нет, ибо они бесполезны == просто отметил, что в выньке нет гибкости работы для кастомных вирт. :)
      3. HoShiMin 25 июн 2020
        UbIvItS, так я и написал, что если включен Hyper-V и изоляция ядра, то берём Hyper-V, а если всё это выключено - берём VMware/VirtualBox. VMware Workstation не поддерживает PCI Passthrough, зато VBox, кажется, умеет. Hyper-V умеет.
        На линуксе изоляции ядра нет, это полностью аналогично винде с отключенным Hyper-V. А если включаешь изоляцию ядра - про самописные гипервизоры забудь, только через майкрософтовский API или юзай Hyper-V, он намного быстрее и оптимальнее вмвари и вбокса.
        Поэтому не понимаю претензию к защите.
      4. UbIvItS 25 июн 2020
        HoShiMin
        для вбокса не пашет, вмвару не проверял.
        вот потому и используют линь для развитых вирт :grin:
      5. HoShiMin 25 июн 2020
        UbIvItS, если надо виртуализовать отдельную операционку, VBox и VMware прекрасно себя чувствуют, а если включена изоляция ядра, есть Hyper-V с PCI Passthrough из коробки.
        Но я разбираю виртуализацию живой системы, там просадок производительности нет и девайсы никуда пробрасывать не надо.
        Нужна защита - есть API для виртуализации. Нужна самописная виртуализация - отключаем защиту. Ведь защита и направлена на атаки, использующие виртуализацию в том числе. Или ты хотел, чтобы они защищали от всего на свете, но можно было бы свободно переводить всё ядро под свой контроль? Это несовместимые вещи, поэтому всё логично.
      6. UbIvItS 25 июн 2020
        Indy_ вирты под бесяткой обречены быть кастратами, пч нет возможности оптить ядро + защита бесятки вАлчарой глядит на pci passthru. в итоге о приличной производительности тамо можно забыть + очередной апдейт от мокрых легко может обрушить уже устоявшиеся решения. корочЪ, приличные вирты токЪ под линем катят :)
      7. Indy_ 21 июн 2020
        Это всё конечно интересно, вот только есть одна фишка, почему гпв не востребован. Для контроля за приложением при использовании сабжа необходимо перевести систему в гипер мод, запустить этот механизм на уровне cpu/kernel. Это не локальное решение, которое можно встроить в апп и оно будет работать везде, где работает апп :)
      8. UbIvItS 18 июн 2020
        HoShiMin сейчас не помню, но при отключке core isolation всё пахало. мне нужна была вбокса, чтоб из-под неё у бесятки был доступ к разделам бтрфс :)
      9. HoShiMin 18 июн 2020
        UbIvItS, а какие галочки поставлены в программах и компонентах?
      10. UbIvItS 18 июн 2020
        HoShiMin я 6.1.0 на лтсц запускал == no luck :)
      11. HoShiMin 18 июн 2020
      12. UbIvItS 18 июн 2020
        HoShiMin сомневаюсь, что для вбокс появится, пч он доступен в сорцах, то бишь подписывать агульно пересобранные анонимусом драверы мокрые не станут. :) даже офиц билд вбокса у мокрых вызывает несварения в силу подозрений на наличие дырок для подгрузки левых модулей.
      13. HoShiMin 18 июн 2020
        UbIvItS, нет, при изоляции, разумеется, виртуализация работать не будет, но не будет не из-за самой изоляции, а из-за Hyper-V, который разрешает вложенную виртуализацию только внутри виртуалок, но не на хосте.
        Специально для этих случаев они выпустили API для Hyper-V, на основе которого можно сделать поддержку сторонних гипервизоров. VMware уже сделали (нужна Win10 2004), VBox или уже сделали, или вот-вот выпустят.
      14. HoShiMin 18 июн 2020
        UbIvItS, а что в VBox'e не пашет? Я им не пользуюсь из-за его тормознутости, юзаю VMware и с ней никаких проблем, работает прекрасно, безо всяких Dev-режимов.
      15. UbIvItS 16 июн 2020
        HoShiMin, мокрые все левые вирты рассматривают акь потенциальные малвари == тот же виртуалбокс только номинально под бесяткой пашет, практически дэв режим надо врубать :)
      16. HoShiMin 15 июн 2020
        UbIvItS, добавил пару предложений. Виртуализовать можно всё, кроме хоста под Hyper-V и кроме систем, в которых отключена поддержка вложенной виртуализации.
      17. UbIvItS 15 июн 2020
        а тут желательно показать об акой бесятке идёт речь и настройки её безопасности == y ltsc при полных настройках левые вирты не пашут :)