Исследуем внутренние механизмы работы Hyper-V: Часть 2
Со времени публикации первой части статьи глобально в мире ничего не изменилось: Земля не наскочила на небесную ось, все так же растет популярность облачных сервисов, все так же в гипервизоре компании 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; } Рис. 1. Массив HvlpInterruptCallback с измененными обработчиками
Массив HvlpInterruptCallback после выполнения функции RegisterInterrupt (если заменить все обработчики одновременно) выглядит следующим образом (см. рис. 1). Замена выполнена по аналогии с оригинальным кодом: один обработчик для сообщений гипервизора и четыре для обработки сообщений от VMBus. Процедуры ArchmWinHvOnInterrupt и ArchXPartEnlightenedIsr выполняют сохранение всех регистров в стеке и передают управление на функции парсинга сообщений ParseHvMessage и ParseVmbusMessage соответственно (mPUSHAD и mPOPAD — макросы, выполняющие сохранение регистров в стеке) (см.рис. 2).
Рис. 2. ArchmWinHvOnInterrupt и ArchXPartEnlightenedIsr
После выполнения парсинга управление передается на оригинальные процедуры WinHvOnInterrupt и XPartEnlightenedIsr. Функция парсинга сообщений гипервизора выглядит следующим образом:
void ParseHvMessage () { PHV\_MESSAGE phvMessage, phvMessage1; // Получаем номер активного логического процессора ULONG uCurProcNum = KeGetCurrentProcessorNumberEx (NULL); Unlock+0×162 vmbkmcl! VmbChannelEnable+0×231 vmbus! PipeStartChannel+0×9e vmbus! PipeAccept+0×81 vmbus! InstanceCreate+0×90 … nt! IopParseDevice+0×7b3 nt! ObpLookupObjectName+0×6d8 nt! ObOpenObjectByName+0×1e3 nt! IopCreateFile+0×372 nt! NtCreateFile+0×78 nt! KiSystemServiceCopyEnd+0×13 ntdll! NtCreateFile+0xa KERNELBASE! CreateFileInternal+0×30a KERNELBASE! CreateFileW+0×66 vmbuspipe! VmbusPipeClientOpenChannel+0×44 icsvc! ICTransportVMBus: ClientNotification+0×60 vmbuspipe! VmbusPipeClientEnumeratePipes+0×1ac icsvc! ICTransportVMBusClient: Open+0xe5 icsvc! ICEndpoint: Connect+0×66 icsvc! ICChild: Run+0×65 icsvc! ICKvpExchangeChild: Run+0×189 icsvc! ICChild: ICServiceWork+0×137 icsvc! ICChild: ICServiceMain+0×8f … У виртуальной машины активирован компонент Data Exchange.После установки галочки на компоненте Data Exchange и нажатия кнопки Apply root ОС через гипервызов HvPostMessage отправляет гостевой ОС сообщение с кодом CHANNELMSG\_OFFERCHANNEL (см. рис. 9). Переданные данные содержат GUID устройства, подключенного к VMBus как дочернее устройство (см. рис. 10). Далее гостевая ОС обрабатывает данные и вызывает функцию vmbus! InstanceDeviceControl.
Рис. 9. GUID устройства в сообщении
Рис. 10. Вывод ! devnode для устройства VMBus
Часть стека:
WINDBG\>kс Call Sitent! IoAllocateMdl vmbus! InstanceCloseChannel+0×22d (адрес возврата для функции, имя которой отсутствует в символах) vmbus! InstanceDeviceControl+0×118 … vmbkmcl! KmclpSynchronousIoControl+0xa7 vmbkmcl! KmclpClientOpenChannel+0×2a6 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).
Рис. 3. Вывод DbgView при обработке гипервизором обращений к MSR-регистрам
Функция ParseVmbusMessage (рис. 4).
Функция получает номер активного логического процессора, адрес страницы SIM и считывает значение четвертого слота SIM. Для примера разобраны сообщения CHANNELMSG\_OPENCHANNEL и CHANNELMSG\_GPADL\_HEADER, но в исходных кодах LIS можно увидеть формат всех типов сообщений и без труда дописать необходимые обработчики. Сообщения для шины VMBus обычно генерируются при включении/выключении виртуальной машины или же одного из компонентов Integration Services. Например, при включении компонента Data Exchange отладчик или DbgView покажет информацию, изображенную на рис. 5.
Рис. 5. Отладочный вывод сообщений при включении компонента Data Exchange
Integration Services — Data Exchange Далее рассмотрим, каким же образом происходит обмен данными между гостевой и root ОС на примере одного из компонентов служб интеграции — Data Exchange. Этот компонент позволяет root ОС считывать данные из определенной ветки реестра гостевой ОС. Для проверки в гостевой ОС создадим в ветке HKEY\_LOCAL\_MACHINE\\SOFTWARE\\Microsoft\\Virtual Machine\\Guest ключ со значением KvPDataValue (см. рис. 6).Рис. 6. Ключ KvPDataValue
Для получения значения ключа в root ОС был использован следующий PowerShell-скрипт (см. рис. 7).
Рис. 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).
Рис. 8. Результат выполнения скрипта
Рис. 11. Значение структуры MDL
Рис. 12. PFN в MDLструктуре
Рис. 13. Участок кода, отвечающий за копирование PFN в отдельный буфер
Рис. 14. Массив PFN, обрабатываемый гипервизором
Первые 16 байт — это общий заголовок сообщения, где, например, 0xF0 — размер тела сообщения, внутри размещается VMBus-пакет, в заголовке которого указан тип пакета — 8 (CHANNELMSG_GPADL_HEADER), rangecount равен 1, из чего следует, что в один пакет вместились все данные, которые было необходимо передать. В случае большого объема данных драйвер гостевой ОС разделил бы их на части и отправил отдельными сообщениями. Далее root ОС шлет сообщение CHANNELMSG\_OPENCHANNEL\_RESULT, затем гостевая ОС шлет CHANNELMSG\_OPENCHANNEL. После этого в root ОС отрабатывает Work Item (см. рис. 15).
Рис. 15. Вызов ChMapGpadlView
В ходе его выполнения вызывается vmbusr! ChMapGpadlView→vmbusr! PkParseGpaRanges, которой, в свою очередь, передается указатель на часть сообщения, содержащую размер буфера 0xC000 и PFN, переданные в сообщении CHANNELMSG\_GPADL\_HEADER. Далее происходит вызов vmbusr! XPartLockChildPagesSynchronous-\>vmbusr! XPartLockChildPages, после чего выполняется функция из драйвера vid.sys (имя функции неизвестно, поскольку символы для драйвера отсутствуют), которой в качестве второго параметра передается блок PFN, отправленный ранее в сообщении из гостевой ОС (см. рис. 16).
Рис. 16. Обработка гостевых PFN драйвером vid.sys
Непосредственно после возврата из функции в [rsp+30h] находится указатель на вновь созданную структуру MDL (см.рис. 17).
Рис. 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).
Рис. 19. Значение DPC, которую ставит в очередь XPartEnlightenedIsr
Из vmbusr! ParentRingInterruptDpc через несколько вызовов будет выполнена vmbusr! PkGetReceiveBuffer.
WINDBG\>k Child-SP RetAddr Call Site fffff800\`fcc1ea38 fffff800\`6cdc440c vmbusr! PkGetReceiveBuffer+0×2c fffff800\`fcc1ea40 fffff800\`6cdc41a7 vmbusr! PipeTryReadSingle+0×3c fffff800\`fcc1eaa0 fffff800\`6cdc4037 vmbusr! PipeProcessDeferredReadWrite+0xe7 fffff800\`fcc1eaf0 fffff800\`6c96535e vmbusr! PipeEvtChannelSignalArrived+0×63 fffff800\`fcc1eb30 fffff800\`6cdc4e3d vmbkmclr! KmclpVmbusManualIsr+0×16 fffff800\`fcc1eb60 fffff800\`fb2d31e0 vmbusr! ParentRingInterruptDpc+0×5d Если просмотреть эту область памяти, то становятся видны параметры гостевой ОС.
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+0×4e: 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/R0U52lgoo.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 с параметром 0×11114444. Для этого 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 = 0×0, NextPartID = 0; // Если в качестве параметра инструкции в RAX-инструкции CPUID будет передано значение 0×11114444, то гипервизор выполнит перехват и отправит сообщение родительскому разделу для обработки результата DbgPrintString («SetupInterception was called»); Parameters.CpuidIndex = 0×11114444; 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) находится число 0×11114444, записывала в поле DefaultResultRdx структуры HV_X64_CPUID_INTERCEPT_MESSAGE, расположенную в нулевом слоте SIM, значение 0×12345678:
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 == 0×11114444){ phvCPUID-\>DefaultResultRdx =0×12345678; DbgLog16(» phvCPUID-\>Header.Rip», phvCPUID-\>Header.Rip); DbgPrintString (» Interception was handled»); } } Для проверки в гостевой ОС запустим тестовую утилиту, которая вызывает CPUID с Eax, равным 0×11114444. До установки перехвата утилита выведет результат, отображенный на рис. 20.
Рис. 20. Результат инструкции CPUID на обычной гостевой ОС
После активации перехвата результат будет следующим (см. рис. 21).
Рис. 21. Результат инструкции CPUID после установки перехвата
При этом в root ОС будет выведено сообщение (см. рис. 22).
Рис. 22. Отладочный вывод обработки сообщения гипервизора при установленном перехвате
Сразу стоит обратить внимание на то, что этот трюк пройдет только в том случае, если root ОС ранее не установила перехваты для заданных условий. В этом случае после того, как драйвер hyperv заменит значение, управление перейдет на оригинальную WinHvOnInterrupt, которая вызовет функцию обработки из драйвера vid.sys (эта функция является четвертым параметром функции winhvr! WinHvCreatePartition, вызываемой в root ОС при создании дочернего раздела при включении виртуальной машины), что может привести к изменению результата. В нашем случае такой обработчик, разумеется, установлен не был, гипервизор проанализировал данные в нулевом слоте SIM и исправил результат инструкции CPUID.
В заключение Несмотря на то что после прочтения моего труда твой мозг наверняка встал в позу речного скорпиона (и если ты вообще досюда дочитал — респект тебе от всей нашей редакции)… так, я отвлекся. Эта статья получилась скорее обзорной, демонстрирующей работу некоторых функций и компонентов системы виртуализации Microsoft на примерах. Однако, надеюсь, эти примеры помогут лучше понять принципы работы этих компонентов и позволят более глубоко проанализировать безопасность, например VMBus, написав свой собственный фаззер.Автор: gerhart
Впервые опубликовано в журнале «Хакер» от 12/2014.
Подпишись на «Хакер»