Skip to content

Commit c44e507

Browse files
authored
Merge pull request #618 from NotRequiem/main
CPUID trap checks
2 parents f0de40a + 0d7252c commit c44e507

1 file changed

Lines changed: 134 additions & 42 deletions

File tree

src/vmaware.hpp

Lines changed: 134 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4943,7 +4943,7 @@ struct VM {
49434943
// we used a rng before running the traditional rdtsc-cpuid-rdtsc trick
49444944

49454945
// sometimes not intercepted in some hvs (like VirtualBox) under compat mode
4946-
auto cpuid = [&]() noexcept -> u64 {
4946+
auto cpuid_ex = [&](int leaf, int subleaf) noexcept -> u64 {
49474947
#if (MSVC)
49484948
// make regs volatile so writes cannot be optimized out, if this isn't added and the code is compiled in release mode, cycles would be around 40 even under Hyper-V
49494949
volatile int regs[4]{};
@@ -4956,7 +4956,7 @@ struct VM {
49564956
// prevent the compiler from moving the __cpuid call before the t1 read
49574957
COMPILER_BARRIER();
49584958

4959-
__cpuid((int*)regs, 0); // not using cpu::cpuid to get a chance of inlining
4959+
__cpuidex((int*)regs, leaf, subleaf);
49604960

49614961
COMPILER_BARRIER();
49624962

@@ -4984,7 +4984,7 @@ struct VM {
49844984
// because the compiler must honor the write to a volatile variable.
49854985
asm volatile("cpuid"
49864986
: "=a"(a), "=b"(b), "=c"(c), "=d"(d)
4987-
: "a"(0)
4987+
: "a"(leaf), "c"(subleaf)
49884988
: "memory");
49894989

49904990
COMPILER_BARRIER();
@@ -5001,40 +5001,6 @@ struct VM {
50015001

50025002
constexpr u16 iterations = 1000;
50035003

5004-
// pre-allocate sample buffer and touch pages to avoid page faults by MMU during measurement
5005-
std::vector<u64> samples;
5006-
samples.resize(iterations);
5007-
for (unsigned i = 0; i < iterations; ++i) samples[i] = 0; // or RtlSecureZeroMemory (memset)
5008-
5009-
/*
5010-
* We want to move our thread from the Running state to the Waiting state
5011-
* When the sleep expires (at the next timer tick), the OS moves VMAware's thread to the Ready state
5012-
* When it picks us up again, it grants VMAware a fresh quantum, typically varying between 2 ticks (30ms) and 6 ticks (90ms) on Windows Client editions
5013-
* The default resolution of the Windows clock we're using is 64Hz
5014-
* Because we're calling NtDelayExecution with only 1ms, the kernel interprets this as "Sleep for at least 1ms"
5015-
* Since the hardware interrupt (tick) only fires every 15.6ms and we're not using timeBeginPeriod, the kernel cannot wake us after exactly 1ms
5016-
* So instead, it does what we want and wakes us up at the very next timer interrupt
5017-
* That's the reason why it's only 1ms and we're not using CreateWaitableTimerEx / SetWaitableTimerEx
5018-
* Sleep(0) would return instantly in some circumstances
5019-
* This gives us more time for sampling before we're rescheduled again
5020-
*/
5021-
5022-
#if (WINDOWS)
5023-
// voluntary context switch to get a fresh quantum
5024-
SleepEx(1, FALSE);
5025-
#else
5026-
// should work similarly in Unix-like operating systems
5027-
std::this_thread::sleep_for(std::chrono::milliseconds(1));
5028-
#endif
5029-
for (int w = 0; w < 128; ++w) {
5030-
volatile u64 tmp = cpuid();
5031-
VMAWARE_UNUSED(tmp);
5032-
}
5033-
5034-
for (unsigned i = 0; i < iterations; ++i) {
5035-
samples[i] = cpuid();
5036-
}
5037-
50385004
auto calculate_latency = [&](const std::vector<u64>& samples_in) -> u64 {
50395005
if (samples_in.empty()) return 0;
50405006
const size_t N = samples_in.size();
@@ -5113,18 +5079,144 @@ struct VM {
51135079
return result;
51145080
};
51155081

5116-
u64 cpuid_latency = calculate_latency(samples);
5082+
// pre-allocate sample buffer and touch pages to avoid page faults by MMU during measurement
5083+
std::vector<u64> samples;
5084+
samples.resize(iterations);
5085+
for (unsigned i = 0; i < iterations; ++i) samples[i] = 0; // or RtlSecureZeroMemory (memset)
5086+
5087+
/*
5088+
* We want to move our thread from the Running state to the Waiting state
5089+
* When the sleep expires (at the next timer tick), the OS moves VMAware's thread to the Ready state
5090+
* When it picks us up again, it grants VMAware a fresh quantum, typically varying between 2 ticks (30ms) and 6 ticks (90ms) on Windows Client editions
5091+
* The default resolution of the Windows clock we're using is 64Hz
5092+
* Because we're calling NtDelayExecution with only 1ms, the kernel interprets this as "Sleep for at least 1ms"
5093+
* Since the hardware interrupt (tick) only fires every 15.6ms and we're not using timeBeginPeriod, the kernel cannot wake us after exactly 1ms
5094+
* So instead, it does what we want and wakes us up at the very next timer interrupt
5095+
* That's the reason why it's only 1ms and we're not using CreateWaitableTimerEx / SetWaitableTimerEx
5096+
* Sleep(0) would return instantly in some circumstances
5097+
* This gives us more time for sampling before we're rescheduled again
5098+
*/
5099+
5100+
#if (WINDOWS)
5101+
// voluntary context switch to get a fresh quantum
5102+
SleepEx(1, FALSE);
5103+
#else
5104+
// should work similarly in Unix-like operating systems
5105+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
5106+
#endif
5107+
for (int w = 0; w < 128; ++w) {
5108+
volatile u64 tmp = cpuid_ex(0, 0);
5109+
VMAWARE_UNUSED(tmp);
5110+
}
5111+
5112+
for (unsigned i = 0; i < iterations; ++i) {
5113+
samples[i] = cpuid_ex(0, 0); // leaf 0 just returns static data so it should be fast
5114+
}
51175115

5118-
debug("TIMER: VMEXIT latency -> ", cpuid_latency);
5116+
const u64 cpuid_latency_leaf0 = calculate_latency(samples);
51195117

5120-
if (cpuid_latency >= cycle_threshold) {
5118+
// Extended Topology requires the hypervisor to calculate dynamic x2APIC IDs
5119+
// we expect this to crash entire VMs if the kernel developer is not enough
5120+
for (unsigned i = 0; i < iterations; ++i) {
5121+
samples[i] = cpuid_ex(0xB, 0);
5122+
}
5123+
const u64 cpuid_latency_leafB = calculate_latency(samples);
5124+
5125+
debug("TIMER: Leaf 0 latency -> ", cpuid_latency_leaf0);
5126+
debug("TIMER: Leaf 0xB latency -> ", cpuid_latency_leafB);
5127+
5128+
// simple differential analysis
5129+
if (cpuid_latency_leaf0 > 0) {
5130+
if (cpuid_latency_leafB > (cpuid_latency_leaf0 * 1.6)) {
5131+
debug("TIMER: VMAware detected a CPUID patch");
5132+
return true;
5133+
}
5134+
}
5135+
5136+
if (cpuid_latency_leaf0 >= cycle_threshold) {
51215137
return true;
51225138
}
5123-
else if (cpuid_latency <= 20) { // cpuid is fully serializing, not even old CPUs have this low average cycles in real-world scenarios
5139+
if (cpuid_latency_leafB >= cycle_threshold) {
51245140
return true;
51255141
}
5126-
// TLB flushes or side channel cache attacks are not even tried due to how unreliable they are against stealthy hypervisors
5142+
else if (cpuid_latency_leaf0 <= 20) { // cpuid is fully serializing, not even old CPUs have this low average cycles in real-world scenarios
5143+
return true;
5144+
}
5145+
5146+
// the core idea is to force the host scheduler's pending signal check (kvm_vcpu_check_block)
5147+
// We detect cpuid patches that just do fast vmexits by spawning a thread on the SAME core that spams the patched instruction
5148+
// If patched, the host core enters an uninterruptible loop, starving the timer interrupt needed for the sleep syscall
5149+
#if (WINDOWS)
5150+
{
5151+
using NtCreateThreadEx_t = NTSTATUS(__stdcall*)(PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID, PVOID, ULONG, ULONG_PTR, ULONG_PTR, ULONG_PTR, PVOID);
5152+
using NtTerminateThread_t = NTSTATUS(__stdcall*)(HANDLE, NTSTATUS);
5153+
using NtWaitForSingleObject_t = NTSTATUS(__stdcall*)(HANDLE, BOOLEAN, PLARGE_INTEGER);
5154+
5155+
const HMODULE ntdll = util::get_ntdll();
5156+
if (ntdll) {
5157+
const char* names[] = { "NtCreateThreadEx", "NtTerminateThread", "NtWaitForSingleObject" };
5158+
void* funcs[3] = {};
5159+
util::get_function_address(ntdll, names, funcs, 3);
5160+
5161+
auto pNtCreateThreadEx = (NtCreateThreadEx_t)funcs[0];
5162+
auto pNtTerminateThread = (NtTerminateThread_t)funcs[1];
5163+
auto pNtWaitForSingleObject = (NtWaitForSingleObject_t)funcs[2];
5164+
5165+
if (pNtCreateThreadEx && pNtTerminateThread && pNtWaitForSingleObject) {
5166+
5167+
// stateless lambda castable to thread routine
5168+
auto spammer_routine = [](PVOID) -> DWORD {
5169+
// This loop exploits the patch's lack of interrupt window checking
5170+
while (true) {
5171+
int regs[4];
5172+
__cpuid(regs, 0);
5173+
}
5174+
return 0;
5175+
};
5176+
5177+
HANDLE hSpammer = nullptr;
5178+
const NTSTATUS status = pNtCreateThreadEx(&hSpammer, MAXIMUM_ALLOWED, nullptr, GetCurrentProcess(),
5179+
(PVOID)(uintptr_t(+spammer_routine)), nullptr, TRUE, 0, 0, 0, nullptr);
5180+
5181+
if (status >= 0 && hSpammer) {
5182+
// forcing contention contention
5183+
THREAD_BASIC_INFORMATION tbi_local{};
5184+
if (pNtQueryInformationThread(hCurrentThread, ThreadBasicInformation, &tbi_local, sizeof(tbi_local), nullptr) >= 0) {
5185+
pNtSetInformationThread(hSpammer, ThreadAffinityMask, &tbi_local.AffinityMask, sizeof(ULONG_PTR));
5186+
}
5187+
5188+
ResumeThread(hSpammer);
5189+
5190+
LARGE_INTEGER qpc_start, qpc_end, qpc_freq;
5191+
QueryPerformanceFrequency(&qpc_freq);
5192+
QueryPerformanceCounter(&qpc_start);
5193+
5194+
// expecting gibberish cpuid patches to lock the interrupt timer
5195+
// by the infinite fastpath loop on the physical core, causing a massive overshoot
5196+
SleepEx(10, FALSE);
5197+
5198+
QueryPerformanceCounter(&qpc_end);
5199+
5200+
// Cleanup
5201+
pNtTerminateThread(hSpammer, 0);
5202+
pNtWaitForSingleObject(hSpammer, FALSE, nullptr);
5203+
CloseHandle(hSpammer);
5204+
5205+
double elapsed_ms = (double)(qpc_end.QuadPart - qpc_start.QuadPart) * 1000.0 / (double)qpc_freq.QuadPart;
5206+
5207+
debug("TIMER: Timer interrupt starvation -> ", elapsed_ms, " ms");
5208+
5209+
if (elapsed_ms > 40.0) {
5210+
debug("TIMER: VMAware detected a CPUID patch");
5211+
return true;
5212+
}
5213+
}
5214+
}
5215+
}
5216+
}
51275217
#endif
5218+
// TLB flushes or side channel cache attacks are not even tried due to how unreliable they are against stealthy hypervisors
5219+
#endif
51285220
return false;
51295221
}
51305222

0 commit comments

Comments
 (0)