intel:x86架構VT虛擬化(二):核心代碼入門介紹


  上次介紹了VT的基本原理和核心流程,今天細說VT的關鍵代碼。核心代碼的git地址:https://github.com/zzhouhe/VT_Learn ;這是一個miniVT框架,實現了最基本的VT框架功能,非常適合初學入門。

VT的基本流程如下,下面就按照這個流程細說關鍵代碼;

         

  •   檢查是否支持VT

  (1)CPUID:CPUID指令是intel IA32架構下獲得CPU信息的匯編指令,可以得到CPU類型,型號,制造商信息,商標信息,序列號,緩存等一系列CPU相關的東西;執行后返回結果保存在;這個指令3環都能執行,本人物理機執行結果如下:RCX是0x64=0110 0100,VMX=1,說明是支持VT的;

  

   (2)檢查CR0和CR4是否開啟了段保護和頁保護,否則是沒法開啟VT的;

   (3)檢查CR4的VMXE是否為1;如果是,說明已經開啟VT(現在很多殺軟、游戲驅動保護的都用了VT,在-1環做各種保護),這時已經處於別人的監控之中,建議關閉后再開啟,收回主動權

        (4)檢查BIOS主板是否鎖定了VT指令,這個一般都沒有;

  完整代碼如下:

BOOLEAN IsVTEnabled()
{
    ULONG       uRet_EAX, uRet_ECX, uRet_EDX, uRet_EBX;
    _CPUID_ECX  uCPUID;
    _CR0        uCr0;
        uCr4;
    IA32_FEATURE_CONTROL_MSR msr;
    //1. CPUID
    Asm_CPUID(1, &uRet_EAX, &uRet_EBX, &uRet_ECX, &uRet_EDX);
    *((PULONG)&uCPUID) = uRet_ECX;

    if (uCPUID.VMX != 1)
    {
        Log("ERROR: 這個CPU不支持VT!",0);
        return FALSE;
    }

    // 2. CR0 CR4
    *((PULONG)&uCr0) = Asm_GetCr0();
    *((PULONG)&uCr4) = Asm_GetCr4();

    if (uCr0.PE != 1     //開啟段保護模式
          || uCr0.PG!=1   //開啟頁保護模式;這兩個必須都是1,VMXON才會成功。並且直到VMXOFF,這兩個都必須是1;
          || uCr0.NE!=1)
    {
        Log("ERROR:這個CPU沒有開啟VT!",0);
        return FALSE;
    }

    if (uCr4.VMXE == 1)//防止嵌套,被別的應用牽着鼻子走;所以初期要求為0,后續自己手工設置成1;
    {
        Log("ERROR:這個CPU已經開啟了VT!",0);
        Log("可能是別的驅動已經占用了VT,你必須關閉它后才能開啟。",0);
        return FALSE;
    }

    // 3. MSR
    *((PULONG)&msr) = (ULONG)Asm_ReadMsr(MSR_IA32_FEATURE_CONTROL);
    if (msr.Lock!=1)//bios主板的那個設置
    {
        Log("ERROR:VT指令未被鎖定!",0);
        return FALSE;
    }
    Log("SUCCESS:這個CPU支持VT!",0);
    return TRUE;
}
  •   啟動虛擬機

  這個簡單,直接一條VMXON即可;不過要注意:需要事先分配4KB的物理空間供host CPU記錄一些信息。這4KB怎么維護就是CPU自己的事了,開發人員不用管;核心代碼如下:

   Vmx_VmxOn(g_VMXCPU.pVMXONRegion_PA.LowPart, g_VMXCPU.pVMXONRegion_PA.HighPart);
    *((PULONG)&uEflags) = Asm_GetEflags();

    if (uEflags.CF != 0)
    {
        Log("ERROR:VMXON指令調用失敗!",0);
        return;
    }
    Log("SUCCESS:VMXON指令調用成功!",0);
  •   vmclear和vmprtload

  清空和加載VMCS塊,用intel提供現成的vmclear和vmprtload、帶上分配的VMCS內存塊即可,代碼如下:

  Vmx_VmClear(g_VMXCPU.pVMCSRegion_PA.LowPart, g_VMXCPU.pVMCSRegion_PA.HighPart);
    *((PULONG)&uEflags) = Asm_GetEflags();
    if (uEflags.CF != 0 || uEflags.ZF != 0)
    {
        Log("ERROR:VMCLEAR指令調用失敗!",0)
        return;
    }
    Log("SUCCESS:VMCLEAR指令調用成功!",0)
    Vmx_VmPtrld(g_VMXCPU.pVMCSRegion_PA.LowPart, g_VMXCPU.pVMCSRegion_PA.HighPart);
  •   初始化VMCS:最重要的設置

  1、先看看運行時的控制execution control field:

  (1)pin-based vm執行控制域:主要各種外部中斷處理的控制,最重要的是第0位的設置:如果為1,那么外部的中斷會觸發exit,從guest退回到host;

  

  從MSR寄存器看,默認是0,說明guest自己處理外部中斷,不用退回host;

       

  設置的代碼:

    // 3.虛擬機運行控制域
    //guest一旦發生CR3讀寫,必須exit到host;后續寫host代碼,必須處理guest讀寫CR3的事件;
    //pin-base(針腳硬件)中斷,host要不要先處理? 要不要攔截CR4和CR0?
    Vmx_VmWrite(PIN_BASED_VM_EXEC_CONTROL, VmxAdjustControls(0, MSR_IA32_VMX_PINBASED_CTLS));

  (2)cpu-based vm執行控制域:必須設置成1的位

    

  查手冊得知:讀取和加載CR3會觸發vm exit事件,這就要求exit處理的函數必須要考慮CR3的讀寫了;

  

  還有另外一個比較重要的位:一旦設置為1,guest所有對DR寄存器的設置都會觸發vm exit,這時可以用來調試和反調試的

     

   代碼如下:

//讀寫msr的時候要不要退出到host?要不要攔截IO?
    Vmx_VmWrite(CPU_BASED_VM_EXEC_CONTROL, VmxAdjustControls(0, MSR_IA32_VMX_PROCBASED_CTLS));

  (3)VMEntry和VMExit控制設置:同上,先讀出對應MSR寄存器的值,再把高32bit清零,保留低32bit,也就是把intel規定的必須設置為1的位設置成1,其他的位開發人員自行操作;

  intel手冊的部分說明如下:

        

        

   // 4.VMEntry運行控制域
    //guest推出到host的時候,要不要保存dr7和msr的debug寄存器?
    Vmx_VmWrite(VM_ENTRY_CONTROLS, VmxAdjustControls(0, MSR_IA32_VMX_ENTRY_CTLS));
    // 5.VMExit運行控制域    Vmx_VmWrite(VM_EXIT_CONTROLS, VmxAdjustControls(0, MSR_IA32_VMX_EXIT_CTLS));

    2、vmm/host宿主機設置:物理CPU要么執行guestOS的執行,要么執行hotsOS的指令。一旦執行VMXON,cpu便開始執行hostOS的指令;既然是執行OS的指令,自然少不了和指令相關的各類段寄存器/描述符、控制寄存器、GDT、IDT等,這些寄存器、各類表的值該怎么設置了?

  •  寫過OS底層代碼的人都知道,開機上電后cpu處於實模式,bios會從內存的0x007c處開始執行。這時會從磁盤加載os代碼,然后設置各種段寄存器的值,再轉到保護模式。這里既然是host os,當然也可以這么干,不過這樣做有兩個問題:(1)現在有guestOS正在運行,擅自更改各種寄存器、GDT/IDT的基址,后續vmresume切回guestOS后還要想辦法回復。這一來二去的麻煩,耗時;  (2)這么多寄存器,還有GDT/IDT表,如果都自己設置,不麻煩么?尤其是GDT/IDT,還要新開辟內存,構造描述符;IDT表還要構造對應的中斷處理代碼,能行么? 有必要么? 
  •    所以這里偷個懶,這些關鍵寄存器(EIP和ESP除外)、GDT/IDT表設置成和當前運行guestOS一樣,hostOS運行時遇到中斷處理方法和guestOS一樣(處理代碼都在物理內存,雙方很容易共享的);
  •    EIP和ESP為什么要單獨設置?guestOS通過vm exit或vmcall退回到host,請求權限更高的上一級幫忙處理。這就涉及到個性化的處理方法了,所以EIP就是這些代碼的入口地址; 既然是函數調用,自然少不了參數、局部變量和返回地址,這些都需要棧來保存,自然也要單獨分配一個棧空間了

  核心代碼如下:(1)因為代碼在0環,各個段選擇子的CPL是00,那么要求RPL也是00,所以要都要&0xFFF8,把最后3位清零0

   Vmx_VmWrite(HOST_CR0, Asm_GetCr0());
    Vmx_VmWrite(HOST_CR3, Asm_GetCr3());
    Vmx_VmWrite(HOST_CR4, Asm_GetCr4());

    Vmx_VmWrite(HOST_ES_SELECTOR, Asm_GetEs() & 0xFFF8);
    Vmx_VmWrite(HOST_CS_SELECTOR, Asm_GetCs() & 0xFFF8);
    Vmx_VmWrite(HOST_DS_SELECTOR, Asm_GetDs() & 0xFFF8);
    Vmx_VmWrite(HOST_FS_SELECTOR, Asm_GetFs() & 0xFFF8);
    Vmx_VmWrite(HOST_GS_SELECTOR, Asm_GetGs() & 0xFFF8);
    Vmx_VmWrite(HOST_SS_SELECTOR, Asm_GetSs() & 0xFFF8);
    Vmx_VmWrite(HOST_TR_SELECTOR, Asm_GetTr() & 0xFFF8);

    Vmx_VmWrite(HOST_TR_BASE, 0x80042000);

    Vmx_VmWrite(HOST_GDTR_BASE, GdtBase);
    Vmx_VmWrite(HOST_IDTR_BASE, IdtBase);

    Vmx_VmWrite(HOST_IA32_SYSENTER_CS,  Asm_ReadMsr(MSR_IA32_SYSENTER_CS)&0xFFFFFFFF);
    Vmx_VmWrite(HOST_IA32_SYSENTER_ESP, Asm_ReadMsr(MSR_IA32_SYSENTER_ESP)&0xFFFFFFFF);
    Vmx_VmWrite(HOST_IA32_SYSENTER_EIP, Asm_ReadMsr(MSR_IA32_SYSENTER_EIP)&0xFFFFFFFF); // KiFastCallEntry,這里也直接簡單粗暴”借用“guestOS的系統調用

