Устройство памяти Hyper-V

Hyper-V

За последние несколько лет в Microsoft создали массу проблем для разработчиков виртуальных машин. Причина тому — ряд новых технологий (VBS, Windows Sandbox, WSL), активно использующих возможности аппаратной виртуализации Hyper-V. Разработчики стороннего ПО для виртуализации больше не могут применять собственный гипервизор и должны полагаться на API, который предоставляет Microsoft.

Устройство памяти Hyper-V

Платформа виртуализации Hyper-V, разработанная в Microsoft, появилась достаточно давно — первый доклад о ней опубликован на конференции WinHEC в 2006 году, сама платформа была интегрирована в Windows Server 2008. На первых порах в Microsoft охотно делились описанием API Hyper-V (он даже присутствовал в Microsoft SDK 7.0), но со временем официальной информации об интерфейсах Hyper-V становилось все меньше. В конце концов она осталась только в виде Hyper-V Top Level Function Specification, которые предоставляют разработчикам операционных систем, желающим создать условия для работы своей ОС внутри Hyper-V.

Еще большие проблемы возникли после того, как в Windows 10 внедрили технологию Virtualization Based Security (VBS), компоненты которой (Device Guard, Code Integrity и Credential Guard) используют Hyper-V для защиты критичных компонентов операционной системы. Оказалось, что существующие системы виртуализации, такие как QEMU, VirtualBox и VMware Workstation, не могут работать в этих условиях при использовании функций аппаратной виртуализации процессора. Работающий Hyper-V просто блокировал их выполнение.

VBS появился в Enterprise-версии Windows 10, build 1511 (ноябрь 2015 года) как отдельный компонент, но в сборке 1607 уже стал частью ОС, а в декабре 2019-го его сделали активным по умолчанию. Из-за этого начались сбои сторонних виртуальных машин.

Для решения этой проблемы Microsoft разработала Windows Hypervisor Platform API, которые предоставляют следующие возможности для сторонних разработчиков:

  • создание «разделов» Hyper-V и управление ими;
  • управление памятью для каждого раздела;
  • управление виртуальными процессорами гипервизора.

Смысл этих API в том, чтобы предоставить приложению возможность управлять ресурсами процессора, читать и писать значения регистров, приостанавливать работу процессора, генерировать прерывания. Это некий абсолютный минимум для работы с виртуальными ресурсами.

API стали доступны в Windows 10 начиная со сборки 1803 (April 2018 update), через компонент Windows Hypervisor Platform (WHPX). Первым поддержкой WHPX обзавелся эмулятор QEMU, для которого программисты Microsoft разработали модуль ускорения WHPX, продемонстрировав, что их API работоспособны. За ними последовали разработчики Oracle VirtualBox, которым пришлось несколько раз переписать код поддержки WHPX по причине изменений в Windows 10 1903.

Компания VMware выпустила версию своей виртуальной машины с поддержкой Hyper-V только 28 мая 2020 года (версия 15.5), аргументировав столь долгую задержку необходимостью переписать весь стек виртуализации.

При этом реализация VMware для Hyper-V потеряла поддержку вложенной виртуализации, и будет ли она добавлена — неизвестно. Также сейчас обсуждают, что производительность заметно снизилась.

Итого в настоящее время WHPX API используются:

Можно сказать, что пока API получается использовать эффективно только в usermode (QEMU, Bochs). И что будет дальше — непонятно. С одной стороны, можно заметить, что API меняются. Новые функции появляются каждые полгода при выходе новой версии Windows и даже при выпуске ежемесячных кумулятивных обновлений.

Например, вот список функций, экспортируемых vid.dll в зависимости от версии Windows.

Как видно, набор функций меняется, особенно для серверных версий Windows.

Для WHVP API все гораздо более стабильнее, что, в общем-то, логично для публичных API.

Официальная документация Hyper-V TLFS обновляется крайне редко — последний апдейт затронул появление вложенной виртуализации, но информации не слишком много, она позволяет считать данные о внутренних структурах гипервизора, что я делал в свое время с помощью утилиты LiveCloudKd. Пока эту информацию получается использовать только в исследовательских целях — применить ее на практике, интегрировав, например, в отладчик, не представляется возможным.

