Содержание статьи
Со времени публикации первой части статьи глобально в мире ничего не изменилось: Земля не наскочила на небесную ось, все так же растет популярность облачных сервисов, все так же в гипервизоре компании 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 <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).
Хакер #191. Анализ безопасности паркоматов
Функция получает номер активного логического процессора, адрес страницы SIM и считывает значение четвертого слота SIM. Для примера разобраны сообщения CHANNELMSG_OPENCHANNEL и CHANNELMSG_GPADL_HEADER, но в исходных кодах LIS можно увидеть формат всех типов сообщений и без труда дописать необходимые обработчики. Сообщения для шины VMBus обычно генерируются при включении/выключении виртуальной машины или же одного из компонентов Integration Services. Например, при включении компонента Data Exchange отладчик или DbgView покажет информацию, изображенную на рис. 5.
Integration Services — Data Exchange
Далее рассмотрим, каким же образом происходит обмен данными между гостевой и root ОС на примере одного из компонентов служб интеграции — Data Exchange. Этот компонент позволяет root ОС считывать данные из определенной ветки реестра гостевой ОС. Для проверки в гостевой ОС создадим в ветке HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Virtual Machine\Guest ключ со значением KvPDataValue (см. рис. 6).
Для получения значения ключа в root ОС был использован следующий PowerShell-скрипт(см. рис. 7).
Скрипт вернет значение ключа 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).
Первые 16 байт — это общий заголовок сообщения, где, например, 0xF0 — размер тела сообщения, внутри размещается VMBus-пакет, в заголовке которого указан тип пакета — 8 (CHANNELMSG_GPADL_HEADER), rangecount равен 1, из чего следует, что в один пакет вместились все данные, которые было необходимо передать. В случае большого объема данных драйвер гостевой ОС разделил бы их на части и отправил отдельными сообщениями. Далее root ОС шлет сообщение CHANNELMSG_OPENCHANNEL_RESULT, затем гостевая ОС шлет CHANNELMSG_OPENCHANNEL. После этого в root ОС отрабатывает Work Item (см. рис. 15).
В ходе его выполнения вызывается vmbusr!ChMapGpadlView->vmbusr!PkParseGpaRanges, которой, в свою очередь, передается указатель на часть сообщения, содержащую размер буфера 0xC000 и PFN, переданные в сообщении CHANNELMSG_GPADL_HEADER. Далее происходит вызов vmbusr!XPartLockChildPagesSynchronous-\>vmbusr!XPartLockChildPages, после чего выполняется функция из драйвера vid.sys (имя функции неизвестно, поскольку символы для драйвера отсутствуют), которой в качестве второго параметра передается блок PFN, отправленный ранее в сообщении из гостевой ОС (см. рис. 16).
Непосредственно после возврата из функции в [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).
Из 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.
После активации перехвата результат будет следующим (см. рис. 21).
При этом в root ОС будет выведено сообщение (см. рис. 22).
Сразу стоит обратить внимание на то, что этот трюк пройдет только в том случае, если root ОС ранее не установила перехваты для заданных условий. В этом случае после того, как драйвер hyperv заменит значение, управление перейдет на оригинальную WinHvOnInterrupt, которая вызовет функцию обработки из драйвера vid.sys (эта функция является четвертым параметром функции winhvr!WinHvCreatePartition, вызываемой в root ОС при создании дочернего раздела при включении виртуальной машины), что может привести к изменению результата. В нашем случае такой обработчик, разумеется, установлен не был, гипервизор проанализировал данные в нулевом слоте SIM и исправил результат инструкции CPUID.
В заключение
Несмотря на то что после прочтения моего труда твой мозг наверняка встал в позу речного скорпиона (и если ты вообще досюда дочитал — респект тебе от всей нашей редакции)… так, я отвлекся. Эта статья получилась скорее обзорной, демонстрирующей работу некоторых функций и компонентов системы виртуализации Microsoft на примерах. Однако, надеюсь, эти примеры помогут лучше понять принципы работы этих компонентов и позволят более глубоко проанализировать безопасность, например VMBus, написав свой собственный фаззер.