Per non rendere troppo tedioso il discorso si eviterà di mostrare nuovamente il codice già analizzato nel corso delle precedenti lezioni. Il progetto completo può essere visionato cliccando sul link fornito a fine lezione.
Variabili globali
Per la corretta esecuzione di questa prima demo serviranno alcune variabili globali, visibili a varie componenti del nostro hypervisor.
// Variabili globali in cui salvare il valore dei registri RSP ed RBP // al fine di ritornare nel codice di DriverEntry una volta che si // decide di uscire dalla VMX operation. UINT64 g_StackPointerForReturning; UINT64 g_BasePointerForReturning; // Risorse allocate nella virtualizzazione di ogni processore logico. typedef struct _VCPU { UINT64 VMXON_REGION_VA; // Indirizzo virtuale di regione VMXON UINT64 VMXON_REGION_PA; // Indirizzo fisico di regione VMXON UINT64 VMCS_REGION_VA; // Indirizzo virtuale di regione VMCS UINT64 VMCS_REGION_PA; // Indirizzo fisico di regione VMCS UINT64 VMM_STACK; // Stack dell'hypervisor durante VM-Exit UINT64 GuestRipVA; // VM Entry point GROUP_AFFINITY OldAffinity; // Original Thread Affinity }VCPU, * PVCPU; // Variabile globale che punterà ad un array di VCPU, con le risorse // allocate per tutti i processori. extern PVCPU vmState;
La struttura VCPU è già stata descritta brevemente in una precedente lezione. Il suo scopo è quello di raccogliere tutte le risorse allocate necessarie alla virtualizzazione di ogni processore, in modo che le varie componenti del nostro hypervisor possano accedervi qualora ce ne fosse bisogno. Il campo OldAffinity non punta propriamente ad una risorsa ma la inseriamo lo stesso in tale struttura perché ci farà comodo. La variabile vmState è esterna perché è quella che verrà inizializzata ed usata dalle varie componenti dell'hypervisor.
Sulle variabili globali g_StackPointerForReturning e g_BasePointerForReturning si ritornerà a breve nelle prossime sezioni, dove si spiegherà la loro utilità ed il loro utilizzo.
DriverEntry
Per semplificare le cose, in questa specifica demo ci si limiterà a virtualizzare solo un processore logico. Il motivo è spiegato brevemente nei commenti al codice seguente ma il tutto sarà più chiaro una volta che si saranno esaminati anche gli altri estratti di codice nelle prossime sezioni.
PVCPU vmState = NULL; NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { // ... // Lo scopo di questo esempio è quello di uscire dalla VMX operation // alla prima istruzione del guest, che sarà HLT. // Dato che il guest non è ancora l'intero sistema in uso, si può usare // un solo processore logico che, una volta uscito dalla VMX operation, // può ritorna qui, per continuare ad eseguire il codice di DriveEntry. // Se si volessero usare tutti i processori, cosa eseguirebbero questi una // volta usciti dalla VMX operation se solo ad uno di essi è permesso di // eseguire DriverEntry? Si ragioni su questo. UINT64 ProcessorCount = 1; // KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS); ULONG ProcessorIndex = KeGetCurrentProcessorIndex(); // Alloca spazio per un solo VCPU nella variabile globale che conserva le // risorse da allocare per la virtualizzazione di ogni processore logico. vmState = ExAllocatePoolWithTag(NonPagedPool, sizeof(VCPU) * ProcessorCount, POOLTAG); RtlZeroMemory(vmState, sizeof(VCPU) * ProcessorCount); // Inizializza VMX, VMCS e lancia il guest if (VmxInitialize(ProcessorCount, ProcessorIndex)) { KdPrint(("[*] Hypervisor loaded successfully :)\n")); KdPrint(("[*] Restoring old thread affinity.\n")); KeRevertToUserGroupAffinityThread(&(vmState->OldAffinity)); KdPrint(("[*] Clearing CR4.VMXE bit.\n")); CpuVmxEnable(FALSE); } else { KdPrint(("[*] Hypervisor was not loaded :(")); KdPrint(("[*] Restoring old thread affinity.\n")); KeRevertToUserGroupAffinityThread(&(vmState->OldAffinity)); KdPrint(("[*] Clearing CR4.VMXE bit.\n")); CpuVmxEnable(FALSE); } return STATUS_SUCCESS; }
Dovendo virtualizzare un solo processore, la variabile globale vmState punterà ad un array di un solo elemento di tipo VCPU.
VmxInitialize
La prima parte di questo metodo si occupa di abilitare la VMX operation e di inizializzare la VMCS. Si noti come le allocazioni di memoria eseguite durante la fase di inizializzazione della VMCS vengano salvate nella variabile globale vmState.
// Inizializza VMX, VMCS e lancia il guest BOOLEAN VmxInitialize(UINT64 LogicalProcessors, ULONG ProcessorIndex) { // Controlla se è possibile entrare in VMX Operation if (!CpuIsVmxSupported()) { KdPrint(("[*] VMX is not supported in this machine !")); return FALSE; } KIRQL OldIrql; PROCESSOR_NUMBER ProcessorNumber; GROUP_AFFINITY Affinity, OldAffinity; KdPrint(("\n=====================================================\n")); for (ULONG i = 0; i < LogicalProcessors; i++) { // Converte da indice di sistema ad indice locale nel relativo gruppo KeGetProcessorNumberFromIndex(ProcessorIndex, &ProcessorNumber); // Imposta affinità del thread corrente per eseguirlo su i-esimo processore RtlSecureZeroMemory(&Affinity, sizeof(GROUP_AFFINITY)); Affinity.Group = ProcessorNumber.Group; Affinity.Mask = (KAFFINITY)((ULONG64)1 << ProcessorNumber.Number); KeSetSystemGroupAffinityThread(&Affinity, &OldAffinity); KdPrint(("\n\t\tCurrent thread is executing in %d th logical processor.\n\n", ProcessorIndex)); // Eleva IRQL a DISPATCH_LEVEL per impedire context switch OldIrql = KeRaiseIrqlToDpcLevel(); // Imposta i bit di CR0 e CR4 (compreso CR4.VMXE) CpuFixBits(); KdPrint(("[*] VMX-Operation Enabled Successfully\n")); // Salva vecchia maschera di affinità del thread. KdPrint(("[*] Saving old thread affinity.\n")); vmState[i].OldAffinity = OldAffinity; // Alloca spazio per VMXON region if (!AllocateVMCSRegion(&vmState[i], TRUE)) { KdPrint(("[*] Error in allocating memory for Vmxon region")); return FALSE; } // Alloca spazio per VMCS region if (!AllocateVMCSRegion(&vmState[i], FALSE)) { KdPrint(("[*] Error in allocating memory for Vmcs region")); return FALSE; } KdPrint(("[*] VMCS Region is allocated at ===============> %llx\n", vmState[i].VMCS_REGION_VA)); KdPrint(("[*] VMXON Region is allocated at ===============> %llx\n", vmState[i].VMXON_REGION_VA)); // Alloca spazio che verrà usato come stack dall'hypervisor durante le Vm exit if (!AllocateVmmStack(i)) return FALSE; KdPrint(("[*] HOST stack is allocated at =================> %llx\n", vmState[i].VMM_STACK)); // Alloca memoria che conterrà il codice del guest. // In questo caso si tratta di una semplice pagina che inizia con l'istruzione HLT. vmState[i].GuestRipVA = (UINT64)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOLTAG); if (vmState[i].GuestRipVA == (UINT64)NULL) return FALSE; RtlZeroMemory((PUINT64)vmState[i].GuestRipVA, PAGE_SIZE); void* TempAsm = "\xF4"; // F4 opcode di HLT memcpy((PUINT64)vmState[i].GuestRipVA, TempAsm, 1); KdPrint(("[*] HOST RIP is allocated at ===================> %llx\n", vmState[i].GuestRipVA)); // Imposta a clear lo stato della VMCS. if (!VmxClearVmcs(&vmState[i])) { return FALSE; } // Imposta la VMCS come corrente ed attiva. if (!VmxLoadVmcs(&vmState[i])) { return FALSE; } // Inizializza la VMCS KdPrint(("[*] Setting up VMCS.\n")); SetupVmcs(&vmState[i], vmState[i].GuestRipVA); // Ristabilisce IRQL. KeLowerIrql(OldIrql); DbgPrint("[*] Executing VMLAUNCH.\n"); KdPrint(("\n=====================================================\n")); // Dopo VMLAUNCH il controllo viene passato al guest. // In questo caso il guest eseguirà HLT che causerà una VM exit: questa verrà gestita // semplicemente spegnendo tutto con VMXOFF. Cosa succede dopo? // L'idea è quella di proseguire da qui, ed in particolare dal return TRUE // finale che ritorna al codice che ha invocato VmxInitialize (in questo caso // DriverEntry) per proseguire normalmente. // Il problema è che return TRUE non verrà mai eseguita. Quindi? // Se si memorizzano i registri RSP ed RBP in questo punto è possibile ripristinarli // successivamente nel gestore della VM exit provocata dall'HLT. A quel punto // si può manipolare lo stack e ricalcolare manualmente l'indirizzo di ritorno che punta // all'istruzione successiva alla chiamata di VmxInitialize in DriverEntry. // Per simulare il valore di ritorno, invece, è sufficiente impostare RAX ad 1 // (si veda codice di AsmSaveVMXOFFState). AsmSaveVMXOFFState(); // Esegue codice guest. // In questo caso si esegue HLT, che provoca una VM exit. // Quindi si può proseguire andando a vedere il codice di AsmVMExitHandler. __vmx_vmlaunch(); // // Se VMLAUNCH viene eseguita con successo non si arriverà mai qui... // // ... In caso contrario legge l'errore ed esegue VMXOFF. ULONG64 ErrorCode = 0; __vmx_vmread(VM_INSTRUCTION_ERROR, &ErrorCode); __vmx_off(); KdPrint(("[*] VMLAUNCH Error : 0x%llx\n", ErrorCode)); DbgBreakPoint(); } // Se tutto è andato per il meglio non si arriverà mai qui. return TRUE; }
Dopo aver inizializzato la VMCS (ed averlo reso quello corrente ed attivo) si è pronti a lanciare il relativo guest, da eseguire sull'unico processore virtualizzato. Prima di fare ciò, però, bisogna valutare bene quello che andrà a fare il guest e cosa dovrebbe avvenire una volta usciti dalla VMX operation con VMXOFF (cioè da dove si riprende ad eseguire il codice?). In questo caso si vuole che il guest esegua una sola istruzione (HLT) e che questo causi una VM exit, il cui gestore esegue una VMXOFF per uscire dalla VMX operation. OK! Semplice. E dopo che si fa? La logica suggerisce di riprendere ad eseguire il codice dal punto in cui si era interrotto a seguito del lancio del guest. A tale scopo si possono salvare i valori dei registri RSP ed RBP poco prima di invocare __vmx_vmlaunch. Una volta usciti dalla VMX operation si possono ricaricare i valori salvati nei rispettivi registri al fine di ricalcolare un indirizzo di ritorno valido (si vedrà come a breve).
Nota: in questo momento la cosa importante da comprendere è che se si riprende da dove si era interrotto (__vmx_vmlaunch) alla fine si rientrerà in DriverEntry (il chiamante di VmxInitialize), il cui codice dovrebbe essere eseguito una sola volta. Ecco spiegato il motivo per cui, almeno in questa demo, si virtualizza un solo processore. Quando si riuscirà ad eseguire l'intero sistema come guest allora le cose saranno diverse (come verrà spiegato al momento opportuno nel corso delle prossime lezioni).
SetupVmcs
Questo metodo non presenta particolari novità rispetto a quanto già visto nel corso delle lezioni precedenti, dedicate all'inizializzazione della VMCS. Il codice completo può essere consultato visitando il repository del progetto, il cui link è fornito a fine lezione. Da notare come il codice guest eseguito al lancio si trovi nella pagina allocata in VmxInitialize (passata a SetupVmcs come parametro), che ha il primo byte uguale ad F4 (opcode di HLT).
VOID SetupVmcs(PVCPU vms, UINT64 guestRIP) { // ... // Primary processor-based control PRIMARY_PROCBASED_CTLS primary_controls = { 0 }; // Imposta bit specifici del primary processor-based control. SetPrimaryCtls(&primary_controls); // ... // Imposta il codice guest da eseguire al VMLAUNCH __vmx_vmwrite(GUEST_RIP, guestRIP); // ... // Imposta il gestore delle VM exit per l'hypervisor __vmx_vmwrite(HOST_RIP, (UINT64)AsmVMExitHandler); // ... // Salva primary processor-based control in VMCS corrente ed attiva __vmx_vmwrite(CONTROL_PRIMARY_PROCESSOR_BASED_VM_EXECUTION_CONTROLS, AdjustControls(primary_controls.Uint32, vmx_basic.Bits.VmxTrueControls ? MSR_IA32_VMX_TRUE_PROCBASED_CTLS : MSR_IA32_VMX_PROCBASED_CTLS)); }
void SetPrimaryCtls(PPRIMARY_PROCBASED_CTLS primary_controls) { // .. /** * This control determines whether the secondary processor-based VM-execution controls are * used. If this control is 0, the logical processor operates as if all the secondary processor-based * VM-execution controls were also 0. */ primary_controls->Bits.ActivateSecondaryControls = TRUE; /** * This control determines whether executions of HLT cause VM exits. */ primary_controls->Bits.HLTExiting = TRUE; }
Il gestore delle VM exit (AsmVMExitHandler) sarà esaminato a breve.
AsmSaveVMXOFFState
Prima di eseguire VMLAUNCH (tramite intrinsic __vmx_vmlaunch) in VmxInitialize, si invoca la funzione AsmSaveVMXOFFState, che salva i valori dei registri RSP ed RBP (per tale motivo deve essere scritta in linguaggio assembly).
EXTERN g_StackPointerForReturning : QWORD EXTERN g_BasePointerForReturning : QWORD AsmSaveVMXOFFState PROC PUBLIC ; Salva RSP e RBP nelle variabili globali. ; Su stack in questo momento c'è Return Address (RA) a VmxInitialize. ; In altre parole, RSP punta ad istruzione dopo call AsmSaveVMXOFFState in ; VmxInitialize, che in questo caso è __vmx_vmlaunch. mov g_StackPointerForReturning, rsp mov g_BasePointerForReturning, rbp ret AsmSaveVMXOFFState ENDP
Al momento dell'invocazione di AsmSaveVMXOFFState sullo stack c'è l'indirizzo di ritorno a VmxInitialize, che punta all'istruzione successiva (e cioè __vmx_vmlaunch). Per il momento è sufficiente essere consapevoli del fatto che RSP punta a __vmx_vmlaunch in VmxInitialize. Il motivo per cui è necessario salvare tali registri in variabili globali sarà spiegato a breve ed allora tutto avrà più senso.
AsmVMExitHandler
Una volta eseguita __vmx_vmlaunch in VmxInitialize viene causata immediatamente una VM exit a seguito del fatto che il primo byte del codice guest rappresenta proprio una istruzione HLT. Come disposto nel codice di inizializzazione della VMCS, tale istruzione causa una VM exit.
EXTERN VmxVMExitHandler : PROC AsmVMExitHandler PROC ; shadow space/storage (4 * 8 byte + altri 8 byte per ; riallineare lo stack a 16 byte, disallineato a causa ; dell'inserimento implicito del return address sullo stack ; al momento della call). Totale 40 = 28h byte. sub rsp, 28h call VmxVMExitHandler add rsp, 28h ret AsmVMExitHandler ENDP
Il gestore delle VM exit non fa altro che invocare la funzione VmxExitHandler. Anche se al momento non si vede una reale utilità nello scrivere il gestore in assembly, sarà evidente nel corso delle prossime lezioni come questo sia essenziale per salvare alcuni dati di vitale importanza.
VmxVMExitHandler
Questo metodo è, di fatto, il reale gestore delle VM exit nell'hypervisor. Per questa demo è sufficiente intercettare le VM exit causate dall'esecuzione dell'istruzione HLT da parte del guest. L'hypervisor può sapere qual è stata la causa della VM exit leggendo un particolare campo della VMCS, all'interno del VM-Exit Information (area della VMCS già vista brevemente nella scorsa lezione). Il valore codificato VM_EXIT_REASON si riferisce proprio a tale campo ed i valori che è possibile leggere in esso sono definiti nell'appendice C del volume 3D dell'Intel SDM.
VM_EXIT_QUALIFICATION invece, che si riferisce ad un altro campo del VM-Exit Information, contiene informazioni aggiuntive sulle cause del VM exit (al momento non ci interessa approfondire tale argomento; maggiori dettagli verranno forniti nelle prossime lezioni).
VOID VmxVMExitHandler() { ULONG ExitReason = 0; __vmx_vmread(VM_EXIT_REASON, &ExitReason); ULONG ExitQualification = 0; __vmx_vmread(VM_EXIT_QUALIFICATION, &ExitQualification); KdPrint(("\nEXIT_REASION 0x%x\n", ExitReason & 0xffff)); KdPrint(("EXIT_QUALIFICATION 0x%x\n\n", ExitQualification)); switch (ExitReason) { case EXIT_REASON_HLT: { DbgPrint("[*] Execution of HLT detected... \n"); // DbgBreakPoint(); // Spegne tutto con VMXOFF e dirotta l'esecuzione in DriverEntry. AsmRestoreToVMXOFFState(); break; } default: { // DbgBreakPoint(); break; } } }
I commenti al codice indicano che se è stata eseguita un'istruzione HLT allora AsmRestoreToVMXOFFState esegue VMXOFF (che permette di uscire dalla VMX operation) e dirotta l'esecuzione del thread in DriverEntry. WOW! Sembra piuttosto interessante perché è molto simile a quello che si sarebbe voluto ottenere una volta usciti dalla VMX operation. Nella sezione sul metodo VmxInitialize si era detto che si voleva riprendere da dopo __vmx_vmlaunch, ma dopo tale istruzione ci sono altre istruzioni dedicate al fallimento del lancio del guest (se __vmx_vmlaunch fallisce il codice riprende normalmente all'istruzione successiva) ed allora è meglio riprendere dal return TRUE finale, che ritorna a DriverEntry.
AsmRestoreToVMXOFFState
La prima cosa che fa AsmRestoreToVMXOFFState è ripristinare i valori di RSP ed RBP al momento dell'invocazione di AsmSaveVMXOFFState. In questo modo lo stato dello stack ritornerà quello di quel preciso istante, dove RSP puntava a __vmx_vmlaunch in VmxInitialize. Ora, come detto nella sezione precedente, non siamo interessati a ritornare a tale punto ma al return finale, che ci conduce nuovamente in DriverEntry. Il modo più semplice per realizzare ciò è quello di simulare il return TRUE inserendo 1 in RAX ed andandosi a recuperare l'indirizzo di ritorno a DriverEntry guardando sullo stack, come spiegato nei commenti al seguente codice.
AsmRestoreToVMXOFFState PROC PUBLIC ; Esce dalla VMX operation vmxoff ; Dopo le seguenti istruzioni RSP punta nuovamente ad indirizzo ; di __vmx_vmlaunch in VmxInitialize e lo stack è nella stessa ; situazione di quando sono state salvati RSP ed RBP in AsmSaveVMXOFFState. mov rsp, g_StackPointerForReturning mov rbp, g_BasePointerForReturning ; NOTA: All'inizio di VmxInitialize c'è: ; push rsi
; push rdi
; sub rsp, 78h
; che serve a salvare (eventualmente) alcuni registri e fare spazio alle var locali: ; potrebbe cambiare da compilazione a compilazione quindi meglio controllare con un disassembler. ; Ma prima di eseguire tali istruzione, su stack c'era Return Address a chiamante di ; VmxInitialize, e cioè RSP puntava ad istruzione successiva a call VmxInitialize in DriverEntry. ; Dopo aver eseguito tali istruzioni, invece, su stack c'è lo spazio allocato all'inizio di ; VmxInitialize (con i vari push e sub) e poi l'indirizzo di __vmx_vmlaunch in VmxInitialize, quindi: ; 0x78(push) + 8(push) + 8(push) + 8(RA vmlaunch) byte prima di arrivare a ; Return Address che punta all'istruzione successiva a call VmxInitialize in DriverEntry. ; Toglie gli 8 byte dell'RA a __vmx_vmlaunch in VmxInitialize dallo stack add rsp, 8 ; Simula il return TRUE in VmxInitialize xor rax, rax mov rax, 1 ; Controlla con un qualsiasi disassembler come termina VmxInitialize. ; Dopo aver tolto anche questi 0x78 + 0x8 +0x8 byte RSP punta ad ; istruzione successiva a call VmxInitialize in DriverCreate. mov rcx, [rsp + 88h -20h]
xor rcx, rsp ; StackCookie
call __security_check_cookie
add rsp, 78h
pop rdi
pop rsi
ret AsmRestoreToVMXOFFState ENDP
Una cosa molto importante da sottolineare è che prologo ed epilogo di VmxInitialize possono variare da compilazione a compilazione, quindi è meglio verificare ogni volta con un disassemblatore ed adattare il codice di AsmRestoreToVMXOFFState di conseguenza.
Conclusioni e note finali
Dopo il ret di AsmRestoreToVMXOFFState il codice riprende dal KdPrint successivo alla chiamata a VmxInitialize in DriverEntry ed è per questo motivo che non si sono virtualizzati tutti i processori: tale codice può essere eseguito una sola volta.
Si noti che se il codice di questa lezione viene eseguito su una macchina virtuale gestita da Hyper-V non è garantito che la prima VM exit dopo il lancio del guest sia quella causata dall'esecuzione dell'HLT da parte di quest'ultimo. Per tale motivo si potrebbe essere costretti a fare più di un tentativo prima che l'esperimento vada a buon fine. Si vedrà come risolvere il problema nel corso delle prossime lezioni.
[4] Intel Software Developer's Manual Vol. 3C
Nessun commento:
Posta un commento