giovedì 9 dicembre 2021

05.A - Lanciare Windows 10 come guest

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 DeviceObjectPIRP 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(IrpIO_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 < LogicalProcessorsProcessorID++)
    {
    	// ...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 ProcessorIndexPVOID Routine)
{
    KIRQL OldIrql;
    PROCESSOR_NUMBER ProcessorNumber;
    GROUP_AFFINITY AffinityOldAffinity;
 
    // 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(&Affinitysizeof(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 ProcessorIDPVOID 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 CurrentGuestStatePVOID 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_RSPCurrentGuestState->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_ADDRESSCurrentGuestState->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 VmcallNumberUINT64 OptionalParam1UINT64 OptionalParam2UINT64 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(OptionalParam1OptionalParam2OptionalParam3);
		break;
	}
 
 
        // ...

 
	default:
	{
		VmcallStatus = STATUS_UNSUCCESSFUL;
		break;
	}
 
    }
 
    return VmcallStatus;
}

NTSTATUS VmxRootVmCallTest(UINT64 Param1UINT64 Param2UINT64 Param3)
{
	KdPrint(("VmcallTest called with @Param1 = 0x%llx , @Param2 = 0x%llx , @Param3 = 0x%llx\n"Param1Param2Param3));
	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.




Repository del progetto: Corso-VT-x (github.com)




Riferimenti:

[4] Intel Software Developer's Manual Vol. 3C

Nessun commento:

Posta un commento