Внутреннее устройство памяти Microsoft Hyper-V

Внутреннее устройство памяти Microsoft Hyper-V

В статье были описаны методы доступа к памяти гостевых разделов Hyper-V, создаваемых в самых разных случаях

image


Память гостевых ОС.


Автор: Gerhart

Программное обеспечение, используемое в статье (операционные системы с патчами от августа 2019):

Windows 10, build 1903 x64

Windows Server 2019

Windows Server 2016

WinDBG Preview

Visual Studio 2019

Process Hacker

PyKd плагин для WinDBG

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

- WDAG – Windows Defender Application Guard.

- Full VM (виртуальная машина) – обычная полноценная виртуальная машина, созданная в Hyper-V manager. В отличие от контейнеров WDAG, Windows Sandbox, docker в режиме изоляции Hyper-V.

- Root ОС – операционная система, в которой установлена серверная часть Hyper-V.

- Гостевая ОС – операционная система, которая работает в контексте эмуляции Hyper-V, в т.ч. используя виртуальные устройства, предоставляемые гипервизором. В контексте статьи, это могут быть как Full VM, так и контейнеры.

- TLFS – документ Hypervisor Top-Level Functional Specification 5.0.

- GPA (guest physical address) – физический адрес памяти гостевой операционной системы.

- SPA (system physical address) – физический адрес памяти root ОС.

- Гипервызов (hypercall) – сервис гипервизора, вызываемый посредством выполнения команды vmcall с указанием номера гипервызова.

Исходники драйвера доступны на github.com:

https://github.com/gerhart01/LiveCloudKd/tree/master/hvmm

Python-скрипт для вывода информации о GPAR и MBlock-объектах

https://github.com/gerhart01/Hyper-V-Internals/blob/master/ParsePrtnStructure.py

Введение

Технологии виртуализации компании Microsoft давно и прочно вошли в нашу жизнь как в серверном сегменте, так и в клиентских ОС. Они используются не только для запуска гостевых ОС, но и для работы защитных механизмов, таких как Virtualization Based Security (VBS), Credential Guard, Device Guard, Hypervisor Code integrity (HVCI).

Компонент Hyper-V впервые появился в Windows Server 2008 и предоставлял достаточно простые на тот момент возможности по созданию гостевых операционных систем. Но Microsoft активно развивает эту технологию, и в настоящее время она глубоко интегрирована в ядро операционной системы Windows. Корневым компонентом является модуль hvix64.exe для процессоров Intel, hvax64.exe для процессоров Amd и hvaa64.exe для ARM.

Также хочется сказать отдельное спасибо компании Microsoft за то, что она опубликовала символы практически для всех основных компонентов Hyper-V (https://docs.microsoft.com/en-us/virtualization/community/team-blog/2018/20180425-hyper-v-symbols-fo.... Без символьной информации анализ работы подсистемы памяти был бы достаточно сложен.

Также описание архитектуры памяти Hyper-V (помимо TLFS) было сделано Andrea Allievi (www.andrea-allievi.com/files/Recon_2017_Montreal_HyperV_public.pptx) на конференции Recon 2017. Но слайды довольно абстрактно описывают модель реализации, и сопоставить эту информацию с реальным кодом, понять, что и как работает, очень сложно. Презентация была сделана до того, как Microsoft опубликовала символьную информацию компонентов виртуализации, так что, возможно, причина в этом.

В 2018 году Microsoft выпустила WDAG, которые представляет из себя надстройку к браузеру, запускающую браузер Microsoft Edge в контейнере Hyper-V со slim RDP-фронтендом, что создаёт ощущение, как будто вы работаете непосредственно в браузере. Подобные технологии очень давно использовала компания Citrix в своих терминальных решениях. У меня появилось желание понять, как же работает WDAG.

Проблема была в том, что было невозможно подключиться отладчиком WinDBG к контейнеру, поскольку запустить bcdedit внутри контейнера и тем более сохранить настройки отладки не представлялось возможным. Также не получалось запустить внутри какое-либо приложение типа Process Explorer для просмотра технической информации, поскольку запуск почти всех приложений внутри контейнера блокировался. Вместе с этим Microsoft сломали совместимость утилиты LiveKd из Sysinternals Suite (опцию -hv) в части подключения отладчика WinDBG (точнее, его консольной версии – kd.exe) к гостевым операционным системам через интерфейсы гипервизора. Честно говоря, это был тупик, но получилось так, что Matt Suiche (@msuiche) из Сomae поделился исходниками своей программы LiveCloudKd, за что ему огромное спасибо! Эта программа позволяла подключаться с помощью отладчика WinDBG напрямую к виртуальной машине через интерфейсы библиотеки vid.dll (Microsoft Hyper-V Virtualization Infrastructure Driver Library), представляя физическую память виртуальная сервера как дамп памяти. Проблема в том, что Microsoft заблокировала выполнения этих функций в последних версиях Hyper-V (в Windows 10 и Windows Server 2019), а WDAG работает только в Windows 10. Плюс утилиту нужно было адаптировать под Windows 10, т.к. некоторые используемые LiveCloudKd приёмы работать перестали. Исходники LiveClouKd Matt вскоре выложил на github (https://github.com/comaeio/LiveCloudKd).

Доработка этой утилиты помогла лучше понять, каким образом работают виртуальные машины и контейнеры Hyper-V, а также каким образом получить доступ к памяти гостевых ОС различными способами.

Сперва я планировал написать отдельную статью про контейнеры Hyper-V, но учитывая количество компонентов (см. скриншот), провести полное исследование слишком трудоёмко.

Сам гипервизор находится в изолированной области памяти, память root-ОС отображается 1 в 1, возможность чтения памяти средствами гипервизора с помощью гипервызова HvReadGpa отсутствует (отдельная блокировка в коде для раздела с идентификатором, равным 0, т.е. для root-ОС). Основные моменты работы подсистемы памяти описаны в TLFS.

Так что основной упор будет сделан на методы доступа к памяти гостевых ОС. Будут описаны механизмы работы памяти для обычных виртуальных машин Microsoft и контейнеров WDAG и Windows Sandbox. Docker будет упомянут кратко и только в контексте Hyper-V, т.к. это отдельная экосистема, которой и так посвящено огромное количество ресурсов.

В ходе исследования был создан драйвер hvmm.sys, который может читать содержимое памяти гостевой ОС напрямую из root-ОС минуя интерфейсы гипервизора и драйвера vid.sys. Драйвер hvmm.sys был интегрирован в проект LiveCloudKd.

В целом статья представляет материал в стиле Windows Internals и описывает то, как работает память ОС. Зачем? Чтобы узнать, как работает достаточно популярная технология Microsoft. Также может пригодиться специалистам, увлекающимся форензикой и разбирающим дампы памяти. Andrea Allievi готовит детальное описание актуальной версии Hyper-V для 2-й части 7-й версии книги Windows Internals, но пока книга не была издана, можно будет почитать эту статью и кратко ознакомиться с архитектурой памяти.

К сожалению, информация о структурах в символах для vid.sys отсутствует, поэтому название таких структур в статье выполнено произвольно исходя из сигнатур, которые в них присутствуют. Andrea Allievi упоминал “bucket” структуры в своей презентации, но как конкретно они реализованы в драйвере vid.sys – неизвестно. Если в следующей части Windows Internals будет детальное описание этих структур, то наименования будут исправлены, технические детали работы от этого не изменятся.

Работа с памятью Full VM и контейнеров посредством прямого доступа

Основным процессом, который управляет работой виртуальной машины, является vmwp.exe. Его запускает vmms.exe в случае запуска полноценной виртуальной машины, или vmcompute.exe в случае запуска контейнеров. При запуске процесс vmwp.exe через интерфейс vid.dll обращается к интерфейсам гипервизора – гипервызовам (hypercalls). Я собрал статистику гипервызовов для VM Windows Server 2019, контейнера Docker в режиме изоляции Hyper-V (образ nanoserver:1809) и контейнера WDAG. WDAG-контейнер генерирует слишком много гипервызовов, поэтому из-за торможения, вызванным записью результатов отладчиком, контейнер сразу начал выключаться после включения (управляющее приложение контролирует таймауты выполнения некоторых процедур), в связи с чем результаты по WDAG содержат общий показатель (надеюсь попробовать dtrace, относительно недавно доработанный под Windows, для сбора подобной статистики – по идее, он должен снизить издержки на запись собранных данных). Отдельно зафиксирован показатель по выключению, так что порядок оценить можно. По сравнению с обычными виртуальными машинами он достаточно большой:

Какие категории гипервызовов можно выделить? Создание раздела, установка его свойств, создание, виртуальных процессоров, виртуальных портов (используются для отправки сигналов, сообщений), установка перехватов (interceptions), и различные гипервызовы для работы с памятью.

Функция winhvr!WinHvMapGpaPagesFromMbpArrayScanLargePages. В Rdx указывается номер страницы, в rsi – размер (так же в страницах).

При запуске Windows Server 2019 с 1500 Mb оперативной памяти получаем.

rdx=0000000000000000 rsi=000000000005dc00

rdx=00000000000f8000 rsi=0000000000000800

rdx=0000000000fff800 rsi=0000000000000800

При запуске Windows Server 2019 с 2300 Mb оперативной памяти получаем

1-й вызов: rdx=0000000000000000 rsi=000000000008fc00

2-й вызов: rdx=00000000000f8000 rsi=0000000000000800

3-й вызов: rdx=0000000000fff800 rsi=000000000000024a

Стек вызовов:

1-й вызов

2 и 3-й вызовы

00 winhvr!WinHvMapGpaPagesFromMbpArrayScanLargePages

01 Vid!VsmmHvpMapGpasFromMbpArray

02 Vid!VsmmHvpMapGpasFromMemoryBlockRange

03 Vid!VsmmHvMapGpasFromMemoryBlock

04 Vid!VsmmAdjustGpaSpaceForMemoryBlockRange

05 Vid!VsmmCreateMemoryBlockGpaRange

06 Vid!VidIoControlPartition

07 Vid!VidIoControlDispatch

08 Vid!VidIoControlPreProcess

…………………WDF Calls………………………………..

0d nt!IofCallDriver

0e nt!IopSynchronousServiceTail

0f nt!IopXxxControlFile

10 nt!NtDeviceIoControlFile

11 nt!KiSystemServiceCopyEnd

12 ntdll!NtDeviceIoControlFile

13 vid_7ffb4de20000!VidCreateMemoryBlockGpaRange

14 vmwp!GpaRangeMbBacked::Initialize

15 vmwp!MemoryManager::CreateGpaRangeInternal

16 vmwp!MemoryManager::CreateMemoryBlock

17 vmwp!MemoryManager::CreateRamMemoryBlocks

18 vmwp!MemoryManager::CreateRam

19 vmwp!VirtualMachine::ConstructGuestRam

1a vmwp!WorkerTaskStarting::RunCleanStartSteps

1b vmwp!WorkerTaskStarting::RunTask

1c vmwp!WorkerAsyncTask<VmPerf::Vmwp::StartingTask>::Execute

1d vmwp!VirtualMachine::DoStateChangeTask

1e vmwp!VirtualMachine::StartInternal

# Call Site

00 winhvr!WinHvMapGpaPagesFromMbpArrayScanLargePages

01 Vid!VsmmHvpMapGpasFromMbpArray

02 Vid!VsmmHvpMapGpasFromMemoryBlockRange

03 Vid!VsmmHvMapGpasFromMemoryBlock

04 Vid!VsmmAdjustGpaSpaceForMemoryBlockRange

05 Vid!VsmmCreateMemoryBlockGpaRange

06 Vid!VidIoControlPartition

07 Vid!VidIoControlDispatch

08 Vid!VidIoControlPreProcess

.............WDF Calls............

0d nt!IofCallDriver

0e nt!IopSynchronousServiceTail

0f nt!IopXxxControlFile

10 nt!NtDeviceIoControlFile

11 nt!KiSystemServiceCopyEnd

12 ntdll!NtDeviceIoControlFile

13 vid_7ffb4de20000!VidCreateMemoryBlockGpaRange

14 vmwp!MemoryManager::CreateMemoryBlockGpaRange

15 vmwp!VmbComGpaRange::VmbComGpaRange

16 vmwp!Vml::VmComMultiInstanceObject<VmbComGpaRange>::CreateInstance

17 vmwp!Vml::CreateComObject<VmbComGpaRange,IMemoryManager

18 vmwp!VmbComMemoryBlock::CreateGpaRange

19 vmuidevices!VideoSynthDevice::SetupVramGpaRange

1a vmuidevices!VideoSynthDevice::SynthVidOnVramLocation

1b vmuidevices!VideoSynthDevice::OnMessageReceived

1c vmuidevices!VMBusPipeIO::OnReadCompletion

1d vmuidevices!VMBusPipeIO::ProcessCompletionList

1e vmuidevices!VMBusPipeIO::HandleCompletions

1f vmuidevices!VMBusPipeIO::OnCompletion

Последний блок памяти – это отображённая (mapping) память видеоадаптера. Блок размером в 1 страницу, используется для устройства ACPI.

 

Драйвер hvmm.sys был написан в связи с тем, что Microsoft в Windows 10 и Windows Server 2019 заблокировала возможность вызова функций из vid.dll из каких-либо модулей, кроме процесса vmwp.exe, создавшего дескриптор раздела. Проверка идёт в драйвере vid.sys на уровне объекта _EPROCESS, который сохраняется в Prtn-структуре (кстати, есть второй тип разделов – это EXO-разделы. Они создаются при использовании WinHv Platform API Library, которые позволяют сторонним разработчикам сделать свои решения по виртуализации совместимыми с Hyper-V и позволяют запускать их одновременно. В настоящее время поддержка была у VirtualBox, Qemu (возможно, Bochs), и VMware спустя год после появления этих API в Windows 1803 наконец-то добавила поддержку в свой продукт VMware Workstation. Вероятно, новую сборку VMware выложат после выхода Windows 10, build 1908).

В Windows Server 2016 и более ранних версиях ОС, тем не менее, всё ещё возможно использовать интерфейс vid.dll без драйвера. Блокировка выполнения API там отсутствует. Но контейнеры WDAG и Windows Sandbox присутствуют только в Windows 10, где блокировка есть по умолчанию. Да, есть возможность внедрить библиотеку в процесс vmwp.exe, но об этом я задумался только после того, как написал бОльшую часть драйвера ) Но в любом случае была цель разобрать механизмы доступа к памяти гостевых ОС.

Эта возможность в дальнейшем реализована в виде отдельной библиотеке vidaux.dll, которая внедряется в процесс vmwp.exe и обменивается данными с LiveCloudKd посредством именованных каналов.

Какие же структуры понадобятся для работы с памятью Hyper-V? Я постарался наглядно изобразить их на схеме. В дальнейшем во время чтения статьи должно стать понятно, каким образом они связаны и как используются.

Объекты:

- дескриптор раздела (partition object);

- GPAR-дескриптор (GPAR - Guest physical Address Range);

- массив GPAR-элементов (GPAR Array) ;

- массив MBlock-элементов (MBlock Array. MBlock – memory block GPA Range);

- GPAR-объект (GPAR_OBJECT);

- MBlock-объект (MBLOCK_OBJECT).

Основной объект, с которым производится работа – это дескриптор раздела. Когда создаётся usermode дескриптор раздела, его kernelmode часть содержит всю необходимую информацию о созданном разделе. Алгоритм поиска usermode составляющей не изменился со времён Windows Server 2008 R2, и эта составляющая может быть получена перебором дескрипторов, открытых процессом vmwp.exe. Для этого нужно найти все открытые дескрипторы типа File с именами \\Device\\000000 и попробовать получить имя раздела.

Если имя удаётся получить, то это значит, что мы нашли действующий дескриптор раздела. На моей практике находится 3 подобных объекта для каждой виртуальной машины или контейнера. Если передать полученные значения ядерной функции nt!ObReferenceObjectByHandle, то в двух случаях она возвращает NULL, т.е. объекты невалидные. Для актуального дескриптора мы получаем нужный нам указатель на дескриптора раздела.

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

В полученном дескрипторе раздела есть поля, которые указывают на массив MBlock-элементов (инициализируется в vid.sys!VsmmMemoryBlockpInitialize) и массив GPAR-элементов (инициализация в vid.sys!VsmmGpaRangepInitialize).

Кстати, не нужно путать дескриптор раздела виртуальной машины и обычный раздел памяти Windows 10, который выводит команда !partition WinDBG. Это структура _MI_PARTITION, в которой находится основная информация о текущем состоянии памяти операционной системы. Объект, описываемый этой структурой, создаётся и без активного гипервизора.

Подробнее о ней можно прочитать опять-таки в книге Windows Internals (7th издание). В бывшем MSDN (нынешнем Microsoft Docs) информацию найти не получилось.

У контейнеров и Full VM методы доступа к памяти отличаются, поэтому рассмотрим примеры чтения памяти для обоих видов. Начнём с Full VM на базе Windows Server 2019. Прямой доступ к памяти реализован в драйвере hvmm.sys, работу которого и рассмотрим в следующем разделе.

Чтение памяти Full VM

Для чтения данных приложение LiveCloudKd передаёт запрос драйверу. Необходимые для запроса данные пакуются в структуру GPA_INFO. Эта структура содержит адрес памяти для чтения, количество байтов для чтения и служебную информацию о разделе виртуальной машины (PID vmwp, partition id).

Сперва получим дескриптор раздела. Для этого достаточно вызвать функцию nt!ObReferenceObjectByHandle с переданным дескриптором.

Полученный объект будет иметь тип FILE_OBJECT. Для получения доступа к телу дескриптора, необходимо получить указать на FsContext.

Kernel-mode часть дескриптор раздела выглядит следующим образом:

Первые 90 байт содержат сигнатуру раздела, имя и его идентификатор. Размер самой структуры достаточно большой, для разных операционных систем он разный. Точный размер структуры можно найти в функции vid.sys!VidCreatePartition (по объёму выделяемой под него памяти). Нам он, в принципе, не понадобится.

Узнав вид раздела (VmType), мы можем выполнить одну из двух процедур чтения блоков. Возможных значений VmType на самом деле достаточно много, и более того, они отличаются для разных версий операционных систем. Например, VmType для Full VM в Windows 10 и Windows Server 2019 имеют разные значения. Не все из них исследованы (особенно для операционных систем типа Linux, т.к. LiveCloudKd с ними не работает). Но по доступу к памяти разделы виртуальных машин разделились на две группы: разделы контейнеров и разделы Full VM.

Функция hvmm.sys!VidGetFullVmMemoryBlock на входе получает дескриптор раздела, буфер, в который необходимо записать полученные данные, размер буфера в байтах и GPA виртуальной машины.

BOOLEAN VidGetFullVmMemoryBlock(PPARTITION_HANDLE pPartitionHandle, PCHAR pBuffer, ULONG len, ULONG64 GPA)

Стоит отметить, что GPA – это номер страницы, который получается просто

GPA = GpaInfo.StartAddress / PAGE_SIZE;

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

Далее потребуется найти GPAR-объект, который описывает запрошенный GPA. Каждый GPA входит в блок памяти, ранее выделенный гипервизором, в свою очередь этот блок памяти описывается GPAR-объектом. По смещениям 0x100 и 0x108 GPAR-объектов располагаются поля GpaIndexStart и GpaIndexEnd соответственно. По значению этих полей можно понять, описывает GPAR-объект нужный нам GPA или нет. Например:

Этот GPAR-объект отвечает за GPA в диапазоне от 0 до 0x8fbff.

У Full VM GPAR-объектов гораздо меньше, чем у контейнеров. Например, у Generation 2 Full VM - 3-4 дескриптора, у контейнеров порядка 780. Чем больше памяти у гостевой ОС, тем больше у неё блоков, выделяемых гипервызовами HvMapGpaPages* и, соответственно, тем больше число GPAR-объектов. Максимальный диапазон GPA, описываемых GPAR-объектом, который я встречал, включал в себя 0x96000 страниц гостевой ОС.

Вернёмся к нашему драйверу. Находим GPAR-объект с помощью функции hvmm.sys!VidGetGparObjectForGpa). Функции передаётся дескриптор раздела и GPA. Как она работает? Как было описано выше, у каждого дескриптора раздела есть указатель на дескриптор блоков GPA. Это некая структура, которая, помимо прочего, содержит указатель на сам дескриптор раздела, указатель на массив с указателями GPAR-объектов, и количество элементов в массиве GPAR-объектов (см. схему взаимосвязи структур выше).

Получив эту информацию, мы можем пробежаться в цикле по GPAR-объектам и найти тот объект, который отвечает за нужный нам GPA. Код достаточно прост, как видите. Это несколько упрощённая реализация того, как выполняет чтение памяти функциz VsmmLookupMemoryBlockByHandle драйвера vid.sys.

Также в драйвере vid.sys есть дополнительные процедуры чтения зашифрованной памяти - vid.sys!VsmmpSecureReadMemoryBlockPageRangeInternal. Используется AES XTS с помощью функций BCryptEncrypt\ BCryptEncrypt из драйвера ksecdd.sys. В каких случаях они используются выяснить не получилось, т.к. даже для Shielded VM с включённым TPM память используется обычная. Возможно, какие-то особые участки шифруются, но пока что найти их не удалось. Понятно, что гостевая ОС при чтении\записи уже выделенной области памяти работает напрямую, без вызовов каких-либо функций из vid.sys. Все исключения должен перехватывать и обрабатывать гипервизор. Соответственно, если root-ОС зашифрует какие-то участи памяти, то гостевая ОС не сможет прозрачно получить к ним доступ.

Вернёмся к коду. Обнаружив подходящий GPAR-объект, выходим из цикла.

Существуют GPAR-объекты, которые не описывают GPA, а вместо необходимых данных содержат указатель на некую usermode-структуру внутри процесса vmwp.exe. Они привязаны к памяти, выделенной для работы виртуальных Hyper-V устройств.

Такие объекты необходимо пропускать.

Какие же данные содержатся в GPAR-объекте и помогут прочитать данные из гостевой ОС? Это очередной тип данных – MBlock-объект. Он содержит данные о гостевых PFN и другую полезную информацию. Достаточно большая структура, в начале содержит сигнатуру «Mb »

Из все полей нам понадобится только указатель на массив GPA. Размер элемента массива - 16 байт. В одном содержатся GPA гостевой ОС, в другой части – SPA root-ОС.

Узнать SPA в root-ОС мы сможем по следующей формуле:

Для того, чтобы прочитать SPA, их нужно отобразить на виртуальное адресное пространство root-ОС. Для этого используем MDL структуру:

В конце каждой структуры MDL идёт массив PFN (Page Frame Number). Указатель на него можно получить с помощью макроса MmGetMdlPfnArray. Получив указатель, запишем полученный HostSPA индекс. Разумеется, за один раз можно поместить в MDL не один указатель на PFN, а гораздо больше. Но есть шанс попасть на границу GPAR-блоков, поэтому чтение производится постранично. Для Full VM это не слишком выгодно, поскольку размер каждого блока достаточно велик, но скорость всё равно получается хорошей.

Далее получаем виртуальный адрес c использованием функции MmMapLockedPagesSpecifyCache и используем его для копирования блока памяти гостевой ОС с помощью RtlCopyMemory. Всё, необходимый блок памяти гостевой ОС получен. Соответственно, чтение выполняется в цикле. Каждая страница памяти копируется на своей итерации. При работе рекомендуется ставить виртуальную машину на паузу, чтобы избежать модификации памяти во время чтения. В LiveCloudKdSdk для этого реализована функция SdkControlVmState, она приостанавливает исполнение виртуальной машины либо обычными powershell-командлетами Suspen-VM\Resume-VM, либо работает со специальным регистром каждого виртуального процессора посредством гипервызова HvWriteVpRegister и установки регистра HvRegisterExplicitSuspend в 0 (Resume) или 1 (Suspend).

Чтение памяти контейнера Hyper-V

Рассмотрим чтение памяти контейнера на примере Windows Defender Application Guard (чтобы его использовать достаточно поставить одноимённый компонент в Windows 10. Он присутствует, начиная с 1803 сборки)

Эту операцию выполняет следующая функция драйвера hvmm.sys:

BOOLEAN VidGetContainerMemoryBlock(PPARTITION_HANDLE pPartitionHandle, PCHAR pBuffer, ULONG len, ULONG64 GPA)

До её выполнения так же, как и для Full VM, сперва необходимо получить дескриптор раздела. Затем дополнительно понадобится дескриптор процесса vmmem. Этот процесс работает только в режиме ядра и создается при работе контейнеров. Примерно так выглядит набор потоков vmmem при запуске на 4-х процессорном сервере.

Дескриптор процесса vmmem присутствует всё в том же дескриптора раздела по сигнатуре ‘scrP’ (см. процедуру hvmm!VidFindVmmemHandle).

Получаем указатель на GPAR по аналогии с чтением памяти у Full VM. Далее идут различия – используются другие поля структуры GPAR для чтения блоков памяти. VmmMemGpaOffset – основное смещение, с помощью которого можно преобразовать GPA в SPA для конкретного блока. В операциях используется дополнительное смещение в дескрипторе раздела – SomeGpaOffset, но во время моих экспериментов оно всегда было равно 0.

Дальше вычисляем Source Address по следующей формуле и копируем нужный блок данных прямо из адресного пространства процесса vmmem.

Т.е. ключевое отличие чтение памяти контейнера от чтения памяти Full VM – это необходимость копирования данных из виртуальной памяти процесса vmmem. Выполнять отображение памяти с использованием MDL необходимости нет.

Встроенные возможности работы с памятью Hyper-V

Прямой доступ к памяти, минуя все возможные API – это интересно, но более надёжным методом является использование API, предоставляемых Microsoft. Но за надёжность придётся платить ограничениями, наложенными Microsoft на эти API. В частности, для гипервызовы работают только с Full VM и для контейнеров всегда возвращают FALSE, плюс читают\пишут не более 0x10 байт за один раз. API функции vid.dll вообще запрещено вызывать из какого-либо модуля, кроме управляющего процесса vmwp.exe.

В библиотеке vid.dll присутствуют следующие функции для чтения\записи памяти:

- VidTranslateGvaToGpa

- VidReadMemoryBlockPageRange (обёртка над vid.sys!VidReadWriteMemoryBlockPageRange)

- VidWriteMemoryBlockPageRange (обёртка над vid.sys!VidReadWriteMemoryBlockPageRange)

и гипервызовы (необходимо выполнять из режима ядра):

- HvTranslateVirtualAddress

- HvWriteGPA

- HvReadGPA

Рассмотрим их.

Использование гипервызовов для чтения\записи памяти

Использование HvReadGpa достаточно просто, если не учитывать, что блок памяти из 0x10 байт не должен попадать на границу страниц. В противном случае операцию чтения нужно разбивать, иначе остаток блока, который должен быть прочитан со второй страницы будет содержать нулевые байты. Разбиение на блоки реализовано в usermode части LiveCloudKdSdk. Для гипервызова присутствует обёртка в драйвере winhvr.sys. Можно вызывать и напрямую через vmcall, но тогда придётся дополнительно выполнять операции по подготовке параметров гипервызова.

Для записи с помощью winhvr.sys!WinHvWriteGPA проверка границ реализована внутри драйвера

Перед чтением виртуальных адресов выполняется дополнительная проверка с помощью winhvr.sys!WinHvTranslateVirtualAddress. Функция преобразует виртуальный адрес в физический, используя текущий контекст CPU, и, соответственно, регистр CR3.

Возможные варианты валидации (в LiveCloudKd используется только HV_TRANSLATE_GVA_VALIDATE_READ и HV_TRANSLATE_GVA_VALIDATE_WRITE).

#define HV_TRANSLATE_GVA_VALIDATE_READ (0x0001)

#define HV_TRANSLATE_GVA_VALIDATE_WRITE (0x0002)

#define HV_TRANSLATE_GVA_VALIDATE_EXECUTE (0x0004)

#define HV_TRANSLATE_GVA_PRIVILEGE_EXEMPT (0x0008)

#define HV_TRANSLATE_GVA_SET_PAGE_TABLE_BITS (0x0010)

#define HV_TRANSLATE_GVA_TLB_FLUSH_INHIBIT (0x0020)

#define HV_TRANSLATE_GVA_CONTROL_MASK (0x003F)

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

Работа функций vid.dll (Microsoft Hyper-V Virtualization Infrastructure Driver Library)

Отдельно стоит упомянут особенности работы функции vid.dll!VidReadMemoryBlockPageRange

VIDDLLAPI

BOOL

WINAPI

VidReadMemoryBlockPageRange(

__in PT_HANDLE Partition,

__in MB_HANDLE MemoryBlock,

__in MB_PAGE_INDEX StartMbp,

__in UINT64 MbpCount,

__out_bcount(BufferSize)

PVOID ClientBuffer,

__in UINT64 BufferSize

);

Параметр Partition – это usermode дескриптор раздела виртуальной машины;

ClientBuffer – указатель на блок памяти, в который будет записан результат;

BufferSize – соответственно, размер буфера;

Отдельный вопрос вызывают два параметра: MemoryBlock и StartMbp. MemoryBlock – это номер MBlock-объекта, из которого будут считываться данные. Если в Windows Server 2008 R2 туда записывался kernel-mode handle (да, usermode приложение содержало kernelmode адреса дескрипторов – на этой логике была построена оригинальная версия LiveCloudKd):

https://github.com/comaeio/LiveCloudKd/blob/07ac5901ff5cac5258033f1dd95cfc2bd0e06815/hvdd/memoryblock.c#L159 (В буфере, соответственно, содержимое памяти vmwp.exe)

StartMbp – это индекс, которую нужно прочитать. Этот индекс равен номеру страницы физической памяти. Т.е. нужно просто получить GPA и разделить его на PAGE_SIZE (0x1000). Размер страницы в данном случае виртуальный. Например, когда страница памяти, на которой размещается ntoskrnl.exe, это обычно LARGE_PAGE размером 2 Mb, но номера страниц всё равно будут гранулярны 4 Kb. Буфер, соответственно, можно указать меньше, тогда в него будет записано меньше данных. Всё понятно, за одним исключением одного – этот индекс указывается относительно начала блока памяти MB_HANDLE MemoryBlock. Например, для первого блока памяти номер индекса будет совпадать с номером страницы физической памяти. Если блоки размещены непрерывно, то индекс второго блока будет равен номеру страницы физической памяти минус размер первого блока. Индекс третьего блока будет равен номеру страницы физической памяти минус размер первого блока и минус размер второго блока. Вроде, всё понятно. Главная проблема в том, что блоки физической памяти не являются непрерывными. Более того, из пользовательского адресного пространства эти границы простым путём определить не получится. Microsoft не предоставляла такие API даже во времена Windows Server 2008 R2.

Matt в своё время использовал для этого отдельную функцию по поиску дескрипторов в памяти, но эту возможность прикрыли, заменив дескрипторы их индексами в таблице, размещённой в kernelmode, и поэтому я использовал уже задействованную функцию vid.dll!VidReadMemoryBlockPageRange.

Сперва мы можем получить номера HANDLE, выполнив простой перебор, считывая из каждого блока первую страницу памяти. Соответственно, если функция возвращает TRUE, то блок существует, если FALSE, то блок не существует. Исходя из практического опыта, я определил максимальный размер индекса равным числу 0x400. Как мы видели выше большое число индексов наблюдается только у контейнеров типа WDAG и Windows Sandbox, за счёт того, что каждый файл отображается в отдельный блок.

Сформировав массив с индексами, мы можем определить максимальный размер блока слегка модифицировав алгоритм двоичного поиска среднего элемента в массиве.

Учитывая, что блок памяти непрерывен, мы можем определить его границу, задав условие, что при чтении блока, последующий блок читаться не должен. Соответственно, сперва мы можем просканировать память и построить первоначальную карту её распределения. Но, как я уже писал выше, между блоками есть разрывы, и поэтому для уточнения распределения памяти нам придётся исследовать структуру _PHYSICAL_MEMORY_DESCRIPTOR гостевой ОС.

0: kd> dt poi(nt!MmPhysicalMemoryBlock) nt!_PHYSICAL_MEMORY_DESCRIPTOR

+0x000 NumberOfRuns : 7

+0x008 NumberOfPages : 0xbfee1

+0x010 Run : [1] _PHYSICAL_MEMORY_RUN

0: kd> dq poi(nt!MmPhysicalMemoryBlock) L20

ffff8b81`91615020 00000000`00000007 00000000`000bfee1 – общее число блоков, общий размер блоков

ffff8b81`91615030 00000000`00000001 00000000`0000009f – блоки, и число страниц в каждом из них.

ffff8b81`91615040 00000000`00000100 00000000`0000027b

ffff8b81`91615050 00000000`0000037d 00000000`00005d86

ffff8b81`91615060 00000000`00006105 00000000`00058dc0

ffff8b81`91615070 00000000`0005ef1b 00000000`00001080

ffff8b81`91615080 00000000`0005ffff 00000000`00000001

ffff8b81`91615090 00000000`00060200 00000000`00060000

У WinDBG есть специальная команда для наглядного отображения nt!MmPhysicalMemoryBlock.

Как видно, часть блоков гостевой ОС умещается в одном блоке, выделенном гипервизором. И часть блоков гостевой ОС соответствуют блокам, выделенным гипервизором, с одинаковым объёмом, но с некоторым смещением. Учитывая, что смещение незначительное, мы можем откорректировать нашу таблицу:

Первый блок мы не корректируем. Память отображается 1 в 1, что, кстати, нам позволяет считывать данные из первого блока, где находится ntoskrnl.exe, чтобы вычислить значения структуры _PHYSICAL_MEMORY_DESCRIPTOR. После вычисления мы можем выполнить корректировку смещений. Я описал в коде случай, когда один гостевой блок может состоять из нескольких блоков, выделенных гипервизором, однако с таким случаем на стенде не сталкивался. Последний из блоков размером 0x800 страниц используется для видеопамяти, как было выяснено выше. В нашем случае в виртуальной машине максимально доступный для считывания физический адрес больше, чем максимальный адрес, указанный в PHYSICAL_MEMORY_DESCRIPTOR. В PHYSICAL_MEMORY_DESCRIPTOR этот блок не указан, поэтому просто будем считать, что он идёт последовательно за последним блоком. Без драйвера в хост ОС смещение этого блока определить не представляется возможным. Можно считать, что это память, используемая устройством, и её можно прочитать, например, средствами LiveCloudKd.

После корректировки мы можем без драйвера достаточно точно читать все данные из гостевой ОС, за исключением тех, которые попадут в файл подкачки.

На этом завершу описание. Оставшиеся детали можно найти в исходниках драйвера hvmm.sys.

Дополнительная информация

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

Таким образом выглядит структура элементов памяти для гостевых ОС для Windows Server 2019:

Для контейнеров количество GPAR и MBlock-объектов значительно больше:

Но все MBlock-объекты для контейнеров содержат нули:

0: kd> dc 0xffff958b7f0d14d0

ffff958b`7f0d14d0 00000000 00000000 00000000 00000000 ................

ffff958b`7f0d14e0 00000000 00000000 00000000 00000000 ................

ffff958b`7f0d14f0 00000000 00000000 00000000 00000000 ................

Также в драйвере vid.sys присутствует ещё один вид блоков: Reserve bucket block

Но для чтения памяти гостевой ОС они не пригодились. Видно, что адреса ссылаются сами на себя (по модулю 0x10).

Работа с памятью контейнеров Docker, запущенных в режиме изоляции

Docker-контейнер в режиме изоляции Hyper-V создаёт достаточно много процессов (процессы для 1 контейнера Windows Server 2019 nanoserver 1809):

Получается 2 дескриптора раздела (по числу процессов vmwp.exe). Имя одного из них совпадает с именем пользователя, в контексте которого работает процесс.

Однако, в этом разделе присутствует неактуальная таблица MBlock-объектов:

Количество элементов указано как 8e, но сам MBlock-объект всего один, и он пустой. 2-й раздел для контейнеров, имя которого совпадает с идентификатором, создаваемым для контейнера, содержит необходимые данные и может быть использован для доступа к памяти самого контейнера.

Базовые адрес совпадает с параметром Vmmem GPA Offset, который используется для чтения блока памяти из контекста процесса vmmem.

Адреса смещений GPAR-объектов в другом экземпляре vmmem совпадают со смещением VmmemGPA offset, используемым драйвером hvmm.sys для чтения памяти.

Разные процессы vmmem загружают разные исполняемые файлы. Но у того процесса, где файлов меньше – количество активных потоков равно 0.

2-й процесс vmmem docker-контейнера не критичен для выполнения. Его можно убить через Process Hacker (размер памяти станет несколько десятков килобайт). Первый vmmem процесс так же не критичен для чтения памяти. Регистры раздела, к которому привязан процесс, имеют корректные значения, но при чтении kernelmode памяти возвращаются нули.

После остановки двух вышеупомянутых vmmem процессов, через docker exec всё ещё можно спокойно запускать процессы внутри контейнера.

1-й PsCreateMinimalProcess

2-й PsCreateMinimalProcess

3-й PsCreateMinimalProcess

: kd> kcn

# Call Site

00 nt!PsCreateMinimalProcess

01 nt!VmCreateMemoryProcess

02 Vid!VsmmNtSlatMemoryProcessCreate

03 Vid!VsmmProcesspMicroVmSetup

………………………………………

14 vmwp!VidPartitionManager::Initialize

15 vmwp!VidPartitionManager::CreateInstance

2: kd> kcn

# Call Site

00 nt!PsCreateMinimalProcess

01 nt!VmCreateMemoryProcess

02 Vid!VsmmNtSlatMemoryProcessCreate

03 Vid!VsmmClonepTemplateCreate

………………………………………

13 vmwp!WorkerTaskSaving::StartSave

14 vmwp!WorkerTaskSaving::RunSaveSteps

15 vmwp!WorkerTaskSaving::RunTask

0: kd> kcn

# Call Site

00 nt!PsCreateMinimalProcess

01 nt!VmCreateMemoryProcess

02 Vid!VsmmNtSlatMemoryProcessCreate

03 Vid!VsmmCloneTemplateApply

………………………………………

13 vmwp!VidPartitionManager::Initialize

14 vmwp!VidPartitionManager::CreateInstance

Мы снова видим псевдо Gpar-объект, указывающий на usermode-структуру (как видели выше, это блок создаётся для взаимодействия с виртуальными устройствами):

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

Открыты дескрипторы файлов docker-контейнера:

Примеры использования

В каких программах можно использовать возможность чтения\записи памяти в гостевую ОС?

LiveCloudKd (как альтернатива Sysinternals LiveKd в части опции -hvl). В данный момент на хост-сервере запущена одна Full VM с Windows Server 2019 R2 и 1 Docker контейнер в режиме изоляции Hyper-V.

EXDi-plugin для WinDBG – возможности те же, но позволяют использовать легальные функции для интеграции WinDBG (LiveCloudKd использует хуки API-функций, используемых при чтении дампа памяти). Работает даже с WinDBG Preview, который сам по себе запускается в отдельном контейнере (UWP-приложение). На момент написания статьи EXDi-plugin плагин работает только с Windows Server 2019\Windows 10 с загруженным драйвером hvmm.sys, поскольку для его работы требуется операция записи в гостевую ОС. На скриншоте показана работа WinDBG Preview в режиме EXDi и плагина mimilb.dll, входящего в состав утилиты mimikatz.

Плагин для программы MemProcFs (https://github.com/ufrisk/MemProcFS) которая интегрирована с pypykatz (https://github.com/skelsec/pypykatz), также позволяет просканировать гостевую ОС на предмет наличия хэшей (на скриншоте в качестве гостевой ОС -контроллер домена на базе Windows Server 2016)

Понятно, что для этого нужно получить доступ к хост-серверу с правами администратора. Так что в первую очередь я позиционирую утилиту как возможность покопаться внутри ОС, когда отладчиком долго\лень или невозможно подключиться (например, активна опция Secure Boot).

Заключение


В статье были описаны методы доступа к памяти гостевых разделов Hyper-V, создаваемых в самых разных случаях. Надеюсь, что работа с памятью Hyper-V стала немного более понятной. Hyper-V очень быстро эволюционирует и всё активнее интегрируется в ядро Windows, вместе с тем оставаясь практически не документированной.

Информация может оказаться полезной тем, кто хочет понять внутреннее устройство Hyper-V, и, возможно, абсолютно прозрачно получить доступ к памяти гостевой ОС, а также произвести её модификацию. Учитывая, что для использования LiveCloudKd необходимо иметь доступ к root-ОС, на которой размещены виртуальные машины, не думаю, что она несёт какую-то угрозу безопасности. Однако для Windows Server 2016 такой доступ может быть получен с использованием только Usermode API, что контролировать довольно-таки проблематично. Для защиты рекомендуется включать либо опцию Shielded VM (тогда для её обхода нужно будет загружать драйвер). Или использовать Windows Server 2019, где Microsoft заблокировала вызов API из vid.dll для сторонних процессов и включила для vmwp.exe запрет инжектирования библиотек, не подписанных компанией Microsoft. Впрочем, недавнее исследование по внедрению кода в процессы, продемонстрированное в августе 2019 на Blackhat в Лас Вегасе (доклад Process Injection Techniques - Gotta Catch Them All от Itzik Kotler and Amit Klein из SafeBreach Labs), показывает, что существуют возможности обойти эти ограничения из usermode (разумеется, для этого нужны права локального администратора). Единственным надёжным средством защиты от подобного доступа к гостевым ОС Microsoft позиционирует Code Integrity в связке с Shielded VM.

Подписывайтесь на каналы "SecurityLab" в TelegramTelegram и Яндекс.ДзенЯндекс.Дзен, чтобы первыми узнавать о новостях и эксклюзивных материалах по информационной безопасности.