domenica 3 ottobre 2021

02 - Hello VMX!

In questa lezione si userà quanto visto nella precedente andando a scrivere un semplice driver da testare sulla nostra VM creata con Hyper-V. Tale driver si limiterà a verificare il supporto della CPU alla virtualizzazione e ad abilitare la possibilità di entrare in VMX operation. Dopo tali operazioni l'interazione tra Host e Guest non è ancora iniziata dato che questa richiederebbe l'esecuzione di VMXON, per entrare in VMX operation, e VMXLAUCH, per la VM entry che passa il controllo al codice guest. Per tale motivo le possibilità di combinare guai sono abbastanza limitate in quanto, praticamente, non cambia nulla. Tuttavia, vedere come scrivere ed eseguire le prime fasi di inizializzazione nel nostro hypervisor è importante per consolidare alcuni concetti prima di passare al resto.






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, NULLOPEN_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, &devNameFILE_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 DeviceObjectPIRP 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(IrpIO_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:

Nessun commento:

Posta un commento