Исследуем внутренние механизмы работы Hyper-V: Часть 2

в 10:50, , рубрики: hyper-v, microsoft, Блог компании Журнал Хакер, виртуализация

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 1

Со времени публикации первой части статьи глобально в мире ничего не изменилось: Земля не наскочила на небесную ось, все так же растет популярность облачных сервисов, все так же в гипервизоре компании Microsoft не были обнаружены новые дыры, а исследователи не хотят тратить свое время на поиск багов в плохо документированной и мало изученной технологии. Поэтому я предлагаю тебе освежить память первой частью из предыдущего номера, пополнить запас своего бара и приступить к чтению, ведь сегодня мы сделаем драйвер, взаимодействующий с интерфейсом гипервизора и отслеживающий передаваемые гипервизором сообщения, а также изучим работу компонентов служб интеграции Data Exchange.

Обработка сообщений гипервизора

На dvd.xakep.ru мы выложили драйвер, написанный с помощью Visual Studio 2013. Он должен быть загружен в root ОС, например с помощью OSRLoader. Для отправки IOCTL-кодов используется простая программа SendIOCTL.exe. После отправки IOCTL-кода INTERRUPT_CODE драйвер начинает выполнять обработку данных, переданных гипервизором через нулевой слот SIM. К сожалению, переменная HvlpInterruptCallback, в которой содержится адрес массива с указателями обработчиков сообщений, ядром не экспортируется, поэтому для ее обнаружения необходимо проанализировать код экспортируемой ядром функции HvlRegisterInterruptCallback, содержащей необходимый нам адрес массива. Также, к сожалению, не получится просто вызвать HvlRegisterInterruptCallback для регистрации своего обработчика сообщений, так как в самом начале функции идет проверка значения переменной HvlpFlags. Если переменная равна 1 (а ей присваивается это значение на начальных этапах загрузки ядра), то функция прекращает выполнение, возвращает код ошибки 0xC00000BB (STATUS_NOT_SUPPORTED) и, соответственно, регистрация обработчика не происходит, поэтому для замены обработчиков придется написать свой вариант функции HvlpInterruptCallback. В драйвере hyperv4 необходимые действия выполняются функцией RegisterInterrupt:

int RegisterInterrupt()
{
UNICODE_STRING uniName;
PVOID pvHvlRegisterAddress = NULL;
PHYSICAL_ADDRESS pAdr = {0};
ULONG i,ProcessorCount;
// Получаем число активных ядер процессоров
ProcessorCount = KeQueryActiveProcessorCount(NULL);
// Выполняем поиск адреса экспортируемой функции HvlRegisterInterruptCallback
DbgLog("Active processor count",ProcessorCount);
RtlInitUnicodeString(&uniName,L"HvlRegisterInterruptCallback");
pvHvlRegisterAddress = MmGetSystemRoutineAddress(&uniName);
if (pvHvlRegisterAddress == NULL){
     DbgPrintString("Cannot find HvlRegisterInterruptCallback!");
     return 0;
}
DbgLog16("HvlRegisterInterruptCallback address ",pvHvlRegisterAddress);
// Выполняем поиск адреса переменной HvlpInterruptCallback, FindHvlpInterruptCallback((unsigned char *)pvHvlRegisterAddress);
// Производим замену оригинальных обработчиков на свои
ArchmHvlRegisterInterruptCallback((uintptr_t)&ArchmWinHvOnInterrupt,(uintptr_t)pvHvlpInterruptCallbackOrig,WIN_HV_ON_INTERRUP_INDEX);
ArchmHvlRegisterInterruptCallback((uintptr_t)&ArchXPartEnlightenedIsr,(uintptr_t)pvHvlpInterruptCallbackOrig,XPART_ENLIGHTENED_ISR0_INDEX);
ArchmHvlRegisterInterruptCallback((uintptr_t)&ArchXPartEnlightenedIsr,(uintptr_t)pvHvlpInterruptCallbackOrig,XPART_ENLIGHTENED_ISR1_INDEX);
ArchmHvlRegisterInterruptCallback((uintptr_t)&ArchXPartEnlightenedIsr,(uintptr_t)pvHvlpInterruptCallbackOrig,XPART_ENLIGHTENED_ISR2_INDEX);
ArchmHvlRegisterInterruptCallback((uintptr_t)&ArchXPartEnlightenedIsr,(uintptr_t)pvHvlpInterruptCallbackOrig,XPART_ENLIGHTENED_ISR3_INDEX);
// Так как значение SIMP для всех ядер разное, то необходимо получить физические адреса всех SIM

WARNING

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

// сделать возможным доступ к содержимому страницы, смапировав ее с помощью Mm MapIoSpace, и сохранить полученные виртуальные адреса каждой страницы в массив для последующего использования
for (i = 0; i < ProcessorCount; i++)
{KeSetSystemAffinityThreadEx(1i64 << i);
DbgLog("Current processor number",KeGetCurrentProcessorNumberEx(NULL));
pAdr.QuadPart = ArchReadMsr (HV_X64_MSR_SIMP) & 0xFFFFFFFFFFFFF000;
pvSIMP[i] = MmMapIoSpace (pAdr, PAGE_SIZE, MmCached);
if (pvSIMP[i] == NULL){ DbgPrintString("Error during pvSIMP MmMapIoSpace");
return 1;
}
DbgLog16("pvSIMP[i] address", pvSIMP[i]);
pAdr.QuadPart = ArchReadMsr (HV_X64_MSR_SIEFP) & 0xFFFFFFFFFFFFF000;
pvSIEFP[i] = MmMapIoSpace(pAdr, PAGE_SIZE, MmCached);
if (pvSIEFP[i] == NULL){DbgPrintString("Error during pvSIEFP MmMapIoSpace");
return 1;
}
DbgLog16("pvSIEFP address", pvSIEFP[i]);
}
return 0;
}

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 2
Рис. 1. Массив HvlpInterruptCallback с измененными обработчиками

