Dit artikel en de broncode zijn uitsluitend bedoeld voor educatieve en onderzoeksdoeleinden op het gebied van beveiliging. Misbruik voor kwaadaardige doeleinden, waaronder ongeautoriseerde systeemtoegang of malwareontwikkeling, is uitdrukkelijk verboden. Door gebruik te maken van dit materiaal gaat u akkoord met onze Algemene Voorwaarden. Gebruik is geheel op eigen risico.

Ophion is een Intel VT-x Type-2-hypervisor die een al draaiend Windows-systeem virtualiseert vanuit een kerneldriver. Het doorstaat gangbare hypervisordetectieprogramma's die worden ingezet door anti-cheat-software (EAC, BattlEye), antivirusprogramma's en VM-detectiebibliotheken zoals VMAware en hvdetecc.
Dit artikel beschrijft hoe het werkt, welke detectievectoren het neutraliseert en op welke manier, en de redenering achter elke ontwerpbeslissing.
- Een Draaiend Systeem Virtualiseren
- VMCS-configuratie
- EPT-ontwerp
- Stealthmechanismen
- Interruptafhandeling
- Privé-host-CR3
- VMCALL-gate
- VM-Exit-handlerstructuur
- XSETBV-validatie
- Overige VM-Exit-afhandeling
- Stealthschakelaars
- Detectietestresultaten
- Bekende Beperkingen en Toekomstig Werk
- Bouwen en Laden
Ophion wordt geladen als een standaard Windows-kerneldriver (.sys). Bij DriverEntry worden de volgende stappen uitgevoerd:
- Per-VCPU-structuren worden gealloceerd voor elke logische processor
- Bare-metal CPUID-reacties worden gecachet, voordat VMX iets wijzigt
- EPT wordt geïnitialiseerd met identiteitsgemapte 2MB grote pagina's
- VMM-stacks, MSR-bitmaps, I/O-bitmaps en VMXON/VMCS-regio's worden per core gealloceerd
- Kernelpaginatabellen worden diep gekopieerd naar een privé-host-CR3
- Een DPC wordt uitgezonden naar elke logische processor
De broadcast maakt gebruik van KeGenericCallDpc, een ongedocumenteerde kernel-API die niet aanwezig is in de WDK-headers en handmatig geprototypeerd is. Deze functie activeert een DPC op elke logische processor en synchroniseert de voltooiing. Elke DPC-callback slaat de huidige CPU-toestand op (GPR's, segmentregisters, RFLAGS), treedt VMX-operatie in via VMXON, programmeert de VMCS en voert VMLAUNCH uit. De gast hervat bij de instructie na de toestandsopslag. Het besturingssysteem blijft normaal draaien, nu als gast onder de hypervisor.
Bij het afladen geeft elke core een VMCALL(VMXOFF) uit via een DPC-broadcast. De handler slaat gast-RIP/RSP/CR3 op, voert __vmx_off() uit en herstelt vervolgens onmiddellijk de gast-CR3 en wist CR4.VMXE:
__vmx_off();
__writecr3(guest_cr3);
__writecr4(__readcr4() & ~CR4_VMX_ENABLE_FLAG);
Zonder het herstel van CR3 blijft het systeem draaien op de privé-HOST_CR3, een verouderde momentopname die geen kernelgeheugen in kaart brengt dat na het laden van de hypervisor is gealloceerd. De eerste toegang tot post-initialisatiegeheugen resulteert in bugcheck 0xA.

De CPUID-cache moet worden opgebouwd vóór VMXON, omdat CPUID-reacties kunnen afwijken zodra de processor zich in VMX-operatie bevindt. De privé-host-CR3 moet worden aangemaakt nadat alle allocaties zijn voltooid, omdat het een momentopname is van de kernelpaginatabellen. Als de momentopname eerst wordt genomen en daarna VMM-stacks worden gealloceerd, zullen die stacks geen PTE's hebben in de momentopname en zal de eerste VM-exit een triple fault veroorzaken bij een poging een niet-gemapte stack te benaderen.
Elk VMCS-veld heeft een reden. Hieronder wordt beschreven wat Ophion programmeert en waarom.
Op pin gebaseerde besturingen: External-interrupt exiting, NMI exiting en virtual NMI's. External-interrupt exiting en NMI exiting worden op de meeste Intel-CPU's toch al afgedwongen door must-be-1-bits, maar zijn ook inhoudelijk vereist. Zonder external-interrupt exiting en ACK-on-exit veroorzaken openstaande interrupts een oneindige VM-exit-lus die leidt tot TDR en een zwart scherm. Virtual NMI's worden expliciet aangevraagd omdat NMI-window exiting deze vereist, conform de SDM: als virtual NMIs 0 is, moet NMI-window exiting eveneens 0 zijn. Zonder virtual NMI's zou het dynamisch instellen van NMI-window exiting om een NMI uit te stellen een VM-entry-fout veroorzaken.
Primaire op processor gebaseerde besturingen: TSC offsetting, MSR-bitmaps, I/O-bitmaps en het activeren van secundaire besturingen. Dit zijn de enige exits die Ophion expliciet aanvraagt. CR3-load/store exiting, INVLPG exiting, HLT exiting, MOV-DR exiting, RDTSC exiting en andere kunnen worden afgedwongen door must-be-1-bits afhankelijk van de CPU. Ophion verwerkt ze allemaal indien afgedwongen. De functie vmx_adjust_controls dwingt must-be-0- en must-be-1-bits af vanuit de capability-MSR:
UINT32 vmx_adjust_controls(UINT32 requested, UINT32 capability_msr)
{
MSR msr_val = {0};
msr_val.Flags = __readmsr(capability_msr);
requested &= msr_val.Fields.High; // bit=0 in high -> must be zero
requested |= msr_val.Fields.Low; // bit=1 in low -> must be one
return requested;
}
Secundaire op processor gebaseerde besturingen: EPT, VPID, RDTSCP passthrough, INVPCID passthrough, XSAVES/XRSTORS passthrough. Dit zijn de kernfuncties die de hypervisor functioneel maken zonder gastsoftware die deze instructies gebruikt te verstoren.
VM-exit-besturingen: 64-bit host, sla debugbesturingen op, ACK interrupt bij exit. De ACK-on-exit-bit is cruciaal: deze zorgt ervoor dat de CPU externe interrupts erkent bij de LAPIC en de vector opslaat in VMCS_VMEXIT_INTERRUPTION_INFORMATION. Zonder deze bit kunnen interrupts niet correct opnieuw worden geïnjecteerd.
VM-entry-besturingen: IA-32e-modus gast, laad debugbesturingen. Laadt gast-DR7 en IA32_DEBUGCTL bij VM-entry zodat de gast de correcte debugtoestand ziet.
De VMCS-gasttoestand vereist volledige programmering van segmentdescriptors. Ophion parseert de huidige GDT om de basis, het limiet, de toegangsrechten en de selector te extraheren voor alle 8 segmentregisters (ES, CS, SS, DS, FS, GS, LDTR, TR). 64-bit systeemssegmenten (TSS) gebruiken 16-byte descriptors, waardoor de TR-basis reconstructie vereist uit beide helften. Null-selectors worden gemarkeerd als onbruikbaar.
Hostselectors moeten RPL- en TI-bits gewist hebben conform de SDM, anders mislukt VM-entry:
__vmx_vmwrite(VMCS_HOST_CS_SELECTOR, asm_get_cs() & 0xF8);
__vmx_vmwrite(VMCS_HOST_SS_SELECTOR, asm_get_ss() & 0xF8);
__vmx_vmwrite(VMCS_HOST_TR_SELECTOR, asm_get_tr() & 0xF8);
// ... idem voor ES, DS, FS, GS
Overige vereiste gasttoestandsvelden:
__vmx_vmwrite(VMCS_GUEST_VMCS_LINK_POINTER, ~0ULL); // geen VMCS shadowing
__vmx_vmwrite(VMCS_GUEST_DEBUGCTL, __readmsr(IA32_DEBUGCTL));
__vmx_vmwrite(VMCS_GUEST_DR7, 0x400); // SDM-standaard (bit 10 altijd ingesteld)
__vmx_vmwrite(VMCS_GUEST_ACTIVITY_STATE, 0); // actief
__vmx_vmwrite(VMCS_GUEST_INTERRUPTIBILITY_STATE, 0); // geen blokkering
__vmx_vmwrite(VMCS_GUEST_PENDING_DEBUG_EXCEPTIONS, 0);
__vmx_vmwrite(VMCS_CTRL_TSC_OFFSET, 0);
__vmx_vmwrite(VMCS_CTRL_PAGEFAULT_ERROR_CODE_MASK, 0); // alle #PF's gaan naar gast
__vmx_vmwrite(VMCS_CTRL_PAGEFAULT_ERROR_CODE_MATCH, 0);
SYSENTER-velden (CS, EIP, ESP) worden geprogrammeerd voor zowel gast als host vanuit de huidige MSR-waarden. FS/GS-bases worden geladen vanuit IA32_FS_BASE en IA32_GS_BASE.
De VCPU-pointer wordt bovenaan de VMM-stack geplaatst zodat het assembly-ingangspunt deze kan ophalen zonder Windows-API's aan te roepen in VMX-rootmodus:
*(PVIRTUAL_MACHINE_STATE *)((UINT64)vcpu->vmm_stack + VMM_STACK_SIZE - VMM_STACK_VCPU_OFFSET) = vcpu;
__vmx_vmwrite(VMCS_HOST_RSP, (UINT64)vcpu->vmm_stack + VMM_STACK_SIZE - 16);
__vmx_vmwrite(VMCS_HOST_RIP, (UINT64)asm_vmexit_handler);
In VMX-rootmodus wordt de huidige processor geïdentificeerd via __readmsr(IA32_TSC_AUX) & 0xFFF. Het besturingssysteem programmeert IA32_TSC_AUX met het processornummer, en het uitlezen hiervan is veilig in rootmodus zonder enige afhankelijkheid van Windows-API's.
CR0-masker = 0: Volledige passthrough. Gastschrijfacties naar CR0 veroorzaken nooit VM-exits.
CR4-masker = bit 13 (VMXE) wanneer stealth is ingeschakeld:
__vmx_vmwrite(VMCS_CTRL_CR4_GUEST_HOST_MASK, CR4_VMX_ENABLE_FLAG); // 0x2000
__vmx_vmwrite(VMCS_CTRL_CR4_READ_SHADOW, __readcr4() & ~CR4_VMX_ENABLE_FLAG);
Gastlezingen van CR4 komen uit de read shadow, waar VMXE 0 is. Gastschrijfacties die bit 13 omschakelen veroorzaken een VM-exit. De handler houdt VMXE=1 in de werkelijke VMCS, vereist voor VMX, terwijl de shadow VMXE=0 toont.

De MSR-bitmap is 4 KB groot, één pagina, verdeeld in vier regio's van 1 KB: laagbereik lezen (0x000-0x1FFF), hoogbereik lezen (0xC0000000-0xC0001FFF), laagbereik schrijven en hoogbereik schrijven. Een ingesteld bit betekent dat de betreffende MSR-toegang wordt onderschept. Ophion stelt interceptie in op byteniveau:
// RDMSR(0x10) -- IA32_TIME_STAMP_COUNTER (alleen lezen)
((PUCHAR)vcpu->msr_bitmap_va)[0x10 / 8] |= (UCHAR)(1 << (0x10 % 8));
// IA32_FEATURE_CONTROL (0x3A) -- lezen + schrijven
((PUCHAR)vcpu->msr_bitmap_va)[0x3A / 8] |= (UCHAR)(1 << (0x3A % 8));
((PUCHAR)vcpu->msr_bitmap_va)[0x800 + 0x3A / 8] |= (UCHAR)(1 << (0x3A % 8));
// VMX capability-MSR's (0x480-0x493) -- lezen + schrijven
for (UINT32 msr_idx = 0x480; msr_idx <= 0x493; msr_idx++)
{
((PUCHAR)vcpu->msr_bitmap_va)[msr_idx / 8] |= (UCHAR)(1 << (msr_idx % 8));
((PUCHAR)vcpu->msr_bitmap_va)[0x800 + msr_idx / 8] |= (UCHAR)(1 << (msr_idx % 8));
}
De schrijfregio begint op offset 0x800 ten opzichte van de leestregio. Al het overige in de bitmap is nul: passthrough op hardwaresnelheid.

EPT brengt al het fysieke geheugen 1:1 in kaart via een drieniveaustructuur:
PML4 (1 item) -> PML3 (512 items) -> PML2 (512 * 512 = 262.144 items)
Elk PML2-item is een 2MB grote pagina met volledige RWX-rechten, die 512 GB aan fysieke adresruimte beslaat. EPT-paginatabellen worden per VCPU gealloceerd, zodat per-core EPT-wijzigingen, zoals hooks, mogelijk zijn zonder synchronisatie tussen cores. Grote pagina's zijn de juiste standaard: ze minimaliseren TLB-druk en vermijden EPT-walks op 4KB-niveau voor 99% van het geheugen.
Het EPT-geheugentype voor elke 2MB-pagina wordt afgeleid uit de MTRR-configuratie van de CPU. Vóór het bouwen van de EPT leest Ophion het volgende:
- Vasterange-MTRR's: 64KB-, 16KB- en 4KB-regio's die 0x00000 tot 0xFFFFF beslaan (legacy-geheugen)
- Variabele-range-MTRR's: Willekeurige basis/masker-paren voor de rest van het fysieke geheugen
Het geheugentype van elk PML2-item komt overeen met het MTRR-type voor het betreffende bereik. Als een 2MB-pagina een MTRR-grens overschrijdt, dus twee verschillende typen binnen hetzelfde 2MB-bereik, wordt deze gemarkeerd voor opsplitsing. Onjuiste geheugentypes veroorzaken subtiele cache-coherentiefouten: UC-geheugen gemapped als WB kan verouderde lezingen veroorzaken, en WB-geheugen gemapped als UC verslechtert de prestaties aanzienlijk.
Wanneer nauwkeurige controle vereist is, bijvoorbeeld voor EPT-hooks, converteert ept_split_large_page() één enkel 2MB PML2-item naar 512 afzonderlijke 4KB PML1-items. Elk PML1-item krijgt zijn eigen MTRR-afgeleid geheugentype. De infrastructuur is aanwezig, maar er zijn geen hooks geïnstalleerd in de huidige build.
VPID-tag 1 wordt toegewezen aan alle VCPU's. Zonder VPID zou elke VM-exit en VM-entry de volledige TLB leegmaken. Met VPID bestaan gast- en host-TLB-items naast elkaar, gelabeld per VPID.
TLB-invalidatie wordt expliciet afgehandeld op de plaatsen waar dit nodig is:
- MOV CR3: INVVPID type 3 (enkelvoudige context met behoud van globals) als de CPU dit ondersteunt, anders type 1 (enkelvoudige context). Dit komt overeen met bare-metal MOV CR3-gedrag: globale kernel-TLB-items worden bewaard. Valt terug op type 2 (alle contexten) als het gewenste type mislukt. Volledig overgeslagen als de PCID no-invalidate-bit (bit 63) is ingesteld.
- INVLPG: INVVPID type 0 (individueel adres) indien ondersteund, anders type 2.
- EPT-wijzigingen: INVEPT type 1 (enkelvoudige context) of type 2.
Alle INVVPID-capabilities worden bij initialisatie opgevraagd uit IA32_VMX_EPT_VPID_CAP en gecachet.
Dit is de kern van wat Ophion onzichtbaar maakt. Elk mechanisme richt zich op specifieke detectievectoren.

Ophion presenteert zichzelf als een situatie waarbij de CPU VMX ondersteunt, maar de firmware het heeft uitgeschakeld en vergrendeld. Dit is een reële configuratie die voorkomt op zakelijke of anderszins vergrendelde machines. De gast ziet het volgende:
| Query | Resultaat | Reden |
|---|---|---|
CPUID.1.ECX[5] |
1 | Hardware-capabilitybit, altijd 1 op VMX-geschikte CPU's |
CPUID.1.ECX[31] |
0 | Geen hypervisor aanwezig |
CPUID.1.ECX[6] |
0 | Geen SMX, consistent met FEATURE_CONTROL |
IA32_FEATURE_CONTROL |
Lock=1, VMX/SMX/SGX=0 | Firmware heeft alles vergrendeld uitgeschakeld |
| VMX-MSR's (0x480-0x493) | Echte hardwarewaarden | Bestaan wanneer ECX[5]=1 |
CR4.VMXE |
0 | VMX niet actief |
| VMX-instructies | #UD |
Verwacht wanneer VMXE=0 |
Elk onderdeel is consistent met elk ander onderdeel. Er is geen enkele query die een tegenstrijdig antwoord oplevert.
VMAware, hvdetecc en de meeste anti-cheat-programma's vergelijken CPUID-reacties over meerdere leaf-bereiken om hypervisors te detecteren. Op bare metal retourneert CPUID(0x40000000) exact dezelfde waarden als CPUID(0x13371337) of CPUID(max_standard_leaf + 1): ze vallen allemaal buiten bereik en Intel-CPU's retourneren voor al deze aanroepen de gegevens van de laatste geldige standaard-leaf. De meeste hypervisors reageren op 0x40000000 met een hypervisor-ID-tekenreeks zoals Hyper-V of KVM, of met nullen. Het vergelijken van een overduidelijk ongeldige leaf met 0x40000000 en het vaststellen van verschillende reacties is een directe detectie-indicator.
Vóór VMXON cachet Ophion wat de echte CPU retourneert:
__cpuidex(cpu_info, 0x13371337, 0); // sla reactie op voor een overduidelijk ongeldige leaf
g_stealth_cpuid_cache.invalid_leaf[0] = cpu_info[0];
g_stealth_cpuid_cache.invalid_leaf[1] = cpu_info[1];
g_stealth_cpuid_cache.invalid_leaf[2] = cpu_info[2];
g_stealth_cpuid_cache.invalid_leaf[3] = cpu_info[3];
Tijdens CPUID-afhandeling bij VM-exit krijgt elke leaf in een ongeldig bereik de gecachede reactie. De bereikcontrole:
BOOLEAN stealth_is_leaf_invalid(UINT32 leaf)
{
if (leaf >= 0x40000000 && leaf <= 0x4FFFFFFF) // gereserveerd hypervisorbereik
return TRUE;
if (leaf > max_std_leaf && leaf < 0x80000000) // tussenruimte standaard en uitgebreid
return TRUE;
if (leaf >= 0x80000000 && leaf > max_ext_leaf) // voorbij uitgebreid bereik
return TRUE;
return FALSE;
}
De reactie is byte-identiek aan wat de bare-metal CPU retourneert. Er is geen waarneembaar verschil.
Twee bits worden gewist uit CPUID leaf 1 wanneer stealth actief is:
if (leaf == 1 && g_stealth_enabled)
{
cpu_info[2] &= ~((1 << 31) | (1 << 6));
}
- ECX[31] (hypervisor aanwezig): De primaire detectiebit die wordt gebruikt door elke anti-cheat- en VM-detectiebibliotheek. Windows zelf controleert deze bit om te beslissen of Hyper-V-enlightenments worden gebruikt.
- ECX[6] (SMX/TXT): Gewist voor consistentie met
IA32_FEATURE_CONTROL, dat alle SENTER/SMX-enable-bits als 0 rapporteert. SMX adverteren in CPUID terwijl de feature-control-MSR aangeeft dat het uitgeschakeld is, vormt een detecteerbare inconsistentie.
ECX[5] (VMX) wordt bewust op 1 gelaten. Dit is onderdeel van het stealthmodel: de gast ziet een CPU die VMX ondersteunt maar het in firmware uitgeschakeld heeft. ECX[5] wissen zou een andere consistentieredenering vereisen waarbij VMX capability-MSR's #GP zouden moeten genereren bij lezen.
Wanneer VMX actief is moet CR4.VMXE (bit 13) zijn ingesteld. Anti-cheat-software leest CR4 en controleert deze bit. Ophion gebruikt het CR4 guest/host mask om toegang tot bit 13 te onderscheppen. Bij een gastschrijfactie naar CR4 dwingt de handler VMXE=1 af in de werkelijke VMCS, terwijl de shadow de intentie van de gast weerspiegelt:
UINT64 actual = desired | CR4_VMX_ENABLE_FLAG;
// dwing VMX-vaste bits af op de werkelijke waarde
fixed.Flags = __readmsr(IA32_VMX_CR4_FIXED0);
actual |= fixed.Fields.Low;
fixed.Flags = __readmsr(IA32_VMX_CR4_FIXED1);
actual &= fixed.Fields.Low;
__vmx_vmwrite(VMCS_GUEST_CR4, actual);
__vmx_vmwrite(VMCS_CTRL_CR4_READ_SHADOW, desired & ~CR4_VMX_ENABLE_FLAG);
CR0/CR4-schrijfacties dwingen ook VMX-vaste bits af via IA32_VMX_CRx_FIXED0/1 op de werkelijke VMCS-waarde. Dit voorkomt VM-entry-mislukkingen door ongeldige CR-toestanden.

De klassieke hypervisortimingaanval voert RDTSC -> CPUID -> RDTSC uit en controleert het tijdsverschil. Op bare metal kost CPUID ruwweg 80-200 cycli op P-cores, en aanzienlijk meer op efficiëntiecores, ongeveer 350 cycli op Gracemont. Onder een hypervisor voegt de VM-exit plus handler plus VM-entry 500 tot meer dan 2000 cycli toe. Een vaste offset aftrekken van TSC_OFFSET is geen oplossing, omdat cumulatieve aanpassingen de TSC-monotoniteit doorbreken en uiteindelijk watchdog-time-outs en TDR veroorzaken.
Ophion gebruikt een aanpak waarbij uitsluitend de RDTSC die direct volgt op een CPUID-exit wordt onderschept.
Bij CPUID-VM-exit wordt de val geactiveerd:
vcpu->tsc_cpuid_entry = exit_tsc_start; // TSC vastgelegd aan het begin van de handler
vcpu->tsc_rdtsc_armed = TRUE;
// schakel RDTSC exiting dynamisch in
size_t proc_ctrl = 0;
__vmx_vmread(VMCS_CTRL_PROCESSOR_BASED_VM_EXECUTION_CONTROLS, &proc_ctrl);
proc_ctrl |= (size_t)CPU_BASED_VM_EXEC_CTRL_RDTSC_EXITING;
__vmx_vmwrite(VMCS_CTRL_PROCESSOR_BASED_VM_EXECUTION_CONTROLS, proc_ctrl);
Bij de volgende RDTSC-VM-exit wordt een gecompenseerde waarde geretourneerd en de val ontschakeld:
tsc = vcpu->tsc_cpuid_entry
+ g_stealth_cpuid_cache.bare_metal_cpuid_cost
+ (UINT64)(INT64)offset_raw;
vcpu->tsc_rdtsc_armed = FALSE;
// schakel RDTSC exiting uit -- terug naar hardware passthrough
Als er tussen de CPUID en de RDTSC een andere exit optreedt, zoals een externe interrupt of NMI, wordt de val ontschakeld. Het aanvalspatroon is daarmee doorbroken.
bare_metal_cpuid_cost wordt gekalibreerd vóór VMXON door het minimum te nemen van 200 afgeschermde RDTSC -> CPUID -> RDTSC-metingen. Dit levert de werkelijke hardwarekosten op zonder planningsruis.
TSC_OFFSET wordt op 0 ingesteld en nooit gewijzigd. Per CPUID-exit wordt slechts één RDTSC onderschept. Elke andere RDTSC in het systeem wordt op hardwaresnelheid doorgegeven via TSC offsetting. Nul afwijking, geen monotoniciteitsproblemen en vrijwel geen prestatie-impact.
RDTSCP gebruikt hetzelfde compensatiepad, met IA32_TSC_AUX als passthrough.
IA32_TIME_STAMP_COUNTER wordt onderschept via de MSR-bitmap. Wanneer onderschept wordt de werkelijke RDMSR nooit uitgevoerd: de VM-exit-handler wordt in plaats daarvan aangeroepen. Omdat __rdtsc() in VMX-root de ruwe hardware-TSC retourneert zonder de VMCS-offset, past de handler deze handmatig toe:
case 0x10:
{
size_t tsc_offset_raw = 0;
__vmx_vmread(VMCS_CTRL_TSC_OFFSET, &tsc_offset_raw);
msr.Flags = (UINT64)((INT64)__rdtsc() + (INT64)tsc_offset_raw);
break;
}
Deze interceptie is uitsluitend nodig omdat de MSR-bitmap dit afdwingt. Conform Intel SDM sectie 27.6.5 past "Use TSC offsetting" dezelfde offset toe op RDTSC, RDTSCP én RDMSR-lezingen van IA32_TIME_STAMP_COUNTER. Zonder het bitmap-bit ingesteld zou hardware de offset identiek toepassen voor alle drie. De interceptie bestaat zodat het TSC-compensatiepad ook RDMSR(0x10)-gebaseerde timingaanvallen kan ondervangen naast reguliere RDTSC-aanvallen.
Een actieve hypervisor heeft EnableVmxOutsideSmx=1 nodig in deze MSR, die leesbaar is vanuit de gast. Op bare metal zonder actieve VMX is dit doorgaans Lock=1 met al het overige op 0. De handler leest de echte MSR en saneert de waarde:
IA32_FEATURE_CONTROL_REGISTER feat = {0};
feat.AsUInt = __readmsr(IA32_FEATURE_CONTROL);
if (g_stealth_enabled)
{
feat.Lock = 1;
feat.EnableVmxInsideSmx = 0;
feat.EnableVmxOutsideSmx = 0;
feat.SenterLocalFunctionEnables = 0;
feat.SenterGlobalEnable = 0;
feat.SgxLaunchControlEnable = 0;
feat.SgxGlobalEnable = 0;
}
Schrijfpogingen injecteren #GP: de MSR is vergrendeld.
IA32_VMX_BASIC, IA32_VMX_PINBASED_CTLS, IA32_VMX_PROCBASED_CTLS, IA32_VMX_EXIT_CTLS, IA32_VMX_ENTRY_CTLS, IA32_VMX_EPT_VPID_CAP en overige. Conform de Intel SDM bestaan deze MSR's wanneer CPUID.1.ECX[5]=1 (VMX ondersteund), ongeacht of VMX is ingeschakeld in IA32_FEATURE_CONTROL. Omdat Ophion ECX[5] intact laat, VMX ondersteund maar uitgeschakeld, dienen lezingen van deze MSR's de echte hardwarewaarden te retourneren. Dat doen ze ook: de leeshandler valt terug op __readmsr() en geeft de werkelijke capability-gegevens terug aan de gast.
Schrijfpogingen injecteren #GP:
if (target_msr == IA32_FEATURE_CONTROL ||
(target_msr >= IA32_VMX_BASIC && target_msr <= 0x493))
{
vmexit_inject_gp();
vcpu->advance_rip = FALSE;
return;
}
Deze MSR's zijn architecturaal alleen-lezen. IA32_FEATURE_CONTROL is vergrendeld.
Hyper-V, KVM en andere hypervisors stellen interface-MSR's beschikbaar in het bereik 0x40000000 tot 0x4FFFFFFF. Op bare metal genereren deze #GP. Ophion injecteert #GP voor zowel lees- als schrijfpogingen over dit gehele bereik.
DR0-DR3 en DR6 worden niet automatisch opgeslagen en hersteld door de VMCS bij VM-exit en VM-entry, in tegenstelling tot DR7 dat dit wel doet. Als de host deze registers overschrijft tijdens VM-exit-verwerking, kan een detectieprogramma dat DR0 instelt op een bekend adres en DR6 controleert na een CPUID de afwijking herkennen. Concreet geldt: als TF (trap flag) is ingesteld en DR0 is ingesteld op het adres na CPUID, zou DR6 op bare metal BS (single-step) en B0 (breakpoint 0 overeenkomst) tonen. Als de hypervisor deze registers niet correct beheert, zal DR6 een onjuiste waarde bevatten.
Ophion slaat DR0-DR3 en DR6 op in per-VCPU-schaduwvelden. Bij elke VM-exit worden ze opgeslagen uit de hardware voordat iets ze kan overschrijven. Vóór VMRESUME worden ze hersteld:
// bovenaan vmexit_handler -- gastoestand opslaan voordat iets overschreven kan worden
vcpu->guest_dr0 = __readdr(0);
vcpu->guest_dr1 = __readdr(1);
vcpu->guest_dr2 = __readdr(2);
vcpu->guest_dr3 = __readdr(3);
vcpu->guest_dr6 = __readdr(6);
// ... handlerverzending, C-code kan DR's overschrijven ...
// vóór VMRESUME -- gastoestand herstellen
__writedr(0, vcpu->guest_dr0);
__writedr(1, vcpu->guest_dr1);
__writedr(2, vcpu->guest_dr2);
__writedr(3, vcpu->guest_dr3);
__writedr(6, vcpu->guest_dr6);
De MOV DR-handler leest en schrijft de per-VCPU-velden in plaats van hardwareregisters. DR7 wordt afgehandeld door de VMCS via hardware opslaan en herstellen. DR4 en DR5 zijn aliassen van respectievelijk DR6 en DR7 wanneer CR4.DE=0, en injecteren #UD wanneer CR4.DE=1, conform de Intel SDM.
Wanneer de hypervisor RIP doorschuift na het afhandelen van een instructie, bijvoorbeeld CPUID, moet worden gecontroleerd of hardware-breakpoints overeenkomen met de nieuwe RIP. Op bare metal doet de CPU dit automatisch. Na een VM-exit slaat de CPU de single-step (BS)-bit op in het veld voor openstaande debug-uitzonderingen, maar controleert DR0-DR3 niet. Dat moet handmatig gebeuren:
for (int i = 0; i < 4; i++)
{
if (!(dr7 & (ln_bits[i] | gn_bits[i]))) // niet ingeschakeld
continue;
if ((dr7 & DR7_RW_MASK(i)) != 0) // geen uitvoerings-BP (R/W moet 00 zijn)
continue;
UINT64 drn;
switch (i)
{
case 0: drn = vcpu->guest_dr0; break;
case 1: drn = vcpu->guest_dr1; break;
case 2: drn = vcpu->guest_dr2; break;
case 3: drn = vcpu->guest_dr3; break;
}
if (drn == new_rip)
bp_matched |= bn_bits[i];
}
if (bp_matched)
{
pending |= bp_matched | PENDING_DEBUG_ENABLED_BP;
__vmx_vmwrite(VMCS_GUEST_PENDING_DEBUG_EXCEPTIONS, pending);
}
Zonder deze controle zouden hardware-breakpoints op de instructie direct na een VM-exit-veroorzakende instructie stilzwijgend niet worden geactiveerd.

Correcte interruptafhandeling is niet onderhandelbaar. Gebrekkige afhandeling resulteert in TDR, BSOD of subtiele geheugencorruptie.
Met ACK_INTERRUPT_ON_EXIT erkent de CPU de interrupt bij de LAPIC bij VM-exit en slaat de vector op. De handler controleert of de gast de interrupt kan verwerken:
BOOLEAN guest_interruptible =
(rflags_raw & (1ULL << 9)) && // RFLAGS.IF=1
!(intr_state & (GUEST_INTR_STATE_BLOCKING_BY_STI |
GUEST_INTR_STATE_BLOCKING_BY_MOV_SS));
if (guest_interruptible)
{
vmexit_inject_interrupt(vector);
}
else
{
// uitstellen: sla de vector op, schakel interrupt-window exiting in
vcpu->pending_ext_vector = (UINT8)vector;
vcpu->has_pending_ext_interrupt = TRUE;
proc_ctrl |= (size_t)CPU_BASED_VM_EXEC_CTRL_INTERRUPT_WINDOW_EXITING;
__vmx_vmwrite(VMCS_CTRL_PROCESSOR_BASED_VM_EXECUTION_CONTROLS, proc_ctrl);
}
Bij de volgende interrupt-window-VM-exit, die optreedt wanneer de gast onderbreekbaar wordt, wordt de uitgestelde vector geïnjecteerd en wordt window exiting uitgeschakeld.
Met virtual NMI's ingeschakeld stelt een NMI-VM-exit (exitreden 0: uitzondering of NMI) blocking-by-NMI in de gastonderbrekbaarheidstoestand in. De handler kan de NMI niet blindelings opnieuw injecteren: VM-entry vereist blocking-by-NMI = 0 bij het injecteren van type NMI, conform SDM 26.3.1.1. Omdat de NMI werd onderschept vóór levering aan de gast, wist de handler blocking-by-NMI vóór herinjectie. Dit is veilig: VM-entry herstelt de blokkering automatisch bij het leveren van de geïnjecteerde NMI, conform SDM 26.6.1.2, en de IRET van de gast wist deze op de gebruikelijke manier.
Wanneer een NMI-VM-exit de IDT-levering van een andere gebeurtenis onderbreekt, gedetecteerd via VMCS_IDT_VECTORING_INFORMATION, kan de NMI niet onmiddellijk worden geïnjecteerd omdat de IDT-gebeurtenis prioriteit heeft. De NMI wordt uitgesteld: opgeslagen in has_pending_nmi en NMI-window exiting wordt ingeschakeld. De NMI-window-exit treedt op nadat de gast IRET heeft uitgevoerd, wat de virtual NMI-blokkering opheft, waarna de uitgestelde NMI wordt geïnjecteerd.
Als een VM-exit optreedt terwijl de CPU midden in de levering van een uitzondering of interrupt via de IDT is, registreert VMCS_IDT_VECTORING_INFORMATION de onderbroken gebeurtenis. Ophion injecteert deze opnieuw bij de volgende VM-entry, met prioriteit boven alles wat de handler mogelijk in de wachtrij heeft geplaatst. Software-uitzonderingen en -interrupts krijgen hun instructielengte doorgegeven. Gebeurtenissen met een foutcode krijgen hun foutcode doorgegeven.
Wanneer de VM-exit-reden een uitzondering is die optrad tijdens IDT-levering, is uitzonderingscombinatie van toepassing, conform SDM Vol. 3 tabel 6-5. Bepaalde combinaties produceren een double fault of triple fault in plaats van sequentiële levering:
- Contributory + contributory -> #DF (contributory omvat #DE, #TS, #NP, #SS, #GP)
- #PF + contributory of #PF + #PF -> #DF
- #DF + elke uitzondering -> shutdown (triple fault)
- Onschadelijke combinaties -> injecteer de IDT-gebeurtenis opnieuw; de exit-uitzondering wordt opnieuw gegenereerd tijdens levering

Het probleem: HOST_CR3 in de VMCS is de paginatabel die wordt gebruikt tijdens VM-exit-verwerking. Als deze verwijst naar de systeempaginatabellen, kan gastmodecode, inclusief anti-cheat-drivers, kernel-PTE's beschadigen en de hypervisor in hostmodus laten crashen.
De oplossing: hostcr3_build() kopieert het kernelgedeelte van de systeempaginatabellen diep:
- Lees de systeemproces-PML4 uit
PsInitialSystemProcess->DirectoryTableBase - Kloon voor elk aanwezig kernel-PML4-item (indices 256-511) recursief PDPT -> PD -> PT
- Grote pagina's (1GB, 2MB) worden als-is gekopieerd: bladitems verwijzen nog steeds naar hetzelfde fysieke RAM
- Corrigeer het zelf-verwijzende PML4-item zodat het verwijst naar de privé-PML4
- Zet gebruikersruimte-items (0-255) op nul: hostmodus voert nooit gebruikerscode uit
De zelf-verwijzende correctie is het subtiele onderdeel. Windows gebruikt één PML4-item dat terugwijst naar zichzelf voor paginatabel-zelfafbeelding. Zonder de update zouden de privépaginatabellen nog steeds verwijzen naar de originele PML4:
for (UINT32 i = 256; i < 512; i++)
{
if ((our_pml4[i] & PTE_PRESENT) &&
((our_pml4[i] & PTE_PFN_MASK) == pml4_pa)) // verwijst naar originele PML4
{
our_pml4[i] = (our_pml4[i] & ~PTE_PFN_MASK) | g_host_pml4_pa;
break;
}
}
Fysieke paginatabelpagina's worden gemapped via MmGetVirtualForPhysical() in plaats van MmMapIoSpace. De eerste functie werkt voor alle RAM-gedekte PFN's en vereist geen ontkoppeling. De tweede mislukt in sommige gevirtualiseerde omgevingen.
De momentopname is statisch: dynamische kernel-PTE-wijzigingen na het laden worden niet bijgehouden. Dit is acceptabel omdat alle hostmodus-allocaties, zoals VMM-stacks en bitmaps, worden uitgevoerd vóórdat de momentopname wordt genomen.
De VMCALL-interface gebruikt een handtekening met drie registers:
if (regs->r10 != 0x48564653ULL || // 'HVFS'
regs->r11 != 0x564d43414c4cULL || // 'VMCALL'
regs->r12 != 0x4e4f485950455256ULL) // 'NOHYPERV'
{
vmexit_inject_ud();
vcpu->advance_rip = FALSE;
return;
}
Twee controles vóór verzending:
- CPL-controle: Lees de toegangsrechten van gast-CS uit de VMCS. DPL moet 0 zijn, uitsluitend ring 0. VMCALL vanuit gebruikersmodus genereert
#UD. - Handtekeningcontrole: Alle drie de handtekeningregisters moeten exact overeenkomen. Elke afwijking genereert
#UD.
Alle overige VMX-instructies, waaronder VMCLEAR, VMPTRLD, VMREAD, VMWRITE, VMXON, VMXOFF, VMLAUNCH, VMRESUME, INVEPT, INVVPID en GETSEC, injecteren eveneens #UD. Dit is consistent met de gast die CR4.VMXE=0 ziet: op bare metal veroorzaakt het uitvoeren van VMX-instructies zonder VMXE ingesteld #UD.

asm_vmexit_handler is HOST_RIP. Bij elke VM-exit springt de CPU naar dit adres. De stackindeling na alle opslagen:
[codeblock]asm ; [rsp + 0x000] rax (GUEST_REGS begint hier) ; [rsp + 0x008] rcx ; ... ; [rsp + 0x078] r15 ; [rsp + 0x080] xmm0..xmm5 + mxcsr (0x110 bytes) ; [rsp + 0x190] rflags ; [rsp + 0x198] opvulling ; [rsp + 0x1A0] <-- originele HOST_RSP ; [rsp + 0x1A8] VCPU-pointer (opgeslagen op HOST_RSP - 8 door vmx_setup_vmcs)
De assembly slaat XMM0-XMM5 en MXCSR op, vluchtig onder de x64 ABI en daardoor overschrijfbaar door C-code, plaatst alle 16 GPR's op de stack als de `GUEST_REGS`-struct en roept vervolgens de C-handler aan:
[codeblock]asm
mov rcx, rsp ; arg1: PGUEST_REGS
mov rdx, [rsp + 01A8h] ; arg2: VCPU-pointer (van VMM-stack)
call vmexit_handler
Bij FALSE-retour: herstel GPR's, XMM, RFLAGS en spring naar vmx_vmresume. Bij TRUE-retour (vmxoff): herstel GPR's, haal gast-RSP/RIP op uit de VCPU-structuur, stel RSP in, plaats RIP op de stack en keer terug naar gastcode via ret.
De C-handler leest de exitreden, kwalificatie en gast-RIP/RSP, slaat debugregisters op en verzendt naar de juiste subhandler. Na verzending:
- Controleer IDT-vectoring en injecteer opnieuw indien nodig, met prioriteit boven handlerinjectie
- Schuif RIP door indien van toepassing, met handmatige BP-controle en samenvoeging in openstaande debug-uitzonderingen
- Herstel gast-DR0-DR3/DR6 naar de hardware
- Retourneer FALSE voor VMRESUME, TRUE voor VMXOFF
XSETBV-exits worden gevalideerd conform Intel SDM Vol. 1 sectie 13.3, met gebruik van het hardware-XCR0-capabiliteitsmasker uit CPUID.0Dh.0, gecachet bij initialisatie. In plaats van hard te coderen welke XCR0-bits geldig zijn, wordt het masker bij initialisatie uitgelezen van de CPU. Dit zorgt automatisch voor ondersteuning van PKRU, AMX en toekomstige uitbreidingen.
Validatieregels, allemaal resulterend in #GP bij overtreding:
- De bovenste 32 bits van ECX moeten 0 zijn
- Alleen XCR0 (index 0) is geldig
- Bit 0 (x87) moet altijd ingesteld zijn
- AVX (bit 2) vereist SSE (bit 1)
- MPX-bits (3-4) moeten beide ingesteld of beide gewist zijn
- AVX-512-bits (5-7) moeten allemaal tegelijk ingesteld zijn, samen met AVX en SSE
- AMX-bits (17-18) moeten beide ingesteld of beide gewist zijn
- Geen bits voorbij de hardwarecapabiliteit
De primaire processorbesturingen vragen uitsluitend TSC offsetting, MSR-bitmaps, I/O-bitmaps en secundaire besturingen aan. Alles in dit onderdeel wordt afgehandeld omdat must-be-1-bits op bepaalde CPU's de betreffende exits kunnen afdwingen, en een onverwerkte exit het gastsysteem zou laten crashen.
- MOV naar CR3: Verwijdert de PCID no-invalidate-bit (bit 63) vóór het schrijven naar
VMCS_GUEST_CR3, omdat VM-entry CR3 met bit 63 ingesteld afwijst. Geeft INVVPID uit voor TLB-coherentie, tenzij no-invalidate was ingesteld. - CLTS: Wist CR0.TS (bit 3) in zowel de werkelijke VMCS-CR0 als de shadow, waarbij VMX-vaste bits opnieuw worden afgedwongen op de werkelijke waarde.
- LMSW: Laadt bits 0-3 van CR0 vanuit de bronoperand. PE (bit 0) kan worden ingesteld maar nooit gewist door LMSW, conform de Intel SDM. VMX-vaste bits worden opnieuw afgedwongen.
- MOV naar/van CR8: Passthrough van het TPR-register, gemaskeerd tot bits [3:0].
- INVD: Geconverteerd naar
WBINVD. INVD verwijdert vuile cacheregels zonder terugschrijven: gebruik op een draaiend systeem zou geheugen beschadigen. - INVLPG: Geeft INVVPID uit, individueel adres of alle contexten als terugval, voor TLB-coherentie.
- RDPMC: Passthrough voor geldige tellers. Ongeldige tellers genereren
#GP. - HLT: Stelt de gastactiviteitstoestand in op HLT. De CPU genereert een VM-exit bij de volgende interrupt.
- WBINVD/MWAIT/MONITOR/PAUSE: Geen bewerking, RIP wordt doorgeschoven.
- GETSEC:
#UD(SMX is verborgen).
Alle stealthfuncties zijn compilatietijd-schakelaars in stealth.h, bewaakt door de runtime-vlag g_stealth_enabled:
| Schakelaar | Standaard | Beschrijving |
|---|---|---|
STEALTH_ENABLED |
1 | Hoofdschakelaar |
STEALTH_HIDE_CR4_VMXE |
1 | Verberg CR4.VMXE via shadow |
STEALTH_COMPENSATE_TIMING |
0 | TSC-compensatie (vang volgende RDTSC) |
STEALTH_CPUID_CACHING |
1 | Cache native CPUID voor ongeldige leaves |
USE_PRIVATE_HOST_CR3 |
1 | Geïsoleerde host-paginatabellen |
Doorstaat alle tests met alle stealthschakelaars ingeschakeld:
- hvdetecc: CR4.VMXE-shadow, MSR 0x10-interceptie, CPUID-timing
- VMAware: DR-val (DR0 + TF op CPUID, DR6 BS+B0), CPUID-leaf-controles
- checkhv_um: RDTSC+CPUID+RDTSC-timing, CPUID-leaf-enumeratie, merktekenreeks

Getest op Intel Core i5-14400F (14e generatie), Windows 10 x64.
- CR4.VMXE schrijven-dan-lezen: Op bare metal toont het teruglesen van CR4 na schrijven met VMXE=1 eveneens VMXE=1. Onder Ophion verwijdert de shadow altijd VMXE, zodat het teruglesen VMXE=0 toont. Een detectieprogramma dat VMXE schrijft en controleert of het behouden bleef, zou een afwijking constateren. Dit is de bewuste afweging van VMXE-verberging: het alternatief, VMXE niet verbergen, stelt een veelgebruiktere en eenvoudiger detectievector bloot.
- Prestatietellers: RDPMC wordt doorgegeven. Een timingaanval met
IA32_MPERF,IA32_APERFof PMC-gebaseerde instructietelling zou VM-exit-overhead kunnen detecteren. Dit zou MSR-interceptie en offset-compensatie vereisen. - Wandkloktiming: De huidige-tellerregisters van HPET en de LAPIC-timer zijn geheugengemapped via MMIO en worden niet onderschept door de huidige EPT-configuratie. Een aanval die HPET uitleest naast RDTSC zou afwijkingen kunnen detecteren. Dit zou EPT-hooks op de HPET-MMIO-pagina vereisen.
- Host-CR3-veroudering: De privé-host-paginatabellen zijn een statische momentopname. Dynamische kernel-PTE-wijzigingen na het laden worden niet weergegeven. Dit is acceptabel voor de eigen allocaties van de hypervisor, die vóór de momentopname zijn gemapped, maar betekent dat hostmodus werkt met licht verouderde kernel-mappings.
- Geen EPT-hooks: De infrastructuur voor het opsplitsen van 2MB naar 4KB is geïmplementeerd, maar er zijn geen hooks geïnstalleerd. EPT-gebaseerde hooking via execute-only-pagina's en gesplitste TLB is de logische volgende stap.
Vereist Visual Studio 2022, WDK 10.0.26100.0 en MSVC met MASM (x64).
MSBuild.exe Ophion.sln /p:Configuration=Release /p:Platform=x64
Uitvoer: build\bin\Release\Ophion.sys (testondertekend).
bcdedit /set testsigning on
sc create Ophion type= kernel binPath= "C:\pad\naar\Ophion.sys"
sc start Ophion
sc stop Ophion
sc delete Ophion