Prima di poter essere in grado di sottoporre l'intero sistema operativo correntemente in esecuzione alla supervisione del nostro hypervisor è necessario trovare un modo per lanciarlo come guest. Questa lezione (insieme alle prossime due) cercherà di spiegare un modo per ottenere tale risultato. Il codice di esempio è fortemente ispirato a quello di alcuni hypervisor (si vedano i riferimenti a fine lezione) che hanno codice sorgente consultabile pubblicamente. A loro vanno tutti riconoscimenti del caso.
L'idea di base, per lanciare Windows 10 come guest, è quella di caricare il driver del nostro hypervisor e salvare lo stato dei registri in un punto X della sua esecuzione prima di entrare in VMX operation. Fatto questo si può dirottare l'esecuzione al solo fine di entrare in tale modalità operativa, inizializzare la VMCS e lanciare il guest. In questo modo sarà sufficiente indicare al guest di riprendere ad eseguire il codice dal punto X nel nostro driver per ottenere il risultato desiderato: eseguire l'intero OS come guest. Vediamo come mettere in codice quanto appena descritto a parole.
Dati globali
Al momento potrebbe risultare difficile capire cosa realmente serva a livello globale e perché. I dettagli saranno più chiari alla fine, quando si chiuderà il trittico di lezioni su come lanciare l'intero OS come guest. Al momento, però, è comunque possibile fornire una breve descrizione sul tipo di informazione contenuta nei dati globali. Si consideri il codice seguente.
#define POOLTAG 0x56542d78 // VT-x #define VMM_STACK_SIZE 0x5000 // Kernel stack è 16 o 20 KB #define VMCALL_TEST 0x1 // Testa VMCALL #define VMCALL_VMXOFF 0x2 // Usa VMCALL per eseguire VMXOFF // Se si esegue VMXOFF in root operation poi non si può più usare VMRESUME // per riprendere l'esecuzione del codice da dove era stato interrotto // (non si è più in VMX operation). Per tale motivo è necessario avere // tutte le informazioni necessarie per riprendere la normale esecuzione // del codice nel punto in cui era stata interrotta dalla VM exit che // ha portato all'esecuzione di VMXOFF. typedef struct _VMXOFF_STATE { BOOLEAN IsVmxoffExecuted; // Indica se VMXOFF è stata eseguita o meno. UINT64 GuestRip; // Indirizzo a cui ritornare dopo VMXOFF. UINT64 GuestRsp; // Stack pointer dopo VMXOFF. } VMXOFF_STATE, * PVMXOFF_STATE; // Risorse allocate nella virtualizzazione di ogni processore logico. // Contiene anche lo stato del guest al momento della VMCALL che // porta all'uscita dalla VMX operation con VMXOFF. // Quest'ultima info serve perché VMXOFF è eseguita a seguito // di una VMCALL, che causa una VM exit: dopo VMXOFF si è fuori dalla // VMX operation e non si può usare VMRESUME per riprendere ad // eseguire del codice. Per tale motivo è necessario salvare l'indirizzo // di ritorno e lo stack pointer per riprendere dall'istruzione // successiva alla vmcall che ha portato ad eseguire VMXOFF. typedef struct _VCPU { VMXOFF_STATE VmxoffState; // Stato del processore in previsione di VMXOFF 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 MSR_BITMAP_VA; // Indirizzo virtuale di MSR Bitmap UINT64 MSR_BITMAP_PA; // Indirizzo fisico di MSR Bitmap BOOLEAN IsOnVmxRootMode; // Indica se il processore è in VMX root operation o meno BOOLEAN IncrementRip; // Indica se eseguire l'istruzione guest successiva dopo VMRESUME }VCPU, * PVCPU; // Variabile globale che punterà ad un array di VCPU. // Dichiarata esterna così da evitare di inserire una definizione // in un file header. extern PVCPU GuestState;
Alla struttura VCPU sono stati aggiunti alcuni campi aggiuntivi:
- MSR Bitmap (VA e PA): Ricordando quanto detto in [5], se il bit "Use MSR bitmaps" del primary processor-based control è 0 si avrà una VM exit ad ogni esecuzione di RDMSR o WRMSR. In un contesto dove si vuole rendere guest l'intero sistema operativo questo degrada parecchio le performance e quindi sarebbe da evitare. Per tale motivo si imposterà tale bit ad 1, si allocherà spazio per le 4 MSR bitmap richieste e si inizializzeranno tali spazi di memoria a 0, in modo da non causare mai VM exit per la lettura o scrittura di MSR (almeno al momento).
- IncrementRip: Quando un'istruzione o un evento causa una VM exit il controllo passa all'hypervisor, che la gestisce e restituisce il controllo al guest. Da dove dovrebbe riprendere il guest ad eseguire codice? La risposta è: Dipende! A volte ripetere la stessa istruzione che ha causato la VM exit può portare ad un loop infinito di VM exit e VM entry da cui non si esce più, se non in modo disastroso. Dall'altra parte, a volte è necessario ripetere l'istruzione che ha causato la VM exit perché la ragione che causato la VM exit originaria era collegata a condizioni che non si verificano nuovamente alla riesecuzione della stessa istruzione. In una prossima lezione verrà ripreso tale argomento e verranno chiariti tutti i dettagli.
- IsOnVmxRootMode: Indica se si è in root o in non-root operation.
- VmxOffState: Per uscire dalla VMX operation si userà una VMCALL che, naturalmente, causerà una VM exit. Il gestore di questa VM exit eseguirà VMXOFF per uscire dalla VMX operation. Dato che siamo in root operation, dopo aver eseguito VMXOFF non si può più usare VMRESUME per riprendere ad eseguire codice. E allora che si fa? Il campo VmxOffState serve proprio a questo: indica dove riprendere ad eseguire codice una volta usciti dalla VMX operation.
Salvare lo stato
DriverCreate è invocata quando cerchiamo di ottenere un handle al device che rappresenta il nostro driver. Si uscirà da tale funzione mentre si è in non-root operation, da guest, quindi anche quello che verrà eseguito dopo, al di fuori del nostro driver, sarà considerato tale: in pratica l'intero sistema operativo verrà eseguito come guest. Ora non resta che capire come. Si consideri il seguente codice.
NTSTATUS DriverCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject); UINT64 ProcessorCount = KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS); // Inizializza VMX, VMCS e lancia il guest if (InitializeVmx(ProcessorCount)) { KdPrint(("[*] Hypervisor loaded successfully :)\n")); } else { KdPrint(("[*] Hypervisor was not loaded :(")); } Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }
In InitializeVmx lo stato del processore viene salvato all'interno di CPUBroadcastRoutine, che deve quindi essere eseguita tante volte quanti sono i processori logici. In particolare, lo stato viene salvato all'interno della funzione AsmSaveState, passata come secondo parametro a CpuBroadcastRoutine. AsmVmxNonRootVmcall serve solo a controllare che tutta abbia funzionato correttamente: in teoria dovremmo essere in VMX operation, eseguiti come codice guest. Per tale motivo usiamo VMCALL per causare una VM exit e vedere se l'hypervisor risponde correttamente. Ma questo è un discorso che vedremo dopo, una volta riusciti a lanciare Windows 10 come guest.
BOOLEAN InitializeVmx(UINT64 LogicalProcessors) { // Controlla se è possibile entrare in VMX Operation if (!CpuIsVmxSupported()) { KdPrint(("[*] VMX is not supported in this machine !")); return FALSE; } // Per ogni processore... for (ULONG ProcessorID = 0; ProcessorID < LogicalProcessors; ProcessorID++) { // ...salva lo stato in un punto all'interno della seguente // funzione in modo da recuperarlo al lancio del guest, dopo // essere entrati in VMX operation. // Impostando opportunamente la VMCS si farà in modo che il // guest cominci ad eseguire il suo codice proprio da tale punto, // riprendendo da quando non si era ancora in VMX operation. // E' un po' come se il thread fosse stato temporaneamente dirottato // per lanciare il guest e questo cominciasse ad eseguire il codice // riprendendo dal punto di deviazione, come se niente in realtà // fosse successo. L'effetto finale è molto interessante in quanto, // in questo modo, si è in grado di rendere guest l'intero sistema // operativo. CpuBroadcastRoutine(ProcessorID, (PVOID)AsmSaveState); } // VMCALL di test per controllare che tutto funzioni. if (AsmVmxNonRootVmcall(VMCALL_TEST, 0x1, 0x22, 0x333) == STATUS_SUCCESS) { return TRUE; } else { return FALSE; } }
In CpuBroadcastRoutine viene invocata la funzione passata come secondo parametro, che in questo caso è AsmSaveState. E' proprio questo il famoso punto X (prima di entrare in VMX operation) in cui verrà salvato lo stato del processore, dirottato il codice e ripresa l'esecuzione una volta lanciato il guest.
BOOLEAN CpuBroadcastRoutine(ULONG ProcessorIndex, PVOID Routine) { KIRQL OldIrql; PROCESSOR_NUMBER ProcessorNumber; GROUP_AFFINITY Affinity, OldAffinity; // Converte da indice di sistema ad indice locale nel relativo gruppo KeGetProcessorNumberFromIndex(ProcessorIndex, &ProcessorNumber); // Imposta l'affinità del thread corrente per eseguirlo sull'i-esimo processore RtlSecureZeroMemory(&Affinity, sizeof(GROUP_AFFINITY)); Affinity.Group = ProcessorNumber.Group; Affinity.Mask = (KAFFINITY)((ULONG64)1 << ProcessorNumber.Number); KeSetSystemGroupAffinityThread(&Affinity, &OldAffinity); // Eleva IRQL a DISPATCH_LEVEL per impedire il context switch OldIrql = KeRaiseIrqlToDpcLevel(); // Esegue funzione passata come secondo param. di CpuBroadcastRoutine e // le passa il numero del processore corrente come unico argomento. ((void(*)(ULONG))Routine)(ProcessorNumber.Number); // Continua in AsmSaveState... // ...Ritorna dopo AsmRestoreState // // Tutto il codice eseguito da questo momento in poi da questo // processore (anche quello che verrà eseguito una volta usciti // dal modulo del driver corrente) è codice guest in VMX non-root // operation. In altre parole, l'intero sistema operativo Windows // è il guest. // // Ristabilisce IRQL e maschera. KeLowerIrql(OldIrql); KeRevertToUserGroupAffinityThread(&OldAffinity); return TRUE; }
In AsmSaveState si salva lo stato del processore mettendo i vari registri sullo stack. Successivamente si invoca la funzione VirtualizeCurrentCpu, che è dove dirottiamo l'esecuzione per abilitare la VMX operation, inizializzare la VMCS e lanciare il guest. Dato che si armeggiare con stack e registri è necessario implementare tale funzione in linguaggio assembly. Viene mostrato anche il codice di AsmRestoreState, per completezza, ma tale funzione verrà ripresa a breve per essere analizzata meglio.
; ------------------------------------------------------------------------ AsmSaveState PROC PUBLIC ; Sullo stack ora c'è l'indirizzo di ritorno dell'istruzione successiva a ; quella dell'invocazione di AsmSaveState in CpuBroadcastRoutine. In altre ; parole, RSP punta a KeLowerIrql(OldIrql) in CpuBroadcastRoutine. ; Salva lo stato del processore pushfq ; salva RFLAGS push rax push rcx push rdx push rbx push rbp push rsi push rdi push r8 push r9 push r10 push r11 push r12 push r13 push r14 push r15 ; shadow space sub rsp, 28h ; Invoca VirtualizeCurrentCpu. ; AsmSaveState e VirtualizeCurrentCpu hanno lo stesso primo parametro. ; Entrambi i metodi usano la convenzione di chiamata __fastcall dunque ; il primo parametro si trova in RCX. Dato che AsmSaveState non tocca ; mai tale registro significa che VirtualizeCurrentCpu riceverà ; lo stesso parametro senza che si debba fare nulla. ; Ma VirtualizeCurrentCpu prende anche un secondo parametro, il valore ; di RSP con cui inizializzare il relativo campo della VMCS e che verrà ; usato come stack pointer dal guest, in non-root operation, una volta lanciato. ; Secondo la convenzione di chiamata __fastcall il secondo parametro va in RDX. ; Perché mai il guest dovrebbe avere uno stack dove ci sono i registri salvati ; al momento dell'invocazione di AsmSaveState? L'idea è quella di inizializzare ; la VMCS e lanciare il guest facendogli eseguire la funzione AsmRestoreState ; (vedi sotto) che ripristina\carica lo stato del processore al momento ; dell'invocazione di AsmSaveState ed al ret ritorna all'istruzione successiva ; all'invocazione di AsmSaveState in CpuBroadcastRoutine, in questo caso ; KeLowerIrql(OldIrql); ; L'effetto sarà quello di virtualizzare la CPU proseguendo con l'esecuzione del ; codice che si stava eseguendo prima di entrare in VMX operation e lanciare il ; guest, rendendo di fatto l'intero OS (Windows) il guest ed il nostro driver ; l'hypervisor. mov rdx, rsp call VirtualizeCurrentCpu ; non si arriva mai qui ret AsmSaveState ENDP ; ------------------------------------------------------------------------ AsmRestoreState PROC PUBLIC ; elimina lo shadow space add rsp, 28h ; ripristina registri, e quindi lo stato del processore pop r15 pop r14 pop r13 pop r12 pop r11 pop r10 pop r9 pop r8 pop rdi pop rsi pop rbp pop rbx pop rdx pop rcx pop rax popfq ; ripristina RFLAGS ret ; ritorna a KeLowerIrql(OldIrql) in CpuBroadcastRoutine AsmRestoreState ENDP ; ------------------------------------------------------------------------
Dirottamento (VMX, VMCS, VMLAUNCH)
In VirtualizeCurrentCpu il parametro ProcessorID ha lo stesso valore di ProcessorNumber.Number in quanto è l'argomento passato nell'invocazione di AsmSaveState in CpuBroadcastRoutine ed identifica il processore corrente da virtualizzare. Il parametro GuestStack, invece, è il valore del registro RSP, che punta ad uno stack in cui ci sono i registri salvati in AsmSaveState. Quest'ultimo è molto importante e viene passato a SetupVmcs. In verità a tale metodo viene passata anche l'istanza di tipo VCPU, collegata al processore corrente. Per tale motivo, prima di invocare SetupVmcs, vengono allocati gli spazi di memoria necessari alla virtualizzazione e conservati nella relativa istanza di tipo VCPU, essenziale in fase di inizializzazione della VMCS.
BOOLEAN VirtualizeCurrentCpu(ULONG ProcessorID, PVOID GuestStack) { KdPrint(("======= Virtualizing Current System (Logical Core : 0x%x) =======\n", ProcessorID)); // Imposta i bit di CR0 e CR4 (compreso CR4.VMXE) CpuFixBits(); // Alloca spazio per la VMXON region if (!AllocateVMCSRegion(&GuestState[ProcessorID], TRUE)) { KdPrint(("[*] Error in allocating memory for Vmxon region")); return FALSE; } // Alloca spazio per la VMCS region if (!AllocateVMCSRegion(&GuestState[ProcessorID], FALSE)) { KdPrint(("[*] Error in allocating memory for Vmcs region")); return FALSE; } KdPrint(("[*] VMCS Region is allocated at ===============> %llx\n", GuestState[ProcessorID].VMCS_REGION_VA)); KdPrint(("[*] VMXON Region is allocated at ===============> %llx\n", GuestState[ProcessorID].VMXON_REGION_VA)); // Alloca spazio che verrà usato come stack dall'hypervisor durante le Vm exit if (!AllocateVmmStack(ProcessorID)) return FALSE; // Alloca spazio la bitmap degli MSR if (!AllocateMsrBitmap(ProcessorID)) return FALSE; KdPrint(("[*] HOST stack is allocated at =================> %llx\n", GuestState[ProcessorID].VMM_STACK)); KdPrint(("[*] Msr Bitmap Virtual Address at ==============> %llx\n", GuestState[ProcessorID].MSR_BITMAP_VA)); // Inizializza la VMCS collegata al guest da eseguire sul processore corrente. KdPrint(("[*] Setting up VMCS.\n")); SetupVmcs(&GuestState[ProcessorID], GuestStack); KdPrint(("[*] Executing VMLAUNCH.\n")); KdPrint(("\n\n")); // Esegue codice guest. __vmx_vmlaunch(); // Continua in AsmRestoreState... // // 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(0x4400, &ErrorCode); __vmx_off(); KdPrint(("[*] VMLAUNCH Error : 0x%llx\n", ErrorCode)); DbgBreakPoint(); return TRUE; }
Come si può notare, nel codice precedente, dopo VMLAUNCH, c'è un commento che indica che l'esecuzione prosegue in AsmRestoreState. Per capire perché è necessario vedere il codice di SetupVmcs.
VOID SetupVmcs(PVCPU CurrentGuestState, PVOID GuestStack) { // ... // Campi di controllo della VMCS PINBASED_CTLS pinbased_controls = { 0 }; PRIMARY_PROCBASED_CTLS primary_controls = { 0 }; SECONDARY_PROCBASED_CTLS secondary_controls = { 0 }; VM_EXIT_CONTROL exit_controls = { 0 }; VM_ENTRY_CONTROL entry_controls = { 0 }; // Imposta a clear lo stato della VMCS. if (!ClearVmcs(CurrentGuestState)) { DbgBreakPoint(); return; } // Imposta la VMCS come corrente ed attiva. if (!LoadVmcs(CurrentGuestState)) { DbgBreakPoint(); return; } // Imposta bit specifici nei campi di controllo. SetPrimaryCtls(&primary_controls); SetSecondaryCtls(&secondary_controls); SetVmExitCtls(&exit_controls); SetVmEntryCtls(&entry_controls); // ... // // Host Area // // ... // Registri general purpose (RSP e RIP) __vmx_vmwrite(HOST_RSP, CurrentGuestState->VMM_STACK + VMM_STACK_SIZE - 16); __vmx_vmwrite(HOST_RIP, (UINT64)AsmVmExitHandler); // ... // // Guest Area // // ... // Registri general purpose (RSP e RIP) ed RFLAGS __vmx_vmwrite(GUEST_RSP, (UINT64)GuestStack); __vmx_vmwrite(GUEST_RIP, (ULONG64)AsmRestoreState); // ... // // Campi di controllo // __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)); // ... // Per evitare di causare VM exit ogni volta che viene eseguita una RDMSR o una WRMSR // è meglio impostare una bitmap con bit impostati ad 1 per i soli MSR per cui una // VM exit è l'azione desiderata a seguito di un accesso in letture o scrittura. if (primary_controls.Bits.UseMSRBitmaps == TRUE) __vmx_vmwrite(CONTROL_MSR_BITMAPS_ADDRESS, CurrentGuestState->MSR_BITMAP_PA); // Imposta la maschera di CR4 in modo che il bit 13 (VMXE) appartenga all'host // (quando il guest legge CR4 il bit 13 esposto al guest sarà quello in CR4 shadow). __vmx_vmwrite(CONTROL_CR4_GUEST_HOST_MASK, 0x2000); // Imposta valore di CR4 shadow. // ~0x2000 equivale a tutti i bit ad 1 tranne il 13-esimo, che è zero. // Quindi se il guest legge CR4.VMXE gli verrà ritornato 0 invece che 1. __vmx_vmwrite(CONTROL_CR4_READ_SHADOW, __readcr4() & ~0x2000); }
Per quanto riguarda i campi di controllo, rispetto alla precedente lezione l'unica novità (anche se non è mostrata nel codice precedente) è rappresentata dall'impostazione ad 1 del bit "Use MSR bitmaps" nel primary processor-based control, dato che non vogliamo causare una VM exit ogni volta che si esegue RDMSR o WRMSR. Inoltre, si noti che il bit "HLT Exiting" non è più impostato ad 1 poiché non siamo più interessati a sfruttare la relativa VM exit.
L'inizializzazione del campo HOST_RIP della VMCS indica che la funzione che verrà eseguita ad ogni VM exit è AsmVmExitHandler. Una cosa ancora più importante, ai fini di questa lezione, è sicuramente il fatto che il guest, una volta lanciato, esegue la funzione AsmRestoreState e lo stack pointer punterà alla stessa zona di memoria in cui sono stati salvati i registri del processore in AsmSaveStore. In altre parole, lo stack frame del guest appena lanciato è lo stesso di quello di AsmSaveState, dove non eravamo ancora in VMX operation. Questo è un aspetto cruciale in quanto ci permette, una volta in VMX operation, di recuperare i registri e per proseguire con l'esecuzione del codice da un punto in cui non si era ancora in tale modalità operativa.
Ritorno al punto di salvataggio
Dopo VMLAUNCH il codice prosegue in AsmRestoreState. Siamo in VMX operation e nello specifico in non-root operation. Quindi, in effetti, stiamo eseguendo codice come guest all'interno del nostro driver, che implementa anche l'hypervisor. Un po' contorta come cosa ma ha perfettamente senso se si pensa che si vuole rendere guest l'intero OS, compreso il nostro driver. Il codice che implementa l'hypervisor è trattato in modo speciale semplicemente perché nell'impostazione della VMCS si è disposto cosa debba essere invocato quando viene causata una VM exit.
Ritornando al codice di AsmRestoreState, lo stack frame è lo stesso di quello di AsmSaveState e quindi è facile recuperare lo stato del processore in quel momento e ritornare al primo indirizzo di ritorno che si incontra sullo stack, che è quello che riporta a CpuBroadcastRoutine.
AsmRestoreState PROC PUBLIC ; elimina lo shadow space add rsp, 28h ; ripristina registri, e quindi lo stato del processore pop r15 pop r14 pop r13 pop r12 pop r11 pop r10 pop r9 pop r8 pop rdi pop rsi pop rbp pop rbx pop rdx pop rcx pop rax popfq ; ripristina RFLAGS ret ; ritorna a KeLowerIrql(OldIrql) in CpuBroadcastRoutine AsmRestoreState ENDP
Una volta usciti da CpuBroadcastRoutine si torna a InitialzeVmx ed infine a DriverCreate. Usciti anche da quest'ultima funzione il codice del sistema operativo continua ad essere eseguito sul processore anche se non appartiene al modulo del nostro driver. Ma dato che siamo ancora in VMX operation, ed in particolare in non-root operation, si può affermare che l'intero sistema operativo è eseguito come guest.
Test di verifica
Per verificare che tutto sia andato per il verso giusto, e che ci troviamo effettivamente in non-root operation, prima di uscire da InitializeVmx si esegue una VMCALL per vedere se il nostro hypervisor risponde alla nostra chiamata.
// VMCALL di test per controllare che tutto funzioni. if (AsmVmxNonRootVmcall(VMCALL_TEST, 0x1, 0x22, 0x333) == STATUS_SUCCESS) { return TRUE; } else { return FALSE; }
Il codice di AsmVmxNonRootVmcall non è mostrato perché implica la spiegazione di alcuni dettagli non rilevanti ai fini di questa lezione (e che verranno ripresi nella prossima). Al momento si può considerare AsmVmxNonRootVmCall come un semplice wrapper scritto in assembly ad una singola istruzione VMCALL, che causa una VM exit. Come indicato nell'inizializzazione della VMCS, tale evento porta all'esecuzione della funzione AsmVmExitHandler, il cui codice è mostrato di seguito. Per ora si può ignorare il commento prima di push 0 così come l'istruzione di comparazione ed il successivo salto a AsmVmxoffHandler (tali argomenti verranno ripresi in una prossima lezione). Si ricordi che a seguito di una VM exit cambia lo stack pointer ma i registri del processore conservano gli stessi valori al momento della VM exit. In questo modo è possibile salvarli sullo stack e passare lo stack pointer ad un'altra funzione, che in effetti si rivela essere il reale gestore delle VM exit. Il fatto che questo gestore abbia la possibilità di accedere ai registri del processore al momento della VM exit permette, di fatto, il controllo del risultato ritornato al guest da parte dell'hypervisor: una volta gestita la VM exit l'hypervisor può alterare i valori dei registri salvati sullo stack, ripristinarli nei registri del processore ed eseguire VMRESUME. A quel punto il guest vedrebbe, come risultato dell'operazione che ha causato la VM exit, solo quello che l'hypervisor gli consente di vedere.
AsmVmExitHandler PROC ; Crea uno spazio sullo stack dove verrà messo il return address ; all'istruzione successiva a VMCALL in AsmVmxNonRootVmcall, quando ; tale funzione è invocata per terminare la VMX operation. In altre ; parole è un modo per permettere al sistema operativo di continuare ; ad eseguire il suo codice anche se non è più guest. In pratica è ; una stessa situazione molto simile a quella che si aveva all'inizio ; (quando si voleva continuare ad eseguire il codice dell'OS una volta ; che questo fosse diventato guest) ma in senso opposto. push 0 ; Salva lo stato del processore (al momento dell'esecuzione dell'istruzione ; che ha portato a causare la VM exit) mettendo i valori dei registri ; sullo stack. ; RFLAGS pushfq ; RSP non serve sullo stack (dato che viene passato come parametro) a ; VmxRootVmExitHandler ma per poter usare correttamente _GUEST_REGS è ; necessario passare comunque un valore per tale registro. In questo ; caso si opta per ripetere push rbp una seconda volta. push r15 push r14 push r13 push r12 push r11 push r10 push r9 push r8 push rdi push rsi push rbp push rbp ; rsp push rbx push rdx push rcx push rax ; Registri XMM sub rsp ,80h movdqa xmmword ptr [rsp], xmm0 movdqa xmmword ptr [rsp+10h], xmm1 movdqa xmmword ptr [rsp+20h], xmm2 movdqa xmmword ptr [rsp+30h], xmm3 movdqa xmmword ptr [rsp+40h], xmm4 movdqa xmmword ptr [rsp+50h], xmm5 movdqa xmmword ptr [rsp+60h], xmm6 movdqa xmmword ptr [rsp+70h], xmm7 ; Passa il valore di RSP (che ora punta ad area di memoria con i registri salvati) ; al gestore delle VM exit come primo parametro (di tipo _GUEST_REGS). In questo ; modo si permettere all'hypervisor di controllare l'esecuzione del codice guest. mov rcx, rsp sub rsp, 28h ; Shadow space call VmxRootVmExitHandler ; invoca il gestore delle VM exit
add rsp, 28h ; Rimuove lo shadow space movdqa xmm0, xmmword ptr [rsp] movdqa xmm1, xmmword ptr [rsp+10h] movdqa xmm2, xmmword ptr [rsp+20h] movdqa xmm3, xmmword ptr [rsp+30h] movdqa xmm4, xmmword ptr [rsp+40h] movdqa xmm5, xmmword ptr [rsp+50h] movdqa xmm6, xmmword ptr [rsp+60h] movdqa xmm7, xmmword ptr [rsp+70h] add rsp, 80h cmp al, 1 ; Controlla se si è eseguito VMXOFF per uscire da VMX operation (risultato in RAX) je AsmVmxoffHandler ; ripristina stato del processore al momento dell'istruzione che ha portato a ; causare la VM exit RestoreState: pop rax pop rcx pop rdx pop rbx pop rbp ; rsp pop rbp pop rsi pop rdi pop r8 pop r9 pop r10 pop r11 pop r12 pop r13 pop r14 pop r15 popfq ; Crea un po' di spazio sullo stack perché VmxRootVmresume è invocata ; con jump e non con call quindi siamo ancora nello stack frame della ; funzione che contiene l'istruzione che ha causato la VM exit. ; In questo caso VmxRootVmresume potrebbe, in teoria, sovrascrivere qualche ; valore che appartiene a tale stack frame e la cosa sarebbe da evitare. sub rsp, 0100h jmp VmxRootVmresume AsmVmExitHandler ENDP
Salvato lo stato del processore viene invocato il reale gestore delle VM exit e gli viene passato lo stack pointer, che gli permette di accedere ai registri salvati.
// Struttura che contiene il valore dei registri al momento // di una VM exit. E' passata al gestore delle VM exit per // permettere all'hypervisor di controllare l'esecuzione // del codice guest. typedef struct _GUEST_REGS { M128A xmm[8]; // 0x00 ULONG64 rax; // 0x80 ULONG64 rcx; ULONG64 rdx; // 0x90 ULONG64 rbx; ULONG64 rsp; // 0xA0 ULONG64 rbp; ULONG64 rsi; // 0xB0 ULONG64 rdi; ULONG64 r8; // 0xC0 ULONG64 r9; ULONG64 r10; // 0xD0 ULONG64 r11; ULONG64 r12; // 0xE0 ULONG64 r13; ULONG64 r14; // 0xF0 ULONG64 r15; } GUEST_REGS, * PGUEST_REGS;
BOOLEAN VmxRootVmExitHandler(PGUEST_REGS GuestRegs) { ULONG ExitReason = 0; __vmx_vmread(VM_EXIT_REASON, &ExitReason); ULONG ExitQualification = 0; __vmx_vmread(VM_EXIT_QUALIFICATION, &ExitQualification); ULONG CurrentProcessorIndex = KeGetCurrentProcessorNumber(); // Segnala che si è in VMX root operation e che, normalmente, // si dovrebbe procedere a rieseguire il guest a partire // dall'istruzione succesiva a quella che ha causato la // VM exit (altrimenti si entra in un loop infinito di // VM exit e VM entry). GuestState[CurrentProcessorIndex].IsOnVmxRootMode = TRUE; GuestState[CurrentProcessorIndex].IncrementRip = TRUE; switch (ExitReason) { // ... // Gestisce VM exit causate da VMCALL case EXIT_REASON_VMCALL: { // ... // Salva il valore di ritorno di VmxRootVmcallHandler in RAX del guest: // in questo modo è possibile informare il codice guest sulla // corretta gestione o meno della VMCALL da parte dell'hypervisor. GuestRegs->rax = VmxRootVmcallHandler(GuestRegs->rcx, GuestRegs->rdx, GuestRegs->r8, GuestRegs->r9); // ... break; } default: { KdPrint(("Unkown Vmexit, reason : 0x%llx", ExitReason)); break; } } // Se non è necessario ripetere l'istruzione che ha portato a causare la // VM exit corrente allora si deve avanzare alla prossima istruzione del // guest prima di eseguire VMRESUME. if (GuestState[CurrentProcessorIndex].IncrementRip) { VmxRootResumeToNextInstruction(); } // Segnala che si sta uscendo dalla VMX root operation GuestState[CurrentProcessorIndex].IsOnVmxRootMode = FALSE; // ... return FALSE; }
Come si può notare, a VmxRootVmcallHandler vengono passati i valori di quei registri salvati sullo stack che, nel momento della VM exit causata dalla VMCALL in non-root operation, contenevano gli argomenti passati a AsmVmxNonRootVmcall, proprio perché sono quelli che si intendeva passare all'eventuale gestore della VMCALL. Si noti inoltre che il valore di ritorno è salvato sullo stack nel punto in cui è stato pushato RAX. In questo modo, dopo aver ripristinato lo stato del processore ed eseguito VMRESUME, il guest potrà vedere come è andata la gestione della VMCALL.
NTSTATUS VmxRootVmcallHandler(UINT64 VmcallNumber, UINT64 OptionalParam1, UINT64 OptionalParam2, UINT64 OptionalParam3) { NTSTATUS VmcallStatus; VmcallStatus = STATUS_UNSUCCESSFUL; // I primi 32 bit sono sufficienti a distinguere tra (2^(32) - 1) VMCALL diverse. // Questo permette di usare, volendo, i restanti 32 bit alti per altro. switch (VmcallNumber & 0xffffffff) { case VMCALL_TEST: { VmcallStatus = VmxRootVmCallTest(OptionalParam1, OptionalParam2, OptionalParam3); break; } // ... default: { VmcallStatus = STATUS_UNSUCCESSFUL; break; } } return VmcallStatus; }
NTSTATUS VmxRootVmCallTest(UINT64 Param1, UINT64 Param2, UINT64 Param3) { KdPrint(("VmcallTest called with @Param1 = 0x%llx , @Param2 = 0x%llx , @Param3 = 0x%llx\n", Param1, Param2, Param3)); return STATUS_SUCCESS; }
In VmxRootVmExitHandler, dopo aver avanzato l'istruzione del guest a quella successiva alla VMCALL che ha causato la VM exit corrente si ritorna FALSE e sullo stack, in direzione di dove era stato pushato RAX del guest al momento della VM exit, c'è VmcallStatus che, se tutto è andato per il verso giusto, dovrebbe avere valore uguale a STATUS_SUCCESS (ossia 0x000000). A questo punto AsmVmExitHandler ripristina lo stato del processore e si esegue VMRESUME (VmxRootVmresume fa praticamente solo questo) per ritorna a InitializeVmx. In particolare si ritorna all'istruzione if e si valuta la sua condizione in base a quanto ritornato da AsmVmxNonRootVmcall. Tale valore è conservato nel registro RAX del guest che, come sappiamo, ora contiene il valore di VmcallStatus e può essere usato per verificare che la VM exit causata dalla VMCALL sia stata gestita correttamente.
Considerazioni finali
Il codice presentato in questa lezione funziona perfettamente ma non è possibile eseguirlo su una macchina virtuale gestita da Hyper-V in quanto il nostro hypervisor si metterebbe tra Hyper-V e Windows 10 senza fornire alcun supporto al primo riguardo le richieste fatte e le informazioni passate a quest'ultimo da parte del secondo. La prossima lezione si occuperà proprio di questo aspetto.
Si noti che il codice di verifica, mostrato nell'ultima sezione, presenta molte funzioni con codice incompleto. Tale mancanza verrà colmata nel corso delle prossime due lezioni.
[4] Intel Software Developer's Manual Vol. 3C
Nessun commento:
Posta un commento