Массив HvlpInterruptCallback после выполнения функции RegisterInterrupt (если заменить все обработчики одновременно) выглядит следующим образом (см. рис. 1). Замена выполнена по аналогии с оригинальным кодом: один обработчик для сообщений гипервизора и  четыре для  обработки сообщений от VMBus. Процедуры ArchmWinHvOnInterrupt и  ArchXPartEnlightenedIsr выполняют сохранение всех регистров в стеке и передают управление на  функции парсинга сообщений ParseHvMessage и ParseVmbusMessage соответственно (mPUSHAD и  mPOPAD  — макросы, выполняющие сохранение регистров в  стеке) (см.рис. 2).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 3
Рис. 2. ArchmWinHvOnInterrupt и ArchXPartEnlightenedIsr

После выполнения парсинга управление передается на оригинальные процедуры WinHvOnInterrupt и XPartEnlightenedIsr. Функция парсинга сообщений гипервизора выглядит следующим образом:

void ParseHvMessage()
{
PHV_MESSAGE phvMessage, phvMessage1;
// Получаем номер активного логического процессора
ULONG uCurProcNum = KeGetCurrentProcessorNumberEx(NULL);
Unlock+0x162
vmbkmcl!VmbChannelEnable+0x231
vmbus!PipeStartChannel+0x9e
vmbus!PipeAccept+0x81
vmbus!InstanceCreate+0x90
..................................
nt!IopParseDevice+0x7b3
nt!ObpLookupObjectName+0x6d8
nt!ObOpenObjectByName+0x1e3
nt!IopCreateFile+0x372
nt!NtCreateFile+0x78
nt!KiSystemServiceCopyEnd+0x13
ntdll!NtCreateFile+0xa
KERNELBASE!CreateFileInternal+0x30a
KERNELBASE!CreateFileW+0x66
vmbuspipe!VmbusPipeClientOpenChannel+0x44
icsvc!ICTransportVMBus::ClientNotification+0x60
vmbuspipe!VmbusPipeClientEnumeratePipes+0x1ac
icsvc!ICTransportVMBusClient::Open+0xe5
icsvc!ICEndpoint::Connect+0x66
icsvc!ICChild::Run+0x65
icsvc!ICKvpExchangeChild::Run+0x189
icsvc!ICChild::ICServiceWork+0x137
icsvc!ICChild::ICServiceMain+0x8f
..................................

У виртуальной машины активирован компонент Data Exchange.После установки галочки на компоненте Data Exchange и нажатия кнопки Apply root ОС через гипервызов HvPostMessage отправляет гостевой ОС сообщение с кодом CHANNELMSG_OFFERCHANNEL (см. рис. 9). Переданные данные содержат GUID устройства, подключенного к  VMBus как дочернее устройство (см. рис. 10). Далее гостевая ОС обрабатывает данные и вызывает функцию vmbus!InstanceDeviceControl.

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 4
Рис. 9. GUID устройства в сообщении

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 5
Рис. 10. Вывод !devnode для устройства VMBus

Часть стека:

WINDBG>kс
Call Sitent!IoAllocateMdl vmbus!InstanceCloseChannel+0x22d (адрес возврата для функции, имя которой отсутствует в символах)
vmbus!InstanceDeviceControl+0x118
..................................
vmbkmcl!KmclpSynchronousIoControl+0xa7
vmbkmcl!KmclpClientOpenChannel+0x2a6
vmbkmcl!KmclpClientFindVmbusAnd
if (pvSIMP[uCurProcNum] != NULL){
   phvMessage = (PHV_MESSAGE)pvSIMP[uCurProcNum];
} else{
   DbgPrintString("pvSIMP is NULL");
   return;
}
// Уведомление об отправке сообщения через 1-й слот SIM phvMessage1 = (PHV_MESSAGE)((PUINT8)pvSIMP[uCurProcNum]+ HV_MESSAGE_SIZE); // for SINT1
if (phvMessage1->Header.MessageType != 0){
   DbgPrintString("SINT1 interrupt");
}
// В зависимости от типа сообщения вызываем процедуры обработчики
// Структуры для каждого типа сообщения описаны в TLFS
switch (phvMessage->Header.MessageType)
{
case HvMessageTypeX64IoPortIntercept:
    PrintIoPortInterceptMessage(phvMessage);
    break;
case HvMessageTypeNone:DbgPrintString("HvMessageTypeNone");
    break;
case HvMessageTypeX64MsrIntercept:PrintMsrInterceptMessage(phvMessage);
    break;
case HvMessageTypeX64CpuidIntercept:PrintCpuidInterceptMessage(phvMessage);
   break;
case HvMessageTypeX64ExceptionIntercept:PrintExceptionInterceptMessage(phvMessage);
   break;
   default:
DbgLog("Unknown MessageType", phvMessage->
Header.MessageType);
break;
}
}

