domenica 12 dicembre 2021

05.C - Uscire dalla VMX operation

Una volta lanciato Windows 10 come guest e supportato Hyper-V, restituendogli le VM exit di sua competenza, sarebbe auspicabile trovare un modo per uscire dalla VMX operation e tornare ad eseguire il sistema operativo normalmente. L'idea è la seguente: eseguire una VMCALL dal nostro driver (in non-root operation) per indicare al nostro hypervisor (in root operation) di eseguire VMXOFF e riprendere ad eseguire codice normalmente.  Come richiamare l'attenzione dell'hypervisor tramite VMCALL è argomento ormai noto in quanto già affrontato nelle precedenti due lezioni. La vera novità è trovare un modo per ritornare ad eseguire codice una volta usciti dalla VMX operation. In realtà, anche questo è un argomento che si è già affrontato, in un certo senso, quando si voleva riprendere ad eseguire codice una volta entrati in VMX operation. Ad ogni modo, nulla verrà lasciato al caso e tutto verrà spiegato nei dettagli nel corso di questa lezione.



Implementazione

L'idea è quella di uscire dalla VMX operation quando il client termina la sua esecuzione o quando viene chiuso l'handle al device del nostro driver.


NTSTATUS DriverClose(PDEVICE_OBJECT DeviceObjectPIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
 
    UINT64 ProcessorCount = KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS);
 
    // Esce dalla VMX operation
    TerminateVmx(ProcessorCount);
 
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(IrpIO_NO_INCREMENT);
    return STATUS_SUCCESS;
}


Come detto in precedenza, si userà una VMCALL indicando all'hypervisor di eseguire VMXOFF per uscire dalla VMX operation. Come visto nelle lezioni precedenti, il nostro driver in non-root operation può usare la funzione AsmVmxNonrootVmcall per eseguire una VMCALL indirizzata al nostro hypervisor. Dal punto di vista del guest il codice si interrompe proprio all'esecuzione di tale VMCALL (che causa una VM exit) quindi è da li che bisogna riprendere una volta usciti dalla VMX operation. Inoltre, si ricordi che è necessario uscire dalla VMX operation su tutti i processori logici.


VOID TerminateVmx(UINT64 LogicalProcessors)
{
    PROCESSOR_NUMBER ProcessorNumber;
    GROUP_AFFINITY AffinityOldAffinity;
    NTSTATUS Status;
 
    for (ULONG i = 0; i < LogicalProcessorsi++)
    {
    	KeGetProcessorNumberFromIndex(i, &ProcessorNumber);
 
    	RtlSecureZeroMemory(&Affinitysizeof(GROUP_AFFINITY));
    	Affinity.Group = ProcessorNumber.Group;
    	Affinity.Mask = (KAFFINITY)((ULONG64)1 << ProcessorNumber.Number);
    	KeSetSystemGroupAffinityThread(&Affinity, &OldAffinity);
 
    	// Esegue VMCALL per segnalare all'hypervisor di usare VMXOFF
    	// per uscire dalla VMX operation.
    	Status = AsmVmxNonRootVmcall(VMCALL_VMXOFF, 0, 0, 0);
 
    	if (Status == STATUS_SUCCESS)
    	{
    		KdPrint(("[*] VMX Terminated on logical core %d\n"i));
 
    		if (GuestState && GuestState[i].VMXON_REGION_VA)
    			MmFreeContiguousMemory((PUINT64)GuestState[i].VMXON_REGION_VA);
 
    		if (GuestState && GuestState[i].VMCS_REGION_VA)
    			MmFreeContiguousMemory((PUINT64)GuestState[i].VMCS_REGION_VA);
 
    		if (GuestState && GuestState[i].VMM_STACK)
    			ExFreePoolWithTag((PUINT64)GuestState[i].VMM_STACK, POOLTAG);
 
    		if (GuestState && GuestState[i].MSR_BITMAP_VA)
    			ExFreePoolWithTag((PUINT64)GuestState[i].MSR_BITMAP_VA, POOLTAG);
    	}
 
    	KeRevertToUserGroupAffinityThread(&OldAffinity);
    }
}


