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

寂靜的羽夏發表於2022-03-06

寫在前面

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

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

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


? 華麗的分割線 ?


概述

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

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

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

VT 入門篇——最小 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,我們可以看一下它的內容:

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

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

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

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

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

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

VT 入門篇——最小 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我們就可以得到如下結果,表示成功:

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

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;
}

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

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

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

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

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

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 實現(下)

相關文章