VT 入門篇——最小 VT 實現(上)

寫在前面

  此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章後面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。

  看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。


🔒 華麗的分割線 🔒


概述

  在學習如何實現最小VT框架的時候,我們先看一下流程圖:

  本篇我們介紹進入虛擬機模式前這部分內容,剩下的部分我們在下一篇繼續。
  如下是更清晰的一些流程,以後我們重點看下面的圖:

  如上實現都必須在具有0環的權限才可以,最方便的當然是在驅動內實現,如何寫驅動我就不贅述了,自己回頭複習去。如下是我們驅動代碼的基本框架:

#include <ntddk.h>
#include <intrin.h>

#define DbgPrintLine(X) DbgPrint(X##"\n")

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    DbgPrintLine("Unloaded Successfully!");
    return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DriverObject->DriverUnload = UnloadDriver;
    DbgPrintLine("Loaded Successfully!");
    return STATUS_SUCCESS;
}

  intrin.h這個頭文件的作用我就不說了,但是提前說一句,並不是所有的指令在32位都是包裝好的,這些指令都是內聯函數。有些包裝好的指令是在64位才能用的,比如__vmx_on等需要傳QWORD參數的函數我們需要自己實現,微軟並沒有幫我們實現該功能。當然在64位的情況下,我們就可以用更多的指令了,由於我們是32位的,就自己實現好了,雖然有點小麻煩。
  話不多說,開始進入正題。

VT 支持啟用檢測

  在之前的老的CPU,它是不支持VT的。現在的新的支持VTCPU,也是有個開關的。就算CPU支持VT,如果被關掉禁用了,你也沒法用,就需要我們進行檢測是否能夠支持啟用VT,故寫了一個函數,如下是完整代碼,後續詳細講解:

BOOLEAN CheckVTEnabled()
{
    //此內部函數將指令返回的受支持功能和 CPU 信息存儲在 cpuInfo 中,
    // cpuInfo 是一個由四個 32 位整數組成的數組,其中依次填充了 EAX、EBX、ECX 和 EDX 寄存器的值。

    int CPUInfo[4];
    __cpuid(CPUInfo, 1);    //調用 CPUID 需要一個參數,參數就是1,通過 ECX 的值的索引5位就是 VMX
    int Info = CPUInfo[2];

    if (!(Info & VMXBit))
    {
        DbgPrintLine("Error : CPUID");
        return FALSE;
    }

    ULONGLONG  CONTROL_MSR = __readmsr(IA32_FEATURE_CONTROL_MSR);
    if (!(CONTROL_MSR & IA32_FEATURE_CONTROL_MSR_Lock))
    {
        DbgPrintLine("Error : FEATURE CONTROL MSR");
        return FALSE;
    }

    ULONG cr0 = __readcr0();
    if (!((cr0 & CR0_PE) && (cr0 & CR0_NE) && (cr0 & CR0_PG)))
    {
        DbgPrintLine("Error : CR0");
        return FALSE;
    }

    ULONG cr4 = __readcr4();
    if (cr4 & CR4_VMXE)
    {
        DbgPrintLine("VT Has Been Occupied!");
        return FALSE;
    }

    return TRUE;
}

  __cpuid是對CPUID彙編指令的封裝,我們先看看Intel是怎樣說明該函數的:

CPUID returns processor identification and feature information in the EAX, EBX, ECX, and EDX registers. The instruction』s output is dependent on the contents of the EAX register upon execution (in some cases, ECX as well).

  現在的CPU一般都支持CPUID,如果實在不放心的話可以檢測EFLAGS的索引21二進制位是否可以修改設置,如下是白皮書說明:

The ID flag (bit 21) in the EFLAGS register indicates support for the CPUID instruction. If a software procedure can set and clear this flag, the processor executing the procedure supports the CPUID instruction.

  這裡我認為現在使用的CPU都支持CPUID指令。該指令是一個非常複雜的指令,具體可以查看白皮書的第764頁,有關eax這個參數每個值的含義,具體看白皮書的第765頁,我們使用的參數是1,我們可以看一下它的內容:

  代碼注釋我明確說明用到的是ecx,我們看一下為什麼:

  這只是表格的一部分,但對於我們有用的就足夠了。注意我們的VMX位,就在這個裏面。通過這條CPUID指令我們只是判斷CPU是否支持VT,但通過vmxon指令啟用VT還有一些必備條件的。

  白皮書開頭說我們的CR4VMXE位需要置1,並且在MSRMSR_IA32_FEATURE_CONTROL成員的索引0位必須是1,否則使用vmxon指令啟用VT會觸發通用保護異常。這個只能在BIOS內進行設置,否則也會觸發,通過這個我們就可以判斷VT是否被禁用了,如下是相關的中文說明:

  當然這些條件遠遠不夠,如下是白皮書的說明:

The first processors to support VMX operation require that the following bits be 1 in VMX operation: CR0.PE, CR0.NE, CR0.PG, and CR4.VMXE. The restrictions on CR0.PE and CR0.PG imply that VMX operation is supported only in paged protected mode (including IA-32e mode). Therefore, guest software cannot be run in unpaged protected mode or in real-address mode.

  也就是說,必須在帶有分頁的保護模式下才能正常使用VT,為了簡單處理我們不使用虛擬機嵌套,所以CR4.VMXE這個位如果是1,說明我再啟用就是套娃了,不跟你套。
  如上是我寫的函數的所有細節了,我們來做個實驗,在做實驗之前我們的驅動入口代碼修改為如下:

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DriverObject->DriverUnload = UnloadDriver;

    DbgPrintLine("Loaded Successfully!");

    if (CheckVTEnabled())
    {
        DbgPrintLine("MiniVT : VT Support!");
    }

    return STATUS_SUCCESS;
}

  然後編譯,拖到虛擬機里進行加載,通過DbgView我們就可以得到如下結果,表示成功:

VMXON

  上面我們實現了VT是否支持啟用的檢測函數,下面我們再實現兩個函數,實現VT技術的啟用和關閉,它的函數原型如下:

BOOLEAN StartVT();
BOOLEAN StopVT();

  由於__vmx_on微軟並沒有在32位幫我們封裝起來,需要我們自行實現,函數如下:

BOOLEAN __vmx_on(DWORD32 LVMXONRegionPA, DWORD32 HVMXONRegionPA)
{
    _asm
    {
        push[HVMXONRegionPA];
        push[LVMXONRegionPA];
        _emit 0xF3;
        _emit 0x0F;
        _emit 0xC7;
        _emit 0x34;
        _emit 0x24;    // vmxon qword ptr [esp]
        add esp, 8;
    }

    UINT32 eflags = __readeflags();
    if (eflags & EFLAG_CF)
    {
        return FALSE;
    }
    return TRUE;
}

  使用_emit是因為編譯器並不支持vmxon編譯,所以只能內嵌了。vmxon這個指令並不是一定會成功的,如果失敗會放到EFLAGCF位,是0表示成功,如下是白皮書的說明:

Execute VMXON with the physical address of the VMXON region as the operand. Check successful execution of VMXON by checking if EFLAGS.CF = 0.

  好,我們開始實現啟用VT的代碼,具體代碼如下:

BOOLEAN StartVT()
{
    if (CheckVTEnabled())
    {
        DbgPrintLine("MiniVT : VT Support!");

        __writecr4(__readcr4() | CR4_VMXE);

        PVOID pVMXONRegion = ExAllocatePoolWithTag(NonPagedPool, 0x1000, 'vmx');
        if (!pVMXONRegion)
        {
            DbgPrintLine("Error : vmx Alloc Error");
            return FALSE;
        }
        RtlZeroMemory(pVMXONRegion, 0x1000);
        *(UINT32*)pVMXONRegion = (UINT32)__readmsr(MSR_IA32_VMX_BASIC)&0x7FFFFFFF;
        g_VMXCPU.pVMXONRegion = pVMXONRegion;
        g_VMXCPU.pVMXONRegion_PA = MmGetPhysicalAddress(pVMXONRegion);
        return __vmx_on(g_VMXCPU.pVMXONRegion_PA.LowPart, g_VMXCPU.pVMXONRegion_PA.HighPart);
    }
    return FALSE;
}

  在解釋之前,我們來看看白皮書是咋說的:

  前兩個小黑點我們已經做完了,下面繼續,它讓我們申請一個4KB對齊的VMXON Region,至於到底多大呢?我們需要查閱IA32_VMX_BASIC_MSR這個寄存器,這個寄存器的信息說明如下:

  然後我們注意到這一句話:

Bits 44:32 report the number of bytes that software should allocate for the VMXON region and any VMCS region. It is a value greater than 0 and at most 4096 (bit 44 is set if and only if bits 43:32 are clear).

  你要4KB對齊,又最大4KB,那我直接申請這麼大不就行了?

Initialize the version identifier in the VMXON region (the first 31 bits) with the VMCS revision identifier reported by capability MSRs. Clear bit 31 of the first 4 bytes of the VMXON region.

  上面的幾句話說明前4個位元組位說明版本號,以讓CPU如何處理VT,這個同樣在IA32_VMX_BASIC_MSR這個寄存器,前31位就是版本號。對於剩下的位元組,需要清0。

Execute VMXON with the physical address of the VMXON region as the operand.

  我們使用vmxon指令需要的是它的物理地址,而不是線性地址,所以需要轉化一下,最後調用我們封裝好的__vmx_on函數,就開啟了VT
  既然開啟了,我們也得會關閉,如下是關閉VT的代碼,實現不難,就不細說了。

BOOLEAN StopVT()
{
    __vmx_off();
    __writecr4(__readcr4() & ~CR4_VMXE);
    ExFreePool(g_VMXCPU.pVMXONRegion);

    return TRUE;
}

  到現在,我們需要略微修改驅動的加載和卸載函數代碼,以做實驗驗證是否成功,具體代碼如下:

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    DbgPrintLine("Unloaded Successfully!");
    return StopVT() ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DriverObject->DriverUnload = UnloadDriver;
    
    DbgPrintLine("Loaded Successfully!");
    return  StartVT() ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL;
}

  如果成功的話,它的輸出和我們VT支持啟用檢測的調試輸出是一樣的,並且驅動加載是成功並且不會藍屏。對於後面的部分,下一篇繼續。

下一篇

  VT 入門篇——最小 VT 實現(下)