Di seguito viene riproposta l'implementazione di AsmVmxNonrootVmcall. Una volta usciti dalla VMX operation si vorrebbe riprendere ad eseguire il codice dall'istruzione successiva a VMCALL, che in questo caso è pop r11. Usciti da AsmVmxNonRootVmcall si torna a TerminateVmx e successivamente a DriverClose. Una volta usciti anche da quest'ultimo metodo il processore continua ad eseguire codice al di fuori del modulo del nostro driver. Dato che non siamo più in VMX operation Windows 10 non è più eseguito come guest. Questo è quello che si vorrebbe ottenere. Non resta che vedere come.


AsmVmxNonRootVmcall PROC
    
    ; NTSTATUS AsmVmxNonRootVmcall(ULONG64 VmcallNumber, ULONG64 OptParam1, ULONG64 OptParam2, LONG64 OptParam3);

    ; Imposta i valori esadecimali delle stringhe ASCII "NOHYPERV" e "VMCALL" nei 
    ; registri R10 ed R11 (che non sono coinvolti nella convenzione di chiamata 
    ; __fastcall) così si è sicuri che la VMCALL è indirizzata al nostro hypervisor 
    ; (e non a Hyper-V).
    ; Segnala al nostro hypervisor di gestire la relativa VM exit.
    pushfq
    push    r10
    push    r11
    mov     r10, 4e4f485950455256H   ; [NOHYPERV]
    mov     r11, 564d43414c4cH       ; [VMCALL]
 
    ; Causa VM exit che porterà ad invocazione di 
    ; VmxRootVmcallHandler(rcx = vmcallnumber, rdx = optparam1, r8 = optparam2, r9 = optparam3)
    ; all'interno del gestore delle VM exit 
    vmcall                           
    pop     r11
    pop     r10
    popfq
    ret          ; Ritorna NTSTATUS che si trova in RAX (valore di ritorno di VmxRootVmcallHandler)
 
AsmVmxNonRootVmcall ENDP


All'esecuzione della VMCALL in AsmVmxNonRootVmcall viene causata una VM exit gestita da AsmVmExitHandler prima e VmxRootVmExitHandler poi. Queste due funzioni sono già state presentate nelle lezioni precedenti ma verranno riprese a breve per chiarire alcuni i dettagli non ancora esaminati finora. Per il momento è sufficiente ricordare che, per la gestione delle VM exit causate da VMCALL, VmxRootVmExitHandler invoca VmxRootVmcallHandler e le passa il primo argomento di AsmVmxNonRootVmcall, che in questo caso è VMCALL_VMXOFF.


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;
    	}
 
    	case VMCALL_VMXOFF:
    	{
    		VmxRootVmxoff();
    		VmcallStatus = STATUS_SUCCESS;
    		break;
    	}
 
    	default:
    	{
    		VmcallStatus = STATUS_UNSUCCESSFUL;
    		break;
    	}
 
    }
 
    return VmcallStatus;
}


VmxRootVmxoff legge sia l'indirizzo della VMCALL in AsmVmxNonRootVmcall (poiché è l'ultima istruzione eseguita dal guest prima della VM exit) che la sua dimensione dalla VMCS. A quel punto calcola l'indirizzo dell'istruzione successiva (pop r11) e salva il risultato in VCPU::VMXOFF_STATE::GuestRip. Allo stesso modo salva anche lo stack pointer del guest al momento della VM exit. Queste due informazioni, come si vedrà a breve, sono sufficienti a riprendere l'esecuzione del codice una volta usciti dalla VMX operation. Infine, si esegue VMXOFF e si imposta IsVmxoffExecuted a TRUE per segnalare che da quel momento in poi non si è più in VMX operation (e quindi non si può usare più VMRESUME per riprendere il codice).