Функция получает номер активного логического процессора, адрес страницы SIM и считывает значение нулевого слота SIM. Сперва производится анализ типа сообщения phvMessage->Header.MessageType, поскольку тело сообщения для каждого типа разное. В DbgView можно увидеть следующую картину (см. рис. 3).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 6
Рис. 3. Вывод DbgView при обработке гипервизором обращений к MSR-регистрам

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 7
Функция ParseVmbusMessage (рис. 4).

Функция получает номер активного логического процессора, адрес страницы SIM и считывает значение четвертого слота SIM. Для  примера разобраны сообщения CHANNELMSG_OPENCHANNEL и  CHANNELMSG_GPADL_HEADER, но в исходных кодах LIS можно увидеть формат всех типов сообщений и без труда дописать необходимые обработчики. Сообщения для шины VMBus обычно генерируются при включении/выключении виртуальной машины или же одного из  компонентов Integration Services. Например, при включении компонента Data Exchange отладчик или DbgView покажет информацию, изображенную на рис. 5.

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 8
Рис. 5. Отладочный вывод сообщений при включении компонента Data Exchange

Integration Services — Data Exchange

Далее рассмотрим, каким же образом происходит обмен данными между гостевой и root ОС  на  примере одного из  компонентов служб интеграции  — Data Exchange. Этот компонент позволяет root ОС считывать данные из определенной ветки реестра гостевой ОС. Для проверки в гостевой ОС создадим в ветке HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Virtual Machine\Guest ключ со значением KvPDataValue (см. рис. 6).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 9
Рис. 6. Ключ KvPDataValue

Для получения значения ключа в root ОС был использован следующий PowerShell-скрипт(см. рис. 7).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 10
Рис. 7. PowerShellскрипт для запроса значений реестра из гостевой ОС

