You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// Detect a hypervisor without giving it time to react (when the hypervisor sees the vmexit, it's already too late for it, as the counter already exceeded the threshold)
5392
-
// Uses our own software-based clock, meaning a hypervisor can't hide time by offsetting TSC or controlling any hardware timer
5391
+
// The timing attack uses our own software-based clock, meaning a hypervisor can't hide time by offsetting TSC or controlling any other timer
5393
5392
double threshold = 3.5;
5394
5393
if (util::is_running_under_translator()) {
5395
5394
debug("TIMER: Running inside a binary translation layer");
@@ -5430,80 +5429,126 @@ struct VM {
5430
5429
return h;
5431
5430
}();
5432
5431
5433
-
// search for the physical sibling of CPU 0, then pick a random CPU excluding it to avoid SMT locks
// middle available logical CPU because statistically it normally has less DPCs/interrupts, we could query the windows api to fetch the interrupt count or DPC time
5450
+
// 20 available CPUs -> idxs[10] -> core 11 in 1-based numbering
5451
+
const DWORD middle_pos = n / 2;
5452
+
return 1ull << idxs[middle_pos];
5453
+
};
5454
+
5455
+
// random logical CPU, but exclude the trigger_thread, first, second and last available logical CPUs, avoiding SMT siblings
return 1ull << idxs[std::uniform_int_distribution<u32>(0, n - 1)(gen)];
5505
-
}
5506
-
return 1ull;
5547
+
// std::random_device{}() uses RDRAND/RDSEED which can be intercepted by hypervisors
5548
+
// we use our own compile-time seed that cannot be taken by examining PE/Linux binary properties and would need static/dynamic analysis
5549
+
// this changes per build and per process session due to hardware ASLR
5550
+
std::mt19937 gen(seq);
5551
+
return 1ull << pick[std::uniform_int_distribution<u32>(0, m - 1)(gen)];
5507
5552
};
5508
5553
5509
5554
// we dont use cpu::cpuid on purpose
@@ -5531,7 +5576,7 @@ struct VM {
5531
5576
#endif
5532
5577
#else
5533
5578
i32 dummy[4];
5534
-
__cpuidex(dummy, 0x0, 0);
5579
+
__cpuidex(dummy, 0x0, 0); // leaf 0 because it's the most stable one for making ratio checks, even if at first glance it may be abusable because it's the fastest one
state.start_test.store(true, std::memory_order_release); // _mm_pause can be exited conditionally, spam hit L3
5825
+
state.start_test.store(true, std::memory_order_release); // _mm_pause can be vm-exited conditionally, spam hit L3
5781
5826
// warm-up to settle caches, scheduler and frequency boosts
5782
5827
for (int i = 0; i < 1000; ++i) {
5783
5828
for (int j = 0; j < 2; ++j) trigger_vmexit();
@@ -5789,6 +5834,7 @@ struct VM {
5789
5834
// cpuid and lfence interpolated so that any turbo boost, thermal throttling, speculation (for the loop overhead itself, not for the serializing instructions), etc affects samples equally
5790
5835
u64 v_pre, v_post, r_pre, r_post, sync;
5791
5836
5837
+
// this is done as a counter to both legitimate and malicious hypervisors interrupts that may pause the counter thread while we measure
5792
5838
sync = state.counter; while (state.counter == sync); // infer if counter got enough quantum momentum (so its currently scheduled)
5793
5839
sync = state.counter; while (state.counter == sync); // fastest busy-waiting strategy, PAUSE affects cache, calling APIs like SwitchToThread() would be even worse
5794
5840
@@ -5797,7 +5843,7 @@ struct VM {
5797
5843
v_pre = state.counter;
5798
5844
std::atomic_signal_fence(std::memory_order_seq_cst); // _ReadWriteBarrier() aka dont emit runtime fences
5799
5845
5800
-
trigger_vmexit(); // this forces the hypervisor to keep interception and try to bypass latency, or disable interception and try to bypass XSAVE states
5846
+
trigger_vmexit(); // this forces the hypervisor to keep interception and try to bypass latency, or disable interception if on AMD and try to bypass XSAVE states
sync = state.counter; while (state.counter == sync); // sync to our counter tick again
5818
5864
sync = state.counter; while (state.counter == sync);
5819
5865
5866
+
// LFENCE check is after CPUID on purpose, so that possible artificial pauses when cpuid is executed affect LFENCE too due to the latency of sending a IPI
debug("TIMER: VMM -> ", cpuid_l, " | nVMM -> ", ref_l, " | Ratio -> ", latency_ratio); // those are NOT cycles
5864
5908
if (latency_ratio >= threshold) hypervisor_detected = true;
5865
5909
5910
+
// Detect IPI-based counter pausing bypasses
5911
+
// For the median itself to exceed baremetal limits (which rarely pass 1000), an interrupt must be occurring on almost EVERY single loop iteration
5912
+
// This is the footprint of a hypervisor continuously spamming cross-core IPIs to try and pause the counter thread (or the trigger_thread to make LFENCE take a lot of time)
debug("TIMER: Detected artificial IPI delivery to VMAware's threads");
5915
+
bypass_detected = true;
5916
+
}
5917
+
5866
5918
// Now detect bypassers disabling cpuid interception with SVM
5867
5919
// Even when a bypasser disables INTERCEPT_CPUID in the VMCB, they often fail to realize that certain CPUID leaves do not return static values from the hardware
5868
5920
// Instead, they return values based on the LAPIC state or internal CPU registers that the hypervisor must initialize for the vCPU to function
0 commit comments