Attivare la virtualizzazione nidificata
Si ricordi che vogliamo testare il nostro hypervisor su una copia di Windows 10 che gira su una VM creata con Hyper-V. Nella precedente lezione si è visto che, di default, le funzionalità di virtualizzazione non vengono esposte al livello dove si trova tale VM e quindi la verifica del supporto alla virtualizzazione fallirebbe se eseguito sulla CPU virtuale su cui si esegue l'hypervisor. Come spiegato nella stessa lezione, è necessario abilitare la virtualizzazione nidificata per fa esporre tali funzionalità anche a tale CPU virtuale. A tale scopo, a VM spenta si avvii una PowerShell come amministratore e si dia il comando Get-VM per recuperare il nome della VM.
Se lo stato della VM non è Off si provi a dare il comando
Stop-VM -Name "NomeVM" (nel mio caso NomeVM è Win10x64, nel vostro sarà la stringa restituita da Get-VM per la proprietà Name).
A questo punto si può dare il comando
Get-VMProcessor -VMName "NomeVM" | FL ExposeVirtualizationExtensions
per verificare se la virtualizzazione nidificata è già attiva. Se il risultato ritornato è True si è già pronti. In caso contrario si dia il comando
Set-VMProcessor -VMName "NomeVM" -ExposeVirtualizationExtensions $true
per attivarla (volendo si può disattivare la virtualizzazione nidificata quando si vuole con lo stesso comando ma passando $false). Infine si può verificare che Get-VMProcessor stavolta ritorni True.
Nota: se si ha necessità di passare a VMWare, oltre a disattivare la virtualizzazione nidificata, è necessario dare anche il seguente comando
bcdedit /set hypervisorlaunchtype off
e riavviare mentre per usare Hyper-V è richiesto che tale opzione sia impostata ad auto. Per verificarlo si può usare
bcdedit /enum | find "hypervisorlaunchtype" (da prompt)
bcdedit /enum | Select-String "hypervisorlaunchtype" (da powershell)
Processore a core singolo
Quanto necessario fare in sistemi multi-core sarà l'argomento principale della prossima lezione. Invece, in questa prima lezione a carattere prettamente pratico non si vuole appesantire ed allungare troppo il discorso. Per tale motivo, almeno per il momento, si controlli nelle impostazioni di Hyper-V Manager che la nostra VM abbia un solo processore virtuale (processore logico).
Se così non fosse si applichi pure tale modifica. Per verificare il numero di core si può usare il comando (naturalmente in una powershell aperta sulla VM)
Get-ComputerInfo -Property CsNumberOfLogicalProcessors
Codice del client in user-mode
Il client non fa altro che recuperare un handle al device creato dal driver che implementa l'hypervisor. In realtà dell'handle non viene fatto praticamente nessun uso ma la chiamata a CreateFile permette di invocare la dispatch routine collegata a IRP_MJ_CREATE, che in questo caso è DriverCreate.
#include <Windows.h> #include <stdio.h> int main() { if (!IsCPUIntel()) { printf("Sorry! Your need an Intel CPU."); return 0; } HANDLE hDevice = CreateFile(L"\\\\.\\MyHypervisorDevice", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hDevice == INVALID_HANDLE_VALUE) { return Error("Failed to open device"); } printf("Press Enter to exit\n\n"); getchar(); CloseHandle(hDevice); return 0; }
Come operazione secondaria e marginale si invoca IsCpuIntel per vedere se si sta eseguendo il programma su una CPU Intel. Il codice completo è quello visto nella scorsa lezione e che si può consultare anche dal repository creato appositamente per questo corso (si veda il link fornito a fine lezione).
Codice del Driver (hypervisor)
Nell'entry point del driver le uniche operazioni degne di nota sono la creazione del device e del symbolic link. Quest'ultimo permette al client di ottenere un handle al relativo device.
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); // Imposta funzione chiamata quando driver verrà stoppato. // Si occuperà di cancellare il device ed il symbolic link creati in DriverEntry. DriverObject->DriverUnload = DriverUnload; // Invocate quando in usermode vengono chiamate CreateFile e CloseHandle DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreate; DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverClose; // Istruzione seguente equivale a // RtlInitUnicodeString(&devName, L"\\Device\\MyDevice"); // ma è più efficiente in quanto RtlInitUnicodeString calcola la lunghezza da // inserire in UNICODE_STRING.Lenght a runtime // mentre la macro RTL_CONSTANT_STRING lo fa a compile-time UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\MyHypervisorDevice"); // Crea device object PDEVICE_OBJECT DeviceObject; NTSTATUS status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject); if (!NT_SUCCESS(status)) { KdPrint(("Failed to create device (0x%08X)\n", status)); return status; } // Crea symbolic link UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\MyHypervisorDevice"); status = IoCreateSymbolicLink(&symLink, &devName); if (!NT_SUCCESS(status)) { KdPrint(("Failed to create symbolic link (0x%08X)\n", status)); IoDeleteDevice(DeviceObject); return status; } KdPrint(("HypertDriver initialized successfully\n")); return STATUS_SUCCESS; }
Quando il client chiama CreateFile, passandogli il symbolic link al device creato dal driver, viene invocata DriverCreate. CpuIsVMXSupported usa l'istruzione CPUID per verificare che il bit 5 in ECX sia impostato ad 1. Dopodiché controlla anche il bit 2 in IA_FEATURE_CONTROL. Infine, CpuFixBits usa alcuni MSR per impostare quei bit in CR0 e CR4 che devono avere un certo valore per tutta la durata della VMX operation.
NTSTATUS DriverCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject); if (CpuIsVMXSupported()) { // CpuEnableVMX(); // inutile farlo in modo isolato CpuFixBits(); KdPrint(("VMX Operation Enabled Successfully !\n")); } Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }
BOOLEAN CpuIsVMXSupported() { CPUID_EAX_01 data = { 0 }; // Check VMX bit (il quinti bit in ecx) // La struct CPUID_EAX_1 definita con 4 campi interi contigui // quindi può essere trattata come fosse un array. __cpuid((PINT32)&data, 1); if (!_bittest((PLONG)&data.ecx, 5)) //if ((data.ecx & (1 << 5)) == 0) return FALSE; // Usa intrinsics __readmsr per leggere MSR IA32_FEATURE_CONTROL IA32_FEATURE_CONTROL_MSR Control = { 0 }; Control.Uint64 = __readmsr(MSR_IA32_FEATURE_CONTROL); // Controlla bit 2 di IA32_FEATURE_CONTROL. // Se non è impostato ad 1 vuol dire che la virtualizzazione // è disabilitata dal BIOS e bisogna attivarla. if (Control.Bits.EnableVmxOutsideSmx == FALSE) { DbgPrint("Please enable Virtualization from BIOS"); return FALSE; } return TRUE; }
VOID CpuFixBits() { CR4 Cr4 = { 0 }; CR0 Cr0 = { 0 }; // Fix Cr0 Cr0.Uint64 = __readcr0(); Cr0.Uint64 |= __readmsr(MSR_IA32_VMX_CR0_FIXED0); Cr0.Uint64 &= __readmsr(MSR_IA32_VMX_CR0_FIXED1); __writecr0(Cr0.Uint64); // Fix Cr4 Cr4.Uint64 = __readcr4(); Cr4.Uint64 |= __readmsr(MSR_IA32_VMX_CR4_FIXED0); Cr4.Uint64 &= __readmsr(MSR_IA32_VMX_CR4_FIXED1); __writecr4(Cr4.Uint64); }
Nella scorsa lezione sono state fornite tutte le informazioni e gli approfondimenti necessari alla comprensione del codice appena esaminato. Per tale motivo, se ci sono dubbi si riveda [1].
Testare il codice
Si trasferisca il driver (insieme al relativo file dei simboli) ed il client compilati sulla VM. Si esegua DebugView come amministratore e ci si assicuri che l'opzione Capture->Capture Kernel abbia la spunta. Si carichi e si avvii il driver con sc.exe, OSR Driver Loader o il vostro tool preferito (si veda [2]). Se l'operazione viene portata a termine con successo, eseguendo il client si dovrebbe vedere qualcosa che assomiglia all'immagine mostrata all'inizio della presente lezione.
Volendo è possibile usare WinDbg, piazzare un break point all'inizio dell'entry point del driver (o della dispatch routine che gestisce IRP_MJ_CREATE) ed eseguire step by step l'esecuzione del driver, come spiegato in [3]. In questo modo è possibile controllare i valori letti dagli MSR e comprendere meglio come funziona il fixing dei bit di CR0 e CR4.
Repository del progetto: Corso-VT-x (github.com)
Riferimenti:
[2] Hello Kernel !!!
Nessun commento:
Posta un commento