VMM Implementation

[0x00] Concept

먼저 기본적인 구현의 경우 인텔의 문서를 따랐으나 막히는 부분에서는 아래의 오픈 소스와 튜토리얼을 참고하였습니다.

많은 내용을 담고 싶었으나, 본인이 구현하면서 난해했던 내용들을 기준으로 핵심적인 내용만 작성하였습니다. 추가적으로는 아래 참고자료들을 활용할 수 있습니다.

  • Gbhv - Simple x64 Hypervisor Framework(Gbps)
  • DdiMon(tandasat)
  • Hypervisor From Scratch(Sina) & Hyperdbg

위에 있는 다양한 오픈 소스와 튜토리얼, 인텔 공식 메뉴얼을 참고하여 실행 중인 논리 프로세서를 가상화하는 방법에 대해 설명합니다.

전체 소스코드는 현재는 공개되지 않습니다. 다른 프로젝트들 처럼 견고하지 못하고 핸들링이 미숙하기 때문에 추후에 기회가 된다면 재밌는 공개 프로젝트를 진행해보도록 하겠습니다.

[0x01] Initialize VMCS Data

초기에 가장 많은 시간을 쏟은 부분이 VMCS Data 설정 부분이었습니다.

해당 부분은 gbhvHyperdbg 의 내용을 참조하였습니다.

여기 내용을 확인하면 크게 3개의 파트로 나눠지는 것을 확인할 수 있습니다.

Guest-State Area, Host-State Area, VMX Control Fields 입니다.

다만 처음 이 부분을 마주하면 다시 메뉴얼을 꼼꼼히 살펴보지 않는 이상 이 모든 내용을 다 설정해줘야 하는가에 대한 막막함에 사로잡힙니다.

우리가 참고해야 할 부분은 Intel SDM Vol.3C Chapter 26 부터 입니다. 현재 블로그에서는 이곳 에 게시되어 있습니다.

VM Entry 를 통해 VMX Non-root operation 으로 진입하는 내용에 대해서는 이전에 확인하였습니다. 실제로 코드에서 VM Entry 동작은 __vmx_vmlaunch 함수를 이용하여 구현됩니다.(Windows 기준)

이 함수를 사용하면 VM Entry 를 통해 게스트로 진입하게 되고, 성공적으로 진입하게 되면 IP(Instruction Pointer) 는 게스트에 지정된 위치로 전환되며 실행됩니다.

이 때 위에서 설정한 3개의 내용들을 토대로 게스트는 설정되며, 게스트로 전환이 가능한지 확인을 하게 됩니다.

[-] AsmInitializeGuest

해당 함수는 VMLAUNCH 가 성공적으로 이루어지면 안정적으로 게스트의 IP 를 설정하기 위해 MASM 을 이용하여 호출되는 함수입니다.

현재는 모든 코어에 대해서 가상화하기 때문에 코어의 개수만큼 해당 루틴을 호출해야 합니다. 이 때 KeGenericCallDpc 또는 아래와 같이 직접 코어의 Affinity 를 설정하여 특정 루틴을 모든 코어에서 호출할 수 있습니다.

NTSTATUS ShHvSupport::CallFromEachProcessor(EACH_PROCESSOR_ROUTINE Routine)
{
	NTSTATUS Status = STATUS_SUCCESS;
	auto ProcessCount = KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS);
	for (ULONG i = 0; i < ProcessCount; i++)
	{
		PROCESSOR_NUMBER ProcessNumber = { 0, };
		GROUP_AFFINITY Affinity = { 0, };
		GROUP_AFFINITY AffinityOrg = { 0, };

		Status = KeGetProcessorNumberFromIndex(i, &ProcessNumber);
		if (!NT_SUCCESS(Status)) 
		{
			ShDrvUtil::NtErrorHandler("KeGetProcessorNumberFromIndex", Status);
			return Status; 
		}

		Affinity.Group = ProcessNumber.Group;
		Affinity.Mask = 1ull << ProcessNumber.Number;
		
		KeSetSystemGroupAffinityThread(&Affinity, &AffinityOrg);

		Status = Routine();
		
		KeRevertToUserGroupAffinityThread(&AffinityOrg);
		
		if (!NT_SUCCESS(Status))
		{
			ShDrvUtil::NtErrorHandler("CallbackRoutine : %p\n", Status);
			return Status;
		}
	}
	return Status;
}

위와 같은 루틴을 이용하여 AsmInitializeGuest 루틴을 호출하게 되며 코드는 아래와 같습니다.

pushaq MACRO
        push r15
        push r14
        push r13
        push r12
        push r11
        push r10
        push r9
        push r8
        push rdi
        push rsi
        push rbp
        push rbx
        push rdx
        push rcx
        push rax
    endm

    popaq MACRO
        pop	rax
        pop	rcx
        pop	rdx
        pop	rbx
        pop	rbp
        pop	rsi
        pop	rdi
        pop	r8
        pop	r9
        pop	r10
        pop	r11
        pop	r12
        pop	r13
        pop	r14
        pop	r15
    endm

.code _text

    ; NTSTATUS AsmInitializeGuest()
    AsmInitializeGuest PROC
        pushfq                  ; Backup general purpose register
        pushaq                  ; Backup RFlags

        mov rcx, GuestHere      ; GuestRIP

        mov rdx, rsp            ; GuestRSP

        call InitializeGuest    ; VOID InitializeGuest(ULONG64 GuestRIP, ULONG64 GuestRSP)

    GuestHere:
        popaq
        popfq

        mov rax, 0              ; dummy.. STATUS_SUCCESS
        ret

    AsmInitializeGuest ENDP

...

위의 코드를 보면 VMLAUNCH 가 실행되면 게스트의 IPGuestHere 으로 넘어오게 됩니다. 현재 계속해서 나오는 게스트의 의미는 가상화 된 호스트를 의미합니다.

호출되는 InitializeGuest 는 전달받은 게스트의 IPSP(Stack-Pointer) 를 이용해 게스트 설정을 진행하게 됩니다.

[-] InitializeGuest

아래와 같이 구현되어 있으며, SetupVMCSData 루틴을 통해 VM Entry 동작을 위해 필요한 요소들을 설정한 후 VMLAUNCH(__vmx_vmlaunch) 가 호출됩니다. 성공적으로 전환되면 위에서 말한 것과 같이 IP 가 변경되어 __vmx_vmlaunch 이후의 코드가 실행되지 않습니다. 때문에 아래에 주석 처리된 부분은 VMLAUNCH 가 실패한 경우에만 동작하게 됩니다.

VOID ShHvVmm::InitializeGuest(ULONG64 GuestRIP, ULONG64 GuestRSP)
{
	VMX_STATUS Status = VMX_SUCCESS;
	auto CurrentProcessor = KeGetCurrentProcessorNumber();
	PlainLog("\t\n======================== Initialize VMCS Data (%d)============================\n", CurrentProcessor);

	Status = SetupVMCSData(CurrentProcessor, GuestRIP, GuestRSP);
	if (Status != VMX_SUCCESS)
	{
		ErrLog("Failed set VMCS data\n");
		return;
	}
	
	Log("Complete settings VMCS data\n");

	g_VmmContext->ProcessorContext[CurrentProcessor].bLaunched = true;
	__vmx_vmlaunch();

  // failed vmlaunch
	ULONG64 ErrorCode = 0;
	VMREAD(VMCS_VM_INSTRUCTION_ERROR, &ErrorCode);
	Log("Failed launch %llX\n", ErrorCode);
}

[-] SetupVMCSData

해당 함수는 래핑함수로 실제로 각각의 데이터들을 설정하는 루틴을 호출하게 됩니다.

NTSTATUS ShHvVmm::SetupVMCSData(ULONG Index, ULONG64 GuestRIP, ULONG64 GuestRSP)
{
	NTSTATUS Status = STATUS_SUCCESS;

	Status = SetupVmxControl(Index);
	if (!NT_SUCCESS(Status))
	{
		ErrLog("Failed setup vmx control fields\n");
		return STATUS_UNSUCCESSFUL;
	}

	Status = SetupGuestArea(GuestRIP, GuestRSP);
	if (!NT_SUCCESS(Status))
	{
		ErrLog("Failed setup guest-state area\n");
		return STATUS_UNSUCCESSFUL;
	}

	Status = SetupHostArea(Index);
	if (!NT_SUCCESS(Status))
	{
		ErrLog("Failed setup host-state area\n");
		return STATUS_UNSUCCESSFUL;
	}

	return Status;
}

[0x02] SetupVmxControl

VM-Execution Control Field 를 시작으로 Vm-Exit, Entry 와 같은 다양한 제어 필드를 설정하는 루틴입니다.

이 루틴에서 설정된 내용에 따라 VM Exit 를 발생시켜 핸들링하거나 게스트에서 금지되는 행위등을 지정할 수 있습니다.

이곳을 확인하여 필수적인 요소들에 대해 확인할 수 있습니다.

NTSTATUS ShHvVmm::SetupVmxControl(ULONG Index)
{
	NTSTATUS Status = STATUS_SUCCESS;
	IA32_VMX_BASIC_REGISTER VmxBasicMsr = { 0, };
	VmxBasicMsr.AsUInt = __readmsr(IA32_VMX_BASIC);
	
// Pin-Based
	VMWRITEEX(
		VMCS_CTRL_PIN_BASED_VM_EXECUTION_CONTROLS, 
		AdjustControlValue(
			0, 
			VmxBasicMsr.VmxControls ? IA32_VMX_TRUE_PINBASED_CTLS : IA32_VMX_PINBASED_CTLS
		)
	);

// Primary Processor-Based (use MSR-Bitmaps, activate secondary controls) 
// todo : tertiary control (IA32_VMX_PROCBASED_CTLS_ACTIVATE_TERTIARY_CONTROLS_FLAG)
	VMWRITEEX(
		VMCS_CTRL_PROCESSOR_BASED_VM_EXECUTION_CONTROLS,
		AdjustControlValue(
			IA32_VMX_PROCBASED_CTLS_USE_MSR_BITMAPS_FLAG | IA32_VMX_PROCBASED_CTLS_ACTIVATE_SECONDARY_CONTROLS_FLAG,
			VmxBasicMsr.VmxControls ? IA32_VMX_TRUE_PROCBASED_CTLS : IA32_VMX_PROCBASED_CTLS
		)
	);

// Secondary Processor-Based (enable EPT, enable RDTSCP, enable VPID, enable INVPCID, enable XSAVES, XRSTORS)
	VMWRITEEX(
		VMCS_CTRL_SECONDARY_PROCESSOR_BASED_VM_EXECUTION_CONTROLS,
		AdjustControlValue(
			IA32_VMX_PROCBASED_CTLS2_ENABLE_EPT_FLAG | IA32_VMX_PROCBASED_CTLS2_ENABLE_RDTSCP_FLAG | IA32_VMX_PROCBASED_CTLS2_ENABLE_VPID_FLAG |
			IA32_VMX_PROCBASED_CTLS2_ENABLE_INVPCID_FLAG | IA32_VMX_PROCBASED_CTLS2_ENABLE_XSAVES_FLAG,
			IA32_VMX_PROCBASED_CTLS2
		)
	);

// VM-Exit Control
	VMWRITEEX(
		VMCS_CTRL_PRIMARY_VMEXIT_CONTROLS, 
		AdjustControlValue(
			IA32_VMX_EXIT_CTLS_HOST_ADDRESS_SPACE_SIZE_FLAG,
			VmxBasicMsr.VmxControls ? IA32_VMX_TRUE_EXIT_CTLS : IA32_VMX_EXIT_CTLS
		)
	);

// VM-Entry Control
	VMWRITEEX(
		VMCS_CTRL_VMENTRY_CONTROLS,
		AdjustControlValue(
			IA32_VMX_ENTRY_CTLS_IA32E_MODE_GUEST_FLAG,
			VmxBasicMsr.VmxControls ? IA32_VMX_TRUE_ENTRY_CTLS : IA32_VMX_ENTRY_CTLS
		)
	);

// for Control register
	VMWRITEEX(VMCS_CTRL_CR0_GUEST_HOST_MASK, 0);
	VMWRITEEX(VMCS_CTRL_CR0_READ_SHADOW, 0);
	VMWRITEEX(VMCS_CTRL_CR4_GUEST_HOST_MASK, 0);
	VMWRITEEX(VMCS_CTRL_CR4_READ_SHADOW, 0);

// for MSRs
	VMWRITEEX(VMCS_CTRL_VMEXIT_MSR_STORE_COUNT, 0);
	VMWRITEEX(VMCS_CTRL_VMEXIT_MSR_LOAD_COUNT, 0);
	VMWRITEEX(VMCS_CTRL_VMENTRY_MSR_LOAD_COUNT, 0);

// MSR-Bitmap Address
	VMWRITEEX(VMCS_CTRL_MSR_BITMAP_ADDRESS, g_VmmContext->ProcessorContext[Index].MsrBitmapPhysical);

// EPTP
	VMWRITEEX(VMCS_CTRL_EPT_POINTER, g_VmmContext->Eptp.AsUInt);

// VPID, If the “enable VPID” VM-execution control is 1, 
// the value of the VPID VM-execution control field must not be 0000H
	VMWRITEEX(VMCS_CTRL_VIRTUAL_PROCESSOR_IDENTIFIER, 1);
	
	return Status;
}

함수에서 사용한 각각의 매크로 함수(VMWRITE, VMWRITE) 는 아래와 같습니다. (gbhv 참조)

#define VMREADEX(Field, Value) Status |= __vmx_vmread(Field, Value)
#define VMREAD(Field, Value) __vmx_vmread(Field, Value)

#define VMWRITEEX(Field, Value) Status |= __vmx_vmwrite(Field, Value)
#define VMWRITE(Field, Value) __vmx_vmwrite(Field, Value)

#define VMWRITE_GUEST_SEGMENT_REGISTER(SegmentName, Selector, Base, Limit, AccessRights)\
        VMWRITEEX(VMCS_GUEST_##SegmentName##_SELECTOR, Selector);\
        VMWRITEEX(VMCS_GUEST_##SegmentName##_BASE, Base);\
        VMWRITEEX(VMCS_GUEST_##SegmentName##_LIMIT, Limit);\
        VMWRITEEX(VMCS_GUEST_##SegmentName##_ACCESS_RIGHTS, AccessRights);

#define VMWRITE_HOST_SEGMENT_REGISTER(SegmentName, Selector, Base)\
        VMWRITEEX(VMCS_HOST_##SegmentName##_SELECTOR, Selector);\
        VMWRITEEX(VMCS_HOST_##SegmentName##_BASE, Base);

#define RESTORE_SEGMENT_BASE(SegmentName, Value)\
        Value = 0;\
        VMREAD(VMCS_GUEST_##SegmentName##_BASE, &Value);\
        __writemsr(IA32_##SegmentName##_BASE, Value);

#define READ_DESCRIPTOR_TABLE(SegmentName, Base, Limit)\
        Base = 0; Limit = 0;\
        VMREAD(VMCS_GUEST_##SegmentName##_BASE, &Base);\
        VMREAD(VMCS_GUEST_##SegmentName##_LIMIT, &Limit);

AdjustControlValue 루틴의 경우에는 0-settings, 1-settings 라고 불리는 각각 해당 값을 허용하는지에 대해 MSR 을 통해 확인하고 설정된 값을 반환해주는 루틴입니다.

상세 내용은 Intel SDM Vol.3D Appendix A.3 VM-Execution Controls 를 참조하였습니다.

ULONG ShHvVmm::AdjustControlValue(ULONG ReqValue, ULONG Msr)
{
	COMMON_MSR Register = { 0, };

	Register.QuadPart = __readmsr(Msr);

	ReqValue &= Register.HighPart; // Allowed 1-Settings, (bit == 0 must be 0)
	ReqValue |= Register.LowPart;  // Allowed 0-Settings  (bit == 1 must be 1)

	return ReqValue;
}

[-] Settings VM-Execution Control

좀 더 상세하게 설명해보면 VM-Execution Control 의 경우 Report MSR 이라고도 불리는 해당 MSR 값(IA32_VMX_PROCBASED_CTL 과 같은)을 이용하여 각각의 허용된 1-settings0-settings 를 확인하여 비트를 설정해야 합니다.

현재 본인이 연산하고 직접 이해한 내용은 다음과 같습니다.

  • bits 31:0 (코드 상 LowPart)은 0으로 허용 가능한 설정을 나타냅니다. 헷갈리면 안되는 부분이 0을 허용한다해서 해당 비트를 1로 표현하는게 아니라 실제 0으로 표현됩니다. 즉 0이 아닌 비트의 경우에는 반드시 1이어야 합니다.
    • 정리하면 0으로 허용 가능한 비트는 0으로 표현되며, 1로 표현된 비트는 꼭 1이어야 합니다. 반드시 1이어야 하는 부분을 default1 이라고도 합니다.
  • bits 63:32(코드 상 HighPart)는 1로 허용 가능한 설정을 나타냅니다. 마찬가지로 1을 허용하는 부분은 1으로 설정되며 1로 설정이 불가한 비트는 0으로 표현됩니다.

이러한 조건으로 인해 설정하기 위한 비트 값을 1을 허용하는 비트 값과 AND 연산하여 맞추고, 0을 허용하는 비트 값과 OR 연산을하여 1로 설정되어야 하는 부분까지 설정하게 됩니다.

아래는 Primary Processor-Based Control 을 설정할 때의 값의 변화를 보여줍니다.

======================== Initialize VMCS Data (0)============================
[Shh0ya] Allowed 1-settings : 0xFFF9FFFE
[Shh0ya] Allowed 0-settings : 0x4006172
[Shh0ya] Value              : 0x94006172 (Request : 0x90000000)

요청 값은 RDMSR, WRMSR 에 대한 핸들링을 위한 UseMsrBitmaps 비트와 EPT 활성화를 위한 ActivateSecondaryControls 비트의 값입니다.(0x90000000, bit 28, 31)

1을 허용하는 값은 0xfff9fffe 로 bit 0(reserved), 17(ActivateTertiaryControls), 18(reserved) 을 제외하고 모두 1로 허용하고 있습니다.

0을 허용하는 값은 0x04006172 로 bit 1, 4-6, 8, 13-14, 26(reserved) 이 꼭 1이어야 하며 나머지는 0으로 설정을 허용하고 있습니다.

Intel SDM 에서는 bit 13-14 가 아닌 bit 13-16까지 default1 인데 실제로는 다른 것 같습니다.

[0x03] SetupGuestArea

위에서 실행될 VM에 대한 다양한 환경들을 설정했다면 이번에는 실제 구동에 필요한 게스트의 범용 레지스터, 세그먼트 레지스터 등을 설정하는 파트입니다.

게스트 상태 영역에는 저장되는 내용들은 두 개로 나누어지며 이는 Register, Non-Register 상태에 대한 값들이 저장됩니다.

지난 포스팅에서 찾아볼 수 있습니다.(링크)

해당 챕터에서 복잡하면서 가장 중요한 부분은 세그먼트 레지스터에 대한 파싱입니다. Segment SelectorGlobal Descriptor Table 을 이용하여 해당 세그먼트의 설명자(Descriptor)를 찾고, Base Address, Limit, AccessRights 를 찾아 설정해야 하기 때문입니다.

아래는 SetupGuestArea 루틴의 전체 코드입니다. 인텔 메뉴얼에 나와있는 순서대로 초기화하였습니다.

NTSTATUS ShHvVmm::SetupGuestArea(ULONG64 GuestRIP, ULONG64 GuestRSP)
{
	NTSTATUS Status = STATUS_SUCCESS;

	VMCS_REGISTER Register = { 0, };

	Status = ShHvSupport::GetCurrentVmcsContext(&Register);
	if (!NT_SUCCESS(Status))
	{
		ErrLog("Can't get context\n");
		return Status;
	}

	SetGuestSegmentRegister(&Register);
	
	// Guest-State Area Register State //
	
  // Control Register & Debug Register
	VMWRITEEX(VMCS_GUEST_CR0, Register.Cr0.AsUInt);
	VMWRITEEX(VMCS_GUEST_CR3, Register.Cr3.AsUInt);
	VMWRITEEX(VMCS_GUEST_CR4, Register.Cr4.AsUInt);
	VMWRITEEX(VMCS_GUEST_DR7, Register.Dr7.AsUInt);

  // RIP, RSP, RFlags
	VMWRITEEX(VMCS_GUEST_RIP, GuestRIP);
	VMWRITEEX(VMCS_GUEST_RSP, GuestRSP);
	VMWRITEEX(VMCS_GUEST_RFLAGS, Register.RFlags.AsUInt);

  // Segment Register
	VMWRITE_GUEST_SEGMENT_REGISTER(SS, Register.Ss.SegmentSelector.AsUInt, Register.Ss.SegmentBaseAddress, Register.Ss.SegmentLimit, Register.Ss.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(CS, Register.Cs.SegmentSelector.AsUInt, Register.Cs.SegmentBaseAddress, Register.Cs.SegmentLimit, Register.Cs.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(DS, Register.Ds.SegmentSelector.AsUInt, Register.Ds.SegmentBaseAddress, Register.Ds.SegmentLimit, Register.Ds.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(ES, Register.Es.SegmentSelector.AsUInt, Register.Es.SegmentBaseAddress, Register.Es.SegmentLimit, Register.Es.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(FS, Register.Fs.SegmentSelector.AsUInt, Register.Fs.SegmentBaseAddress, Register.Fs.SegmentLimit, Register.Fs.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(GS, Register.Gs.SegmentSelector.AsUInt, Register.Gs.SegmentBaseAddress, Register.Gs.SegmentLimit, Register.Gs.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(TR, Register.Tr.SegmentSelector.AsUInt, Register.Tr.SegmentBaseAddress, Register.Tr.SegmentLimit, Register.Tr.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(LDTR, Register.Ldtr.SegmentSelector.AsUInt, Register.Ldtr.SegmentBaseAddress, Register.Ldtr.SegmentLimit, Register.Ldtr.SegmentAccessRights.AsUInt);

	VMWRITEEX(VMCS_GUEST_FS_BASE, __readmsr(IA32_FS_BASE));
	VMWRITEEX(VMCS_GUEST_GS_BASE, __readmsr(IA32_GS_BASE));

  // GDTR, IDTR
	VMWRITEEX(VMCS_GUEST_GDTR_BASE, Register.Gdtr.BaseAddress);
	VMWRITEEX(VMCS_GUEST_GDTR_LIMIT, Register.Gdtr.Limit);
	VMWRITEEX(VMCS_GUEST_IDTR_BASE, Register.Idtr.BaseAddress);
	VMWRITEEX(VMCS_GUEST_IDTR_LIMIT, Register.Idtr.Limit);

  // MSR
	VMWRITEEX(VMCS_GUEST_DEBUGCTL, Register.DebugCtl.AsUInt);
	VMWRITEEX(VMCS_GUEST_SYSENTER_CS, Register.SysEnterCS.AsUInt);
	VMWRITEEX(VMCS_GUEST_SYSENTER_EIP, Register.SysEnterEIP);
	VMWRITEEX(VMCS_GUEST_SYSENTER_ESP, Register.SysEnterESP);

	// Guest-State Area Non-Register State

	/*VMWRITEEX(VMCS_GUEST_ACTIVITY_STATE, 0);
	VMWRITEEX(VMCS_GUEST_INTERRUPT_STATUS, 0);
	VMWRITEEX(VMCS_GUEST_PENDING_DEBUG_EXCEPTIONS, 0);*/

	VMWRITEEX(VMCS_GUEST_VMCS_LINK_POINTER, ~0ULL);

	return Status;
}

먼저 현재 프로세서에 대한 컨텍스트를 가져오기 위해 GetCurrentVmcsContext 라는 루틴을 생성하였습니다. 해당 루틴에 진입하면 게스트 상태 영역에 설정해야 하는 각각의 레지스터들을 미리 저장하고 해당 값들을 알맞게 수정한 후 VMWRITE 를 통해 게스트 상태 영역을 설정합니다.

(함수의 접두사에 AsmMASM 으로 작성된 코드이며, hyperdbg 를 참조하였습니다.)

// for Register state
NTSTATUS ShHvSupport::GetCurrentVmcsContext(VMCS_REGISTER* Register)
{
	NTSTATUS Status = STATUS_SUCCESS;

	RtlZeroMemory(Register, sizeof(VMCS_REGISTER));

	Register->Rsp = AsmGetRsp();

	Register->Cr0.AsUInt = __readcr0();
	Register->Cr3.AsUInt = __readcr3();
	Register->Cr4.AsUInt = __readcr4();
	
	Register->Dr7.AsUInt = __readdr(7);
	
	Register->RFlags.AsUInt = __readeflags();

	Register->Ss.SegmentSelector   = AsmGetSs();
	Register->Cs.SegmentSelector   = AsmGetCs();
	Register->Ds.SegmentSelector   = AsmGetDs();
	Register->Es.SegmentSelector   = AsmGetEs();
	Register->Fs.SegmentSelector   = AsmGetFs();
	Register->Gs.SegmentSelector   = AsmGetGs();
	Register->Tr.SegmentSelector   = AsmGetTr();
	Register->Ldtr.SegmentSelector = AsmGetLdtr();

	AsmGetGdtr(&Register->Gdtr);
	AsmGetIdtr(&Register->Idtr);

	Status = ShHvVmm::GetSegmentDescriptor(Register);

	return Status;
}

여기서 주로 사용되는 VMCS_REGISTER 의 구조는 아래와 같습니다.

typedef struct _VMCS_REGISTER {
	ULONG64 Rsp;
	ULONG64 Rip;

	CR0 Cr0;
	CR3 Cr3;
	CR4 Cr4;
	DR7 Dr7;

	EFLAGS RFlags;

	SEGMENT_DESCRIPTOR_REGISTER_64 Gdtr;
	SEGMENT_DESCRIPTOR_REGISTER_64 Idtr;

	VMCS_SEGMENT_REGISTER Ss;
	VMCS_SEGMENT_REGISTER Cs;
	VMCS_SEGMENT_REGISTER Ds;
	VMCS_SEGMENT_REGISTER Es;
	VMCS_SEGMENT_REGISTER Fs;
	VMCS_SEGMENT_REGISTER Gs;
	VMCS_SEGMENT_REGISTER Tr;
	VMCS_SEGMENT_REGISTER Ldtr;

	IA32_DEBUGCTL_REGISTER DebugCtl;
	IA32_SYSENTER_CS_REGISTER SysEnterCS;
	ULONG64 SysEnterESP;
	ULONG64 SysEnterEIP;

}VMCS_REGISTER, * PVMCS_REGISTER;

위의 구조는 게스트 상태 영역의 Register 상태 부분을 위한 구조입니다.

먼저 세그먼트 레지스터에 대한 파싱을 설명합니다.

[-] Segment Selector, Base, Limit and Access Rights

위의 GetCurrentVmcsContext 루틴 내 GetSegmentDescriptor 코드 기준으로 설명하겠습니다. 크게 복잡한 내용은 없으며, 제가 정렬해둔 구조의 포인터를 기준으로 증가시키며 각 세그먼트의 SelectorGDT 를 이용하여 설명자를 구합니다.

NTSTATUS ShHvVmm::GetSegmentDescriptor(VMCS_REGISTER* Register)
{
	NTSTATUS Status = STATUS_SUCCESS;
	ULONG64 RegisterPtr = (ULONG64)&Register->Ss;

	for (int i = 0; i < MAX_SEGMENT_REGISTER; i++)
	{
		auto TempReg = (VMCS_SEGMENT_REGISTER*)(RegisterPtr + (i * sizeof(VMCS_SEGMENT_REGISTER)));
		
		TempReg->SegmentDescriptor = GetSegmentDescriptorEx(TempReg->SegmentSelector, Register->Gdtr.BaseAddress);
		if (TempReg->SegmentDescriptor == nullptr)
		{
			Status = STATUS_UNSUCCESSFUL;
			break;
		}
	}

	return Status;
}

Segment Register 는 위의 제목과 같이 4개의 파트로 나누어집니다. Selector, Base Address, Limit, Access-Rights 파트입니다. (링크에서 검사 항목에 대해 확인할 수 있습니다.)

Segment Register 는 아래와 같이 Visible partHidden Part 로 나누어지며 기본적으로 Selector 를 제외한 필드는 모두 Hidden part 로 구성됩니다. 이러한 Hidden partSegment Descriptor 라고 하며, 해당 설명자를 통해 Base Address, Limit, Access-Rights 에 대한 정보를 획득할 수 있습니다.

[+] Segment Selector

위와 같은 구조로 되어있으며 16 bit 길이로 구성되어 있습니다. 먼저 우리는 Selector 에 대한 검사를 통과하기 위해 적절한 값을 설정해야 합니다.

VOID ShHvVmm::SetGuestSegmentSelector(VMCS_REGISTER* Register)
{
	// LDTR. If LDTR is usable, the TI flag (bit 2) must be 0
	if (Register->Ldtr.SegmentSelector.AsUInt == 0)
	{
		Register->Ldtr.SegmentAccessRights.Unusable = 1;
	}
	else
	{
		Register->Ldtr.SegmentSelector.Table = GDT;
	}

	// TR. The TI flag (bit 2) must be 0.
	Register->Tr.SegmentSelector.Table = GDT;

	Register->Ss.SegmentSelector.RequestPrivilegeLevel = Register->Cs.SegmentSelector.RequestPrivilegeLevel;
}

링크 를 확인하면 아래와 같은 검증 로직을 확인할 수 있습니다.

  1. TR 의 경우 Table indicator 가 0(GDT)이어야 하며, LDTR 이 사용 가능한 경우(Unusable = 0)에 Table indicator 가 0이어야 합니다.
  2. SS 의 경우 virtual-8086 이 아니고 Secondary Processor Based Control Fieldunrestricted guest 가 0인 경우 RPL(Request Privilege Level)CS 의 값과 동일해야 합니다.

SetGuestSegmentSelector 함수는 위와 같은 검증 과정을 위해 Selector 값을 적절하게 설정합니다.

다음으로 Selector 를 이용하여 Segment Descriptor 를 찾아야 하며 이는 GDT(Global Descriptor Table)Base Address 와 해당하는 SelectorIndex 값이 필요합니다.

Global Descriptor Table 은 이름 그대로, 설명자에 대한 전역 테이블로써 64-bit mode 기준으로 64 bits 로 구성된 각각의 설명자가 배열로 구성되어 있습니다. 아래는 GDTLDT(Local Descriptor Table) 에 대한 그림입니다.

위와 같은 설명을 토대로 설명자를 구하는 코드는 다음과 같이 구현할 수 있습니다.

SEGMENT_DESCRIPTOR_64* ShHvVmm::GetSegmentDescriptorEx(SEGMENT_SELECTOR Selector, ULONG64 GdtBase)
{
	if (Selector.Table != GDT)
	{
		ErrLog("Can't get descriptor of selector(TI = Local Descriptor Table)\n");
		return nullptr;
	}

	return (SEGMENT_DESCRIPTOR_64*)(GdtBase + (Selector.Index << 3));  // gdt + index * 8
}

[+] Segment Descriptor

Segment Descriptor 는 아래와 같이 구성되어 있습니다.

게스트 상태 영역에 저장할 Base Address, Limit, AccessRights 를 설정하는 코드는 다음과 같습니다. 코드에서 복잡한 부분은 없습니다. 그저 Descriptor 를 파싱하는 일 뿐입니다.

또한 AccessRights 의 경우 별도의 게스트를 생성하는 것이 아니라 우리는 실행 중인 호스트를 가상화하기 때문에 동일하게 구성해주어도 문제 없습니다. VM Entry 에서 게스트 상태 영역을 체크로 인한 제약 사항은 주석으로 설명되어 있습니다.

VOID ShHvVmm::SetSegmentBaseAddress(VMCS_SEGMENT_REGISTER* Register)
{
	Register->SegmentBaseAddress = (
		Register->SegmentDescriptor->BaseAddressHigh << 24 | Register->SegmentDescriptor->BaseAddressMiddle << 16 | Register->SegmentDescriptor->BaseAddressLow
		);

	// CS. Bits 63:32 of the address must be zero.
	// SS, DS, ES. If the register is usable, bits 63:32 of the address must be zero.
	Register->SegmentBaseAddress &= 0xFFFFFFFF;
	
	// If it's a System segment, treat it as a 64-bit base address.
	if (Register->SegmentDescriptor->DescriptorType == 0)
	{
		Register->SegmentBaseAddress |= ((ULONG64)Register->SegmentDescriptor->BaseAddressUpper << 32);
	}
}

VOID ShHvVmm::SetSegmentLimit(VMCS_SEGMENT_REGISTER* Register)
{
	Register->SegmentLimit = __segmentlimit(Register->SegmentSelector.AsUInt);
}

VOID ShHvVmm::SetGuestSegmentAccessRights(VMCS_SEGMENT_REGISTER* Register)
{
	if (Register->SegmentSelector.AsUInt == 0) { Register->SegmentAccessRights.Unusable = 1; return; }

	Register->SegmentAccessRights.Type = Register->SegmentDescriptor->Type;
	Register->SegmentAccessRights.DescriptorType = Register->SegmentDescriptor->DescriptorType;
	Register->SegmentAccessRights.DescriptorPrivilegeLevel = Register->SegmentDescriptor->DescriptorPrivilegeLevel;
	Register->SegmentAccessRights.Present = Register->SegmentDescriptor->Present;
	Register->SegmentAccessRights.AvailableBit = Register->SegmentDescriptor->System;
	Register->SegmentAccessRights.LongMode = Register->SegmentDescriptor->LongMode;
	Register->SegmentAccessRights.DefaultBig = Register->SegmentDescriptor->DefaultBig;
	Register->SegmentAccessRights.Granularity = Register->SegmentDescriptor->Granularity;

	Register->SegmentAccessRights.Reserved1 = 0;
	Register->SegmentAccessRights.Reserved2 = 0;
	Register->SegmentAccessRights.Unusable = 0; // usable
}

세그먼트 레지스터와 관련된 함수들의 로직을 정리하면 다음과 같습니다.

  1. AsmGetxx 함수를 통해 현재 각 세그먼트 레지스터 등을 구한다.(GetCurrentVmcsContext)
  2. 위에서 구한 Segment Selector 를 이용하여 Segment Descriptor 를 구한다.(GetSegmentDescriptor)
  3. Segment Descriptor 를 분해하여 Base Address, Limit, Access-Rights 를 구한다.

위의 로직을 통과하면 게스트 상태 영역에 설정 될 각 세그먼트 레지스터의 Selector, Base Address, Limit, Access-Rights 가 준비됩니다.

[-] Write to Guest-State Area

게스트 상태 영역에 값을 쓰기 전에 주의해야 할 점은 세그먼트 레지스터만 주의하면 됩니다. 이후에는 VMWRITE 명령을 통해 쓰기만 하면 됩니다.

VMWRITE_GUEST_SEGMENT_REGISTER 의 경우 위에서 준비된 Selector, Base Address, Limit, Access-Rights 값을 적절한 영역에 VMWRITE 를 통해 쓰는 역할을 합니다.(상단에 매크로 참조)

  ... // SetupGuestArea
	VMWRITEEX(VMCS_GUEST_CR0, Register.Cr0.AsUInt);
	VMWRITEEX(VMCS_GUEST_CR3, Register.Cr3.AsUInt);
	VMWRITEEX(VMCS_GUEST_CR4, Register.Cr4.AsUInt);
	VMWRITEEX(VMCS_GUEST_DR7, Register.Dr7.AsUInt);
	VMWRITEEX(VMCS_GUEST_RFLAGS, Register.RFlags.AsUInt);

	VMWRITE_GUEST_SEGMENT_REGISTER(SS, Register.Ss.SegmentSelector.AsUInt, Register.Ss.SegmentBaseAddress, Register.Ss.SegmentLimit, Register.Ss.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(CS, Register.Cs.SegmentSelector.AsUInt, Register.Cs.SegmentBaseAddress, Register.Cs.SegmentLimit, Register.Cs.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(DS, Register.Ds.SegmentSelector.AsUInt, Register.Ds.SegmentBaseAddress, Register.Ds.SegmentLimit, Register.Ds.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(ES, Register.Es.SegmentSelector.AsUInt, Register.Es.SegmentBaseAddress, Register.Es.SegmentLimit, Register.Es.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(FS, Register.Fs.SegmentSelector.AsUInt, Register.Fs.SegmentBaseAddress, Register.Fs.SegmentLimit, Register.Fs.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(GS, Register.Gs.SegmentSelector.AsUInt, Register.Gs.SegmentBaseAddress, Register.Gs.SegmentLimit, Register.Gs.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(TR, Register.Tr.SegmentSelector.AsUInt, Register.Tr.SegmentBaseAddress, Register.Tr.SegmentLimit, Register.Tr.SegmentAccessRights.AsUInt);
	VMWRITE_GUEST_SEGMENT_REGISTER(LDTR, Register.Ldtr.SegmentSelector.AsUInt, Register.Ldtr.SegmentBaseAddress, Register.Ldtr.SegmentLimit, Register.Ldtr.SegmentAccessRights.AsUInt);

	VMWRITEEX(VMCS_GUEST_FS_BASE, __readmsr(IA32_FS_BASE));
	VMWRITEEX(VMCS_GUEST_GS_BASE, __readmsr(IA32_GS_BASE));

	VMWRITEEX(VMCS_GUEST_GDTR_BASE, Register.Gdtr.BaseAddress);
	VMWRITEEX(VMCS_GUEST_GDTR_LIMIT, Register.Gdtr.Limit);
	VMWRITEEX(VMCS_GUEST_IDTR_BASE, Register.Idtr.BaseAddress);
	VMWRITEEX(VMCS_GUEST_IDTR_LIMIT, Register.Idtr.Limit);

	VMWRITEEX(VMCS_GUEST_DEBUGCTL, Register.DebugCtl.AsUInt);

	VMWRITEEX(VMCS_GUEST_SYSENTER_CS, Register.SysEnterCS.AsUInt);
	VMWRITEEX(VMCS_GUEST_SYSENTER_EIP, Register.SysEnterEIP);
	VMWRITEEX(VMCS_GUEST_SYSENTER_ESP, Register.SysEnterESP);

	VMWRITEEX(VMCS_GUEST_RIP, GuestRIP);
	VMWRITEEX(VMCS_GUEST_RSP, GuestRSP);
  
  // If the field contains a value other than FFFFFFFF_FFFFFFFFH, VMCS link pointer checks are applied.
  VMWRITEEX(VMCS_GUEST_VMCS_LINK_POINTER, ~0ULL);
...

[0x04] SetupHostArea

호스트 상태 영역의 경우에도 게스트 상태 영역과 약간의 검증 과정을 제외하면 거의 흡사합니다.

NTSTATUS ShHvVmm::SetupHostArea(ULONG Index)
{
	NTSTATUS Status = STATUS_SUCCESS;
	VMCS_REGISTER Register = { 0, };

	Status = ShHvSupport::GetCurrentVmcsContext(&Register);
	if (!NT_SUCCESS(Status))
	{
		ErrLog("Can't get context\n");
		return Status;
	}

	SetHostSegmentRegister(&Register);

	// Host-State Area Processor Register
	VMWRITEEX(VMCS_HOST_CR0, Register.Cr0.AsUInt);
	VMWRITEEX(VMCS_HOST_CR3, (ULONG64)g_ShVar.SystemDirectoryBase);
	VMWRITEEX(VMCS_HOST_CR4, Register.Cr4.AsUInt);

	VMWRITEEX(VMCS_HOST_SS_SELECTOR, Register.Ss.SegmentSelector.AsUInt);
	VMWRITEEX(VMCS_HOST_CS_SELECTOR, Register.Cs.SegmentSelector.AsUInt);
	VMWRITEEX(VMCS_HOST_DS_SELECTOR, Register.Ds.SegmentSelector.AsUInt);
	VMWRITEEX(VMCS_HOST_ES_SELECTOR, Register.Es.SegmentSelector.AsUInt);

	VMWRITE_HOST_SEGMENT_REGISTER(FS, Register.Fs.SegmentSelector.AsUInt, Register.Fs.SegmentBaseAddress);
	VMWRITE_HOST_SEGMENT_REGISTER(GS, Register.Gs.SegmentSelector.AsUInt, Register.Gs.SegmentBaseAddress);
	VMWRITE_HOST_SEGMENT_REGISTER(TR, Register.Tr.SegmentSelector.AsUInt, Register.Tr.SegmentBaseAddress);

	VMWRITEEX(VMCS_HOST_GDTR_BASE, Register.Gdtr.BaseAddress);
	VMWRITEEX(VMCS_HOST_IDTR_BASE, Register.Idtr.BaseAddress);

	VMWRITEEX(VMCS_HOST_FS_BASE, __readmsr(IA32_FS_BASE));
	VMWRITEEX(VMCS_HOST_GS_BASE, __readmsr(IA32_GS_BASE));

	VMWRITEEX(VMCS_HOST_SYSENTER_CS, Register.SysEnterCS.AsUInt);
	VMWRITEEX(VMCS_HOST_SYSENTER_EIP, Register.SysEnterEIP);
	VMWRITEEX(VMCS_HOST_SYSENTER_ESP, Register.SysEnterESP);

  // Preallocated host stack
	VMWRITEEX(VMCS_HOST_RSP, g_VmmContext->ProcessorContext[Index].HostStackPointer + HOST_STACK_SIZE - PAGE_SIZE);
	VMWRITEEX(VMCS_HOST_RIP, (ULONG64)ShHvVmx::AsmVmExitHandler);

	return Status;
}

GetCurrentVmcsContext 는 게스트 상태 영역과 마찬가지로 현재 레지스터들의 값들을 복사하는 용도입니다.

[-] Check Host Segment Register

링크 를 참조하면 검증 과정을 확인할 수 있습니다.

위의 함수에서 SetHostSegmentSelector 를 통해 먼저 Selector 의 값을 적절한 값으로 설정합니다.

VOID ShHvVmm::SetHostSegmentSelector(VMCS_REGISTER* Register)
{
	ULONG64 RegisterPtr = (ULONG64)&Register->Ss;

	for (int i = 0; i < MAX_CODE_DATA_SEGMENT + 1; i++)  // ss, cs, ds, es, fs, gs + tr
	{
		auto TempReg = (VMCS_SEGMENT_REGISTER*)(RegisterPtr + (i * sizeof(VMCS_SEGMENT_REGISTER)));
		
		/*
		* 26.2.3 Checks on Host Segment and Descriptor-Table Registers
		* In the selector field for each of CS, SS, DS, ES, FS, GS and TR, the RPL(bits 1:0) and the TI flag(bit 2) must be 0
		*/
		TempReg->SegmentSelector.RequestPrivilegeLevel = 0;
		TempReg->SegmentSelector.Table = GDT;
	}
}

마찬가지로 복잡한 검증 과정은 없지만 CS, SS, DS, ES, FS, GS, TR 의 경우 RPL 비트와 TI 비트가 0이어야 합니다.

게스트 상태 영역과 다른 점이 더 있는데, 호스트 상태 영역에서는 세그먼트 레지스터, 설명자 테이블은 아래와 같이 구분되어 저장됩니다.

  • Selector Field
    • CS, SS, DS, ES, FS, GS, TR
  • Base Address Field
    • FS, GS, TR, GDTR, IDTR

Limit, Access-Rights 는 저장되지 않습니다.

[-] Write to Host-State Area

추후에 Shh0yaToolVMM 을 이용한 기능이 추가 예정이므로, __readcr3() 를 사용했을 때 적절한 시스템 디렉토리 베이스가 나오지 않을 수 있습니다. 때문에 앞서 구한 전역 변수를 통해 CR3 를 설정합니다.

  ... // SetupHostArea
  // Host-State Area Processor Register
	VMWRITEEX(VMCS_HOST_CR0, Register.Cr0.AsUInt);
	VMWRITEEX(VMCS_HOST_CR3, (ULONG64)g_ShVar.SystemDirectoryBase);
	VMWRITEEX(VMCS_HOST_CR4, Register.Cr4.AsUInt);

	VMWRITEEX(VMCS_HOST_SS_SELECTOR, Register.Ss.SegmentSelector.AsUInt);
	VMWRITEEX(VMCS_HOST_CS_SELECTOR, Register.Cs.SegmentSelector.AsUInt);
	VMWRITEEX(VMCS_HOST_DS_SELECTOR, Register.Ds.SegmentSelector.AsUInt);
	VMWRITEEX(VMCS_HOST_ES_SELECTOR, Register.Es.SegmentSelector.AsUInt);

	VMWRITE_HOST_SEGMENT_REGISTER(FS, Register.Fs.SegmentSelector.AsUInt, Register.Fs.SegmentBaseAddress);
	VMWRITE_HOST_SEGMENT_REGISTER(GS, Register.Gs.SegmentSelector.AsUInt, Register.Gs.SegmentBaseAddress);
	VMWRITE_HOST_SEGMENT_REGISTER(TR, Register.Tr.SegmentSelector.AsUInt, Register.Tr.SegmentBaseAddress);

	VMWRITEEX(VMCS_HOST_GDTR_BASE, Register.Gdtr.BaseAddress);
	VMWRITEEX(VMCS_HOST_IDTR_BASE, Register.Idtr.BaseAddress);

	VMWRITEEX(VMCS_HOST_FS_BASE, __readmsr(IA32_FS_BASE));
	VMWRITEEX(VMCS_HOST_GS_BASE, __readmsr(IA32_GS_BASE));

	VMWRITEEX(VMCS_HOST_SYSENTER_CS, Register.SysEnterCS.AsUInt);
	VMWRITEEX(VMCS_HOST_SYSENTER_EIP, Register.SysEnterEIP);
	VMWRITEEX(VMCS_HOST_SYSENTER_ESP, Register.SysEnterESP);

  // Preallocated host stack
	VMWRITEEX(VMCS_HOST_RSP, g_VmmContext->ProcessorContext[Index].HostStackPointer + HOST_STACK_SIZE - PAGE_SIZE);
	VMWRITEEX(VMCS_HOST_RIP, (ULONG64)ShHvVmx::AsmVmExitHandler);
  
  return Status;
}

추가로 각 프로세서 별 Host Stack 을 미리 할당하고 이를 사용합니다. VMCS_HOST_RIP 의 경우, 게스트에서 VM Exit 가 발생 시 VMX Root-Operation 으로 전환되면 Exit Reason 에 따라 핸들링을 할 수 있는 루틴을 저장합니다.

[0x05] EPT Hook Concept

위와 같은 모든 설정을 통해 VMLAUNCH 가 실행되면, 이제 모든 논리 프로세서는 가상화되어 게스트 상태가 됩니다.

이 때 VMX Control 의 설정에 따라 VM Exit 가 발생하고 이를 핸들링 함으로써 VMM 의 역할을 수행하게 됩니다.

실제로 이를 구현해보면 CPUID 가 지속적으로 VM Exit 를 유발하는 것을 확인할 수 있습니다. CPUID 는 무조건 VM Exit 를 유발시키는 명령으로 해당 내용은 링크 에서 확인할 수 있습니다.

이러한 내용을 이야기하는 이유는 EPT Hooking 이라는 내용에 대한 기본적인 원리를 이해하기 위함입니다.

VMX Control Field 들에 대한 설정에 따라 정해진 특정한 행위들은 VM Exit 를 유발하게 됩니다. 계속해서 강조하지만 이를 핸들링할 수 있습니다.

EPT Violation 은 실제로 액세스 위반 외에도 여러 이유로 인해 발생하며 이는 여기 링크에서도 찾아볼 수 있습니다.


이러한 EPT 를 구현하는 것은 EPT Paging-Structure 를 구현하는 것을 의미합니다. 만약 물리 주소 0x1000이 EPT Paging-Structure Entry 중 어떠한 엔트리에 매핑되어 있다고 생각해보겠습니다.

이 때 해당 엔트리의 Read Access 비트가 0으로 설정되어 있다면, 0x1000 을 읽으려는 시도가 발생할 때 마다 EPT Violation 이 발생하게 되고 이를 핸들링할 수 있게 됩니다.

이것이 바로 EPT Page Hooking 의 기본적인 아이디어 입니다.

일반적으로 메모리 접근은 페이징 구조를 통해 가상 주소를 물리 주소로 변환하여 처리됩니다. 이 때 EPT 가 사용 중인 경우 게스트의 물리 주소를 호스트의 물리 주소로 변환하여 처리됩니다.

그리고 VMM 을 구현하며 EPT 에 대한 구조를 생성하고 수정하는 것이 가능합니다.

EPT 가 사용 중이고, 호스트의 물리 주소와 적절하게 매핑이 되어 있다고 가정해보겠습니다.

게스트는 게스트의 물리 주소를 호스트 물리 주소로 변환하여 접근하기 위해 EPT Paging-Structure 의 정해진 레벨에 따라 변환을 계산할 것이고, 이에 따른 물리 주소에 접근하여 요청받은 액세스를 처리할 것 입니다.

4-Level 기준으로 EPT PTE 에서 최종 물리 주소를 계산하게 됩니다. 위에서 말했 듯 이러한 EPTVMM 의 설계자가 생성하고 수정하는 것이 가능합니다.


Hyperdbg, gbhv, ddimon 이나 여러 경량화 된 하이퍼바이저의 소스코드들을 살펴보면, EPT Paging-Structure 를 설계할 때 2-MByte 페이지로 매핑된 EPT PDE 로 구성하여 4-Level 페이징이 아닌 구성으로 구현하는 것을 확인할 수 있습니다.

이는 메모리 낭비를 없애기 위해 구현된 것이며, 실제 후킹을 하기위해 페이지 매핑을 진행할 때 해당하는 PDE 를 찾고, 이를 4-KByte 단위로 나누어 EPT PTE 를 구성하여 사용하는 것을 볼 수 있습니다.


너무 많은 이야기들로 내용이 지저분해진 것 같습니다. 아래는 최대한 간략하게 표현해본 그림입니다.

먼저 타겟 루틴이 포함된 페이지를 구하고, 해당 페이지의 내용을 가짜 버퍼에 복사합니다. 그리고 가짜 버퍼의 물리 주소를 구하고 이를 미리 구성한 EPT 에 복사합니다. 이미 이것만으로도 모든 이해가 될 것이라 생각합니다.

이러한 상태(EPT 가 활성화되어 있고, 해당 엔트리가 설정된)에서 아래와 같이 타겟으로 한 루틴이 실행되면 아래와 같이 조작한 페이지에서 실행되는 것 입니다.

이러한 동작때문에 Hidden Hook, EPT Shadow Hook 등으로 불립니다. 패치가드가 트리거되지 않는 이유도 충분히 설명되었을 것이라 생각됩니다.

[0x06] Proof Of Concept

현재 POC 는 EPT(Extended Page Table) 를 이용한 후킹까지 구현이 되어 있습니다.

EPT 를 이용하여 Fake Page 를 통해 read, write, execute 권한을 변경합니다. 이로 인해 지정한 루틴에서 EPT Violation 을 발생시키며 이로 인해 VM Exit 가 발생하고 내가 원하는 구현으로 후킹을 구현할 수 있습니다.

해당 부분은 추후에 게시할 예정입니다.

[0x07] Conclusion

내가 이를 구현한 이유는 내가 사용할 분석 도구에 적용하기 위함입니다.

첫 번째는 패치가드로부터 자유로워지는 것 입니다. 물론 다른 방법도 있지만 충분히 매력적인 방법이며 불편한 점도 없습니다.

두 번째는 Shadow Hook이나, Event Injection 에 대한 관심 때문입니다.

많은 내용을 공개하진 못하지만 아래의 참조에 더 자세한 내용들이 많이 있습니다.

추후에는 Shh0yaToolVMM 을 이용한 기능을 추가 예정입니다.

[0x06] Reference

  1. Intel 64 and IA-32 Architectures Software Developer’s Manual
  2. Hypervisor From Scratch, HyperDbg
  3. Gbhv
  4. DDiMon