В статье продемонстрированы примеры использования функциональности аппаратной виртуализации процессоров Intel для реализации Anti-Vm Detect для VMware и трассировки кода режима ядра.
VMCS (virtual-machine control structure) – структура, определяющая логику работы гипервизора (условия, при которых происходит VmExit; адрес процедуры, вызываемой при VmExit и т.д.). Поля структуры описаны в приложении B к Intel SDM, том 3.
VMX root – режим, в котором работает гипервизор.
VMX non-root – режим, в котором работает операционная система и обслуживаемое ею прикладное программное обеспечение.
VM exit – переход из VMX non-root в VMX root. Происходит при выполнении инструкций или условий, заданных в VMCS или заложенных непосредственно в логику работы процессора. В результате этого перехода процессор заполняет некоторые поля VMCS (например, поле GUEST_RIP и поле EXIT_REASON) и продолжает выполнение кода с адреса, указанного в поле HOST_RIP.
Vm entry – переход из VMX root в VMX non-root. Происходит при вызове инструкций VMLAUNCH или VMRESUME.
Начиная с 8-й версии VMWare Workstation (при условии поддержки процессором EPT) поддерживает расширения аппаратной виртуализации внутри виртуальной машины (подробности здесь: http://www.veeam.com/blog/nesting-hyper-v-with-vmware-workstation-8-and-esxi-5.html), что даёт возможность полноценной отладки гипервизора в виртуальной среде.
Тестирование кода производилась в виртуальной машине под управлением Windows 7 SP1 x86, которую необходимо настроить следующим образом:
Теоретически, при выполнении таких настроек драйвер может работать и на обычном, не виртуальном, железе.
Для сборки драйвера достаточно запустить Windows 7 x86 Free Build Environment из WDK 7.1, перейти в каталог с исходными кодами драйвера и выполнить команду build.
Перед запуском драйвера необходимо запустить DbgView от Sysinternals для просмотра отладочных сообщений.
Для загрузки драйвера надо запустить loader.exe (грузит любой драйвер с именем test.sys, расположенный в той же директории). Если загрузка прошла успешно, появится сообщение «test.sys loaded. Ok to unload». Однако если нажать на «ОК» ничего не произойдёт, т.к выгрузка драйвера не реализована. Также драйвер можно грузить любым лоадером, например, Osrloader.
I/O bitmap
25-й бит Use I/O bitmaps поля VMCS primary processor-based VM-execution controls определяет возможность контроля операций с портами ввода\вывода гипервизором.
Также VMCS имеет 2 поля, каждое из которых содержит 64-битный физический адрес региона памяти. Эти регионы называются I/O bitmap A и I/O bitmap B, размер каждого региона – 4 КБайта. Регион I/O bitmap A контролирует диапазон портов ввода\вывода с 0 по 7FFFh, регион I/O bitmap A – с 8000h по FFFFh.
IN, INS/INSB/INSW/INSD, OUT, OUTS/OUTSB/OUTSW/OUTSD – инструкции, выполнение которых можно контролировать.
При появлении необходимости контроля операций, производимых с определённым портом, необходимо, во-первых установить бит Use I/O bitmaps в 1, во-вторых выставить соответствующий бит в I/O bitmaps регионах. Например, если необходимо контролировать порт с номером 100, то в I/O bitmap A сотый по счёту бит устанавливается в 1, а все биты I/O bitmap B - в 0.
Необходимость указания ErrorCode зависит от обрабатываемого исключения и указана в Intel SDM, том 3, глава 6 «Interrupt and Exception Handling. Exception and interrupt Reference». Определение структуры:
typedef struct{
unsigned Vector:8;
unsigned InterruptionType:3;
unsigned DeliverErrorCode:1; Если 1, то через поле VM_ENTRY_EXCEPTION_ERROR_CODE
дополнительно указывается код ошибки.
unsigned Reserved:19;
unsigned Valid:1; если 0, то Event Injection не используется
}INTERRUPTION_INFORMATION_FIELD, *PINTERRUPTION_INFORMATION_FIELD;
Например, если из гипервизора управление необходимо передать обработчику DEBUG EXCEPTION операционной системы, то значения элементов структуры будет выглядеть следующим образом:
pinject_event->Vector = DEBUG_EXCEPTION;
pinject_event->InteruptionType = HARDWARE_EXCEPTION;
pinject_event->DeliverErrorCode = 0;
pinject_event->Valid = 1;
Если необходимо контролировать Page Fault Exception (далее - PF), то дополнительно необходимо указывать значения полей page-fault error-code mask (PFEC_MASK) и pagefault error-code match (PFEC_MATCH).
При каждом PF процессор будет побитно сравнивать значения в левой и правой части выражения:
PFEC & PFEC_MASK = PFEC_MATCH
Где PFEC – это PF Error Code, возможные значения которого приведены в Intel SDM, том 3, рисунок 4-12. Page-Fault Error Code.
При совпадении произойдёт VM exit.
Описание работы драйвера
При загрузке драйвер вычисляет необходимые параметры для работы гипервизора и для каждого процессора в системе выполняет процедуру RunVirtualMachine, основным назначением которой является запуск гипервизора и настройка VMCS. Тем не менее, драйвер работает только на однопроцессорной системе, о чём упоминает автор в readme к архиву с исходным кодом.
Для детального изучения процесса запуска гипервизора всё же лучше начать, например, с просмотра процедуры SetupVMCS в virtdbg, исходный код которого гораздо лучше структурирован.
Режим работы гипервизора зависит от значения переменной ControlMode в loadvm.c. Возможные значения переменной:
enum CONTROL_MODE {
KERNEL_TRACE_MODE, трассировка драйверов
NTAPI_TRACE_MODE, контроль выполнения NtCreateFile
IO_CONTROL_MODE, anti VM detect
};
1. Контроль выполнения I/O инструкций (anti-VM detect)
Довольно широко известен метод детектирования исполнения кода внутри виртуальных машин производства VMware посредством обращения к определённому порту ввода\вывода. Простейший вариант обнаружения VM может быть представлен в виде следующего кода
mov eax, 0x564D5868
mov ebx,0
mov ecx, 0x0A
mov edx, 0x5658
in eax,dx
cmp ebx, 0x564D5868
je VMWare_detected
Если приложение работает внутри VmWare, то после выполнения инструкции in eax,dx в ebx окажется число 564D5868h. Если приложение выполняется на физическом хосте или же внутри VirtualBox (на виртуальных машинах других производителей тестирование не производилось), то в результате выполнения инструкции in eax,dx Windows сгенерирует исключение PRIVILEGED_INSTRUCTION.
Чтобы получить аналогичный результат внутри VMware потребуется модифицировать драйвер таким образом, чтобы любое обращение к порту 5658h приводило к возникновению VM exit и передаче управления гипервизору:
в I/O bitmap A бит с номером 22104 (5658h) устанавливается в 1, все остальные биты - в 0. Все биты в I/O bitmap B равны 0:
memtmp = MmAllocateNonCachedMemory(4096); выделяем память. Выравнивание по 4 КБ границе
происходит автоматически.
memset(memtmp, 0, 4096); обнуляем регион
*((PUINT8)memtmp+2763) = 0x1; устанавливаем нужный бит
PhysicalIOBitmapA[i] = MmGetPhysicalAddress(memtmp); записываем физический адрес региона
в VMCS поле. i содержит номер логического процессора (в нашем случае всегда 0, т.к.
система однопроцессорная и одноядерная).
memtmp = MmAllocateNonCachedMemory(4096);
memset(memtmp, 0, 4096);
PhysicalIOBitmapB[i] = MmGetPhysicalAddress(memtmp);
Далее в процедуру RunVirtualMachine необходимо добавить код, который активирует обработку I/O bitmap:
if (IO_CONTROL_MODE == TRUE)
{
dummy = VmRead(CPU_BASED_VM_EXEC_CONTROL) | CPU_BASED_ACTIVATE_IO_BITMAP;
VmWrite(CPU_BASED_VM_EXEC_CONTROL,dummy);
VmWrite(IO_BITMAP_A,PhysicalIOBitmapA[current_cpu].LowPart);
VmWrite(IO_BITMAP_B,PhysicalIOBitmapB[current_cpu].LowPart);
}
В процедуру VM_Handler и HandleVMX необходимо добавить следующие блоки кода:
HandleVMX:
VM_Handler:
После VM exit в eax содержится номер BASIC_EXIT_REASON, по которому можно установить причину VM exit (Таблица C-1. Basic Exit Reasons Intel SDM. Том 3). Далее внутри HandleVMX происходит обработка в зависимости от BASIC_EXIT_REASON.
Логика обработки следующая:
При выполнении инструкции in eax,dx происходит VM exit и управление передаётся на процедуру VMHandler и далее в HandleVMX. В этой процедуре происходит анализ BASIC_EXIT_REASON (при выполнении I/O инструкции это будет EXIT_REASON_IO_INSTRUCTION (30)). При обработке необходимо учитывать, на каком уровне привилегий произошёл VM exit – в пользовательском режиме или в режиме ядра. Если VM exit произошёл во время выполнения I/O инструкции в режиме ядра, то достаточно проверить параметры, передаваемые I/O инструкции, чтобы удостовериться в том, что драйвер пытается обнаружить VMware:
if ((x86->regEax == 0x564D5868) && (x86->regEdx == 0x5658) && (x86->regEcx == 0xA))
{
DbgLog("EXIT_REASON_IO_INSTRUCTION.KM.VMWare Magic Number Check", vMCS.GUEST_RIP);
VMMagicNumberDetect = TRUE;
}
x86 – это структура, содержащая значения основных регистров процессора непосредственно перед VM exit. Далее нужно выполнить I/O инструкцию. Если просто вернуться обратно на эту же инструкцию, то VM exit будет выполнен снова, и система впадёт в бесконечный цикл. Для того, чтобы избежать зацикливания, перед возвратом из гипервизора необходимо временно деактивировать контроль над выполнением I/O инструкций:
dummy = VmRead(CPU_BASED_VM_EXEC_CONTROL);
Cразу же после выполнения I/O инструкции необходимо снова активировать I/O bitmap, иначе будут пропущены последующие возможные попытки обнаружения работы внутри VMware. Для этого будет использоваться MTF:
dummy &= ~CPU_BASED_ACTIVATE_IO_BITMAP; выключаем IO control
VmWrite(CPU_BASED_VM_EXEC_CONTROL,dummy); записываем новое значение в VMCS
В случае если VM exit был произведён при выполнении I/O инструкции в пользовательском режиме, то обработка происходит несколько иначе:
if ((x86->regEax == 0x564D5868) && (x86->regEdx == 0x5658) && (x86->regEcx == 0xA))
{
DbgLog("EXIT_REASON_IO_INSTRUCTION. UM. VMWare Magic Number Check",
vMCS.GUEST_RIP); выводим адрес инструкции
VMMagicNumberDetect = TRUE; для последующего обнуления ebx
VmWrite(GUEST_RIP, (ULONG) vMCS.GUEST_RIP); после VM entry инструкция, вызвавшая
Vm exit, будет исполнена снова
dummy = 0;
pinject_event = (PINTERUPTION_INFORMATION_FIELD)&dummy;формируется Event Injection
pinject_event->Vector = GENERAL_PROTECTION_EXCEPTION; Номер вектора 14
pinject_event->InteruptionType = HARDWARE_EXCEPTION;т.к. GP
pinject_event->DeliverErrorCode = 1;раздел 6.15, GP
pinject_event->Valid = 1;
VmWrite(VM_ENTRY_INTR_INFO_FIELD, dummy);
dummy = 0;
VmWrite(VM_ENTRY_EXCEPTION_ERROR_CODE, dummy);в VMCS записывается Error Code
}
Для эмуляции исключения PRIVILEGED_EXCEPTION необходимо выполнить Event Injection для GENERAL_PROTECTION_EXCEPTION с указанием кода ошибки - 0. В этом случае после VM entry управление будет передано процедуре обработки GENERAL_PROTECTION_EXCEPTION KiTrap0D, которая, в свою очередь, самостоятельно проанализирует инструкцию, адрес которой указан в GUEST_RIP, обнаружит, что она является инструкцией ввода\вывода и затем доставит ошибку PRIVILEGED_INSTRUCTION приложению.
Если ошибиться и указать GUEST_RIP неправильно, то исключение будет другого типа (ACCESS_VIOLATION)
Также можно не доставлять исключение, а просто обнулить EBX перед возвратом управления приложению, но это зависит от того, каким образом происходит анализ выполнения внутри VMware самим приложением.
К примеру, если использовать следующий код для проверки, то в гипервизоре достаточно непосредственно перед выполнением vmresume обнулить ebx:
Без гипервизора результат будет:
С активным гипервизором:
P.S. Если для отладки драйвера используется VirtualKD, то система будет зависать. Это связано с тем, что библиотека kdbazis.dll, устанавливаемая в гостевой системе и работающая в kernel mode, также использует порт 5658h для работы.
2. Контроль вызовов Native API (на примере NtCreateFile)
Обнуление Present Bit в PTE страницы памяти, на которой размещается первая инструкция NtCreateFile, приводит к возникновению Page Fault Exception, который и будет обрабатываться гипервизором. Включается MTF и после MTF VM exit, происходящем после выполнения каждой инструкции, производится анализ выполнения кода в пределах заданной страницы. Как только начнёт выполняться код, расположенный на другой странице, Present Bit предыдущей страницы сбрасывается в 0, а MTF выключается.
Драйвер компилируется с поддержкой Exception Bitmap для PF:
if ((ControlMode == KERNEL_TRACE_MODE) | (ControlMode == USER_TRACE_MODE) | (ControlMode
== NTAPI_TRACE_MODE)) Режим трассировки
{
VmWrite(EXCEPTION_BITMAP, 0x4000); PageFault VMExit enable
VmWrite(PAGE_FAULT_ERROR_CODE_MASK, 0);
VmWrite(PAGE_FAULT_ERROR_CODE_MATCH, 0);
}
Для отправки IOCTL можно использовать приложенную к исходникам утилиту io.exe:
При получении IOCTL NTAPI_ACCESS драйвер вызывает функцию CreateKernelAccessAddressTable, которая ищет адрес NtCreateFile в SDT и передаёт его функции NtapiCreateDriverPageTable, заполняющую таблицу DriverPageTable (состояющую в данном случае из одного элемента, содержащего адрес NtCreateFile). Каждый элемент таблицы - это структура, содержащая указатель на PTE, виртуальный адрес начала страницы памяти NtCreateFile, а также непосредственно виртуальный адрес начала NtCreateFile. Затем функция CreateKernelAccessAddressTable вызывает DisablePresentBitDriverTable, которая для всех элементов таблицы DriverPageTable обнуляет Present Bit PTE, что, соответственно, приводит к PF при любой попытке доступа к этим страницам. Также в этой процедуре определяется минимальный и максимальный виртуальный адрес для множества всех страниц элементов DriverPageTable, что позволяет несколько увеличить скорость обработки PF в случае, если страницы памяти расположены близко друг к другу.
Также не следует забывать о том, что даже при отключенном файле подкачки PF исключения всё равно постоянно генерируются операционной системой и требуют соответствующей обработки. Если причиной PF является попытка доступа к страницам в DriverPageTable, то его необходимо обработать таким образом, чтобы ядро Windows не узнало о возникшем исключении. В ином случае информация о PF должна быть доставлена ОС через Event Injection.
При возникновении PF происходит VM exit и в HandleVMX обработчик PF cчитывает необходимые для обработки PF поля VMCS:
EXIT_QUALIFICATION – содержит адрес памяти, попытка доступа к которому вызвала PF. Если PF происходит при попытке выполнения кода в недоступной странице, то в этом случае EXIT_QUALIFICATION равен GUEST_RIP.
GUEST_RIP – адрес инструкции, выполнение которой вызвало PF.
Далее происходит проверка попадания EXIT_QUALIFICATION в диапазон заблокированных страниц. Если EXIT_QUALIFICATION находится вне пределов диапазона, то обработка прерывается и исключение доставляется до ОС через Event Injection.
if (((ExitQualification[ccpu] > DriverPageTable.maxVirtualAddress+MMU_PAGE_SIZE)) |
(ExitQualification[ccpu] < DriverPageTable.minVirtualAddress))
break;
Если адрес памяти, доступ к которому вызвал PF, входит в указанный диапазон, то происходит проверка попадания адреса в страницы из таблицы DriverPageTable. Если адрес находится в одной из страниц, то происходят дополнительные проверки:
if (vMCS.GUEST_RIP == DriverPageTable.DrvPageInfo[i].NtapiVirtualAddress)
Если адрес совпал с адресом NtCreateFile, то выводится отладочное сообщение о вызове этой функции и в качестве дополнительной информации выводится имя объекта из структуры OBJECT_ATTRIBUTES, являющейся одним из параметров функции. Для отображения POBJECT_ATTRIBUTES, расположенной в пользовательском адресном пространстве, выполняется функция vmm_memcpy_objattr, которая копирует имя объекта из пользовательского адресного пространства в адресное пространство ядра, временно изменяя значение регистра CR3 на значение поля GUEST_CR3 из VMCS.
Если адрес находится в одной из страниц в DriverPageTable, но не совпадает с началом NT-сервиса ядра, то Present Bit этой страницы устанавливается в 1 и включается MTF для того, чтобы отследить, когда закончит выполняться код с этой страницы.
Далее при выполнении каждой инструкции происходит VM exit с EXIT_REASON = EXIT_REASON_MONITOR_TRAP_FLAG (37) до тех пор, пока GUEST_RIP не выйдет за пределы страницы, попытка доступа к которой сгенерировала PF.
В итоге в DbgView можно получить следующую картину:
Метод сильно снижает производительность системы и его применение имеет смысл только в том случае, когда по каким-то причинам нельзя изменять SDT или использовать сплайсинг.
3. Трассировка кода
Если слегка изменить логику работы гипервизора из предыдущего примера, то можно получить трассу выполняемого кода.
Трассировка кода в ring-0.
При получении IOCTL DRIVER_TRACE драйвер в первую очередь создаёт отдельный поток, который будет записывать результаты трассировки в файл C:\data.trace, формат которого совместим с визуализатором трасс кода, генерируемых Intel PIN framework и гипервизорным трассировщиком на базе XEN Ether, под названием VERA, разработанным Danny Quist’ом.
Далее драйвер, используя функцию WindowsGetDriverCodeSection, получает виртуальный адрес и размер секции кода драйвера.
Затем вызывается функция CreateDriverPageTable, которая заполняет уже знакомую по предыдущему примеру таблицу DriverPageTable исходя из адреса и размера кода, который необходимо трассировать. Далее функция DisablePresentBitDriverTable обнуляет Present Bit PTE страниц из таблицы DriverPageTable.
Можно задать несколько секций одного или нескольких драйверов последовательно вызывая эти функции.
После выполнения вышеописанных действий доступ к таким страницам генерирует PF, при обработке которого включается MTF. После каждого MTF VM exit в массив RawTraceBuffer добавляется значения GUEST_RIP из VMCS.
Если размер буфера достигнет предела, определяемого TRACE_COUNTER_LIMIT, то трассировка прекратится, Present Bit PTE всех страниц из таблицы DriverPageTable установится в 1, содержимое RawTraceBuffer сбросится в файл функцией WriteTraceFile, выполняемой в отдельном потоке и ожидающей, когда переменная EndOfTrace станет TRUE.
if (EndOfTrace == FALSE)
{
RawTraceBuffer[TraceCounter] = (ULONG)vMCS.GUEST_RIP;
TraceCounter++;
if (TraceCounter == TRACE_COUNTER_LIMIT)
{
EnablePresentBitDriverTable();
EndOfTrace = TRUE;
DbgPrint("Trace finished\n");
}
}
Остановить трассировку и сбросить данные в файл можно принудительно, если отправить драйверу IOCTL WRITE_TRACE_FILE.
Для примера посмотрим трассировку драйвера srv2.sys, выполненную при старте службы lanmanserver:
s_info = WindowsGetDriverCodeSection(s_info,"srv2.sys",".text");
CreateDriverPageTable(s_info);
DisablePresentBitDriverTable();
s_info = WindowsGetDriverCodeSection(s_info,"srv2.sys","PAGE");
CreateDriverPageTable(s_info);
DisablePresentBitDriverTable();
Трассировке подвергнется код, размещённый в секциях .text и PAGE. Для проверки предварительно нужно остановить службу lanmanserver: net stop lanmanserver.
После загрузки драйвера гипервизора и отправки ему IOCTL DRIVER_TRACE будет выведена информация об адресе загрузки драйвера:
Driver Load base cpu:[0][8D0DA000]
Она пригодится для загрузки драйвера в IDA PRO.
Далее служба lanmanserver запускается обратно: net start lanmanserver. В DbgView появится следующая информация
После отправки драйверу IOCTL WRITE_TRACE_FILE вся данные из буфера RawTraceBuffer сбросятся в файл C:\data.trace:
Файл data.trace лучше скопировать на физическую машину, т.к. VERA для отображения графа будет активно использовать ресурсы видеоадаптера.
После запуска wxVera.exe необходимо открыть файл data.trace, указать трассируемый драйвер и путь для сохранения gml-файла:
Получится подобный граф:
Для настройки взаимодействия VERA c IDA Pro необходимо:
vera_ida: successfully connected to 127.0.0.1:2005
После этого при щелчке правой кнопкой мыши на адресе в графе, отображаемом VERA, в окне Ida View происходит переход к инструкции, расположенной по этому адресу.
Например, на графе хорошо заметны циклы:
Щёлкнув ПКМ на 8d0f07e6 попадаем в
По перекрёстным ссылкам можно увидеть, что функция SrvLibGetDWord используется при считывании параметров службы из реестра:
Подробности использования VERA изложены в презентации автора (http://www.shmoocon.org/2012/presentations/Danny_Quist-3dmalware-shmoocon2012.pdf):
Трассировка кода в ring-3. В настоящее время разработано огромное количество разнообразных трассировщиков ring-3 приложений, так что выдумывать ещё один является довольно бессмысленным занятием. В драйвере реализована возможность трассировки кода в жёстко заданном диапазоне адресов 403000 – 403FFF. Для проверки драйвер нужно скомпилировать с ControlMode = USER_TRACE_MODE, запустить утилиту cpuid_loader_mod.exe и указать ей приложение для запуска (На Windows 7 ASLR не даст загрузить приложение со стандартной базой 400000, так что с помощью PE Tools у тестового приложения можно отключить релоки).
Если появятся вопросы или замечания, пишите на gerhart xakep ru.
Скачать архив с файлами к этой статье можно по адресу: http://www.securitylab.ru/upload/files-427855.zip
Источники:
Цифровые следы - ваша слабость, и хакеры это знают. Подпишитесь и узнайте, как их замести!