1. 程式人生 > 實用技巧 >intel:x86架構VT虛擬化(二):核心程式碼入門介紹

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框架