Скрипт вернет значение ключа KvPDataKey (см. рис. 8). Скрипт получает весь доступный набор значений с  помощью $vm.GetRelated(«Msvm_KvpExchangeComponent»).GuestExchangeItems и только после этого выполняет разбор каждого полученного объекта на предмет поиска ключа KvPDataKey. Соответственно, скрипт будет работать только в  том случае, если в  свойствах из  этой функции вызывается nt!IoAllocateMdl с размером выделяемого буфера 0xC000. Результат выполнения функции — сформированная структура MDL (см. рис. 11). Далее вызывается MmProbeAndLockPages, после завершения выполнения которой структура MDL дополняется элементами PFN (см. рис. 12). В  данном примере была выделена непрерывная область физической памяти, хотя это условие выполняется необязательно. Далее вызывается vmbus!ChCreateGpadlFromNtmdl (вторым параметром передается адрес MDL), которая вызывает vmbus!ChpCreateGpaRanges, передавая ей в качестве первого параметра все тот же MDL. Далее выполняется копирование элементов PFN из структуры MDL в отдельный буфер (см. рис. 13), который станет телом сообщения CHANNELMSG_GPADL_HEADER, отправляемого гостевой ОС  в  root ОС  посредством вызова vmbus!ChSendMessage. hv!HvPostMessage или в  winhv!WinHvPostMessage можно увидеть сообщение (рис.14).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 11
Рис. 8. Результат выполнения скрипта

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 12
Рис. 11. Значение структуры MDL

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 13
Рис. 12. PFN в MDLструктуре

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 14
Рис. 13. Участок кода, отвечающий за копирование PFN в отдельный буфер

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 15
Рис. 14. Массив PFN, обрабатываемый гипервизором

Первые 16 байт  — это общий заголовок сообщения, где, например, 0xF0 — размер тела сообщения, внутри размещается VMBus-пакет, в заголовке которого указан тип пакета — 8 (CHANNELMSG_GPADL_HEADER), rangecount равен 1, из  чего следует, что  в  один пакет вместились все данные, которые было необходимо передать. В  случае большого объема данных драйвер гостевой ОС  разделил бы их на  части и  отправил отдельными сообщениями. Далее root ОС шлет сообщение CHANNELMSG_OPENCHANNEL_RESULT, затем гостевая ОС шлет CHANNELMSG_OPENCHANNEL. После этого в root ОС отрабатывает Work Item (см. рис. 15).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 16
Рис. 15. Вызов ChMapGpadlView

В  ходе его выполнения вызывается vmbusr!ChMapGpadlView->vmbusr!PkParseGpaRanges, которой, в свою очередь, передается указатель на часть сообщения, содержащую размер буфера 0xC000 и PFN, переданные в сообщении CHANNELMSG_GPADL_HEADER. Далее происходит вызов vmbusr!XPartLockChildPagesSynchronous->vmbusr!XPartLockChildPages, после чего выполняется функция из драйвера vid.sys (имя функции неизвестно, поскольку символы для  драйвера отсутствуют), которой в качестве второго параметра передается блок PFN, отправленный ранее в  сообщении из гостевой ОС (см. рис. 16).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 17
Рис. 16. Обработка гостевых PFN драйвером vid.sys

Непосредственно после возврата из функции в [rsp+30h] находится указатель на вновь созданную структуру MDL (см.рис. 17).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 18
Рис. 17. Структура MDL, возвращаемая драйвером vid.sys

Размер выделенного буфера также равен 0xC000. После этого root ОС шлет сообщение CHANNELMSG_OPENCHANNEL_RESULT. На  этом процесс активации компонента Data Exchange завершается. Таким образом создается некий shared-буфер, видимый как гостевой, так и root ОС. Это можно проверить, выполнив запись произвольных данных в буфер в гостевой ОС, например с помощью команды:

WINDBG>!ed 2d5bb000 aaaaaaaa
WINDBG>!db 2d5bb000
#2d5bb000 aa aa aa aa 10 19 00

А в root ОС посмотреть содержимое страницы, соответствующей PFN, возвращенной функцией драйвера vid.sys:

**WINDBG>!db 1367bb000**
#1367bb000 aa aa aa aa 10 19

Как видно, значения совпали, так что это действительно одна и та же физическая область памяти. Вспомним, что на предыдущих этапах мы определили, что при активации компонента Data Exchange создается порт типа HvPortTypeEvent с TargetSint = 5. Соответственно, все операции с  этим портом в root ОС будет обрабатывать KiVmbusInterrupt1, из которой происходит вызов vmbusr!XPartEnlightenedIsr, а  она, в свою очередь, вызывает KeInsertQueueDpc с параметром DPC (его значение показано на рис. 19).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 19
Рис. 19. Значение DPC, которую ставит в очередь XPartEnlightenedIsr

Из vmbusr!ParentRingInterruptDpc через несколько вызовов будет выполнена vmbusr!PkGetReceiveBuffer.

WINDBG>k
Child-SP RetAddr Call Site
fffff800`fcc1ea38 fffff800`6cdc440c
vmbusr!PkGetReceiveBuffer+0x2c
fffff800`fcc1ea40 fffff800`6cdc41a7
vmbusr!PipeTryReadSingle+0x3c
fffff800`fcc1eaa0 fffff800`6cdc4037
vmbusr!PipeProcessDeferredReadWrite+0xe7
fffff800`fcc1eaf0 fffff800`6c96535e
vmbusr!PipeEvtChannelSignalArrived+0x63
fffff800`fcc1eb30 fffff800`6cdc4e3d
vmbkmclr!KmclpVmbusManualIsr+0x16
fffff800`fcc1eb60 fffff800`fb2d31e0
vmbusr!ParentRingInterruptDpc+0x5d

Если просмотреть эту область памяти, то становятся видны параметры гостевой ОС.

WINDBG> dc ffffd0016fe33000 L1000
…………………………………………………………………………………………………………………
ffffd001`6fe35b30 0065004e 00770074 0072006f
0041006b N.e.t.w.o.r.k.A.
ffffd001`6fe35b40 00640064 00650072 00730073
00500049 d.d.r.e.s.s.I.P.
ffffd001`6fe35b50 00340076 00000000 00000000 00000000
v.4.............
…………………………………………………………………………………………………………………
ffffd001`6fe35d20 00000000 00000000 00000000 00000000
................
ffffd001`6fe35d30 00300031 0030002e 0030002e 0033002e
1.0...0...0...3.
ffffd001`6fe35d40 00000000 00000000 00000000 00000000
................
WINDBG>!pte ffffd001`6fe35b30
VA ffffd0016fe35b30
PXE at FFFFF6FB7DBEDD00 PPE at FFFFF6FB7DBA0028 PDE at
FFFFF6FB74005BF8
PTE at FFFFF6E800B7F1A8
contains 0000000000225863 contains 00000000003B7863 contains
000000010FB12863 contains 80000001367BD963
pfn 225 ---DA--KWEV pfn 3b7 ---DA--KWEV pfn 10fb12 ---DA-KWEV pfn
1367bd -G-DA--KW-V
pfn 1367bd — это PFN 3-й страницы из конвертированного MDL

Также этой же функции в rdx передается указатель, содержащий смещение относительно адреса начала общих с гостевой ОС страниц (в примере он равен 4448h), по которому необходимо произвести чтение:

vmbusr!PkGetReceiveBuffer+0x4e:
mov r8,r10 (в r10d был ранее загружено смещение из rdx)
add r8,qword ptr [rcx+20h] — в rcx+20 содержится указатель на одну из общих с гостевой ОС страницу
WINDBG>!pte @r8
VA ffffd0016ff22448
PXE at FFFFF6FB7DBEDD00 PPE at FFFFF6FB7DBA0028 PDE at FFFFF6FB74005BF8
PTE at FFFFF6E800B7F910
contains 0000000000225863 contains 00000000003B7863 contains
000000010FB12863 contains 80000001367C0963
pfn 225 ---DA--KWEV pfn 3b7 ---DA--KWEV pfn 10fb12 ---DA-KWEV pfn
1367c0 -G-DA--KW-V

Поставим точку останова на начало функции vmbusr!PkGetReceiveBuffer и  выполним PowerShell-скрипт. Точка останова сработает, при  этом будет видно, что функции передается структура (указатель в rcx) и в rcx+18 находится указатель на блок памяти:


WINDBG>? poi(@rcx+18)
Evaluate expression: -52770386006016 = ffffd001`6fe33000
WINDBG>!pte ffffd001`6fe33000
VA ffffd0016fe33000
PXE at FFFFF6FB7DBEDD00 PPE at FFFFF6FB
7DBA0028 PDE at FFFFF6FB74005BF8
PTE at FFFFF6E800B7F198
contains 0000000000225863 contains
00000000003B7863 contains
000000010FB12863 contains
80000001367BB963
pfn 225 ---DA--KWEV pfn 3b7 ---DA--KWEV
pfn 10fb12 ---DA--KWEV pfn
1367bb -G-DA--KW-V
WINDBG>r cr3
cr3=00000000001ab000
WINDBG>!vtop 1ab000 ffffd 0016fe33000
Amd64VtoP: Virt ffffd001`6fe33000,
pagedir 1ab000
Amd64VtoP: PML4E 1abd00
Amd64VtoP: PDPE 225028
Amd64VtoP: PDE 3b7bf8
Amd64VtoP: PTE 00000001`0fb12198
Amd64VtoP: Mapped phys 00000001`367bb000
Virtual address ffffd0016fe33000 translates to
physical address
1367bb000.

INFO

Информацию о технологии KvP можно найти в блогах MSDN:
goo.gl/R0U52l
goo.gl/UeZRK2

Если поставить точку останова на инструкцию add r8,qword ptr [rcx+20h], то через несколько итераций в r8 можно увидеть имя и  значение ключа KvpDataKey:

WINDBG>dc @r8
ffffd001`6ff21d10
....H...........
ffffd001`6ff21d20
....(........... ffffd001`6ff21d30

00020006 00000148 00000000 00000000
00000001 00000 a28  00000003 00050002
размер передаваемого блока
0a140000 00000000 00000515 00000103
................
ffffd001`6ff21d40 00000004 00000001 00000016 0000001a
................
ffffd001`6ff21d50 0076004b 00440050 00740061 004b0061
K.v.P.D.a.t.a.K.
ffffd001`6ff21d60 00790065 00000000 00000000 00000000
e.y.............
.............................................................
..........
ffffd001`6ff21f50 0076004b 00440050 00740061 00560061
K.v.P.D.a.t.a.V.
ffffd001`6ff21f60 006c0061 00650075 00000000 00000000
a.l.u.e.........
WINDBG>!pte ffffd001`6ff21f50
VA ffffd0016ff21f50
PXE at FFFFF6FB7DBEDD00 PPE at FFFFF6FB7DBA0028 PDE at
FFFFF6FB74005BF8
PTE at FFFFF6E800B7F908
contains 0000000000225863 contains 00000000003B7863 contains
000000010FB12863 contains 80000001367BF963
pfn 225 ---DA--KWEV pfn 3b7 ---DA--KWEV pfn 10fb12 ---DA-KWEV pfn
1367bf -G-DA--KW-V

Затем после завершения PkGetReceiveBuffer функция PipeTryReadSingle копирует данные из shared-буфера с помощью функции memmove.

При этом размер блока (в данном случае A28) указан непосредственно в самом блоке, но если будет задано число больше, чем 4000h, то копирование не  будет произведено. Таким образом, видно, что  обмен данными между root ОС  и  гостевой ОС  использует общий буфер, а  интерфейс гипервизора используется лишь для уведомления root ОС о том, что необходимо выполнить считывание данных из этого буфера. В принципе, ту же операцию можно было бы выполнить при  помощи отправки нескольких сообщений, используя winhv!HvPostMessage, но это привело бы к значительному снижению производительности.

Использование интерфейса перехвата гипервизора

Настроим гипервизор таким образом, чтобы он отправлял уведомление root ОС в случае, если в одной из  гостевой ОС  выполняется инструкция cpuid с  параметром 0x11114444. Для  этого Hyper-V предоставляет интерфейс в  виде гипервызова HvInstallIntercept. В  драйвере hyperv4 реализована функция SetupIntercept, которая получает список идентификаторов всех активных гостевых ОС и вызывает для каждой WinHvInstallIntercept.

int SetupIntercept()
{
HV_INTERCEPT_DESCRIPTOR Descriptor;
HV_INTERCEPT_PARAMETERS Parameters = {0};
HV_STATUS hvStatus = 0;
HV_PARTITION_ID PartID = 0x0, NextPartID = 0;
// Если в качестве параметра инструкции в RAX-инструкции CPUID будет передано значение 0x11114444, то гипервизор выполнит перехват и отправит сообщение родительскому разделу для обработки результата
DbgPrintString("SetupInterception was called");
Parameters.CpuidIndex = 0x11114444;
Descriptor.Type = HvInterceptTypeX64Cpuid;
Descriptor.Parameters = Parameters;
hvStatus = WinHvGetPartitionId(&PartID);
do{
   hvStatus = WinHvGetNextChildPartition(PartID,NextPartID,&NextPartID);
   if (NextPartID != 0){
      DbgLog("Child partition id", NextPartID);
      hvStatus = WinHvInstallIntercept(NextPartID,
      HV_INTERCEPT_ACCESS_MASK_EXECUTE, &Descriptor);
      DbgLog("hvstatus of WinHvInstallIntercept = ",hvStatus);
} } while ((NextPartID != HV_PARTITION_ID_INVALID) &&
(hvStatus == 0));
return 0;}

Также изменим функцию PrintCpuidInterceptMessage таким образом, чтобы она в случае, если в гостевой ОС в регистре EAX (или RAX, если код, выполняющий инструкцию CPUID, выполняется в  longmode) находится число 0x11114444, записывала в  поле DefaultResultRdx структуры HV_X64_CPUID_INTERCEPT_MESSAGE, расположенную в нулевом слоте SIM, значение 0x12345678:

void PrintCpuidInterceptMessage(PHV_MESSAGE hvMessage)
{PHV_X64_CPUID_INTERCEPT_MESSAGE_phvCPUID = (PHV_X64_CPUID_NTERCEPT_MESSAGE)_hvMessage->Payload;
DbgLog(" phvCPUID->DefaultResultRax",phvCPUID->DefaultResultRax);
DbgLog(" phvCPUID->DefaultResultRbx",phvCPUID->DefaultResultRbx);
DbgLog(" phvCPUID->DefaultResultRcx",phvCPUID->DefaultResultRcx);
DbgLog(" phvCPUID->DefaultResultRdx",phvCPUID->DefaultResultRdx);
if (phvCPUID->Rax == 0x11114444){
  phvCPUID->DefaultResultRdx =0x12345678;
  DbgLog16(" phvCPUID->Header.Rip",phvCPUID->Header.Rip);
  DbgPrintString(" Interception was handled");
}
}

Для проверки в гостевой ОС запустим тестовую утилиту, которая вызывает CPUID с Eax, равным 0x11114444. До установки перехвата утилита выведет результат, отображенный на рис. 20.

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 20
Рис. 20. Результат инструкции CPUID на обычной гостевой ОС

После активации перехвата результат будет следующим (см. рис. 21).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 21
Рис. 21. Результат инструкции CPUID после установки перехвата

При этом в root ОС будет выведено сообщение (см. рис. 22).

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 22
Рис. 22. Отладочный вывод обработки сообщения гипервизора при установленном перехвате

Сразу стоит обратить внимание на то, что этот трюк пройдет только в том случае, если root ОС ранее не установила перехваты для заданных условий. В этом случае после того, как драйвер hyperv заменит значение, управление перейдет на оригинальную WinHvOnInterrupt, которая вызовет функцию обработки из драйвера vid.sys (эта функция является четвертым параметром функции winhvr!WinHvCreatePartition, вызываемой в root ОС  при создании дочернего раздела при включении виртуальной машины), что может привести к изменению результата. В нашем случае такой обработчик, разумеется, установлен не был, гипервизор проанализировал данные в нулевом слоте SIM и  исправил результат инструкции CPUID.

В заключение

Несмотря на то что после прочтения моего труда твой мозг наверняка встал в позу речного скорпиона (и если ты вообще досюда дочитал  — респект тебе от  всей нашей редакции)… так, я  отвлекся. Эта статья получилась скорее обзорной, демонстрирующей работу некоторых функций и компонентов системы виртуализации Microsoft на примерах. Однако, надеюсь, эти примеры помогут лучше понять принципы работы этих компонентов и позволят более глубоко проанализировать безопасность, например VMBus, написав свой собственный фаззер.

Автор: gerhart

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 23

Впервые опубликовано в журнале «Хакер» от 12/2014.

Подпишись на «Хакер»

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 24

Исследуем внутренние механизмы работы Hyper-V: Часть 2 - 25

Автор: XakepRU

Источник

Поделиться

* - обязательные к заполнению поля