/*
為什么要單獨給棧分配空間?這里的代碼還在驅動里面,驅動的entry一旦執行完,線程可能釋放,堆棧也就沒了,所以host最好單獨開辟
一塊內存作為棧使用;另外,host和guest的棧肯定也是要分開的,類似從3環提權進0環,棧都不會用同一個;
pStack是棧頂,pStack是棧頂+0x2000是棧底;
*/

    Vmx_VmWrite(HOST_RSP,   ((ULONG)g_VMXCPU.pStack) + 0x2000);     //Host 臨時棧
    Vmx_VmWrite(HOST_RIP,   (ULONG)VMMEntryPoint);                  //這里定義我們的VMM處理程序入口,相當於回調

  3、guest state area:客戶機狀態設置

  guestOS一旦產生exit事件,或主動調用vmcall,便會回退到host指定的函數處理這些事件。處理完后host會執行vmresume回到guest繼續執行,這時該到guest的那里執行了?寄存器、GDT/IDT這些周邊環境的上下文怎么恢復了?這里就需要挨個保存了;具體保存的環境信息如下:

      

   從上面的要求可以看出:不但要求保存選擇子可見的16位,剩余不可見的80位也要保存,這就麻煩了。作者偷了個懶,直接先把這些字段設置成不可用,后續在entry時再通過mov ax,cs;mov cs,ax的方式刷新選擇子不可見的80位;

   Vmx_VmWrite(GUEST_CR0, Asm_GetCr0());
    Vmx_VmWrite(GUEST_CR3, Asm_GetCr3());
    Vmx_VmWrite(GUEST_CR4, Asm_GetCr4());

    Vmx_VmWrite(GUEST_DR7, 0x400);
    Vmx_VmWrite(GUEST_RFLAGS, Asm_GetEflags() & ~0x200);

    Vmx_VmWrite(GUEST_ES_SELECTOR, Asm_GetEs() & 0xFFF8);
    Vmx_VmWrite(GUEST_CS_SELECTOR, Asm_GetCs() & 0xFFF8);
    Vmx_VmWrite(GUEST_DS_SELECTOR, Asm_GetDs() & 0xFFF8);
    Vmx_VmWrite(GUEST_FS_SELECTOR, Asm_GetFs() & 0xFFF8);
    Vmx_VmWrite(GUEST_GS_SELECTOR, Asm_GetGs() & 0xFFF8);
    Vmx_VmWrite(GUEST_SS_SELECTOR, Asm_GetSs() & 0xFFF8);
    Vmx_VmWrite(GUEST_TR_SELECTOR, Asm_GetTr() & 0xFFF8);

    Vmx_VmWrite(GUEST_ES_AR_BYTES,      0x10000);//段選擇子隱藏部分的屬性attribute設置成不可用,避免cpu自行加載各種屬性后導致出錯;后續進入GuestEntry手動刷新獲取
    Vmx_VmWrite(GUEST_FS_AR_BYTES,      0x10000);
    Vmx_VmWrite(GUEST_DS_AR_BYTES,      0x10000);
    Vmx_VmWrite(GUEST_SS_AR_BYTES,      0x10000);
    Vmx_VmWrite(GUEST_GS_AR_BYTES,      0x10000);
    Vmx_VmWrite(GUEST_LDTR_AR_BYTES,    0x10000);

    Vmx_VmWrite(GUEST_CS_AR_BYTES,  0xc09b);//CS和TR不能像前面一樣設置成不可用,然后進入GuestEntry刷新獲取
    Vmx_VmWrite(GUEST_CS_BASE,      0);
    Vmx_VmWrite(GUEST_CS_LIMIT,     0xffffffff);

    Vmx_VmWrite(GUEST_TR_AR_BYTES,  0x008b);
    Vmx_VmWrite(GUEST_TR_BASE,      0x80042000);
    Vmx_VmWrite(GUEST_TR_LIMIT,     0x20ab);


    Vmx_VmWrite(GUEST_GDTR_BASE,    GdtBase);
    Vmx_VmWrite(GUEST_GDTR_LIMIT,   Asm_GetGdtLimit());
    Vmx_VmWrite(GUEST_IDTR_BASE,    IdtBase);
    Vmx_VmWrite(GUEST_IDTR_LIMIT,   Asm_GetIdtLimit());

    Vmx_VmWrite(GUEST_IA32_DEBUGCTL,        Asm_ReadMsr(MSR_IA32_DEBUGCTL)&0xFFFFFFFF);
    Vmx_VmWrite(GUEST_IA32_DEBUGCTL_HIGH,   Asm_ReadMsr(MSR_IA32_DEBUGCTL)>>32);

    Vmx_VmWrite(GUEST_SYSENTER_CS,          Asm_ReadMsr(MSR_IA32_SYSENTER_CS)&0xFFFFFFFF);
    Vmx_VmWrite(GUEST_SYSENTER_ESP,         Asm_ReadMsr(MSR_IA32_SYSENTER_ESP)&0xFFFFFFFF); 
    Vmx_VmWrite(GUEST_SYSENTER_EIP,         Asm_ReadMsr(MSR_IA32_SYSENTER_EIP)&0xFFFFFFFF); // KiFastCallEntry,客戶機系統調用的入口

    Vmx_VmWrite(GUEST_RSP,  ((ULONG)g_VMXCPU.pStack) + 0x1000);     //Guest 臨時棧
    Vmx_VmWrite(GUEST_RIP,  (ULONG)GuestEntry);                     // 客戶機的入口點

    Vmx_VmWrite(VMCS_LINK_POINTER, 0xffffffff);
    Vmx_VmWrite(VMCS_LINK_POINTER_HIGH, 0xffffffff);

  以上便是VMCS結構體的設置。注意:這里用intel提供的vmwrite和vmread讀寫,自己用memset等函數時不行的;

  • vmlaunch

  執行后從hostOS進入guestOS。guest執行什么代碼了?對於絕大部分開發人員來說,VT都是用來獲取-1權限、達到調試和反調試目的。這種情況下就盡快讓自定義的guest代碼執行完畢,然后由guestOS拿到vCPU繼續執行。所以guest代碼如下:guest函數最后一句跳轉到g_exit函數執行,但這個函數啥都沒有,此時直接由原guestOS繼續執行(以前該干啥,現在接着干,盡量不打擾)

  這里用裸函數,避免了編譯器擅自添加push ebp,mov ebp,esp, sub esp, xxxh等行為破壞堆棧平衡、改動esp等重要寄存器的值!

  如果我們在vmware做測試,那么里面的os就是guestOS,vmware相當於物理機,我們自己寫的VMMEntryPoint就是hostOS的代碼入口,這個關系一定要捋清楚,否則后續很多代碼邏輯是想不通的!