Отдельно стоит упомянуть, что облако Microsoft Azure использует одну и ту же кодовую базу с Hyper-V, о чем говорит менеджер Hyper-V Бен Армстронг (запись сессии — на третьей минуте). Однако основной модуль Hyper-V в Azure отличается и явно собран с некоторыми директивами условной компиляции (достаточно сравнить hvix64/hvax64 для Windows Server 2019 и Windows 10, чтобы определить, что они отличаются достаточно сильно).

Термины и определения

  • WDAG — Windows Defender Application Guard (или MDAG — Microsoft Defender Application Guard в более новых версиях Windows).
  • Full VM — стандартная полноценная виртуальная машина, созданная в Hyper-V Manager. Отличается от контейнеров WDAG, Windows Sandbox, Docker в режиме изоляции Hyper-V.
  • Root ОС — операционная система, в которой установлена серверная часть Hyper-V.
  • Гостевая ОС — операционная система, которая работает в контексте эмуляции Hyper-V, в том числе используя виртуальные устройства, предоставляемые гипервизором. В статье могут иметься в виду как Full VM, так и контейнеры.
  • TLFS — официальный документ Microsoft Hypervisor Top-Level Functional Specification 6.0.
  • GPA (guest physical address) — физический адрес памяти гостевой операционной системы.
  • SPA (system physical address) — физический адрес памяти root ОС.
  • Гипервызов (hypercall) — сервис гипервизора, вызываемый посредством выполнения команды vmcall (для процессоров Intel) с указанием номера гипервызова.
  • VBS (Virtualization Based Security) — средство обеспечения безопасности на основе виртуализации.
  • EXO-раздел — объект «раздел», создаваемый при запуске виртуальных машин, работающих под управлением Windows Hypervisor Platform API.
  • WHVP API — Windows Hypervisor Platform API.

Устройство памяти EXO-разделов

В своем исследовании я использовал Windows 10 x64 Enterprise 20H1 (2004) в качестве root ОС и для некоторых случаев Windows 10 x64 Enterprise 1803 с апдейтами на июнь 2020-го (ее поддержка закончится в ноябре 2020-го, поэтому информация предоставлена исключительно для сравнения). В качестве гостевой ОС — Windows 10 x64 Enterprise 20H1 (2004).

В Windows SDK 19041 (для Windows 10 2004) присутствуют три заголовочных файла:

  • WinHvPlatform.h;
  • WinHvPlatformDefs.h;
  • WinHvEmulation.h.

Функции экспортируются библиотекой winhvplatform.dll и описаны в заголовочном файле WinHvPlatform.h. Эти функции — обертки над сервисами, предоставляемыми vid.dll (библиотека драйверов инфраструктуры виртуализации Microsoft Hyper-V), которая, в свою очередь, вызывает сервисы драйвера vid.sys (Microsoft Hyper-V Virtualization Infrastructure Driver).

Кратко рассмотрим, что происходит при запуске виртуальной машины. В качестве референса воспользуемся исходным кодом QEMU WHPX.

При запуске QEMU в режиме аппаратного ускорения WHPX создаются два дескриптора \Device\VidExo, которые позволяют получить доступ к устройству, создаваемому драйвером vid.sys, из пользовательского режима.

Оба дескриптора — файловые объекты.

Если посмотреть FsContext каждого, то он указывает на различные структуры данных, имеющие сигнатуры Exo и Prtn.

Логику работу с Prtn-разделом (структурой VM_PROCESS_CONTEXT) можно посмотреть в исходниках драйвера hvmm.sys, который используется в LiveCloudKd (ссылка).

С помощью WinDBG и плагина PyKD можно пропарсить эту структуру и вытащить значимые элементы.

Как видно, в драйвере winhvr.sys в массиве WinHvpPartitionArray EXO-объект не регистрируется (присутствует только один Prtn-объект), то есть он не полноценный объект раздела. EXO-объект — это адрес переменной vid.sys!VidExoDeviceContext.

Prtn-объект, создаваемый для EXO-разделов, не содержит имени раздела. Например, для full VM Prtn-объект содержит имя виртуальной машины, для контейнеров постоянное имя — Virtual machine. Тем не менее GUID раздела в объекте EXO-раздела присутствует.

Количество EXO-функций в драйвере vid.sys не так велико.

В ключе HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Vid\Parameters существует два параметра:

Если значение обоих параметров равно 0, то ничего не происходит, но, как только один из них меняется, сразу срабатывает функция Vid.sys!VidExopRegKeyNotificationHandler (перехват изменений в указанном ключе реестра регистрируется через nt!ZwNotifyChangeKey на раннем этапе загрузке драйвера vid.sys).

Если хотя бы одна из переменных равна 1, то выполняется функция VidExopDeviceSetupInternal, которая создает объект-устройство \Device\VidExo, символическую ссылку \DosDevices\VidExo и регистрирует функции-обработчики:

Также она зарегистрирует отдельные обработчики для быстрого ввода-вывода (fast I/O):

Функция завершается вызовом

Vid.sys!VidExopIoControlPreProcess — функция, которая используется для обработки IOCTL-запросов, направляемых объекту \Device\VidExo. Из нее вызывается функция vid.sys!VidIoControlPreProcess, в качестве первого параметра которой передается структура VM_PROCESS_CONTEXT. Если VM_PROCESS_CONTEXT содержит сигнатуру Prtn, то будет выполнена vid.sys!VidExoIoControlPartition, если Exo, то VidExoIoControlDriver (последняя сводится к выполнению вызова winhvr!WinHvGetSystemInformation с определенными параметрами; впрочем, я не видел, чтобы эта функция выполнялась, ведь EXO-раздел — это не полноценный раздел). Соответственно, даже в случае WHVP API почти вся работа ведется с Prtn-объектом.

Из функции vid.sys!VidExoIoControlPartition могут быть вызваны:

Из vid.sys!VidIoControlPartition может вызываться ограниченный набор запросов IOCTL.

Он соответствует ограниченному набору функций, предоставляемых WHVP API. При выполнении запрещенного запроса будет возвращен код ошибки C0000002h.

Как видно, функции чтения и записи памяти недоступны через API, поэтому доступ к ней штатными средствами невозможен. Необходимо углубиться во внутренности драйвера vid.sys и рассмотреть структуру создаваемых блоков памяти.

В целом организация памяти объектов, управляемых Hyper-V, выглядит следующим образом.

Если кратко, то для каждой виртуальной машины создается объект VM_PROCESS_CONTEXT. Память виртуальной машины описывается структурами MEMORY_BLOCK и GPAR_BLOCK.

Для обычных виртуальных машин, созданных через Hyper-V Manager, в структуре MEMORY_BLOCK находится указатель на массив guest OS GPA array, который сопоставляет SPA и GPA. Каждый MEMORY_BLOCK описывает свой диапазон GPA. Найдя определенный блок и получив GPA, можно выполнить функции nt!IoAllocateMdl, nt!MmMapLockedPagesSpecifyCache и прочитать данные из памяти гостевой ОС или записать их туда.

При работе с контейнерами создается отдельный kernel mode процесс vmmem (minimal process). При этом в объекте VM_PROCESS_CONTEXT содержится ссылка на массив GPAR-объектов, в которых находятся GPA и смещения блоков в процессе vmmem. То есть отображение (mapping) памяти уже выполнено, и для чтения/записи нужно найти объект, описывающий необходимый GPA, определить смещение соответствующего блока памяти в адресном пространстве vmmem и прочитать его либо записать в данные, например с помощью встроенной в ядро Windows функции MmCopyVirtualMemory.

EXO-разделы имеют другую организацию памяти.

Блоки памяти сопоставляются через вызов vid.sys!VsmmExoGpaRangeIoctlMap, из которой вызывается vid.sys!VsmmVaGpaCoreMapGpaRange.

Нас в первую очередь интересует vid.sys!VsmmVaGpaCorepFindRange, которая вызывается из vid.sys!VsmmVaGpaCorepCreateGpaToVaMappings и дает указатели на две функции.

VsmmVaGpaCorepGpnCompareFunctionByPage

VsmmVaGpaCorepVpnCompareFunctionByPage

VsmmVaGpaCorepGpaRangeAllocate — выделяет пул размером 0x70h байт.

Видим следующие куски кода:

Получается, что в Prtn-объекте по смещению 0x57A0 и 0x57B0 содержатся структуры, которые передаются первым параметром функции nt!RtlRbRemoveNode(_In_ PRTL_RB_TREE Tree, _In_ PRTL_BALANCED_NODE Node).

Определение RtlRbRemoveNode:

Получается, мы имеем дело с красно-черными деревьями. В теорию углубляться не буду, просто посмотрим, каким образом это реализовано у Microsoft.

Два дерева VPN (вероятно, virtual page number) и GPN (guest page number), адреса вершин которых расположены по смещениям 0x57A0h и 0x57B0h от начала Prtn-структуры (для 20H1) соответственно.

Рассмотрим каждую структуру в отдельности. В GPN-дереве есть узлы и листья, включающие, помимо ссылок на другие элементы дерева (заголовки), полезную нагрузку — адреса guest page number и ссылку на VPN-узел, содержащий стартовый и конечный адрес соответствующего блока памяти в процессе, обслуживающем виртуальную машину:

Работать будем с GPN-деревом. Заголовок выглядит примерно так (можно посмотреть, какой узел черный, какой красный):

Нас в первую очередь интересует полезная нагрузка, содержащаяся в теле листа дерева:

Для процесса QEMU можно увидеть, что базовый адрес региона памяти совпадает с началом VPN-блока:

Чем-то эта организация памяти напоминает обычное VAD-дерево, которое описывает адресное пространство процесса, построенное на основе AVL-деревьев. Также присутствуют минимальное и максимальное значение диапазона блока памяти:

Таким образом, для чтения и записи в виртуальном адресном пространстве гостевой ОС, запущенной в QEMU в режиме ускорения WHPX, сперва необходимо сделать следующее.

  1. Транслировать виртуальный адрес в физический с помощью vid.dll!VidTranslateGvatoGpa.
  2. Найти необходимый GPN-лист или узел в дереве, сравнивая начальный и конечный номера страниц с полученным физическим адресом.
  3. Затем получить VPN-элемент и узнать смещение блока памяти в адресном пространстве процесса qemu-system-x86_64.exe или vmware-vmx.exe.
  4. Прочитать соответствующий блок памяти или выполнить запись (в зависимости от операции).

Вариант 2 (теоретический, не требует kernel mode операций, но не проверялся).

  1. Транслировать адрес с помощью WHvTranslateGva из набора Windows HV Platform API.
  2. Просканировать адресное пространство процесса qemu-system-x86_64.exe или vmware-vmx.exe, найти блок, совпадающий размером с оперативной памятью (надеяться, что он будет один и без фрагментации).
  3. Считать физический адрес смещением в блоке памяти процесса.
  4. Выполнить считывание или запись и надеяться, что повезет).

При запуске QEMU с параметрами

в WinDbg можно снять трассу с помощью команды

Скриптом мы можем посмотреть элементы деревьев.

Для QEMU результат будет таким.

Размер блоков в гостевой ОС и размер блоков, описываемых VPN- и GPN-деревьями, примерно совпадает, но вполне может и отличаться, то есть взаимно однозначного соответствия между размером этих блоков нет.

Для VirtualBox 6.1.8.

Несмотря на то что разработчики VirtualBox используют CreatePartition, отображение памяти c помощью winhvr!WinHvMapGpaPagesSpecial они не делают. Значительная часть эмуляции в VirtualBox выполняется в режиме ядра, а производительность user mode WHPX недостаточна для нормального функционирования подсистемы виртуализации. За развитием темы поддержки Hyper-V можно наблюдать на официальном форуме VirtualBox

Основная подсистема, работающая с Hyper-V, описана в этом файле.

Пример использования API можно увидеть в приложении-трассировщике Simpleator.

После добавления этого алгоритма в LiveCloudKd появилась возможность читать память всех разделов, созданных с помощью WHVP API. Операция записи реализуется тем же способом, только с одним отличием — данные копируются в адресное пространство процесса vmmem по найденному смещению.

Организация памяти для Windows 10 1803 подобна модели, сделанной для контейнеров Windows Defender Application Guard / Windows Sandbox или контейнеров Docker, запущенных в режиме изоляции Hyper-V.

Google Android emulator (тот же QEMU).

Выводы

В целом можно сказать, что часть эмуляторов успешно работают на WHVP API (QEMU, Android emulator), а часть так и не смогла перейти на них полноценно (VirtualBox, VMware). Microsoft явно не стремится упрощать жизнь конкурентным продуктам, хотя прямой выгоды для них в этом не прослеживается. Производительность виртуальных операционных систем, работающих с этими API, также пока вызывает вопросы.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий