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 DeviceObject, PIRP 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(Irp, IO_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 Affinity, OldAffinity; NTSTATUS Status; for (ULONG i = 0; i < LogicalProcessors; i++) { KeGetProcessorNumberFromIndex(i, &ProcessorNumber); RtlSecureZeroMemory(&Affinity, sizeof(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 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; } 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)
Nessun commento:
Posta un commento