void g_exit(void);

void __declspec(naked) GuestEntry()
{
    __asm{
        mov eax, cr3
        mov cr3, eax

        mov ax, es
        mov es, ax

        mov ax, ds
        mov ds, ax

        mov ax, fs
        mov fs, ax

        mov ax, gs
        mov gs, ax

        mov ax, ss
        mov ss, ax
    }

    __asm{
        jmp g_exit
    }
}
  •   VMexit/Vmcall

  guestOS在運行過程中可能會主動調用vmcall或 “無意間” 觸發vmexit,退回到hostOS處理這些“異常”事件。hostOS一般需要根據不同的“異常”事件類型采取不同的動作。guestOS的哪些“異常”操作會觸發vmexit,直接關系到hostOS需要接管和處理哪些“異常”,那么這些“異常”事件都是在哪定義的了?---  同樣是在VMCS結構里面,cpu-based vm excution control field能查到

  host的入口函數:

  (1)同樣用裸函數,避免棧平衡被破壞、esp等重要寄存器的值被篡改;

  (2)VMCS結構保存了段選擇子和其他部分重要信息,通用寄存器卻沒保存,先在內存保存好這些通用寄存器的值,hostOS處理完這些異常resume到guest時才好恢復。整個過程像不像線程切換?

  (3)先在終於可以愉快地在hostOS處理guest的異常了,真正的處理函數是VMMEntryPointEbd;

void __declspec(naked) VMMEntryPoint(void)
{
    __asm{
        mov g_GuestRegs.eax, eax
        mov g_GuestRegs.ecx, ecx
        mov g_GuestRegs.edx, edx
        mov g_GuestRegs.ebx, ebx
        mov g_GuestRegs.esp, esp
        mov g_GuestRegs.ebp, ebp
        mov g_GuestRegs.esi, esi
        mov g_GuestRegs.edi, edi

        pushfd
        pop eax
        mov g_GuestRegs.eflags, eax

        mov ax, fs
        mov fs, ax
        mov ax, gs
        mov gs, ax
    }
    VMMEntryPointEbd();
    __asm{
        mov  eax, g_GuestRegs.eax
        mov  ecx, g_GuestRegs.ecx
        mov  edx, g_GuestRegs.edx
        mov  ebx, g_GuestRegs.ebx
        mov  esp, g_GuestRegs.esp
        mov  ebp, g_GuestRegs.ebp
        mov  esi, g_GuestRegs.esi
        mov  edi, g_GuestRegs.edi

        //vmresume
        __emit 0x0f
        __emit 0x01
        __emit 0xc3
    }
}

   退出處理函數的邏輯如下:從VMCS結構體中讀取guestOS退出原因、產生退出那條指令的長度、退出時刻重要寄存器的值,緊接着根據退出原因分別處理;末尾處把新的EIP(原EIP+異常指令長度)、ESP、eflags寄存器重新寫回VMCS,resume的時候CPU會根據這個結構體的信息接着運行;

  個人觀點:對於逆向、安全攻防人員而言,這個方法是最核心和重要的。前面做了大量的工作,就是為了讓guestOS執行的時候出各種“異常”,好由hostOS接管,開發人員就可以通過這個entryPoit“為所欲為”了,比如通過改寄存器或內存的值達到hook的目的;或則把頁面的讀和寫分開,達到無痕掛鈎(shadow walker)的目的等等

static void  VMMEntryPointEbd(void)
{
    ULONG ExitReason;
    ULONG ExitInstructionLength;
    ULONG GuestResumeEIP;

    ExitReason              = Vmx_VmRead(VM_EXIT_REASON);//guestOS退出的原因編號
    ExitInstructionLength   = Vmx_VmRead(VM_EXIT_INSTRUCTION_LEN);//產生退出那條指令的長度

    g_GuestRegs.eflags  = Vmx_VmRead(GUEST_RFLAGS);//退出時各大重要寄存器的值
    g_GuestRegs.esp     = Vmx_VmRead(GUEST_RSP);
    g_GuestRegs.eip     = Vmx_VmRead(GUEST_RIP);
    g_GuestRegs.cr3     = Vmx_VmRead(GUEST_CR3);


    switch(ExitReason)
    {
    case EXIT_REASON_CPUID:
        HandleCPUID();
        //Log("EXIT_REASON_CPUID", 0)
                break;

    case EXIT_REASON_VMCALL:
        HandleVmCall();
        //Log("EXIT_REASON_VMCALL", 0)
        break;

    case EXIT_REASON_CR_ACCESS:
        HandleCrAccess();
        //Log("EXIT_REASON_CR_ACCESS", 0)
        break;

    case EXIT_EPT_VIOLATION:
        *test_data = 0x5678;
        *hook_ept_pt = ((hook_pa.LowPart & 0xFFFFF000) | 0x37);
        break;

    default:
        __asm int 3
        break;
    }

//Resume:
    GuestResumeEIP = g_GuestRegs.eip + ExitInstructionLength;
    Vmx_VmWrite(GUEST_RIP,      GuestResumeEIP);
    Vmx_VmWrite(GUEST_RSP,      g_GuestRegs.esp);
    Vmx_VmWrite(GUEST_RFLAGS,   g_GuestRegs.eflags);
}
  •  EPT:extend page table

  上面所有的功能都可以看成是計算虛擬化,本質上就是讓物理CPU在hostOS和guestOS之間來回切換執行代碼。hostOS權限最高,可以監控和干預guestOS的執行;除了計算虛擬化,還有一塊很重要的就是“內存虛擬化”,也就是EPT:extend page table;

  在沒有虛擬化的時候,3環的exe或0環驅動通過memalloc、exallocatepage等函數分配的地址都是虛擬地址,需要通過頁表轉成物理地址才能完成最終的讀寫;OS需要做2件事:(1)生成並維護頁表 (頁表每項直接都是物理地址,在os進入保護模式前就要在內存設置好)  (2)頁表基址寫入CR3; 至於代碼執行時虛擬地址轉換成物理地址的過程全程由CPU負責,不需要開發人員額外提供啥;

  引入虛擬化后,guestOS的虛擬地址最重要轉換成hostOS的物理地址才能順利地讀寫數據,這個過程是怎么實現的了?guestOS的虛擬地址轉成guestOS的物理地址方式不變(否則市面上現有主流OS做虛擬機OS時都要重新適配地址轉換規則,改動太大,傷筋動骨,兼容性一點都不好),guestOS的物理地址(簡稱GPA)是怎么轉成hostOS的物理地址(簡稱HPA)的了?

  hostOS也需要建立並維護一個多級表,每次guestOS需要轉換物理地址的時候,都需要在這個多級表中逐級查詢。舉個栗子:GPA中的PML4E index要轉成HPA,需要經過下面PML4E\PDPTE\PDE\PTE一共4級才能最終達到HPA;GPA其他諸如PDPTE\PDE\PTE等也要通過這種轉換才能讀寫HPA,所以guestOS一個虛擬地址轉成HPA,一共需要4*4=16次轉換,這時虛擬機效率打折的重要原因之一;

    

 

   EPT表建立的關鍵代碼如下;注意:虛擬機內存大小不同,4級表內的entry數量也不同,這份代碼不能直接簡單粗暴復制;

void initEptPagesPool()
{
    pagesToFree = ExAllocatePoolWithTag(NonPagedPool, 12*1024*1024, 'ept');
    if(!pagesToFree)
        __asm int 3
    RtlZeroMemory(pagesToFree, 12*1024*1024);
}

static ULONG64* AllocateOnePage()
{
    PVOID page;
    page = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'ept');
    if(!page)
        __asm int 3
    RtlZeroMemory(page, PAGE_SIZE);
    pagesToFree[index] = page;
    index++;
    return (ULONG64 *)page;
}

extern PULONG test_data;
PHYSICAL_ADDRESS hook_pa;
ULONG64 *hook_ept_pt;

