За последние несколько лет в 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 используются:
- в QEMU;
- VirtualBox;
- VMware Workstation, начиная с версии 15.5 (а также Preview версии 20H2);
- Android emulator, созданном Google;
- Applepie — A Hypervisor For Fuzzing Built With WHVP And Bochs.
Можно сказать, что пока 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 существует два параметра:
1 2 |
ExoDeviceEnabled ExoDeviceEnabledClient |
Если значение обоих параметров равно 0, то ничего не происходит, но, как только один из них меняется, сразу срабатывает функция Vid.sys!VidExopRegKeyNotificationHandler (перехват изменений в указанном ключе реестра регистрируется через nt!ZwNotifyChangeKey на раннем этапе загрузке драйвера vid.sys).
Если хотя бы одна из переменных равна 1, то выполняется функция VidExopDeviceSetupInternal, которая создает объект-устройство \Device\VidExo, символическую ссылку \DosDevices\VidExo и регистрирует функции-обработчики:
1 2 3 4 |
VidExopFileCreate VidExopFileClose VidExopFileCleanup VidExopIoControlPreProcess |
Также она зарегистрирует отдельные обработчики для быстрого ввода-вывода (fast I/O):
1 2 |
VidExoFastIoControlPartition VidExoFastIoControlDriver |
Функция завершается вызовом
1 |
VidObjectHeaderInitialize(VidExoDeviceContext, ' oxE') |
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 могут быть вызваны:
1 2 3 4 5 6 7 8 9 10 |
VidIoControlPartition WinHvInstallIntercept WinHvSetLocalInterruptControllerState WinHvGetLocalInterruptControllerState VsmmExoGpaRangeIoctlAccessTrackingControl VsmmExoGpaRangeIoctlUnmap VsmmExoGpaRangeIoctlMap |
Из 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
1 2 |
cmp rax, [rdx+40h] ; верхняя граница диапазона GPA cmp rax, [rdx+38h] ; нижняя граница диапазона GPA |
VsmmVaGpaCorepVpnCompareFunctionByPage
1 2 |
cmp rax, [rdx+20h] ; верхняя граница диапазона памяти процесса vmmem cmp rax, [rdx+18h] ; нижняя граница диапазона памяти процесса vmmem |
VsmmVaGpaCorepGpaRangeAllocate — выделяет пул размером 0x70h байт.
Видим следующие куски кода:
1 2 3 4 5 6 |
lea rcx, [r13+57A0h] mov rdx, rdi call cs:__imp_RtlRbRemoveNode lea rcx, [r13+57B0h] call cs:__imp_RtlRbRemoveNode |
Получается, что в Prtn-объекте по смещению 0x57A0 и 0x57B0 содержатся структуры, которые передаются первым параметром функции nt!RtlRbRemoveNode(_In_ PRTL_RB_TREE Tree, _In_ PRTL_BALANCED_NODE Node).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
3: kd> dt -r1 nt!_RTL_RB_TREE +0x000 Root : Ptr64 _RTL_BALANCED_NODE +0x000 Children : [2] Ptr64 _RTL_BALANCED_NODE +0x000 Left : Ptr64 _RTL_BALANCED_NODE +0x008 Right : Ptr64 _RTL_BALANCED_NODE +0x010 Red : Pos 0, 1 Bit +0x010 Balance : Pos 0, 2 Bits +0x010 ParentValue : Uint8B +0x008 Encoded : Pos 0, 1 Bit +0x008 Min : Ptr64 _RTL_BALANCED_NODE +0x000 Children : [2] Ptr64 _RTL_BALANCED_NODE +0x000 Left : Ptr64 _RTL_BALANCED_NODE +0x008 Right : Ptr64 _RTL_BALANCED_NODE +0x010 Red : Pos 0, 1 Bit +0x010 Balance : Pos 0, 2 Bits +0x010 ParentValue : Uint8B |
Получается, мы имеем дело с красно-черными деревьями. В теорию углубляться не буду, просто посмотрим, каким образом это реализовано у Microsoft.
Два дерева VPN (вероятно, virtual page number) и GPN (guest page number), адреса вершин которых расположены по смещениям 0x57A0h и 0x57B0h от начала Prtn-структуры (для 20H1) соответственно.
1 2 3 4 5 6 7 |
2: kd> dps 0xffffd88344414000+0x57a0 ffffd883`444197a0 ffffd883`44dc9b70 — VPN-дерево (адрес _RTL_RB_TREE) ffffd883`444197a8 ffffd883`44dcb060 — VPN-дерево (адрес корня _RTL_BALANCED_NODE) ffffd883`444197b0 ffffd883`443b2890 — GPN-дерево (адрес _RTL_RB_TREE) ffffd883`444197b8 ffffd883`48d75890 — GPN-дерево (адрес корня _RTL_BALANCED_NODE) ffffd883`444197c0 00000000`00000000 ffffd883`444197c8 00000000`00000000 |
Рассмотрим каждую структуру в отдельности. В GPN-дереве есть узлы и листья, включающие, помимо ссылок на другие элементы дерева (заголовки), полезную нагрузку — адреса guest page number и ссылку на VPN-узел, содержащий стартовый и конечный адрес соответствующего блока памяти в процессе, обслуживающем виртуальную машину:
1 2 3 4 5 6 7 8 9 10 11 |
2: kd> dt _RTL_RB_TREE ffffd883`444197a0 — VPN-дерево nt!_RTL_RB_TREE +0x000 Root : 0xffffd883`44dc9b70 _RTL_BALANCED_NODE +0x008 Encoded : 0y0 +0x008 Min : 0xffffd883`44dcb060 _RTL_BALANCED_NODE 2: kd> dt _RTL_RB_TREE ffffd883`444197b0 — GPN-дерево nt!_RTL_RB_TREE +0x000 Root : 0xffffd883`443b2890 _RTL_BALANCED_NODE +0x008 Encoded : 0y0 +0x008 Min : 0xffffd883`48d75890 _RTL_BALANCED_NODE |
Работать будем с GPN-деревом. Заголовок выглядит примерно так (можно посмотреть, какой узел черный, какой красный):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
2: kd> dx -id 0,0,ffffd8833e087040 -r1 ((ntkrnlmp!_RTL_BALANCED_NODE *)0xffffd883443b2890) ((ntkrnlmp!_RTL_BALANCED_NODE *)0xffffd883443b2890) : 0xffffd883443b2890 [Type: _RTL_BALANCED_NODE *] [+0x000] Children [Type: _RTL_BALANCED_NODE * [2]] [+0x000] Left : 0xffffd883443a8e10 [Type: _RTL_BALANCED_NODE *] [+0x008] Right : 0xffffd88342f4a690 [Type: _RTL_BALANCED_NODE *] [+0x010 ( 0: 0)] Red : 0x0 [Type: unsigned char] [+0x010 ( 1: 0)] Balance : 0x0 [Type: unsigned char] [+0x010] ParentValue : 0x0 [Type: unsigned __int64] 2: kd> dx -id 0,0,ffffd8833e087040 -r1 ((ntkrnlmp!_RTL_BALANCED_NODE *)0xffffd883443a8e10) ((ntkrnlmp!_RTL_BALANCED_NODE *)0xffffd883443a8e10) : 0xffffd883443a8e10 [Type: _RTL_BALANCED_NODE *] [+0x000] Children [Type: _RTL_BALANCED_NODE * [2]] [+0x000] Left : 0xffffd88344398490 [Type: _RTL_BALANCED_NODE *] [+0x008] Right : 0xffffd883443ae090 [Type: _RTL_BALANCED_NODE *] [+0x010 ( 0: 0)] Red : 0x1 [Type: unsigned char] [+0x010 ( 1: 0)] Balance : 0x1 [Type: unsigned char] [+0x010] ParentValue : 0xffffd883443b2891 [Type: unsigned __int64] 2: kd> dx -id 0,0,ffffd8833e087040 -r1 ((ntkrnlmp!_RTL_BALANCED_NODE *)0xffffd88344398490) ((ntkrnlmp!_RTL_BALANCED_NODE *)0xffffd88344398490) : 0xffffd88344398490 [Type: _RTL_BALANCED_NODE *] [+0x000] Children [Type: _RTL_BALANCED_NODE * [2]] [+0x000] Left : 0xffffd88348d75890 [Type: _RTL_BALANCED_NODE *] [+0x008] Right : 0xffffd88344398510 [Type: _RTL_BALANCED_NODE *] [+0x010 ( 0: 0)] Red : 0x0 [Type: unsigned char] [+0x010 ( 1: 0)] Balance : 0x0 [Type: unsigned char] [+0x010] ParentValue : 0xffffd883443a8e10 [Type: unsigned __int64] |
Нас в первую очередь интересует полезная нагрузка, содержащаяся в теле листа дерева:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
2: kd> dps 0xffffd88346564610 ffffd883`46564610 00000000`00000000 ffffd883`46564618 00000000`00000000 ffffd883`46564620 ffffd883`46564910 ffffd883`46564628 ffffd883`4802e558 ffffd883`46564630 ffffd883`4802e558 ffffd883`46564638 fffffc0f`022a73a0 ffffd883`46564640 fffffc0f`022a73a0 ffffd883`46564648 00000000`0000000e — Start GPA ffffd883`46564650 00000000`0000009f — End GPA ffffd883`46564658 ffffd883`44414000 — Prtn object ffffd883`46564660 ffffd883`4802e530 — соответствующий элемент VPN-дерева ffffd883`46564668 00000000`00000040 ffffd883`46564670 00000000`00000003 ffffd883`46564678 00000000`00000000 2: kd> dps 0xffffd8834802e530 ffffd883`4802e530 ffffd883`4802caa0 ffffd883`4802e538 ffffd883`4802f250 ffffd883`4802e540 ffffd883`4802f7a0 ffffd883`4802e548 00000000`281af2ee — start virtual page address ffffd883`4802e550 00000000`281af37f — end virtual page address ffffd883`4802e558 ffffd883`46564628 ffffd883`4802e560 ffffd883`46564628 ffffd883`4802e568 00000000`00000001 |
Для процесса QEMU можно увидеть, что базовый адрес региона памяти совпадает с началом VPN-блока:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
2: kd> dps 0xffffad04c7872c10 ffffad04`c7872c10 00000000`00000000 ffffad04`c7872c18 ffffad04`cc1b7610 ffffad04`c7872c20 ffffad04`cc269610 ffffad04`c7872c28 ffffad04`c96a75b8 ffffad04`c7872c30 ffffad04`c96a75b8 ffffad04`c7872c38 ffffbf8d`0ef103a0 ffffad04`c7872c40 ffffbf8d`0ef103a0 ffffad04`c7872c48 00000000`00000000 — Start GPA ffffad04`c7872c50 00000000`0000009f — End GPA ffffad04`c7872c58 ffffad04`c8bd1000 ffffad04`c7872c60 ffffad04`c96a7590 — соответствующий элемент VPN-дерева ffffad04`c7872c68 00000000`00000040 ffffad04`c7872c70 00000000`00000003 ffffad04`c7872c78 00000000`00000000 2: kd> dps ffffad04`c96a7590 ffffad04`c96a7590 ffffad04`ccb86120 ffffad04`c96a7598 ffffad04`c6cd8af0 ffffad04`c96a75a0 ffffad04`c6cd94a0 ffffad04`c96a75a8 00000000`0007fff0 — Start virtual page address ffffad04`c96a75b0 00000000`0008008f — end virtual page address ffffad04`c96a75b8 ffffad04`c7872c28 ffffad04`c96a75c0 ffffad04`c7872c28 ffffad04`c96a75c8 00000000`00000001 ffffad04`c96a75d0 53646156`02050000 ffffad04`c96a75d8 00000000`00000000 ffffad04`c96a75e0 00000000`00000000 ffffad04`c96a75e8 00000000`00000000 |
Чем-то эта организация памяти напоминает обычное VAD-дерево, которое описывает адресное пространство процесса, построенное на основе AVL-деревьев. Также присутствуют минимальное и максимальное значение диапазона блока памяти:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
kd> dt ntkrnlmp!_MMVAD_SHORT +0x000 NextVad : Ptr64 _MMVAD_SHORT +0x008 ExtraCreateInfo : Ptr64 Void +0x000 VadNode : _RTL_BALANCED_NODE +0x018 StartingVpn : Uint4B +0x01c EndingVpn : Uint4B +0x020 StartingVpnHigh : UChar +0x021 EndingVpnHigh : UChar +0x022 CommitChargeHigh : UChar +0x023 SpareNT64VadUChar : UChar +0x024 ReferenceCount : Int4B +0x028 PushLock : _EX_PUSH_LOCK +0x030 u : <anonymous-tag> +0x034 u1 : <anonymous-tag> +0x038 EventList : Ptr64 _MI_VAD_EVENT_BLOCK kd> dt ntkrnlmp!_MMVAD +0x000 Core : _MMVAD_SHORT +0x040 u2 : <anonymous-tag> +0x048 Subsection : Ptr64 _SUBSECTION +0x050 FirstPrototypePte : Ptr64 _MMPTE +0x058 LastContiguousPte : Ptr64 _MMPTE +0x060 ViewLinks : _LIST_ENTRY +0x070 VadsProcess : Ptr64 _EPROCESS +0x078 u4 : <anonymous-tag> +0x080 FileObject : Ptr64 _FILE_OBJECT kd> dt ntkrnlmp!_MI_VAD_SEQUENTIAL_INFO +0x000 Length : Pos 0, 12 Bits +0x000 Vpn : Pos 12, 52 Bits |
Таким образом, для чтения и записи в виртуальном адресном пространстве гостевой ОС, запущенной в QEMU в режиме ускорения WHPX, сперва необходимо сделать следующее.
- Транслировать виртуальный адрес в физический с помощью vid.dll!VidTranslateGvatoGpa.
- Найти необходимый GPN-лист или узел в дереве, сравнивая начальный и конечный номера страниц с полученным физическим адресом.
- Затем получить VPN-элемент и узнать смещение блока памяти в адресном пространстве процесса qemu-system-x86_64.exe или vmware-vmx.exe.
- Прочитать соответствующий блок памяти или выполнить запись (в зависимости от операции).
Вариант 2 (теоретический, не требует kernel mode операций, но не проверялся).
- Транслировать адрес с помощью WHvTranslateGva из набора Windows HV Platform API.
- Просканировать адресное пространство процесса qemu-system-x86_64.exe или vmware-vmx.exe, найти блок, совпадающий размером с оперативной памятью (надеяться, что он будет один и без фрагментации).
- Считать физический адрес смещением в блоке памяти процесса.
- Выполнить считывание или запись и надеяться, что повезет).
При запуске QEMU с параметрами
1 |
qemu-system-x86_64.exe -m 3072M -smp 1 -drive file= Win1020H1.gcow2, index=0, media=disk, cache=writeback -accel whpx |
в WinDbg можно снять трассу с помощью команды
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
bp winhvr!WinHvMapGpaPagesSpecial "r rcx, rdx,r8,r9;g" 2: kd> g rcx=000000000000000c rdx=0000000000000000 r8=00000000000c0000 r9=0000000000080400 rcx=000000000000000c rdx=00000000000fffc0 r8=0000000000000040 r9=0000000000080400 rcx=000000000000000c rdx=0000000000000000 r8=00000000000000c0 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000c0 r8=0000000000000020 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000e0 r8=0000000000000020 r9=0000000000080400 rcx=000000000000000c rdx=0000000000000100 r8=00000000000bff00 r9=0000000000080400 rcx=000000000000000c rdx=0000000000000000 r8=00000000000000a0 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000c0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000d0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000e0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000f0 r8=00000000000bff10 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000c0 r8=00000000000bff40 r9=0000000000080400 rcx=000000000000000c rdx=00000000000fd000 r8=0000000000001000 r9=0000000000080400 rcx=000000000000000c rdx=00000000000febe0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000feb80 r8=0000000000000040 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000c0 r8=000000000000000b r9=0000000000080400 rcx=000000000000000c rdx=00000000000000cb r8=0000000000000003 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000ce r8=0000000000000002 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000d0 r8=0000000000000020 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000f0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=0000000000000100 r8=00000000000bff00 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000ce r8=000000000000001a r9=0000000000080400 rcx=000000000000000c rdx=00000000000000e8 r8=0000000000000008 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000fd000 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000fd010 r8=0000000000000ff0 r9=0000000000080400 rcx=000000000000000c rdx=00000000000fd000 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000fd010 r8=0000000000000ff0 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 rcx=000000000000000c rdx=00000000000000a0 r8=0000000000000010 r9=0000000000080400 |
Скриптом мы можем посмотреть элементы деревьев.
Для 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, также пока вызывает вопросы.