Киберпреступники постоянно совершенствуют методы атак, используя среди прочего знания о принципах работы систем защиты. Например, появилось целое направление техник обхода песочниц: такие методы позволяют определять, что вредоносное ПО выполняется в контролируемой виртуальной среде, и, исходя из этого, менять его поведение или завершать работу. К наиболее популярным техникам из указанной категории можно отнести проверки:
бита гипервизора в регистре ECX инструкции CPUID(1);
имени гипервизора в результате выполнения инструкции CPUID(0x40000000);
имени текущего пользователя или компьютера по списку;
MAC-адресов сетевых адаптеров, присутствующих в системе;
количества ядер процессора, общего объема оперативной памяти, размера жесткого диска, разрешения экрана;
наличия файлов, путей реестра, сервисов, драйверов и процессов, специфичных для виртуальных сред;
времени, прошедшего с момента последнего запуска системы.
CPUID — инструкция, позволяющая получить основную информацию о процессоре (например, его модель, поддерживаемые расширения и инструкции). Подробнее можно узнать на сайте Microsoft или в спецификации производителя конкретного процессора.
Детальнее о техниках обхода песочниц в таргетированных атаках — в другой нашей статье. А сегодня поговорим о более продвинутых временных атаках, использующих особенности работы гипервизора для детектирования виртуального окружения. Механизм борьбы с ними реализован в PT Sandbox.
Что такое временные атаки
Главное в такой атаке ― замерить время выполнения определенных действий в виртуальной среде и сравнить его с результатами, полученными при выполнении тех же действий на реальном устройстве. Например, реализация инструкции CPUID на реальном компьютере в среднем занимает 100–200 процессорных тиков, а на виртуальной машине (VM) — более 2000 тиков в зависимости от используемого гипервизора. Чтобы понять, откуда появляется такая разница, обратимся к спецификации Intel.
Согласно документации, CPUID является одной из инструкций, которые всегда вызывают выход в гипервизор из VM (VM exit). Гипервизор эмулирует эту инструкцию, что сильно увеличивает время ее обработки. Пример обработчика CPUID есть в проекте Xen.
Из описанного выше становится ясно, что вызывает такую разницу. Результат измерения представляет собой суммарное время, необходимое:
для переключения с гостевой VM на гипервизор;
выполнения обработчика и функций, которые он вызывает в процессе;
возврата из гипервизора в гостевую VM.
В качестве одного из методов замера времени может использоваться инструкция RDTSC.
Инструкция RDTSC считывает состояние внутреннего счетчика временных меток TSC, который содержит количество тиков процессора с последнего получения сигнала RESET. Результат возвращается в регистрах EDX:EAX в виде 64-битного значения. Подробнее о счетчике TSC можно узнать из Википедии.
Примеры временных проверок In The Wild
Примеры временных проверок можно найти в открытых инструментах Pafish и Al-Khaser. В обоих присутствует проверка с замером времени выполнения CPUID с той лишь разницей, что Pafish вызывает Sleep после каждого этапа. Это повышает надежность проверки: пока текущий поток приостановлен, планировщик ресурсов ОС может выделить квант времени на выполнение других потоков. Таким образом снижается вероятность переключения на другой поток или процесс в ходе замера.
bool rdtsc_diff_vmexit()
{
uint64_t tsc1 = 0;
uint64_t tsc2 = 0;
uint64_t avg = 0;
int cpuInfo[4] = {};
// Try this 10 times in case of small fluctuations
for (int i = 0; i < 10; i++)
{
tsc1 = __rdtsc();
__cpuid(cpuInfo, 0);
tsc2 = __rdtsc();
// Get the delta of the two RDTSC
avg += (tsc2 - tsc1);
Sleep(500); // Not present in Al-Khaser
}
// Process repeated 10 times to make it more reliable
avg = avg / 10;
return (avg < 1000 && avg > 0) ? FALSE : TRUE;
}
В Pafish можно также найти другой вариант проверки, в котором не используется CPUID. Она рассчитана на гипервизоры, эмулирующие счетчик TSC.
bool rdtsc_diff()
{
uint64_t avg = 0;
for (int i = 0; i < 10; i++) {
tsc1 = __rdtsc();
avg += __rdtsc() - tsc1;
Sleep(500);
}
avg = avg / 10;
return (avg < 750 && avg > 0) ? FALSE : TRUE;
}
Если CPUID всегда вызывает VM exit, то для RDTSC такое поведение опционально. Согласно спецификации Intel, RDTSC тоже может вызывать выход в гипервизор при условии, что включен флаг RDTSC_EXITING. Этот факт пригодится нам позднее.
Временные проверки в ВПО
FatalRat единоразово проверяет разницу между двумя последовательными считываниями счетчика TSC на превышение 255 тиков.
IcedID использует слегка другой подход. В цикле измеряется и аккумулируется время на выполнение последовательностей RDTSC → CPUID → RDTSC и RDTSC → RDTSC, после чего вычисляется их отношение. При этом для повышения надежности проверки применяется вызов SwitchToThread() (аналогичный Sleep в Pafish).
GuLoader использует две временных проверки. Первая заключается в замере времени выполнения все того же CPUID, однако в качестве источника используется не счетчик TSC, а поле SystemTime из структуры KUSER_SHARED_DATA (см. документацию Microsoft). Это возможно, потому что такая структура всегда расположена по фиксированному адресу.
Вторая проверка похожа на те, что были показаны ранее, но вызов RDTSC находится в отдельной функции. Замеры выполняются 100 000 раз, на каждом этапе результат добавляется к общему. Кроме того, отдельно проверяются случаи, когда дельта составила менее 49 тиков.
По итогам замеров с помощью сравнения проверяется, что:
количество раз, когда дельта была менее 49 тиков, не превышает 60 000;
сумма результатов не превышает 110 000 000 тиков.
Интересным также является тот факт, что проверки выполняются, пока не будут успешно пройдены, то есть до получения результата, характерного для реального устройства. За счет этого в некоторых песочницах GuLoader бесконечно работает вхолостую, не доходя до развертывания полезной нагрузки.
Существуют и более продвинутые варианты атак. Прочитать о них можно, например, в докладе My Ticks Don’t Lie: New Timing Attacks for Hypervisor Detection, который был представлен на конференции Black Hat.
Если резюмировать описанное выше, можно сделать несколько выводов:
В большинстве ВПО и инструментов для совершения атак используется совокупность вызовов RDTSC и CPUID.
Почти всегда эти вызовы расположены в рамках одной функции, идут подряд или на небольшом расстоянии друг от друга, чтобы минимизировать шанс переключения контекста во время проверки.
Инструкция RDTSC может вызывать VM exit при включенном флаге RDTSC_EXITING.
Сокрытие VM от временных атак
Существуют разные методы обхода временных атак. Попробуем рассмотреть один из вариантов сокрытия VM на базе связки Xen и DRAKVUF. Однако прежде нужно найти ответы на следующие вопросы:
как различать процессы, вызывающие VM exit на уровне гипервизора;
как переключать флаг RDTSC_EXITING в Xen;
как сдвигать время для конкретного ядра VM;
когда требуется его сдвигать.
Регистр CR3
Большинство современных процессоров используют четырехуровневую адресацию при работе в 64-разрядном режиме. Вместе с тем регистр CR3 содержит физический адрес начала таблицы PML4, используемой для трансляции физического адреса в виртуальный. Подробнее об этом процессе можно узнать в статьях Exploring Virtual Memory and Page Structures и Turning the Pages: Introduction to Memory Paging on Windows 10 x64.
Поскольку каждый процесс работает в рамках своего виртуального адресного пространства, то и адреса PML4 в CR3 будут уникальными.
Манипуляции со временем в Xen
Xen поддерживает два режима работы со счетчиком TSC: нативный и режим эмуляции. Переключение между ними осуществляется за счет установки ранее упомянутого флага RDTSC_EXITING (далее — ловушка). Включить или отключить ловушку можно так:
vmx_set_rdtsc_exiting(v, 1); // v — vCPU where flag should be enabled
Добавим обработчик, который будет заполнять регистры EDX:EAX заданным временем:
void hvm_rdtsc_intercept_fixed(struct cpu_user_regs *regs, uint64_t set_time)
{
// We already know the TSC value that we have to write
msr_split(regs, set_time);
HVMTRACE_2D(RDTSC, regs->eax, regs->edx);
}
Помимо результата в регистрах также требуется подменить и синхронизировать время на ядре, чтобы при отключении ловушки изменения сохранились на гостевой ОС. Делается это путем модификации поля TSC offset. Спецификация Intel дает четкое представление о том, как и в каких случаях оно используется.
То есть, чтобы установить время для конкретного ядра, требуется:
отключить флаг RDTSC_EXITING;
рассчитать новое время на основании текущего значения TSC offset и получить дельту;
вычесть дельту из значения TSC offset или добавить ее в зависимости от того, в какую сторону требуется сдвинуть счетчик.
В коде это выглядит следующим образом:
static void try_set_tsc_offset(struct vcpu *v)
{
if (!v->ts.override_tsc)
return;
// Turn off RDTSC trap
disable_hook(v);
// Calculate delta between current time and the time we want to set
delta_to_set = hvm_get_guest_tsc(v) - v->ts;
// Subtract delta from currently cached tsc_offset
v->arch.hvm.cache_tsc_offset -= delta_to_set;
// Write new tsc_offset to vCPU
vmx_set_tsc_offset(v, v->arch.hvm.cache_tsc_offset, 0);
// Not updating system time might cause VM to crash after a while
if (v == current)
force_update_vcpu_system_time(v);
}
Важный момент: поскольку установка счетчика и его синхронизация между гипервизором и VM также занимают некоторое время, идеально точная настройка не представляется возможной.
Логика работы сокрытия
По умолчанию в Xen инструкция RDTSC выполняется нативно. Это означает, что последовательные проверки RDTSC → RDTSC инструменту нестрашны. Соответственно, в первую очередь будет рассмотрен вариант, при котором используется CPUID.
После каждого выхода по CPUID сохраняется текущее время гостевой VM, состояние регистров (CR3, RBP, RIP) и включается ловушка RDTSC, если она не была включена ранее. CR3 используется как уникальный идентификатор процесса, RBP — как идентификатор потока или функции, RIP — для определения принадлежности инструкций к одному блоку кода. Требуется учитывать тот факт, что время, затрачиваемое на переход из гостевой VM в гипервизор, неизвестно, и корректировать сохраняемое значение.
На последующих выходах проверяется, что:
причиной выхода является RDTSC;
выход произошел из того же процесса;
выход произошел из того же потока или функции;
выход произошел не дальше, чем за N байт от сохраненных значений.
Если все условия выполнены, среднее время добавляется к сохраненному времени нативного выполнения RDTSC и устанавливается в регистры EDX:EAX, а перед возвратом исполнения кода в VM также обновляется TSC offset. Далее приведены упрощенные функции — обработчики логики для сохранения и перезаписи значений:
Функция, отвечающая за общую логику, заполнение и сравнение используемых параметров:
// Called on each VM exit
static void timeoverride_get_params(struct vcpu *v, struct cpu_user_regs *regs, unsigned long exit_reason, unsigned long *cr3, int *override_method, bool *donotadvance)
{
unsigned long _cr3 = 0;
int _override_method = TIMEOVERRIDE_OVERRIDE_NONE;
...
get_curr_cr3(&_cr3);
// Checks are only required when RDTSC hook is enabled
if ( v->arch.hvm.vmx.exec_control & CPU_BASED_RDTSC_EXITING)
{
// Is it the same process?
if ( _cr3 == v->ts.ts_cr3 )
{
// Is it the same thread or function?
if (v->ts.ts_rbp == regs->rbp)
{
// Is it one of the interesting VM exits?
if( check_vmexit_for_override(exit_reason))
{
// Some other exits within the same process & thread occurred
// Disable hook & skip override
disable_hook(v);
}
// Is it the same chunk of code?
if (is_close(regs->rip, v->ts.ts_rip))
{
_override_method = TIMEOVERRIDE_OVERRIDE_CURRENT;
}
else
{
// It's too far, disable hook
disable_hook(v);
}
}
// It's another thread or func
else
{
...
}
}
// It's another process
else
{
...
}
}
*override_method = _override_method;
*cr3 = _cr3;
}
Обработчик CPUID:
static void timeoverride_cpuid_handler(struct vcpu *v, struct cpu_user_regs *regs, unsigned long cr3, unsigned long exit_reason, uint64_t time)
{
// Enable hook if it's not on
if (!rdstc_exiting(v))
vmx_set_rdtsc_exiting(v, 1);
...
v->ts.ts = time - TIMEOVERRIDE_CPUID_FIX_DELTA;
// Save regs, etc.
...
}
Обработчик RDTSC:
static void timeoverride_rdtsc_handler(struct vcpu *v, struct cpu_user_regs *regs, unsigned long cr3, unsigned long exit_reason, int override_method, bool donotadvance, uint64_t time)
{
if (override_method != TIMEOVERRIDE_OVERRIDE_NONE)
{
...
v->ts.ts = v->ts.ts + v->ts.ts_rdtsc_time;
// Set override flag
v->ts.override_tsc = true;
...
// Fill regs
hvm_rdtsc_intercept_fixed(regs, v->ts.ts);
...
}
else
{
// Handle RDTSC normally
...
}
}
Казалось бы, все просто, однако есть несколько подводных камней:
Ловушка на RDTSC отключается только в случае, если сработал механизм установки времени или выход произошел далее, чем за N байт от сохраненных значений. При этом каждый такой выход сильно влияет на производительность VM.
Программы и ОС часто легитимно вызывают CPUID, то есть ловушка будет включена почти всегда.
При проверке может переключиться контекст (например, на какой-то другой процесс, выполнивший CPUID), тогда сохраненные данные сбросятся.
Программа может начать временную проверку после того, как ловушка была включена.
Проверку RDTSC + RDTSC требуется обрабатывать вручную, пока включена ловушка.
Чтобы устранить проблемы, для первых двух пунктов вводятся дополнительные условия на отключение ловушки:
счетчик «пропущенных» тиков, который при определенных условиях увеличивается на среднее время нативного выполнения RDTSC. При превышении заданного лимита ловушка отключается;
выполнение двух последовательных инструкций CPUID в рамках одной функции;
выполнение процессом прочих VM exit (за исключением частных случаев);
выполнение RDTSC или CPUID в рамках одной функции на расстоянии больше заданного.
Для решения третьей задачи можно хранить две копии параметров: для текущего и предыдущего процессов. Если контекст переключится на несколько других процессов подряд, сокрытие не сработает (данные об исходном процессе будут перезаписаны), однако можно считать, что на реальном устройстве проверка также бы провалилась. Для четвертого и пятого пунктов используется эвристика, отслеживающая потенциальное начало проверки и позволяющая устанавливать правильное время.
Полный вариант обработчиков и их более подробное описание можно найти в приложении «Полный алгоритм сокрытия» в конце статьи.
Повышение приоритета процессов с помощью DRAKVUF
Рассмотренный выше алгоритм стабилен только для одноядерной VM. Если же ядер несколько, есть вероятность, что при проверке исполнение будет перенесено на другое ядро, где ловушка отключена. Чтобы снизить вероятность подобной ситуации, можно поправить приоритет заданного процесса и всех его потоков при старте с помощью фреймворка DRAKVUF. Подробнее о приоритетах и их значениях — в документации Microsoft.
Не будем сильно углубляться в детали. Поля BasePriority (базовый приоритет) и Priority (текущий приоритет) потока расположены в структуре KTHREAD. Они заполняются при вызове KeStartThread. Соответственно, наиболее удобным этапом для перезаписи будет момент возврата исполнения из функции KeStartThread. Следует установить перехваты не только на KeStartThread, но и на функции PspInsertProcess и NtTerminateProcess. Они нужны, чтобы поддерживать список новых процессов на VM и, например, не повышать приоритет недавно появившихся системных потоков и прочих «неинтересных» процессов.
std::vector<addr_t> new_procs;
setpriority::setpriority(drakvuf_t drakvuf,
const setpriority_config* config
, output_format_t output): pluginex(drakvuf, output)
, offsets(new size_t[__OFFSET_MAX])
)
{
// Load required offsets from profile
if (!drakvuf_get_kernel_struct_members_array_rva(drakvuf, offset_names, __OFFSET_MAX, offsets))
{
PRINT_DEBUG("[SETPRIORITY] Failed to get kernel struct member offsets\n");
throw -1;
}
...
// Set hooks to monitor new processes & save them to new_procs
hooks.push_back(createSyscallHook("PspInsertProcess", &setpriority::hook_insertprocess_cb));
// Hook to remove terminated process from new_procs
hooks.push_back(createSyscallHook("NtTerminateProcess", &setpriority::hook_terminate_process_cb));
hooks.push_back(createSyscallHook("KeStartThread", &setpriority::hook_threadstart_cb));
}
Установка ловушки на возврат из вызова KeStartThread и сохранение адреса KTHREAD в параметрах перехвата:
event_response_t setpriority::hook_threadstart_cb(drakvuf_t drakvuf, drakvuf_trap_info_t* info)
{
// Get KTHREAD parameter from function arguments
auto kthread = drakvuf_get_function_argument(drakvuf, info, 1);
addr_t process;
// Find EPROCESS parameter from KTHREAD
if (!drakvuf_get_process_from_thread(drakvuf, kthread, &process))
{
PRINT_DEBUG("[SETPRIORITY] Failed to get KTHREAD_PROCESS\n");
return VMI_EVENT_RESPONSE_NONE;
}
// We only want to monitor new processes
if (is_new_process(process))
{
// Create and save return hook
auto hook_id = this->make_hook_id(info);
auto hook = createReturnHook<createthread_result_t>(info, &return_hook_threadstart_cb);
auto params = libhook::GetTrapParams<createthread_result_t>(hook->trap_);
// Save KTHREAD parameters to use later
params->thread = kthread;
ret_hooks[hook_id] = std::move(hook);
}
return VMI_EVENT_RESPONSE_NONE;
}
Требуется проверить, что возврат происходит в том же контексте, где установлена ловушка. В ином случае модификации параметров потока не произойдет.
static event_response_t return_hook_threadstart_cb(drakvuf_t drakvuf, drakvuf_trap_info_t* info)
{
auto plugin = GetTrapPlugin<setpriority>(info);
auto params = libhook::GetTrapParams<createthread_result_t>(info);
// Ensure it's one of the processes we want to fix
if (!params->verifyResultCallParams(drakvuf, info))
return VMI_EVENT_RESPONSE_NONE;
vmi_lock_guard vmi(drakvuf);
// Get KTHREAD pointer from trap parameters
auto thread = params->thread;
// Attempt to fix its priority fields
if (!plugin->set_thread_priority_fields(drakvuf, thread))
return VMI_EVENT_RESPONSE_NONE;
auto hook_id = plugin->make_hook_id(info);
plugin->ret_hooks.erase(hook_id);
return VMI_EVENT_RESPONSE_NONE;
}
Наконец, функция перезаписи базового и текущего приоритетов для заданного потока:
bool setpriority::set_thread_priority_fields(drakvuf_t drakvuf, addr_t thread)
{
vmi_lock_guard vmi(drakvuf);
if (VMI_SUCCESS != vmi_write_8_va(vmi, thread + offsets[KTHREAD_BASE_PRIORITY], 4, &cfg_base_priority))
{
PRINT_DEBUG("[SETPRIORITY] Failed to set KTHREAD_BASE_PRIORITY\n");
return false;
}
if (VMI_SUCCESS != vmi_write_8_va(vmi, thread + offsets[KTHREAD_PRIORITY], 4, &cfg_priority))
{
PRINT_DEBUG("[SETPRIORITY] Failed to set KTHREAD_BASE_PRIORITY\n");
return false;
}
return true;
}
Проверка результатов
Результат проверки Pafish после всех вышеперечисленных манипуляций представлен на рисунке ниже.
Ограничения подхода и возможности для доработки
Безусловно, описанный подход неидеален и не покрывает все потенциально возможные сценарии. Он представляет собой скорее proof of concept сокрытия гостевой VM от простых вариантов временных атак.
Важно иметь фиксированную частоту процессора и ограничить используемые C-состояния до C0. Иначе время переключения между гостевой VM и гипервизором может сильно варьироваться, что скажется на стабильности работы алгоритма. Кроме того, на текущий момент отсутствует синхронизация офсетов между ядрами VM, из-за чего в некоторых случаях проверки могут не проходить.
Александр Тюков
Специалист отдела обнаружения вредоносного ПО экспертного центра безопасности Positive Technologies (PT Expert Security Center)
Приложение «Полный алгоритм сокрытия»
Используемые поля
Начать стоит с пояснения сути используемых в коде полей и их назначения.
Расширим структуру vcpu, добавив в нее еще одну структуру, которая будет хранить все необходимые для механизма сокрытия поля. Для каждого виртуального ядра используется своя копия полей.
struct vcpu
{
int vcpu_id;
struct timeoverride ts; // <- Struct containing all our stuff
...
#endif
Сама структура выглядит следующим образом:
struct timeoverride {
uint64_t ts; // Time to be used for override
uint64_t ts_rip; // Last saved RDTSC (CPUID) address
uint64_t ts_rbp; // Last saved RDTSC (CPUID) thread, func address
unsigned long ts_cr3; // Last saved process identifier
// Used for backing up ts_* fields
uint64_t prev_ts;
uint64_t prev_ts_rip;
uint64_t prev_ts_rbp;
unsigned long prev_ts_cr3;
int ts_prev_exit_reason; // Last saved exit reason
int skipped_time_threshold; // Threshold used to check whether RDTSC trap should be disabled
int skipped_ctr_prev; // Used for keeping count of currently «skipped» exits
int skipped_ctr_curr; // Used for keeping count of skipped exits for prev-* fields*
bool override_tsc; // Flag indicating if we have to «fix» time
uint64_t ts_cpuid_time; // Native CPUID time
uint64_t ts_rdtsc_time; // Native RDTSC time
bool ts_timing_init_done; // Flag indicating if native RDTSC (CPUID) times have been initialized
// Fields used to check whether new check might have started within the current process
uint64_t temp_ts;
uint64_t temp_rip;
uint64_t temp_rbp;
};
Заполним поля ts_cpuid_time и ts_rdtsc_time значениями времени нативного выполнения. Для этого десять раз замерим скорость выполнения RDTSC и CPUID в гипервизоре.
void hvm_ts_init_timings(struct vcpu *v)
{
uint64_t r1, _avg = 0;
const int AVG_STEPS = 10;
int eax = 0, ebx = 0, ecx = 0, edx = 0;
for (int i = 0; i < AVG_STEPS; i++)
{
r1 = rdtsc();
_avg = _avg + rdtsc() - r1;
}
v->ts.ts_rdtsc_time = _avg / AVG_STEPS;
printk("Got native RDTSC time -> %"PRIu64"\n", v->ts.ts_rdtsc_time);
for (int i = 0; i < AVG_STEPS; i++)
{
r1 = rdtsc();
cpuid(0, &eax, &ebx, &ecx, &edx );
_avg = _avg + rdtsc() - r1;
}
v->ts.ts_cpuid_time = _avg / AVG_STEPS;
printk("Got native CPUID time -> %"PRIu64"\n", v->ts.ts_cpuid_time);
v->ts.ts_timing_init_done = true;
}
Обработчик CPUID
Ловушка отключается при выполнении двух CPUID подряд.
// Last exit from the same thread was CPUID, so it's most likely not a timing check
if (v->ts.ts_cr3 == cr3 && v->ts.ts_rbp == regs->rbp && v->ts.ts_prev_exit_reason == exit_reason && rdstc_exiting(v))
{
disable_hook(v);
}
Когда CPUID вызывается после RDTSC, перехваченного ранее из того же потока или функции, вычитать константу не требуется: имеется сохраненное время предыдущего выхода. Достаточно добавить к нему время нативного выполнения инструкции.
// CPUID goes after some other exit that we saw
if (is_close(v->ts.ts_rip, regs->rip) && regs->rip > v->ts.ts_rip && regs->rbp == v->ts.ts_rbp)
{
init_time_if_needed(v);
// Emulate CPUID time
v->ts.ts = v->ts.ts + v->ts.ts_cpuid_time + time % 30; // Add native time + some randomness
}
Итоговый вид обработчика CPUID:
static void timeoverride_cpuid_handler(struct vcpu *v, struct cpu_user_regs *regs, unsigned long cr3, unsigned long exit_reason, uint64_t time)
{
// Last exit from the same thread was CPUID, so it's most likely not a timing check
if (v->ts.ts_cr3 == cr3 && v->ts.ts_rbp == regs->rbp && v->ts.ts_prev_exit_reason == exit_reason && v->arch.hvm.vmx.exec_control & CPU_BASED_RDTSC_EXITING)
{
disable_hook(v);
}
else
{
// Enable hook if it's not on
if (!rdstc_exiting(v))
vmx_set_rdtsc_exiting(v, 1);
// CPUID goes after some other exit that we saw
if (is_close(v->ts.ts_rip, regs->rip) && regs->rip > v->ts.ts_rip && regs->rbp == v->ts.ts_rbp)
{
// Emulate CPUID time
v->ts.ts = v->ts.ts + v->ts.ts_cpuid_time + time % 30;
}
else
{
if (v->ts.ts_cr3 == cr3 && v->ts.temp_rip != regs->rip && is_close(v->ts.temp_rip, regs->rip))
{
// Previous RDTSC call was caught and was considered another thread
// Use saved temp time & emulate
v->ts.ts = v->ts.temp_ts + v->ts.ts_cpuid_time + time % 30;
}
else
{
// Delta is experimental and might differ between different CPUs
v->ts.ts = time - TIMEOVERRIDE_CPUID_FIX_DELTA;
}
}
// Save regs
v->ts.ts_rip = regs->rip;
v->ts.ts_rbp = regs->rbp;
v->ts.ts_cr3 = cr3;
v->ts.skipped_ctr_curr = 0;
v->ts.skipped_ctr_prev = 0;
v->ts.ts_prev_exit_reason = exit_reason;
}
}
Расширенная логика сохранения и получения параметров
Добавим счетчик «пропущенных» выходов для текущего процесса. Значение будет увеличиваться каждый раз, когда выход происходит из другого потока или функции.
if ( _cr3 == v->ts.ts_cr3 )
{
// Is it the same thread or function?
if (v->ts.ts_rbp == regs->rbp)
{
...
}
else
{
if (v->ts.skipped_ctr_curr * v->ts.ts_rdtsc_time < v->ts.skipped_time_threshold)
{
_override_method = TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION;
v->ts.skipped_ctr_curr = v->ts.skipped_ctr_curr + 1;
}
else
{
// Turn off RDTSC trap if we're above the threshold
disable_hook(v);
}
}
}
Дополнительно проверим, что ранее не было выхода из этого же потока или функции. Если он был и находился рядом с сохраненными значениями, это может быть индикатором выполнения новой временной проверки; такой случай требуется дополнительно обработать.
if ( _cr3 == v->ts.ts_cr3 )
{
// Is it the same thread or function?
if (v->ts.ts_rbp == regs->rbp)
{
...
}
else
{
if (v->ts.skipped_ctr_curr * v->ts.ts_rdtsc_time < v->ts.skipped_time_threshold)
{
_override_method = TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION;
v->ts.skipped_ctr_curr = v->ts.skipped_ctr_curr + 1;
}
else
{
// We might be in the middle of the check
// If so, do not drop saved values or disable hook
if (v->ts.temp_rbp == regs->rbp && is_close(v->ts.temp_rip, regs->rip))
{
_override_method = TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION;
}
else
{
// Turn off RDTSC trap if we're above the threshold
disable_hook(v);
}
}
}
}
Кроме того, требуется дополнительно проконтролировать, что ловушка не отключается в начале новой проверки. Им можно считать инструкцию RDTSC.
inline bool check_vmexit_no_advance(unsigned long exit_reason)
{
if (exit_reason == EXIT_REASON_RDTSC ||
exit_reason == EXIT_REASON_RDTSCP)
return true;
return false;
}
Таким образом, можно будет безопасно отключить ловушку, не влияя на результат потенциальной проверки. При этом инструкция RDTSC выполнится повторно, но уже не вызовет выхода в гипервизор.
if ( _cr3 == v->ts.ts_cr3 )
{
// Is it the same thread or function?
if (v->ts.ts_rbp == regs->rbp)
{
...
}
else
{
if (v->ts.skipped_ctr_curr * v->ts.ts_rdtsc_time < v->ts.skipped_time_threshold)
{
_override_method = TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION;
v->ts.skipped_ctr_curr = v->ts.skipped_ctr_curr + 1;
}
else
{
// We might be in the middle of the check
// If so, do not drop saved values or disable hook
if (v->ts.temp_rbp == regs->rbp && is_close(v->ts.temp_rip, regs->rip))
{
_override_method = TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION;
}
else
{
// Turn off RDTSC trap if we're above the threshold
disable_hook(v);
_donotadvance = check_vmexit_no_advance(exit_reason);
}
}
}
}
В случае если выход вызван другим процессом, проверим, что параметры совпадают с сохраненными ранее. При совпадении выставим соответствующий флаг для обработчика RDTSC.
if (v->ts.prev_ts_cr3 == _cr3 && v->ts.prev_ts_rbp == regs->rbp)
{
_override_method = TIMEOVERRIDE_OVERRIDE_PREV;
}
В ином случае сохраним параметры или, если они уже сохранены, увеличим счетчик пропущенных выходов. При переполнении счетчика отключаем ловушку.
if (v->ts.skipped_ctr_prev * v->ts.ts_rdtsc_time < v->ts.skipped_time_threshold )
{
if (v->ts.prev_ts_cr3 == v->ts.ts_cr3 && v->ts.prev_ts_rbp == v->ts.ts_rbp)
{
// Values for this process were already backed up, update counters & time
v->ts.prev_ts = v->ts.prev_ts + v->ts.ts_rdtsc_time;
v->ts.skipped_ctr_prev = v->ts.skipped_ctr_prev + 1;
}
else
{
// It hasn't been saved, save it now
v->ts.prev_ts = v->ts.ts;
v->ts.prev_ts_rbp = v->ts.ts_rbp;
v->ts.prev_ts_rip = v->ts.ts_rip;
v->ts.prev_ts_cr3 = v->ts.ts_cr3;
v->ts.skipped_ctr_prev = v->ts.skipped_ctr_curr;
v->ts.skipped_ctr_curr = 0;
}
}
else
{
disable_hook(v);
}
Итоговый вид функции timeoverride_get_params:
static void timeoverride_get_params(struct vcpu *v, struct cpu_user_regs *regs, unsigned long exit_reason, unsigned long *cr3, int *override_method, bool *donotadvance)
{
unsigned long _cr3 = 0;
int _override_method = TIMEOVERRIDE_OVERRIDE_NONE;
bool _donotadvance = false;
get_curr_cr3(&_cr3);
// The following is only required if RDTSC trap was enabled
if ( v->arch.hvm.vmx.exec_control & CPU_BASED_RDTSC_EXITING )
{
// Is it the same process?
if ( _cr3 == v->ts.ts_cr3 )
{
// Is it the same thread or function?
if (v->ts.ts_rbp == regs->rbp)
{
// Is it one of the interesting vm_exits?
if( check_vmexit_for_override(exit_reason))
{
// Some other exits within the same process & thread occurred, disable hook & skip override
disable_hook(v);
}
// Is it the same chunk of code?
if (is_close(regs->rip, v->ts.ts_rip))
{
_override_method = TIMEOVERRIDE_OVERRIDE_CURRENT;
}
else
{
disable_hook(v);
// Check if we should not advance RIP for current exit
_donotadvance = check_vmexit_no_advance(exit_reason);
}
}
else
{
if (v->ts.skipped_ctr_curr * v->ts.ts_rdtsc_time < v->ts.skipped_time_threshold)
{
_override_method = TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION;
v->ts.skipped_ctr_curr = v->ts.skipped_ctr_curr + 1;
}
else
{
// We might be in the middle of the check
// If so, do not drop values
if (v->ts.temp_rbp == regs->rbp && is_close(v->ts.temp_rip, regs->rip))
{
_override_method = TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION;
}
else
{
// Turn off RDTSC trap if we're above the threshold
disable_hook(v);
_donotadvance = check_vmexit_no_advance(exit_reason);
}
}
}
}
else
{
// Do values match with the saved ones?
if (v->ts.prev_ts_cr3 == _cr3 && v->ts.prev_ts_rbp == regs->rbp)
{
_override_method = TIMEOVERRIDE_OVERRIDE_PREV;
}
else
{
// Check if it's above the threshold, if so, disable hook
if (v->ts.skipped_ctr_prev * v->ts.ts_rdtsc_time < v->ts.skipped_time_threshold )
{
// It's another process, save current values as backup in case it was switched during override
// If they're not saved already
if (v->ts.prev_ts_cr3 == v->ts.ts_cr3 && v->ts.prev_ts_rbp == v->ts.ts_rbp)
{
v->ts.prev_ts = v->ts.prev_ts + v->ts.ts_rdtsc_time;
v->ts.skipped_ctr_prev = v->ts.skipped_ctr_prev + 1;
}
else
{
// It hasn't been saved yet, save it now
v->ts.prev_ts = v->ts.ts;
v->ts.prev_ts_rbp = v->ts.ts_rbp;
v->ts.prev_ts_rip = v->ts.ts_rip;
v->ts.prev_ts_cr3 = v->ts.ts_cr3;
v->ts.skipped_ctr_prev = v->ts.skipped_ctr_curr;
v->ts.skipped_ctr_curr = 0;
}
}
else
{
// Missed too much exits, drop saved values, disable hook
disable_hook(v);
}
}
}
}
// init variables for further use
*cr3 = _cr3;
*override_method = _override_method;
*donotadvance = _donotadvance;
}
Обработчик RDTSC
Большая часть логики обработчика строится на значении флага override_method, в зависимости от которого выбирается то или иное время для установки, а также на значении флага donotadvance, позволяющего, как упоминалось выше, не сдвигать регистр RIP. Чтобы сделать последнее, достаточно убрать вызов update_guest_eip.
if (!donotadvance)
update_guest_eip();
else
if (TIMEOVERRIDE_PRINT_DBG)
printk("[SKIPPED EIP UPDATE]\n");
Как можно понять из кода функции timeoverride_get_params, для флага override_method есть четыре варианта значений:
TIMEOVERRIDE_OVERRIDE_NONE,
TIMEOVERRIDE_OVERRIDE_CURRENT,
TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION,
TIMEOVERRIDE_OVERRIDE_PREV.
В случае TIMEOVERRIDE_OVERRIDE_NONE все просто: достаточно вызвать стандартный обработчик RDTSC без каких-либо дополнительных манипуляций.
if (override_method != TIMEOVERRIDE_OVERRIDE_NONE)
{
...
}
else
{
// Default handler
hvm_rdtsc_intercept(regs);
}
В случае TIMEOVERRIDE_OVERRIDE_PREV потребуется достать сохраненные значения из соответствующих полей.
if (override_method == TIMEOVERRIDE_OVERRIDE_PREV)
{
v->ts.ts = v->ts.prev_ts;
time = v->ts.prev_ts;
v->ts.ts_rip = v->ts.prev_ts_rip;
v->ts.ts_rbp = v->ts.prev_ts_rbp;
v->ts.ts_cr3 = v->ts.prev_ts_cr3;
}
При TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION требуется проверить, что параметры для текущего выхода не совпадают с сохраненными ранее. Иначе это может быть началом или продолжением временной проверки.
if (override_method == TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION)
{
if (v->ts.temp_rip != regs->rip)
{
// RDTSC exit with differing RBP might indicate start of a new timing check
// Save values to check on the next exit
if (is_close(v->ts.temp_rip, regs->rip))
{
// Possible new check found, updating values
v->ts.ts = v->ts.temp_ts;
v->ts.ts_rbp = v->ts.temp_rbp;
v->ts.ts_rip = v->ts.temp_rip;
drop_temp_values(v);
}
else
{
// Saving new temp values
v->ts.temp_rip = regs->rip;
v->ts.temp_ts = time;
v->ts.temp_rbp = regs->rbp;
}
}
}
Случай TIMEOVERRIDE_OVERRIDE_CURRENT не требует дополнительных манипуляций: все поля уже имеют необходимые значения.
Соединив описанные выше части вместе, получим итоговый обработчик:
static void timeoverride_rdtsc_handler(struct vcpu *v, struct cpu_user_regs *regs, unsigned long cr3, unsigned long exit_reason, int override_method, bool donotadvance, uint64_t time)
{
if (override_method != TIMEOVERRIDE_OVERRIDE_NONE)
{
// Save the last exit reason
if (override_method == TIMEOVERRIDE_OVERRIDE_CURRENT)
v->ts.ts_prev_exit_reason = exit_reason;
if (override_method == TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION)
{
if (v->ts.temp_rip != regs->rip)
{
// RDTSC exit with differing RBP might indicate start of a new timing check
// Save values to check on the next exit
if (is_close(v->ts.temp_rip, regs->rip))
{
// Possible new check found, updating values
v->ts.ts = v->ts.temp_ts;
v->ts.ts_rbp = v->ts.temp_rbp;
v->ts.ts_rip = v->ts.temp_rip;
drop_temp_values(v);
}
else
{
// Saving new temp values
v->ts.temp_rip = regs->rip;
v->ts.temp_ts = time;
v->ts.temp_rbp = regs->rbp;
}
}
}
else
{
if (override_method == TIMEOVERRIDE_OVERRIDE_PREV)
{
v->ts.ts = v->ts.prev_ts;
time = v->ts.prev_ts;
v->ts.ts_rip = v->ts.prev_ts_rip;
v->ts.ts_rbp = v->ts.prev_ts_rbp;
v->ts.ts_cr3 = v->ts.prev_ts_cr3;
}
}
// logic to process some borderline cases
if (!is_close(v->ts.ts_rip, regs->rip))
{
if (override_method != TIMEOVERRIDE_OVERRIDE_ANOTHER_THREAD_OR_FUNCTION)
{
// Long call delta from the same thread (func)
v->ts.ts = time;
v->ts.ts_rip = regs->rip;
v->ts.ts_rbp = regs->rbp;
v->ts.ts_cr3 = cr3;
hvm_rdtsc_intercept_fixed(regs, v->ts.ts);
}
else
{
// Long call from other thread
hvm_rdtsc_intercept_fixed(regs, time);
}
}
else
{
// Fix time
v->ts.ts = v->ts.ts + v->ts.ts_rdtsc_time;
// If it was something we fixed, allow to update rip
donotadvance = false;
v->ts.ts_prev_exit_reason = exit_reason;
v->ts.override_tsc = true;
v->ts.skipped_ctr_curr = 0;
hvm_rdtsc_intercept_fixed(regs, v->ts.ts);
}
}
else
{
hvm_rdtsc_intercept(regs);
}
if (!donotadvance)
update_guest_eip();
else
if (TIMEOVERRIDE_PRINT_DBG)
printk("[SKIP EIP UPDATE]\n");
}
Осталось лишь добавить обработчики в соответствующие места.
void vmx_vmexit_handler(struct cpu_user_regs *regs)
{
uint64_t time;
unsigned long cr3;
int override_method = TIMEOVERRIDE_OVERRIDE_NONE;
bool donotadvance = false;
time = hvm_get_guest_tsc(v);
...
timeoverride_get_params(v, regs, exit_reason, &cr3, &override_method, &donotadvance);
...
switch ( (uint16_t)exit_reason )
{
...
case EXIT_REASON_CPUID:
{
int rc;
rc = hvm_vmexit_cpuid(regs, get_instruction_length());
timeoverride_cpuid_handler(v, regs, cr3, exit_reason, time);
if ( rc < 0 )
goto exit_and_crash;
if ( !rc )
update_guest_eip(); /* Safe: CPUID */
break;
}
...
case EXIT_REASON_RDTSC:
{
timeoverride_rdtsc_handler(v, regs, cr3, exit_reason, override_method, donotadvance, time);
break;
}
}
}
Нужно также добавить вызов функции перезаписи времени при возврате в VM.
bool vmx_vmenter_helper(const struct cpu_user_regs *regs)
{
...
try_set_tsc_offset(curr); // curr — macro for current vCPU
return true;
}