VOID VmxRootVmxoff()
{
    int CurrentProcessorIndex;
    UINT64 GuestRSP = 0;
    UINT64 GuestRIP = 0;
    UINT64 ExitInstructionLength = 0;
 
    CurrentProcessorIndex = KeGetCurrentProcessorNumber();
 
    // Legge guest RSP e RIP al momento della VMCALL che ha causato la VM exit
    // che porterà ad eseguire VMXOFF.
    // Questi valori verranno usati in AsmVmxoffHandler per ritornare all'
    // istruzione successiva alla VMCALL in AsmVmxNonRootVmcall.
    __vmx_vmread(GUEST_RIP, &GuestRIP);
    __vmx_vmread(GUEST_RSP, &GuestRSP);
 
    // Legge la lunghezza in byte di VMCALL e la aggiunge a RIP per
    // farlo puntare all'istruzione successiva: pop r11 in AsmVmxNonRootVmcall.
    __vmx_vmread(VM_EXIT_INSTRUCTION_LENGTH, &ExitInstructionLength);
    GuestRIP += ExitInstructionLength;
 
    // Salva il risultato nello stato del processore prima di eseguire VMXOFF.
    // Tali dati serviranno a riprendere l'esecuzione da dove si era interrotta
    // una volta usciti dalla VMX operation (poiché non c'è più possibilità di
    // usare VMRESUME per riprendere il codice guest).
    GuestState[CurrentProcessorIndex].VmxoffState.GuestRip = GuestRIP;
    GuestState[CurrentProcessorIndex].VmxoffState.GuestRsp = GuestRSP;
 
    // Ripristina FS, GS , GDTR e IDTR poiché, se volutamente o per errore
    // qualcosa è cambiato, patchguard potrebbe non prenderla benissimo.
    VmxRootRestoreRegisters();
 
    // Esegue VMXOFF per uscire dalla VMX operation.
    __vmx_off();
 
    // Notifica che si è eseguita VMXOFF e non si è più in VMX operation.
    GuestState[CurrentProcessorIndex].VmxoffState.IsVmxoffExecuted = TRUE;
 
    // Dato che si è usciti dalla VMX operation si può disabilitare CR4.VMXE
    CpuVmxEnable(FALSE);
}


Una volta usciti da VmxRootVmxoff si torna a VmxRootVmExitHandler, che controlla se IsVmxoffExecuted è TRUE. In caso affermativo non si avanza l'instruction pointer del guest (operazione già effettuata in VmxRootVmxoff) e si ritorna TRUE a AsmVmExitHandler.


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:
    	{
	    // Controlla se è stata una delle nostre funzioni (quelle contenute nel
    	    // driver che implementa l'hypervisor e che vengono eseguite in non-root 
	    // operation in quanto ora l'intero sistema è il guest) ad eseguire la 
	    // VMCALL oppure se è stato qualcun altro che intendeva rivolgersi ad Hyper-V.
	    // Nota che, a prescindere dalla funzione invocata, il valore di ritorno
	    // viene caricato nel registro RAX del guest, in modo che questo lo possa 
	    // vedere una volta tornati in non-root operation.
	    if (GuestRegs->r10 == 0x4e4f485950455256 && GuestRegs->r11 == 0x564d43414c4c)
	    {
		// La VMCALL è roba nostra e la gestiamo nel nostro hypervisor
		// 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);
	    }
	    else
	    {
		// Rimandiamo la VMCALL a Hyper-V perché è roba sua.
		// Il valore di ritorno della HyperCall viene salvato in RAX del guest.
		GuestRegs->rax = AsmHypervVmcall(GuestRegs->rcx, GuestRegs->rdx, GuestRegs->r8);
	    }

	 break;
        }

 
        // ...

 
        default:
        {
  	    KdPrint(("Unkown Vmexit, reason : 0x%llx"ExitReason));
	    break;
        }
 
    }
 
    // Se siamo ancora in VMX operation e 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].VmxoffState.IsVmxoffExecuted && GuestState[CurrentProcessorIndex].IncrementRip)
    {
    	VmxRootResumeToNextInstruction();
    }
 
    // Segnala che si sta uscendo dalla VMX root operation
    GuestState[CurrentProcessorIndex].IsOnVmxRootMode = FALSE;
 
    // Se si è eseguito VMXOFF ritorna TRUE a AsmVmExitHandler...
    if (GuestState[CurrentProcessorIndex].VmxoffState.IsVmxoffExecuted)
    {
    	return TRUE;
    }
 
    // ... altrimenti ritorna FALSE. 
    return FALSE;
}


A questo punto si può dare un senso a quel push 0 all'inizio di AsmVmExitHandler. In pratica, è utile solo quando si esce dalla VMX operation ed in particolare serve a conservare l'indirizzo di ritorno a pop r11 in AsmVmxNonRootVmcall, che è il punto dove vogliamo iniziare a rieseguire il codice del sistema operativo una volta usciti dalla VMX operation. Ad ogni modo, prima di arrivare a ciò, dobbiamo continuare l'analisi e vedere cosa succede una volta tornati ad AsmVmExitHandler. La condizione nell'istruzione di comparazione è verificata e quindi si salta a AsmVmxoffHandler.


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. A pensarci bene
    ; è una 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


Prima di vedere il codice di AsmVmxoffHandler è utile mostrare altre due funzioni di supporto usate da tale metodo.


UINT64 ReturnRSPForVmxoff()
{
	return GuestState[KeGetCurrentProcessorNumber()].VmxoffState.GuestRsp;
}
UINT64 ReturnRIPForVmxoff()
{
	return GuestState[KeGetCurrentProcessorNumber()].VmxoffState.GuestRip;
}


I commenti in AsmVmxoffHandler dovrebbero essere sufficienti a convincersi che, una volta eseguito il RET finale, si ritorna a AsmVmxNonRootVmcall e si prosegue normalmente con un'unica eccezione: non si è più in VMX operation e Windows 10 non è più guest, che è esattamente il risultato che si voleva ottenere.


AsmVmxoffHandler PROC
    
    ; Shadow space 
    ; (20h e non 28h perché AsmVmxoffHandler non è invocato con call ma con jump quindi
    ; non viene inserito un indirizzo di ritorno sullo stack che lo disallinea)
    sub rsp, 020h
 
    ; Ritorna in RAX il valore di RSP al momento della VMCALL che ha causato la VM exit
    ; che ha portato ad eseguire VMXOFF: in questo caso quella in AsmVmxNonRootVmcall.
    call ReturnRSPForVmxoff
 
    add rsp, 020h       ; rimuove shadow space
 
    ; Salva il valore ritornato da ReturnRSPForVmxoff in spazio creato da push 0
    ; all'inizio di AsmVMExitHandler.
    ; In questo momento RSP punta a RAX pushato in AsmVMExitHandler. Ci sono 17 push
    ; da push rax a push 0: 
    ; 17 x 8 = 136 = 0x88 byte per arrivare ad indirizzo di 0 pushato sullo stack 
    ; all'inizio di AsmVMExitHandler.
    mov [rsp+088h], rax
 
    ; Mette in RAX il valore di RIP al momento della VMCALL che ha causato la VM exit
    ; che ha portato ad eseguire VMXOFF, aggiornato opportunamente per puntare
    ; all'istruzione successiva: pop r11 in AsmVmxNonRootVmcall.
    sub rsp, 020h
    call ReturnRIPForVmxoff
    add rsp, 020h
 
    ; Salva in RDX il valore di RSP corrente, che punta a valore di RAX pushato con 
    ; in AsmVmExitHandler
    mov rdx, rsp
 
    ; Salva in RBP il valore ritornato da ReturnRSPForVmxoff
    mov rbx, [rsp+088h]
 
    ; Ora RSP corrente punta a ciò che puntava al momento della VMCALL 
    ; che ha causato la VM exit che ha portato ad eseguire VMXOFF.
    mov rsp, rbx
 
    ; Mette l'indirizzo di pop r11 (che si trova in AsmVmxNonRootVmcall) su 
    ; stack usato dal guest al momento della VM exit.
    push rax
 
    ; Ripristina RSP, che punta nuovamente a valore di RAX pushato 
    ; in AsmVmExitHandler
    mov rsp, rdx
          
    ; Il valore ritornato da ReturnRSPForVmxoff è RSP al momento
    ; della VM exit che ha causato la VM exit che ha portato ad
    ; eseguire VMXOFF. Su tale stack, però, ora è stato aggiunto
    ; l'indirizzo di ritorno a pop r11. Quindi bisogna aggiornare
    ; lo stack pointer del guest per farlo puntare a tale valore.
    ; Ma RSP punta a RAX pushato in AsmVmExitHandler.
    ; E' questo il motivo per cui prima si è salvato lo stack pointer 
    ; in RBX, che punta ancora al valore ritornato da ReturnRSPForVmxoff 
    ; e che quindi può essere sottratto di 8.
    sub rbx, 08h
 
    ; Salva lo stack pointer aggiornato nello spazio creato da push 0
    ; all'inizio di AsmVmExitHandler.
    mov [rsp+088h], rbx
 
    ; Ripristina registri salvati all'inizio di AsmVmExitHandler
    ; (si ricordi che RSP punta ancora a valore di RAX pushato).
    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
 
    ; Ora in cima allo stack c'è l'indirizzo di ritorno a
    ; pop r11 in AsmVmxNonRootVmcall. Mettendolo in RSP ed
    ; eseguento il ret si ritorna proprio a tale istruzione.
    pop rsp
    ret
 
AsmVmxoffHandler ENDP



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



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

Nessun commento:

Posta un commento