/*自己維護從PML4E到PA的轉換表*/
ULONG64* MyEptInitialization()
{
    ULONG64 *ept_PDPT, *ept_PDT, *ept_PT;
    PHYSICAL_ADDRESS FirstPtePA, FirstPdePA, FirstPdptePA;
    int a, b, c;

    hook_pa = MmGetPhysicalAddress(test_data);

    initEptPagesPool();
    ept_PML4T = AllocateOnePage();
    ept_PDPT = AllocateOnePage();
    FirstPdptePA = MmGetPhysicalAddress(ept_PDPT);
    *ept_PML4T = (FirstPdptePA.QuadPart) + 7;//最后12位是屬性,7=0111,表示可讀可寫可執行;類似linux下chmod 777;
    for (a = 0; a < 4; a++)
    {
        ept_PDT = AllocateOnePage();
        FirstPdePA = MmGetPhysicalAddress(ept_PDT);
        *ept_PDPT = (FirstPdePA.QuadPart) + 7;
        ept_PDPT++;
        for (b = 0; b < 512; b++)
        {
            ept_PT = AllocateOnePage();
            FirstPtePA = MmGetPhysicalAddress(ept_PT);
            *ept_PDT = (FirstPtePA.QuadPart) + 7;
            ept_PDT++;
            for (c = 0; c < 512; c++)
            {
                *ept_PT  = ((a << 30) | (b << 21) | (c << 12) | 0x37) & 0xFFFFFFFF;// 0x37:可讀可寫可執行,並且有緩存,write-back;
                if ((((a << 30) | (b << 21) | (c << 12) | 0x37) & 0xFFFFF000) == (hook_pa.LowPart & 0xFFFFF000))
                {
                    *ept_PT = 0;
                    hook_ept_pt = ept_PT;
                }
                ept_PT++;
            }
        }
    }

    return ept_PML4T;
}

  自己建立EPT表后,還要在VMCS結構里面”注冊“,代碼如下:

//自己維護的地址轉換表,在VMCS中注冊一下;
    Vmx_VmWrite(EPT_POINTER, (EPTP | 6 | (3 << 3)) & 0xFFFFFFFF);
    Vmx_VmWrite(EPT_POINTER_HIGH, (EPTP | 6 | (3 << 3)) >> 32);
    Vmx_VmWrite(EPT_POINTER_HIGH, EPTP >> 32);
    Vmx_VmWrite(SECONDARY_VM_EXEC_CONTROL, VmxAdjustControls(0x2, MSR_IA32_VMX_PROCBASED_CTLS2));
//for EPT with PAE;
/*
29912:前面2bit對應4項,這4項寫入VMCS;注意:c0600000是CR3的PDE入口,是虛擬地址,要轉換成物理地址;
*/
    Vmx_VmWrite(GUEST_PDPTR0, MmGetPhysicalAddress((PVOID)0xc0600000).LowPart | 1);
    Vmx_VmWrite(GUEST_PDPTR0_HIGH, MmGetPhysicalAddress((PVOID)0xc0600000).HighPart);
    Vmx_VmWrite(GUEST_PDPTR1, MmGetPhysicalAddress((PVOID)0xc0601000).LowPart | 1);
    Vmx_VmWrite(GUEST_PDPTR1_HIGH, MmGetPhysicalAddress((PVOID)0xc0601000).HighPart);
    Vmx_VmWrite(GUEST_PDPTR2, MmGetPhysicalAddress((PVOID)0xc0602000).LowPart | 1);
    Vmx_VmWrite(GUEST_PDPTR2_HIGH, MmGetPhysicalAddress((PVOID)0xc0602000).HighPart);
    Vmx_VmWrite(GUEST_PDPTR3, MmGetPhysicalAddress((PVOID)0xc0603000).LowPart | 1);
    Vmx_VmWrite(GUEST_PDPTR3_HIGH, MmGetPhysicalAddress((PVOID)0xc0603000).HighPart);

  還有個問題隨之而來:為什么需要EPT轉換一下?為什么不讓GPA直接等於HPA? 這個和guestOS內部的分頁原理是一樣的。OS內部多進程同時運行,進程都有自己的4GB虛擬地址空間。每個進程都可以直接用低2GB的空間,而不同擔心和其他進程沖突;核心就是有頁轉換;在不同進程中,即使有同樣的虛擬地址,但進程之間的頁表是不同的,隱射到的物理地址自然也不一樣;一個物理機可以同時運行多個虛擬機,虛擬機之間可以有相同的GPA,但是經過hostOS的EPT轉換后得到不同的HPA,完美規避了不同虛擬機GPA一樣的沖突和尷尬

   以上便是VT最簡單框架的核心代碼,理解起來其實並不難: 物理CPU在host和guest之間的切換可以和線程切換類比,EPT的地址轉換可以和CR3分頁類比

 

參考:1、https://space.bilibili.com/37877654/channel/detail?cid=70349  miniVT框架


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM