虚拟化

目录(点击跳转)

[TOC]

检查硬件虚拟化

1
2
3
4
5
6
7
8
BOOLEAN HvmIsHVSupported()
{
CPU_VENDOR vendor = UtilCPUVendor();// CPU供应商
if (vendor == CPU_Intel)
return VmxHardSupported();

return TRUE;
}

UtilCPUVendor获取CPU制造商信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// Get CPU vendor
/// </summary>
/// <returns>Intel or AMD. If failed - Other</returns>
CPU_VENDOR UtilCPUVendor()
{
CPUID data = { 0 };
char vendor[0x20] = { 0 };
__cpuid( (int*)&data, 0 );// #include <intrin.h>
*(int*)(vendor) = data.ebx;
*(int*)(vendor + 4) = data.edx;
*(int*)(vendor + 8) = data.ecx;

if (memcmp( vendor, "GenuineIntel", 12 ) == 0)
return CPU_Intel;
if (memcmp( vendor, "AuthenticAMD", 12 ) == 0)
return CPU_AMD;

return CPU_Other;
}

VmxHardSupported检查硬件虚拟化是否支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// <summary>
/// Check if VT-x is supported
/// </summary>
/// <returns>TRUE if supported</returns>
BOOLEAN VmxHardSupported()// 检查VT-x是否支持
{
CPUID data = { 0 };

// VMX bit
__cpuid( (int*)&data, 1 );
if ((data.ecx & (1 << 5)) == 0)// 检查ecx寄存器的第5位是否为1
return FALSE;// 如果是0,说明不支持VT-x

IA32_FEATURE_CONTROL_MSR Control = { 0 };// 读取IA32_FEATURE_CONTROL_MSR寄存器
Control.All = __readmsr( MSR_IA32_FEATURE_CONTROL );// IA32_FEATURE_CONTROL_MSR寄存器的地址为0x3A

// BIOS lock check
if (Control.Fields.Lock == 0)// 检查是否锁定
{
Control.Fields.Lock = TRUE;// 将锁定位置1
Control.Fields.EnableVmxon = TRUE;// 将VMXON使能位置1
__writemsr( MSR_IA32_FEATURE_CONTROL, Control.All );// 写入IA32_FEATURE_CONTROL_MSR寄存器
}
else if (Control.Fields.EnableVmxon == FALSE)// 检查VMXON是否使能
{
DPRINT( "HyperBone: CPU %d: %s: VMX locked off in BIOS\n", CPU_IDX, __FUNCTION__ );// 如果没有使能,打印错误信息
return FALSE;
}

return TRUE;
}

UtilSSDTEntry获取系统服务描述表特定索引处函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// Gets the SSDT entry address by index.
/// </summary>
/// <param name="index">Service index</param>
/// <returns>Found service address, NULL if not found</returns>
PVOID UtilSSDTEntry( IN ULONG index )
{
ULONG size = 0;
PSYSTEM_SERVICE_DESCRIPTOR_TABLE pSSDT = UtilSSDTBase();// 获取SSDT表的基址
PVOID pBase = UtilKernelBase( &size );// 获取内核基址

if (pSSDT && pBase)
{
// 对索引进行范围检查
if (index > pSSDT->NumberOfServices)// 如果索引大于服务数量
return NULL;

return (PUCHAR)pSSDT->ServiceTableBase + (((PLONG)pSSDT->ServiceTableBase)[index] >> 4);// 返回服务地址,服务表基址+函数地址/16,因为函数地址按照16字节对齐
}

return NULL;
}

获取SSDT表函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// Gets the SSDT entry address by index.
/// </summary>
/// <param name="index">Service index</param>
/// <returns>Found service address, NULL if not found</returns>
PVOID UtilSSDTEntry( IN ULONG index )
{
ULONG size = 0;
PSYSTEM_SERVICE_DESCRIPTOR_TABLE pSSDT = UtilSSDTBase();// 获取SSDT表的基址
PVOID pBase = UtilKernelBase( &size );// 获取内核基址

if (pSSDT && pBase)
{
// 对索引进行范围检查
if (index > pSSDT->NumberOfServices)// 如果索引大于服务数量
return NULL;

return (PUCHAR)pSSDT->ServiceTableBase + (((PLONG)pSSDT->ServiceTableBase)[index] >> 4);// 返回服务地址,服务表基址+函数地址/16,因为函数地址按照16字节对齐
}

return NULL;
}

获取SSDT表基地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/// <summary>
/// Gets SSDT base - KiServiceTable
/// </summary>
/// <returns>SSDT base, NULL if not found</returns>
PSYSTEM_SERVICE_DESCRIPTOR_TABLE UtilSSDTBase()
{
/*
NTOS内核是Windows操作系统的核心部分,
负责管理操作系统的基本功能,
如进程管理、内存管理、设备管理、文件系统等。
在NT内核中,大部分的系统服务都是通过系统调用来实现的,
而系统服务描述符表(SSDT)则是记录这些系统服务函数的重要数据结构之一。
如果要在Windows操作系统内核中进行一些工作
,必须首先得到NTOS内核的基地址。
因为所有的系统组件和模块都是基于内核加载的,
只有获得NTOS内核的基地址,才能找到内核中具体的系统函数、数据结构,以便进行修改和操作。
*/
PUCHAR ntosBase = UtilKernelBase( NULL );// 获取NTOS内核的基址
// Already found
if (g_SSDT != NULL)
return g_SSDT;

if (!ntosBase)
return NULL;

PIMAGE_NT_HEADERS pHdr = RtlImageNtHeader( ntosBase );// 获取NTOS内核的PE头
PIMAGE_SECTION_HEADER pFirstSec = (PIMAGE_SECTION_HEADER)(pHdr + 1);// 获取NTOS内核的第一个节表
for (PIMAGE_SECTION_HEADER pSec = pFirstSec; pSec < pFirstSec + pHdr->FileHeader.NumberOfSections; pSec++)
{ // 遍历NTOS内核的所有节表
// Non-paged, non-discardable, readable sections
// Probably still not fool-proof enough...
if (pSec->Characteristics & IMAGE_SCN_MEM_NOT_PAGED &&
pSec->Characteristics & IMAGE_SCN_MEM_EXECUTE &&
!(pSec->Characteristics & IMAGE_SCN_MEM_DISCARDABLE) &&
(*(PULONG)pSec->Name != 'TINI') &&
(*(PULONG)pSec->Name != 'EGAP'))
{// 判断是否为非分页、可执行和非可丢弃的、名称不为INIT和PAGE的代码节表
PVOID pFound = NULL;

// KiSystemServiceRepeat pattern 模式序列
UCHAR pattern[] = "\x4c\x8d\x15\xcc\xcc\xcc\xcc\x4c\x8d\x1d\xcc\xcc\xcc\xcc\xf7";
// 模式匹配
NTSTATUS status = UtilSearchPattern( pattern, 0xCC, sizeof( pattern ) - 1, ntosBase + pSec->VirtualAddress, pSec->Misc.VirtualSize, &pFound );
if (NT_SUCCESS( status ))
{ // 找到SSDT表
g_SSDT = (PSYSTEM_SERVICE_DESCRIPTOR_TABLE)((PUCHAR)pFound + *(PULONG)((PUCHAR)pFound + 3) + 7);
//DPRINT( "BlackBone: %s: KeSystemServiceDescriptorTable = 0x%p\n", CPU_NUM, __FUNCTION__, g_SSDT );
return g_SSDT;
}
}
}

return NULL;
}

获取NTOS内核基地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/// <summary>
/// Get ntoskrnl base address
/// </summary>
/// <param name="pSize">Size of module</param>
/// <returns>Found address, NULL if not found</returns>
PVOID UtilKernelBase( OUT PULONG pSize )
{
NTSTATUS status = STATUS_SUCCESS;
ULONG bytes = 0;
PRTL_PROCESS_MODULES pMods = NULL;
PVOID checkPtr = NULL;
UNICODE_STRING routineName;

// Already found
if (g_KernelBase != NULL)
{
if (pSize)
*pSize = g_KernelSize;
return g_KernelBase;
}

RtlInitUnicodeString( &routineName, L"NtOpenFile" );// 初始化查找名称

checkPtr = MmGetSystemRoutineAddress( &routineName );// 获取系统函数地址
if (checkPtr == NULL)
return NULL;

// Protect from UserMode AV
// 保护来自用户模式的AV(防止访问冲突)
__try
{
// 查询系统模块信息的缓冲区大小
status = ZwQuerySystemInformation( SystemModuleInformation, 0, bytes, &bytes );
if (bytes == 0)
{
DPRINT( "BlackBone: %s: Invalid SystemModuleInformation size\n", CPU_IDX, __FUNCTION__ );
return NULL;
}
// 分配缓冲区
pMods = (PRTL_PROCESS_MODULES)ExAllocatePoolWithTag( NonPagedPoolNx, bytes, HB_POOL_TAG );
// 清空缓冲区
RtlZeroMemory( pMods, bytes );

// 获取系统模块信息
status = ZwQuerySystemInformation( SystemModuleInformation, pMods, bytes, &bytes );

if (NT_SUCCESS( status ))
{
// 模块的信息
PRTL_PROCESS_MODULE_INFORMATION pMod = pMods->Modules;

for (ULONG i = 0; i < pMods->NumberOfModules; i++)
{
// System routine is inside module
if (checkPtr >= pMod[i].ImageBase &&
checkPtr < (PVOID)((PUCHAR)pMod[i].ImageBase + pMod[i].ImageSize))
{
g_KernelBase = pMod[i].ImageBase;// 找到的内核基址
g_KernelSize = pMod[i].ImageSize;// 找到的内核大小
if (pSize)
*pSize = g_KernelSize;// 从参数返回内核大小
break;
}
}
}

}
__except (EXCEPTION_EXECUTE_HANDLER)
{
DPRINT( "BlackBone: %s: Exception\n", CPU_IDX, __FUNCTION__ );
}

if (pMods)
ExFreePoolWithTag( pMods, HB_POOL_TAG );// 释放缓冲区

return g_KernelBase;
}

对给定区域进行模式匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/// <summary>
/// Search for pattern
/// </summary>
/// <param name="pattern">Pattern to search for</param>
/// <param name="wildcard">Used wildcard</param>
/// <param name="len">Pattern length</param>
/// <param name="base">Base address for searching</param>
/// <param name="size">Address range to search in</param>
/// <param name="ppFound">Found location</param>
/// <returns>Status code</returns>
/// 遍历给定内存区域,与指定模式进行字节比较,进行模式匹配
NTSTATUS UtilSearchPattern( IN PCUCHAR pattern, IN UCHAR wildcard, IN ULONG_PTR len, IN const VOID* base, IN ULONG_PTR size, OUT PVOID* ppFound )
{
NT_ASSERT( ppFound != NULL && pattern != NULL && base != NULL );// 断言
if (ppFound == NULL || pattern == NULL || base == NULL)
return STATUS_INVALID_PARAMETER;

__try
{
for (ULONG_PTR i = 0; i < size - len; i++)
{
BOOLEAN found = TRUE;
for (ULONG_PTR j = 0; j < len; j++)
{
if (pattern[j] != wildcard && pattern[j] != ((PCUCHAR)base)[i + j])
{
found = FALSE;
break;
}
}

if (found != FALSE)
{
*ppFound = (PUCHAR)base + i;
return STATUS_SUCCESS;
}
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_UNHANDLED_EXCEPTION;
}

return STATUS_NOT_FOUND;
}

初始化全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/// <summary>
/// Allocate global data
/// </summary>
/// <returns>Allocated data or NULL</returns>
PGLOBAL_DATA AllocGlobalData()
{
/*
VCPU代表虚拟中央处理单元(Virtual Central Processing Unit),也称为逻辑处理器。
它是在虚拟化环境中为虚拟机创建的一种模拟的CPU。
每个虚拟机可以有一个或多个VCPU,它们负责执行虚拟机中的指令和处理计算任务。

EPT代表扩展页面表(Extended Page Table)。
EPT是Intel处理器中的一种硬件技术,
用于虚拟地址到物理地址的转换。在虚拟化环境中,EPT用于管理虚拟机中的内存访问,
实现虚拟机之间和虚拟机与宿主操作系统之间的地址隔离和保护。
EPT表存储了虚拟地址与物理地址之间的映射关系。

Page表示内存页,是计算机系统中内存管理的最小单位。在常见的架构中,一个页面的大小通常为4KB。
内存页被用于分配和管理内存,用于存储程序指令和数据。
在这段代码中,Page指的是用于存储EPT表的一块连续的内存页。
通过预分配这些页面并将页面地址存储在VCPU的EPT.Pages数组中,来管理EPT表的相关信息。
*/
PHYSICAL_ADDRESS low = { 0 }, high = { 0 };// 物理地址
high.QuadPart = MAXULONG64;// 最大的64位ULONG

ULONG cpu_count = KeQueryActiveProcessorCountEx( ALL_PROCESSOR_GROUPS );// 当前CPU数量
ULONG_PTR size = FIELD_OFFSET( GLOBAL_DATA, cpu_data ) + cpu_count * sizeof( VCPU );// 计算全局数据结构大小
// 在非分页池中分配内存,大小为size
PGLOBAL_DATA pData = (PGLOBAL_DATA)ExAllocatePoolWithTag( NonPagedPoolNx, size, HB_POOL_TAG );
if (pData == NULL)
return NULL;

RtlZeroMemory( pData, size );// 将分配的内存清零
// 在非分页池中分配内存,大小为一页,存储MSR寄存器位图
pData->MSRBitmap = ExAllocatePoolWithTag( NonPagedPoolNx, PAGE_SIZE, HB_POOL_TAG );
if (pData->MSRBitmap == NULL)
{
// 分配失败,释放空间
ExFreePoolWithTag( pData, HB_POOL_TAG );
return NULL;
}

RtlZeroMemory( pData->MSRBitmap, PAGE_SIZE );
// 获取CPU制造商信息
pData->CPUVendor = UtilCPUVendor();
// 遍历cpu_data数组,对VCPU进行初始化
for (ULONG i = 0; i < cpu_count; i++)
{
PVCPU Vcpu = &pData->cpu_data[i];
// 初始化VCPU中的页链表
InitializeListHead( &Vcpu->EPT.PageList );
// 循环预分配EPT页面
for (ULONG j = 0; j < EPT_PREALLOC_PAGES; j++)
{
// 分配一个连续页面
Vcpu->EPT.Pages[j] = MmAllocateContiguousMemorySpecifyCache( PAGE_SIZE, low, high, low, MmNonCached );
if (Vcpu->EPT.Pages[j] != NULL)
{
// 设置页面为可读写
UtilProtectNonpagedMemory( Vcpu->EPT.Pages[j], PAGE_SIZE, PAGE_READWRITE );
RtlZeroMemory( Vcpu->EPT.Pages[j], PAGE_SIZE );
}
}
}

return pData;
}

获取物理内存信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/// <summary>
/// Gather info about used physical pages
/// </summary>
/// <returns>Status code</returns>
NTSTATUS UtilQueryPhysicalMemory()
{
if (g_Data->Memory != NULL)
return STATUS_SUCCESS;

// 获取物理内存范围信息
PPHYSICAL_MEMORY_RANGE pBaseRange = MmGetPhysicalMemoryRanges();
if (pBaseRange == NULL)
return STATUS_UNSUCCESSFUL;

// 遍历链表,计算物理内存页面数目pageCount和区域数量runsCount
ULONG runsCount = 0, pageCount = 0;
for (PPHYSICAL_MEMORY_RANGE pRange = pBaseRange; pRange->NumberOfBytes.QuadPart != 0; pRange++)
{
pageCount += (ULONG)PFN( pRange->NumberOfBytes.QuadPart );
runsCount++;
}

/*
APIC(Advanced Programmable Interrupt Controller)是一种高级可编程中断控制器,用于处理中断和异常。
它是一种在计算机体系结构中实现中断控制的硬件设备。

在多核处理器系统中,每个核心都有自己的本地 APIC。本地 APIC 负责接收和处理与该核心相关的中断,并将其分发给适当的处理器。
此外,还有一个 I/O APIC,负责管理外部设备的中断信号以及将它们分发给适当的处理器。

APIC 提供了一种灵活和可扩展的方式来管理和处理中断,使得系统能够高效地响应各种设备和事件引起的中断请求。
它支持优先级、屏蔽、中断分发和处理、中断共享等功能,能够提高系统的可靠性和性能。

APIC 物理页是指用于存储 APIC 相关数据结构的物理内存页。在x86 架构的计算机系统中,
APIC 物理页通常位于物理内存的固定地址上,
并用于存储本地 APIC、I/O APIC 和其他与 APIC 相关的寄存器和数据结构。
*/

// 获取APIC物理页信息
IA32_APIC_BASE apic = { 0 };
apic.All = __readmsr( MSR_APIC_BASE );// IA32_APIC_BASE寄存器的值
runsCount += 2;// 添加到 runsCount 中

// 计算Memory需要的大小,并为全局变量分配内存
ULONG size = sizeof( PPHYSICAL_MEMORY_DESCRIPTOR ) + runsCount * sizeof( PHYSICAL_MEMORY_RUN );
g_Data->Memory = ExAllocatePoolWithTag( NonPagedPoolNx, size, HB_POOL_TAG );
if (g_Data->Memory != NULL)
{
RtlZeroMemory( g_Data->Memory, size );

// 将物理内存范围信息复制到全局变量中,设置每个范围的起始页和页面数目
g_Data->Memory->NumberOfPages = pageCount;
g_Data->Memory->NumberOfRuns = runsCount;

runsCount = 0;
for (PPHYSICAL_MEMORY_RANGE pRange = pBaseRange; pRange->BaseAddress.QuadPart != 0; pRange++, runsCount++)
{
g_Data->Memory->Run[runsCount].BasePage = PFN( pRange->BaseAddress.QuadPart );
g_Data->Memory->Run[runsCount].PageCount = PFN( pRange->NumberOfBytes.QuadPart );
}

// 添加用于存储APIC的单个页
g_Data->Memory->Run[runsCount].BasePage = apic.Fields.Apic_base;
g_Data->Memory->Run[runsCount].PageCount = 1;
// 添加从0xF0000000共计0x10000页面的保留页
g_Data->Memory->Run[runsCount + 1].BasePage = PFN( 0xF0000000 );
g_Data->Memory->Run[runsCount + 1].PageCount = 0x10000;

ExFreePool( pBaseRange );
return STATUS_SUCCESS;
}

ExFreePool( pBaseRange );
return STATUS_UNSUCCESSFUL;
}

释放全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary>
/// Free global data
/// </summary>
/// <param name="pData">Data pointer</param>
VOID FreeGlobalData( IN PGLOBAL_DATA pData )
{
if (pData == NULL)
return;

// 获取当前系统中活动处理器的数量
ULONG cpu_count = KeQueryActiveProcessorCountEx( ALL_PROCESSOR_GROUPS );
for (ULONG i = 0; i < cpu_count; i++)
{
//访问Vcpu数组成员,释放VMXON、VMCS、VMMStack和EPT.Pages数据结构内存
PVCPU Vcpu = &pData->cpu_data[i];
if (Vcpu->VMXON)
MmFreeContiguousMemory( Vcpu->VMXON );
if (Vcpu->VMCS)
MmFreeContiguousMemory( Vcpu->VMCS );
if (Vcpu->VMMStack)
MmFreeContiguousMemory( Vcpu->VMMStack );

for (ULONG j = 0; j < EPT_PREALLOC_PAGES; j++)
if (Vcpu->EPT.Pages[j] != NULL)
MmFreeContiguousMemory( Vcpu->EPT.Pages[j] );
}

// 释放物理内存和MSR位图内存
if (pData->Memory)
ExFreePoolWithTag( pData->Memory, HB_POOL_TAG );
if (pData->MSRBitmap)
ExFreePoolWithTag( pData->MSRBitmap, HB_POOL_TAG );

// 释放内存池内存
ExFreePoolWithTag( pData, HB_POOL_TAG );
}

检查CPU虚拟化特性

1
2
3
4
5
6
7
8
9
/// <summary>
/// CPU virtualization features
/// </summary>
VOID HvmCheckFeatures()
{
CPU_VENDOR vendor = UtilCPUVendor();
if (vendor == CPU_Intel)
VmxCheckFeatures();
}

检查并记录相应的CPU虚拟化特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/// <summary>
/// Check various VMX features: EPT, VPID, VMFUNC, etc.
/// </summary>
VOID VmxCheckFeatures()
{
IA32_VMX_BASIC_MSR basic = { 0 };
IA32_VMX_PROCBASED_CTLS_MSR ctl = { 0 };
IA32_VMX_PROCBASED_CTLS2_MSR ctl2 = { 0 };
IA32_VMX_EPT_VPID_CAP_MSR vpidcap = { 0 };

// True MSRs
basic.All = __readmsr( MSR_IA32_VMX_BASIC );// 读取MSR_IA32_VMX_BASIC寄存器
g_Data->Features.TrueMSRs = basic.Fields.VmxCapabilityHint;// 检查硬件直接提供的真实MSR

// Secondary control
ctl.All = __readmsr( MSR_IA32_VMX_PROCBASED_CTLS );
g_Data->Features.SecondaryControls = ctl.Fields.ActivateSecondaryControl;// 检查是否支持次级控制
/*
Secondary control(次级控制)是指在虚拟化环境中,
用于控制虚拟机对处理器部分功能的访问和使用的一组寄存器或设置。
在虚拟化技术中,虚拟机监视器(VMM)负责管理和控制虚拟机,包括对处理器的访问和配置。
为了实现安全的虚拟化环境,处理器提供了一些特殊的次级控制寄存器或标志位,
用于限制虚拟机的某些操作或行为,以防止虚拟机绕过VMM直接访问处理器的敏感功能或破坏虚拟化隔离。
*/

if (ctl.Fields.ActivateSecondaryControl)
{
//检查 EPT, VPID, VMFUNC
/*
EPT(Extended Page Tables):EPT是Intel VT-x虚拟化技术中的一项功能,
它允许在虚拟机监控程序(VMM)和虚拟机(VM)之间进行内存管理。
EPT使用了额外的页表层级,将虚拟地址转换为物理地址,并提供了更高效的内存虚拟化支持。
EPT可以提高虚拟机的性能和隔离性。

VPID(Virtual Processor Identifier):VPID是Intel VT-x虚拟化技术中的一个特性,
用于改进虚拟机切换时的TLB(Translation Lookaside Buffer)缓存效率。
每个虚拟机都被分配一个唯一的VPID,当虚拟机切换时,可以保留TLB中与新虚拟机关联的页表条目,
加速虚拟机的上下文切换。

VMFUNC(VM Functions):VMFUNC是Intel VT-x虚拟化技术中的一项功能,
它允许虚拟机中的代码调用一些特殊的处理器功能。
通过VMFUNC,虚拟机可以执行一些特权指令,包括对虚拟化扩展的访问控制、访问虚拟机控制结构等。
VMFUNC使得虚拟化软件可以更加灵活地扩展和控制虚拟机的功能。
*/
ctl2.All = __readmsr( MSR_IA32_VMX_PROCBASED_CTLS2 );
g_Data->Features.EPT = ctl2.Fields.EnableEPT;
g_Data->Features.VPID = ctl2.Fields.EnableVPID;
g_Data->Features.VMFUNC = ctl2.Fields.EnableVMFunctions;

if (ctl2.Fields.EnableEPT != 0)
{
// Execute only
vpidcap.All = __readmsr( MSR_IA32_VMX_EPT_VPID_CAP );
g_Data->Features.ExecOnlyEPT = vpidcap.Fields.ExecuteOnly;
g_Data->Features.InvSingleAddress = vpidcap.Fields.IndividualAddressInvVpid;

if (vpidcap.Fields.ExecuteOnly == 0)
DPRINT( "HyperBone: CPU %d: %s: No execute-only EPT translation support\n", CPU_IDX, __FUNCTION__ );
}
else
DPRINT( "HyperBone: CPU %d: %s: No EPT/VPID support\n", CPU_IDX, __FUNCTION__ );
}
else
DPRINT( "HyperBone: CPU %d: %s: No secondary contol support\n", CPU_IDX, __FUNCTION__ );
}

HVM与VMX

VMX代表虚拟机扩展(Virtual Machine Extensions),是Intel处理器提供的硬件虚拟化技术。通过VMX,处理器可以在同一个物理主机上同时运行多个虚拟机,每个虚拟机都能拥有自己的操作系统和应用程序,并且相互之间是隔离的。

HVM代表硬件辅助虚拟化(Hardware-assisted Virtualization),是一种虚拟化技术。它利用处理器中的硬件特性来提高虚拟化的性能和效率。HVM通常与VMX结合使用,以实现更快速、更可靠的虚拟化环境。

简而言之,VMX指的是Intel处理器提供的硬件虚拟化技术,而HVM则是一种利用硬件辅助虚拟化的技术。这两者都是为了实现高效、安全的虚拟化环境而存在的。

开启虚拟化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <summary>
/// Virtualize each CPU
/// </summary>
/// <returns>Status code</returns>
NTSTATUS StartHV()
{
// Unknown CPU
if (g_Data->CPUVendor == CPU_Other)
return STATUS_NOT_SUPPORTED;// 不支持

KeGenericCallDpc( HvmpHVCallbackDPC, (PVOID)__readcr3() );// 读取CR3寄存器的值

// Some CPU failed
ULONG count = KeQueryActiveProcessorCountEx( ALL_PROCESSOR_GROUPS );// 查询活动处理器的数量
if (count != (ULONG)g_Data->vcpus)// 活动处理器的数量不等于虚拟处理器的数量
{// 有处理器失败,停止虚拟化并返回失败
DPRINT( "HyperBone: CPU %d: %s: Some CPU failed to subvert\n", CPU_IDX, __FUNCTION__ );
StopHV();
return STATUS_UNSUCCESSFUL;
}

return STATUS_SUCCESS;
}

DPC回调进行虚拟化处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
VOID HvmpHVCallbackDPC( PRKDPC Dpc, PVOID Context, PVOID SystemArgument1, PVOID SystemArgument2 )
{
/*
DPC(Deferred Procedure Call)是一种延迟执行的过程调用机制,
用于在计算机系统的中断或系统事件处理过程完成后,延迟执行一些需要高优先级处理的任务或回调函数。
DPC的执行是在系统抢占模式下进行的,因此可以保证其执行的稳定性和一致性。
使用DPC可以实现一些延迟处理的任务,例如缓冲区刷新、资源回收、数据处理等。
通过将这些任务置于DPC中执行,可以避免阻塞正常的中断处理程序或系统服务例程,提高系统的效率和响应性能。
*/
UNREFERENCED_PARAMETER( Dpc );// Dpc是一个未使用的参数
PVCPU pVCPU = &g_Data->cpu_data[CPU_IDX];// 获取当前处理器的虚拟处理器

// Check if we are loading, or unloading
if (ARGUMENT_PRESENT( Context ))// 检测Context是否被传递,通过Context参数判断是加载还是卸载
{
// 执行对应的初始化操作
g_Data->CPUVendor == CPU_Intel ? IntelSubvertCPU( pVCPU, Context ) : AMDSubvertCPU( pVCPU, Context );
}
else
{
// 恢复虚拟处理器
g_Data->CPUVendor == CPU_Intel ? IntelRestoreCPU( pVCPU ) : AMDRestoreCPU( pVCPU );
}

// 等待所有DPC同步,传入同步信号量
KeSignalCallDpcSynchronize( SystemArgument2 );

// 标记DPC执行完成,传入完成信号量
KeSignalCallDpcDone( SystemArgument1 );
}

初始化IntelCPU

1
2
3
4
5
6
// HVM.c
// Vendor-specific calls
inline VOID IntelSubvertCPU( IN PVCPU Vcpu, IN PVOID SystemDirectoryTableBase )
{
VmxInitializeCPU( Vcpu, (ULONG64)SystemDirectoryTableBase );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// VMX.c
/// <summary>
/// Virtualize LP 初始化逻辑处理器
/// </summary>
/// <param name="Vcpu">Virtual CPU data</param>
/// <param name="SystemDirectoryTableBase">Kernel CR3</param>
VOID VmxInitializeCPU( IN PVCPU Vcpu, IN ULONG64 SystemDirectoryTableBase )
{
/*
存储处理器的休眠状态,其中包含所有特殊寄存器和MSR(模型特定寄存器),
VMCS(虚拟机控制结构)将需要这些作为其设置的一部分。这样可以避免使用汇编序列
并手动读取这些数据。
*/
/*
KeSaveStateForHibernate是Windows内核函数,用于保存处理器的状态以进行休眠操作。

在Windows系统中,当计算机进入休眠状态时,操作系统需要保存处理器的当前状态,
包括寄存器、程序计数器(指令指针)以及其他关键的执行上下文信息。
这样,在计算机重新唤醒时,系统可以从先前保存的状态中恢复,并继续执行之前的操作。

KeSaveStateForHibernate函数的作用就是将当前处理器的状态保存到内存中,
以便后续的休眠操作使用。它会将所有的特殊寄存器、模型特定寄存器、以及一般寄存器的值保存起来,
确保在休眠期间数据不会丢失。具体而言,该函数会将处理器状态保存到与休眠操作相关的数据结构中。

这个函数通常由操作系统内核在准备进入休眠状态之前调用,以便将当前的处理器状态保存下来。
然后,在计算机唤醒时,操作系统可以使用相应的函数来还原保存的状态,以实现无缝的恢复。
*/
KeSaveStateForHibernate( &Vcpu->HostState );// 启动之前确保可以保存现有处理器状态,确保在虚拟化期间数据不会丢失

/*
捕获整个寄存器状态。因为一旦启动虚拟机,它将从定义的客户机指令指针处开始执行,
该指针会在此调用中被捕获。换句话说,我们将返回到我们的原始位置,但由于VMCS/VMX初始化代码的影响,
我们的寄存器都被破坏了(因为客户机状态不包括寄存器状态)。通过在这里保存上下文,
包括所有通用寄存器,我们保证返回时我们也能恢复我们的起始寄存器值
*/
RtlCaptureContext( &Vcpu->HostState.ContextFrame );

/*
根据上述情况,在这里可以判断虚拟机实际上是否已启动。
我们可以通过验证VmxEnabled字段的值来检查这一点,该字段在执行VMXLAUNCH之前设置为1。
在此函数中,我们不使用Data参数或任何其他局部寄存器,并且事实上VmxEnabled是不确定的,
因为根据上述情况,由于VMCALL本身,我们的寄存器状态当前是脏的。
通过结合全局变量和API调用使用它,我们还确保编译器不会以任何方式优化此访问,
即使在LTGC/Ox构建上也是如此。
*/
if (g_Data->cpu_data[CPU_IDX].VmxState == VMX_STATE_TRANSITION)
{
/*
表示虚拟机已启动,因此即将将GPR(通用寄存器)还原为其原始值。
这将使我们再次回到上一行代码,但是这次VmxEnabled的值将为2,绕过if和else if检查。
*/
g_Data->cpu_data[CPU_IDX].VmxState = VMX_STATE_ON;

/*
最后,恢复上下文,以便最终恢复所有寄存器和堆栈状态。通过继续以这种方式引用每个VP数据,
编译器将继续生成非优化的访问,确保不会破坏先前的寄存器状态。
*/
VmRestoreContext( &g_Data->cpu_data[CPU_IDX].HostState.ContextFrame );
}
/*
尚未尝试启动虚拟机,也没有启动它。换句话说,
这是第一次调用VmxInitializeCPU。因此,我们可以自由地使用所有寄存器状态。
*/
else if (g_Data->cpu_data[CPU_IDX].VmxState == VMX_STATE_OFF)
{
/*
首先,捕获SYSTEM进程的PML4(页目录表)的值,以便无论当前LP中断哪个进程,
所有虚拟处理器都可以共享正确的内核地址空间。
*/
/*
在x86-64架构下,操作系统内核和用户进程共享同一个物理地址空间。为了实现这个地址空间共享,
x86-64架构定义了一个层次化的虚拟内存地址转换机制,这个机制包括页表、页目录表和页全局目录等数据结构。
其中,页全局目录是整个地址空间的根节点,每个进程都有一个自己的页全局目录。
页目录表用来描述一个进程的用户地址空间,
而具有特殊权限的内核空间也需要一个页目录表来映射内核地址空间。
"捕获SYSTEM进程的PML4(页目录表)的值",
实际上就是获取当前系统的内核页目录表的物理地址,以便后续的操作可以正确地访问内核的地址空间。

由于LP调度器会将不同进程调度到不同的逻辑处理器上执行,
为了保证多个逻辑处理器之间能够正确地共享内核地址空间,
必须要使所有的虚拟处理器共享同一个内核页目录表。
因此,在运行任意一个进程之前,需要将其对应的页目录表设置为内核页目录表的地址,即代码中所描述的部分。
以保证该进程可以正确地访问内核地址空间。这可以通过在LP上下文切换时,
将当前进程的页目录表设置为系统内核页目录表的物理地址来实现。
*/
Vcpu->SystemDirectoryTableBase = SystemDirectoryTableBase;

// 尝试在该处理器上初始化VMX
VmxSubvertCPU( Vcpu );
}
}

从 CONTEXT 结构体中恢复寄存器上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
VmRestoreContext PROC
; 与RtlCaptureContext()兼容。使用此函数代替RtlRestoreContext()函数,以便在Win10 15063+版本上不会由于RSP检查而导致蓝屏。
push rbp
push rsi
push rdi
sub rsp, 30h
mov rbp, rsp
movaps xmm0, xmmword ptr [rcx+1A0h]
movaps xmm1, xmmword ptr [rcx+1B0h]
movaps xmm2, xmmword ptr [rcx+1C0h]
movaps xmm3, xmmword ptr [rcx+1D0h]
movaps xmm4, xmmword ptr [rcx+1E0h]
movaps xmm5, xmmword ptr [rcx+1F0h]
movaps xmm6, xmmword ptr [rcx+200h]
movaps xmm7, xmmword ptr [rcx+210h]
movaps xmm8, xmmword ptr [rcx+220h]
movaps xmm9, xmmword ptr [rcx+230h]
movaps xmm10, xmmword ptr [rcx+240h]
movaps xmm11, xmmword ptr [rcx+250h]
movaps xmm12, xmmword ptr [rcx+260h]
movaps xmm13, xmmword ptr [rcx+270h]
movaps xmm14, xmmword ptr [rcx+280h]
movaps xmm15, xmmword ptr [rcx+290h]
ldmxcsr dword ptr [rcx+34h]

mov ax, [rcx+42h]
mov [rsp+20h], ax
mov rax, [rcx+98h] ; RSP
mov [rsp+18h], rax
mov eax, [rcx+44h]
mov [rsp+10h], eax
mov ax, [rcx+38h]
mov [rsp+08h], ax
mov rax, [rcx+0F8h] ; RIP
mov [rsp+00h], rax ; set RIP as return address (for iretq instruction).

mov rax, [rcx+78h]
mov rdx, [rcx+88h]
mov r8, [rcx+0B8h]
mov r9, [rcx+0C0h]
mov r10, [rcx+0C8h]
mov r11, [rcx+0D0h]
cli
mov rbx, [rcx+90h]
mov rsi, [rcx+0A8h]
mov rdi, [rcx+0B0h]
mov rbp, [rcx+0A0h]
mov r12, [rcx+0D8h]
mov r13, [rcx+0E0h]
mov r14, [rcx+0E8h]
mov r15, [rcx+0F0h]
mov rcx, [rcx+80h]
iretq

VmRestoreContext ENDP

使用Intel虚拟化技术启动虚拟机,创建虚拟CPU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/// <summary>
/// Execute VMLAUNCH
/// </summary>
/// <param name="Vcpu">Virtyal CPU data</param>
VOID VmxSubvertCPU( IN PVCPU Vcpu )
{
PHYSICAL_ADDRESS phys = { 0 };
phys.QuadPart = MAXULONG64;

// 初始化所有与VMX相关的MSR,通过读取它们的值来初始化
for (ULONG i = 0; i <= VMX_MSR( MSR_IA32_VMX_VMCS_ENUM ); i++)
Vcpu->MsrData[i].QuadPart = __readmsr( MSR_IA32_VMX_BASIC + i );

// 根据系统支持的功能,初始化次级控制(secondary controls)、真实模型专用寄存器(true MSRs)和VMFUNC
// 见VmxCheckFeatures()
if (g_Data->Features.SecondaryControls)
Vcpu->MsrData[VMX_MSR( MSR_IA32_VMX_PROCBASED_CTLS2 )].QuadPart = __readmsr( MSR_IA32_VMX_PROCBASED_CTLS2 );

// True MSRs, if present
if (g_Data->Features.TrueMSRs)
for (ULONG i = VMX_MSR( MSR_IA32_VMX_TRUE_PINBASED_CTLS ); i <= VMX_MSR( MSR_IA32_VMX_TRUE_ENTRY_CTLS ); i++)
Vcpu->MsrData[i].QuadPart = __readmsr( MSR_IA32_VMX_BASIC + i );

// VMFUNC, if present
if(g_Data->Features.VMFUNC)
Vcpu->MsrData[VMX_MSR( MSR_IA32_VMX_VMFUNC )].QuadPart = __readmsr( MSR_IA32_VMX_VMFUNC );

// 分配VMXON、VMCS和VMM堆栈内存
Vcpu->VMXON = MmAllocateContiguousMemory( sizeof( VMX_VMCS ), phys );
Vcpu->VMCS = MmAllocateContiguousMemory( sizeof( VMX_VMCS ), phys );
Vcpu->VMMStack = MmAllocateContiguousMemory( KERNEL_STACK_SIZE, phys );

if (!Vcpu->VMXON || !Vcpu->VMCS || !Vcpu->VMMStack)
{
DPRINT( "HyperBone: CPU %d: %s: Failed to allocate memory\n", CPU_IDX, __FUNCTION__ );
goto failed;
}

// 进行保护设置
UtilProtectNonpagedMemory( Vcpu->VMXON, sizeof( VMX_VMCS ), PAGE_READWRITE );
UtilProtectNonpagedMemory( Vcpu->VMCS, sizeof( VMX_VMCS ), PAGE_READWRITE );
UtilProtectNonpagedMemory( Vcpu->VMMStack, KERNEL_STACK_SIZE, PAGE_READWRITE );

// 初始化内存空间
RtlZeroMemory( Vcpu->VMXON, sizeof( VMX_VMCS ) );
RtlZeroMemory( Vcpu->VMCS, sizeof( VMX_VMCS ) );
RtlZeroMemory( Vcpu->VMMStack, KERNEL_STACK_SIZE );

// 尝试进入VMX Root模式
if (VmxEnterRoot( Vcpu ))
{
// I继续配置VMCS虚拟机控制结构(包括guest和host状态)
/*
VMCS是是一种数据结构,用于存储虚拟机的状态信息,如处理器寄存器、段描述符、中断描述符等等。
VMCS包含了虚拟机执行环境的所有状态信息,是虚拟机管理软件和硬件虚拟化扩展之间的接口。
VMCS中有两种状态:guest状态和host状态。

guest状态是指在虚拟机内执行的代码所使用的状态。
当虚拟化软件将控制权转移到虚拟机内部时,处理器会使用VMCS中的guest状态信息来切换到虚拟机的执行环境。
guest状态包含了虚拟机内部的所有状态信息,如处理器寄存器、页表、分段描述符等等。

host状态是指在虚拟机外执行的代码所使用的状态。当虚拟机执行完成后,
处理器需要恢复到虚拟化软件的执行环境中。
VMCS中的host状态信息就提供了这个执行环境所需要的状态信息,如处理器寄存器、页表、分段描述符等等。

在VMCS中,不同类型的状态信息被分为不同的区域,
如控制区、guest状态区、host状态区等等。通过对VMCS的设置,虚拟化软件可以控制虚拟机的行为,
如内存地址翻译、中断处理等等。
*/
VmxSetupVMCS( Vcpu );

// 配置EPT
if(g_Data->Features.EPT)
{
if (!NT_SUCCESS( EptBuildIdentityMap( &Vcpu->EPT ) ))
{
DPRINT( "HyperBone: CPU %d: %s: Failed to build EPT identity map\n", CPU_IDX, __FUNCTION__ );
goto failedvmxoff;
}

EptEnable( Vcpu->EPT.PML4Ptr );
}

// 记录VMX已开启
Vcpu->VmxState = VMX_STATE_TRANSITION;

// 在VmxSetupVmcs函数中设置了各种VMCS字段(VMCS fields),
// 让处理器跳转到VmxInitializeCPU中调用VmxSetupVmcs的RtlCaptureContext函数的返回地址。
/*
RtlCaptureContext是一个Windows操作系统提供的函数,用于获取当前线程的上下文信息,
包括寄存器的值和程序计数器的值。
通过将处理器的控制流转移到RtlCaptureContext的返回地址,
可以在VmxInitializeCPU函数中恢复处理器的状态,继续执行后续的代码逻辑。
这样做的目的可能是为了在VmxInitializeCPU函数中进行额外的处理或记录虚拟机的初始化状态。
*/
// 原子自增操作,意味着有一个虚拟CPU被创建或启动。该函数确保在多线程环境下,对变量进行原子操作,避免了竞争条件。
InterlockedIncrement( &g_Data->vcpus );
int res = __vmx_vmlaunch();// 启动虚拟机

// 原子自减操作,表示一个虚拟CPU的执行已经结束或销毁。
InterlockedDecrement( &g_Data->vcpus );
/* 这两个函数有c++11新特性atomic,详见文档 */

/*
如果程序执行到了这里,那么可能存在两种情况:VMCS设置失败,或者启动操作没有按照计划进行。
无论是哪种情况,由于VmxEnabled没有被设置为1,都代表失败的情况。
*/
Vcpu->VmxState = VMX_STATE_OFF;

DPRINT( "HyperBone: CPU %d: %s: __vmx_vmlaunch failed with result %d\n", CPU_IDX, __FUNCTION__, res );

failedvmxoff:
__vmx_off();// 关闭VMX
}

failed:;// 失败处理 释放内存
if (Vcpu->VMXON)
MmFreeContiguousMemory( Vcpu->VMXON );
if (Vcpu->VMCS)
MmFreeContiguousMemory( Vcpu->VMCS );
if (Vcpu->VMMStack)
MmFreeContiguousMemory( Vcpu->VMMStack );

Vcpu->VMXON = NULL;
Vcpu->VMCS = NULL;
Vcpu->VMMStack = NULL;
}
设置保护
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary>
/// Change protection of nonpaged system address
/// </summary>
/// <param name="ptr">Address</param>
/// <param name="size">Size of region</param>
/// <param name="protection">New protection flags</param>
/// <returns>Status code</returns>
NTSTATUS UtilProtectNonpagedMemory( IN PVOID ptr, IN ULONG64 size, IN ULONG protection )
{
/*
MDL是Windows内核中的数据结构,Memory Descriptor List(内存描述列表)。
它用于描述一个或多个连续物理内存页的详细信息。
在Windows操作系统中,内存管理模块使用MDL来跟踪和管理内存分配、映射和保护。
MDL通常与虚拟内存映射、物理内存页面和驱动程序之间的交互密切相关。

MDL的主要字段包括:
StartVa:指向MDL所描述的内存区域的起始虚拟地址。
ByteCount:描述MDL涵盖的内存区域的总字节数。
MappedSystemVa:如果MDL中的内存区域被映射到了系统地址空间,则该字段表示映射后的虚拟地址;否则为NULL。
Process:指向拥有该MDL的进程对象的指针。
Flags:用于标识MDL的一些特性,如是否映射到系统地址空间、是否是锁定的等。
驱动程序可以通过创建和操作MDL来管理内存。
*/
NTSTATUS status = STATUS_SUCCESS;
PMDL pMdl = IoAllocateMdl( ptr, (ULONG)size, FALSE, FALSE, NULL );// 创建一个包含给定地址的MDL
if (pMdl)
{
MmBuildMdlForNonPagedPool( pMdl );// 将这个MDL描述的物理内存区域与非分页池相关联
pMdl->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA;// 该MDL描述的虚拟地址映射到了内核的虚拟地址空间中
status = MmProtectMdlSystemAddress( pMdl, protection );// 在MDL中描述的物理内存区域的保护属性设置为给定的属性
IoFreeMdl( pMdl );// 释放MDL
return status;
}

return STATUS_UNSUCCESSFUL;
}
启动Vmx Root模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/// <summary>
/// Switch CPU to root mode
/// </summary>
/// <param name="Vcpu">Virtual CPU data</param>
/// <returns>TRUE on success</returns>
BOOLEAN VmxEnterRoot( IN PVCPU Vcpu )
{
// 将CPU切换到root模式,虚拟机监视器VMM运行的特权级别最高的模式,用于管理和控制虚拟机的执行
PKSPECIAL_REGISTERS Registers = &Vcpu->HostState.SpecialRegisters;// 主机特殊寄存器
PIA32_VMX_BASIC_MSR pBasic = (PIA32_VMX_BASIC_MSR)&Vcpu->MsrData[VMX_MSR( MSR_IA32_VMX_BASIC )];// VMX基本MSR

// 检查虚拟机控制结构VMCS是否能够适应单个页面
if (pBasic->Fields.RegionSize > PAGE_SIZE)
{
DPRINT( "HyperBone: CPU %d: %s: VMCS region doesn't fit into one page\n", CPU_IDX, __FUNCTION__ );
return FALSE;
}

// 检查VMCS所需的内存类型是否为写回缓存类型
if (pBasic->Fields.MemoryType != VMX_MEM_TYPE_WRITEBACK)
{
DPRINT( "HyperBone: CPU %d: %s: Unsupported memory type\n", CPU_IDX, __FUNCTION__ );
return FALSE;
}

// 检查是否支持使用真实MSR获取虚拟化功能信息
if (pBasic->Fields.VmxCapabilityHint == 0)
{
DPRINT( "HyperBone: CPU %d: %s: No true MSR support\n", CPU_IDX, __FUNCTION__ );
return FALSE;
}

// 保存VMXON、VMCS的修订号
/*
在硬件层面,VMXON指令用于激活处理器的虚拟机扩展(Intel VT-x或AMD-V),这是一种硬件虚拟化技术,
允许多个虚拟机同时运行,并将它们隔离开来。VMXON指令的执行会初始化虚拟机相关的数据结构,
并将处理器的运行模式从非虚拟化模式切换到虚拟化模式。

在虚拟化环境中,VMXON指令通常由虚拟机监视器(VMM)或Hypervisor在启动虚拟机时调用。
它必须在特权级别最高的模式下执行,通常是在操作系统内核中。
执行VMXON指令后,处理器将转入根模式,VMM可以通过VMCS(虚拟机控制结构)对虚拟机进行管理和控制。
*/
Vcpu->VMXON->RevisionId = pBasic->Fields.RevisionIdentifier;
Vcpu->VMCS->RevisionId = pBasic->Fields.RevisionIdentifier;

// 根据MSR_IA32_VMX_CR0_FIXED0和MSR_IA32_VMX_CR0_FIXED1的要求修正CR0寄存器
Registers->Cr0 &= Vcpu->MsrData[VMX_MSR( MSR_IA32_VMX_CR0_FIXED1 )].LowPart;
Registers->Cr0 |= Vcpu->MsrData[VMX_MSR( MSR_IA32_VMX_CR0_FIXED0 )].LowPart;

// 根据MSR_IA32_VMX_CR4_FIXED0和MSR_IA32_VMX_CR4_FIXED1的要求修正CR4寄存器
Registers->Cr4 &= Vcpu->MsrData[VMX_MSR( MSR_IA32_VMX_CR4_FIXED1 )].LowPart;
Registers->Cr4 |= Vcpu->MsrData[VMX_MSR( MSR_IA32_VMX_CR4_FIXED0 )].LowPart;

// 提交r0、r4寄存器的更新
__writecr0( Registers->Cr0 );
__writecr4( Registers->Cr4 );

// 开启VMX root模式
PHYSICAL_ADDRESS phys = MmGetPhysicalAddress( Vcpu->VMXON );
int res = __vmx_on( (PULONG64)&phys );
if (res)
{
DPRINT( "HyperBone: CPU %d: %s: __vmx_on failed with status %d\n", CPU_IDX, __FUNCTION__, res );
return FALSE;
}

// 清除VMCS, 修改状态为Inactive
phys = MmGetPhysicalAddress( Vcpu->VMCS );
if (__vmx_vmclear( (PULONG64)&phys ))
{
DPRINT( "HyperBone: CPU %d: %s: __vmx_vmclear failed\n", CPU_IDX, __FUNCTION__ );
return FALSE;
}

// 加载VMCS, 设置状态为Active
if (__vmx_vmptrld( (PULONG64)&phys ))
{
DPRINT( "HyperBone: CPU %d: %s: __vmx_vmptrld failed\n", CPU_IDX, __FUNCTION__ );
return FALSE;
}

// VMX Root模式启动, 以及一个激活的VMCS.
return TRUE;
}
设置VMCS字段

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/// <summary>
/// Setup VMCS fields
/// </summary>
/// <param name="VpData">Virtual CPU data</param>
VOID VmxSetupVMCS( IN PVCPU VpData )
{
PKPROCESSOR_STATE state = &VpData->HostState;
VMX_GDTENTRY64 vmxGdtEntry = { 0 };
VMX_VM_ENTER_CONTROLS vmEnterCtlRequested = { 0 };
VMX_VM_EXIT_CONTROLS vmExitCtlRequested = { 0 };
VMX_PIN_BASED_CONTROLS vmPinCtlRequested = { 0 };
VMX_CPU_BASED_CONTROLS vmCpuCtlRequested = { 0 };
VMX_SECONDARY_CPU_BASED_CONTROLS vmCpuCtl2Requested = { 0 };

/*
Hypervisor(超级监控程序)是一种虚拟化技术的关键组成部分。
它是在物理计算机上创建和管理虚拟机(Virtual Machine,VM)的软件或固件。
Hypervisor 在物理硬件和虚拟机之间充当中间层,允许多个虚拟机同时在同一台物理机上运行,
并提供虚拟机对硬件资源的访问。

Hypervisor 有两种主要类型:
直接运行在物理硬件上,它可以直接管理物理资源和虚拟机实例,将物理资源划分给各个虚拟机,
并在它们之间进行调度和隔离。常见的类型 1 Hypervisor 包括 VMware ESXi、Microsoft Hyper-V 和 Xen。
作为一个应用程序在操作系统之上运行。它需要一个宿主操作系统提供硬件访问和资源管理功能,
然后在其上创建和管理虚拟机。常见的类型 2 Hypervisor 包括 Oracle VirtualBox 和 VMware Workstation。
*/

// 进入控制字段,虚拟机在进入hypervisor时确保以x64模式进入
vmEnterCtlRequested.Fields.IA32eModeGuest = TRUE;

// 退出控制字段,虚拟机退出hypervisor时,确保以x64模式退出
vmExitCtlRequested.Fields.AcknowledgeInterruptOnExit = TRUE;
vmExitCtlRequested.Fields.HostAddressSpaceSize = TRUE;

// 虚拟机CPU控制字段,启用MSR位图,激活次级控制和CR3退出,在CR3改变时无效VPID缓存
vmCpuCtlRequested.Fields.UseMSRBitmaps = TRUE;
vmCpuCtlRequested.Fields.ActivateSecondaryControl = TRUE;
//vmCpuCtlRequested.Fields.UseTSCOffseting = TRUE;
//vmCpuCtlRequested.Fields.RDTSCExiting = TRUE;

// VPID caches must be invalidated on CR3 change
if(g_Data->Features.VPID)
vmCpuCtlRequested.Fields.CR3LoadExiting = TRUE;

/*
在虚拟机中启用对于 RDTSCP 和 XSAVES/XRESTORES 指令的支持。
在 Windows 10 系统中,如果 CPU 支持这些指令,操作系统会使用它们来提高性能。
通过使用 VmxpAdjustMsr 函数,如果处理器不支持这些指令,将会忽略对这些选项的设置。
这样可以确保在不支持这些指令的处理器上仍然可以正常运行虚拟机监控程序,
而不会因为这些指令的不支持而导致错误。
*/
vmCpuCtl2Requested.Fields.EnableRDTSCP = TRUE;
vmCpuCtl2Requested.Fields.EnableXSAVESXSTORS = TRUE;

// Begin by setting the link pointer to the required value for 4KB VMCS.
// 从设置VMCS的链接指针字段开始,设置为4KB VMCS所需的值
__vmx_vmwrite( VMCS_LINK_POINTER, MAXULONG64 );
// 将VMCS需要的各个控制字段写入VMCS
__vmx_vmwrite(
PIN_BASED_VM_EXEC_CONTROL,
VmxpAdjustMsr( VpData->MsrData[VMX_MSR( MSR_IA32_VMX_TRUE_PINBASED_CTLS )], vmPinCtlRequested.All )
);
__vmx_vmwrite(
CPU_BASED_VM_EXEC_CONTROL,
VmxpAdjustMsr( VpData->MsrData[VMX_MSR( MSR_IA32_VMX_TRUE_PROCBASED_CTLS )], vmCpuCtlRequested.All )
);
__vmx_vmwrite(
SECONDARY_VM_EXEC_CONTROL,
VmxpAdjustMsr( VpData->MsrData[VMX_MSR( MSR_IA32_VMX_PROCBASED_CTLS2 )], vmCpuCtl2Requested.All )
);
__vmx_vmwrite(
VM_EXIT_CONTROLS,
VmxpAdjustMsr( VpData->MsrData[VMX_MSR( MSR_IA32_VMX_TRUE_EXIT_CTLS )], vmExitCtlRequested.All )
);
__vmx_vmwrite(
VM_ENTRY_CONTROLS,
VmxpAdjustMsr( VpData->MsrData[VMX_MSR( MSR_IA32_VMX_TRUE_ENTRY_CTLS )], vmEnterCtlRequested.All )
);

// 加载MSR位图
PUCHAR bitMapReadLow = g_Data->MSRBitmap; // 0x00000000 - 0x00001FFF
PUCHAR bitMapReadHigh = bitMapReadLow + 1024; // 0xC0000000 - 0xC0001FFF

RTL_BITMAP bitMapReadLowHeader = { 0 };
RTL_BITMAP bitMapReadHighHeader = { 0 };
RtlInitializeBitMap( &bitMapReadLowHeader, (PULONG)bitMapReadLow, 1024 * 8 );
RtlInitializeBitMap( &bitMapReadHighHeader, (PULONG)bitMapReadHigh, 1024 * 8 );
// 设置需要捕获的MSR
RtlSetBit( &bitMapReadLowHeader, MSR_IA32_FEATURE_CONTROL ); // MSR_IA32_FEATURE_CONTROL
RtlSetBit( &bitMapReadLowHeader, MSR_IA32_DEBUGCTL ); // MSR_DEBUGCTL
RtlSetBit( &bitMapReadHighHeader, MSR_LSTAR - 0xC0000000 ); // MSR_LSTAR

// VMX MSRs
for (ULONG i = MSR_IA32_VMX_BASIC; i <= MSR_IA32_VMX_VMFUNC; i++)
RtlSetBit( &bitMapReadLowHeader, i );

__vmx_vmwrite( MSR_BITMAP, MmGetPhysicalAddress( g_Data->MSRBitmap ).QuadPart );

// 设置异常位图 Exception bitmap,捕获断点异常
ULONG ExceptionBitmap = 0;
//ExceptionBitmap |= 1 << VECTOR_DEBUG_EXCEPTION;
ExceptionBitmap |= 1 << VECTOR_BREAKPOINT_EXCEPTION;

__vmx_vmwrite( EXCEPTION_BITMAP, ExceptionBitmap );

// CS (Ring 0 Code)
// 代码段描述符中包含了代码段的基地址和大小,并且还包含了一些权限信息。CS 寄存器存放的是代码段描述符在 GDT 表中的索引(Selector)。
VmxpConvertGdtEntry( state->SpecialRegisters.Gdtr.Base, state->ContextFrame.SegCs, &vmxGdtEntry );
__vmx_vmwrite( GUEST_CS_SELECTOR, vmxGdtEntry.Selector );
__vmx_vmwrite( GUEST_CS_LIMIT, vmxGdtEntry.Limit );
__vmx_vmwrite( GUEST_CS_AR_BYTES, vmxGdtEntry.AccessRights );
__vmx_vmwrite( GUEST_CS_BASE, vmxGdtEntry.Base );
__vmx_vmwrite( HOST_CS_SELECTOR, state->ContextFrame.SegCs & ~RPL_MASK );

// SS (Ring 0 Data)
// 栈段描述符中包含了栈段的基地址和大小,并且还包含了一些权限信息。SS 寄存器存放的是栈段描述符在 GDT 表中的索引(Selector)。
VmxpConvertGdtEntry( state->SpecialRegisters.Gdtr.Base, state->ContextFrame.SegSs, &vmxGdtEntry );
__vmx_vmwrite( GUEST_SS_SELECTOR, vmxGdtEntry.Selector );
__vmx_vmwrite( GUEST_SS_LIMIT, vmxGdtEntry.Limit );
__vmx_vmwrite( GUEST_SS_AR_BYTES, vmxGdtEntry.AccessRights );
__vmx_vmwrite( GUEST_SS_BASE, vmxGdtEntry.Base );
__vmx_vmwrite( HOST_SS_SELECTOR, state->ContextFrame.SegSs & ~RPL_MASK );

// DS (Ring 3 Data)
// 数据段描述符中包含了数据段的基地址和大小,并且还包含了一些权限信息。DS、ES、FS、GS 寄存器分别存放数据段描述符在 GDT 表中的索引(Selector)。
VmxpConvertGdtEntry( state->SpecialRegisters.Gdtr.Base, state->ContextFrame.SegDs, &vmxGdtEntry );
__vmx_vmwrite( GUEST_DS_SELECTOR, vmxGdtEntry.Selector );
__vmx_vmwrite( GUEST_DS_LIMIT, vmxGdtEntry.Limit );
__vmx_vmwrite( GUEST_DS_AR_BYTES, vmxGdtEntry.AccessRights );
__vmx_vmwrite( GUEST_DS_BASE, vmxGdtEntry.Base );
__vmx_vmwrite( HOST_DS_SELECTOR, state->ContextFrame.SegDs & ~RPL_MASK );

// ES (Ring 3 Data)
VmxpConvertGdtEntry( state->SpecialRegisters.Gdtr.Base, state->ContextFrame.SegEs, &vmxGdtEntry );
__vmx_vmwrite( GUEST_ES_SELECTOR, vmxGdtEntry.Selector );
__vmx_vmwrite( GUEST_ES_LIMIT, vmxGdtEntry.Limit );
__vmx_vmwrite( GUEST_ES_AR_BYTES, vmxGdtEntry.AccessRights );
__vmx_vmwrite( GUEST_ES_BASE, vmxGdtEntry.Base );
__vmx_vmwrite( HOST_ES_SELECTOR, state->ContextFrame.SegEs & ~RPL_MASK );

// FS (Ring 3 Compatibility-Mode TEB)
VmxpConvertGdtEntry( state->SpecialRegisters.Gdtr.Base, state->ContextFrame.SegFs, &vmxGdtEntry );
__vmx_vmwrite( GUEST_FS_SELECTOR, vmxGdtEntry.Selector );
__vmx_vmwrite( GUEST_FS_LIMIT, vmxGdtEntry.Limit );
__vmx_vmwrite( GUEST_FS_AR_BYTES, vmxGdtEntry.AccessRights );
__vmx_vmwrite( GUEST_FS_BASE, vmxGdtEntry.Base );
__vmx_vmwrite( HOST_FS_BASE, vmxGdtEntry.Base );
__vmx_vmwrite( HOST_FS_SELECTOR, state->ContextFrame.SegFs & ~RPL_MASK );

// GS (Ring 3 Data if in Compatibility-Mode, MSR-based in Long Mode)
VmxpConvertGdtEntry( state->SpecialRegisters.Gdtr.Base, state->ContextFrame.SegGs, &vmxGdtEntry );
__vmx_vmwrite( GUEST_GS_SELECTOR, vmxGdtEntry.Selector );
__vmx_vmwrite( GUEST_GS_LIMIT, vmxGdtEntry.Limit );
__vmx_vmwrite( GUEST_GS_AR_BYTES, vmxGdtEntry.AccessRights );
__vmx_vmwrite( GUEST_GS_BASE, state->SpecialRegisters.MsrGsBase );
__vmx_vmwrite( HOST_GS_BASE, state->SpecialRegisters.MsrGsBase );
__vmx_vmwrite( HOST_GS_SELECTOR, state->ContextFrame.SegGs & ~RPL_MASK );

// Task Register (Ring 0 TSS)
// 存放任务状态段 (TSS) 的选择符,在进行任务切换时使用。
VmxpConvertGdtEntry( state->SpecialRegisters.Gdtr.Base, state->SpecialRegisters.Tr, &vmxGdtEntry );
__vmx_vmwrite( GUEST_TR_SELECTOR, vmxGdtEntry.Selector );
__vmx_vmwrite( GUEST_TR_LIMIT, vmxGdtEntry.Limit );
__vmx_vmwrite( GUEST_TR_AR_BYTES, vmxGdtEntry.AccessRights );
__vmx_vmwrite( GUEST_TR_BASE, vmxGdtEntry.Base );
__vmx_vmwrite( HOST_TR_BASE, vmxGdtEntry.Base );
__vmx_vmwrite( HOST_TR_SELECTOR, state->SpecialRegisters.Tr & ~RPL_MASK );

// LDT
// 进程的局部任务表描述符(LDT),存放 LDT 描述符在 GDT 表中的索引(Selector)。
VmxpConvertGdtEntry( state->SpecialRegisters.Gdtr.Base, state->SpecialRegisters.Ldtr, &vmxGdtEntry );
__vmx_vmwrite( GUEST_LDTR_SELECTOR, vmxGdtEntry.Selector );
__vmx_vmwrite( GUEST_LDTR_LIMIT, vmxGdtEntry.Limit );
__vmx_vmwrite( GUEST_LDTR_AR_BYTES, vmxGdtEntry.AccessRights );
__vmx_vmwrite( GUEST_LDTR_BASE, vmxGdtEntry.Base );

// GDT
// 全局描述符表 (GDT),存放系统运行过程中所有描述符的表。GDT 表包含了各种类型的描述符,如代码段描述符、数据段描述符、TSS 描述符等。
__vmx_vmwrite( GUEST_GDTR_BASE, (ULONG_PTR)state->SpecialRegisters.Gdtr.Base );
__vmx_vmwrite( GUEST_GDTR_LIMIT, state->SpecialRegisters.Gdtr.Limit );
__vmx_vmwrite( HOST_GDTR_BASE, (ULONG_PTR)state->SpecialRegisters.Gdtr.Base );

// IDT
// 中断描述符表(IDT),存放中断处理程序的入口地址,当中断发生时 CPU 会根据中断号查找该表并跳转到对应的处理程序。
__vmx_vmwrite( GUEST_IDTR_BASE, (ULONG_PTR)state->SpecialRegisters.Idtr.Base );
__vmx_vmwrite( GUEST_IDTR_LIMIT, state->SpecialRegisters.Idtr.Limit );
__vmx_vmwrite( HOST_IDTR_BASE, (ULONG_PTR)state->SpecialRegisters.Idtr.Base );

// CR0
// 包含了控制处理器运行方式和操作系统的特定行为的控制位,比如保护模式、分页机制等。
__vmx_vmwrite( CR0_READ_SHADOW, state->SpecialRegisters.Cr0 );
__vmx_vmwrite( HOST_CR0, state->SpecialRegisters.Cr0 );
__vmx_vmwrite( GUEST_CR0, state->SpecialRegisters.Cr0 );

// CR3
// 存放页面目录表 (Page Directory Table) 的地址,页面目录表描述了虚拟地址空间和物理地址空间的映射关系。
/*
“不要使用当前进程的地址空间作为 host”,因为此时可能正在任意的用户模式进程中执行,
这里的 host 指的是 hypervisor。也就是说,当 hypervisor 在处理 DPC 中断时,
DPC 可能正在运行某个用户进程的上下文中,没有特权访问 CR3 寄存器来获取当前进程的页目录表或页表。
因此,需要手动映射一份特殊的地址空间(例如 kernel 物理地址空间)作为 hypervisor 的地址空间,
并将其指定为 CR3 的值。这里做了这样的优化,避免使用错误的地址空间导致虚拟地址到物理地址的解析错误。
*/
__vmx_vmwrite( HOST_CR3, VpData->SystemDirectoryTableBase );
__vmx_vmwrite( GUEST_CR3, state->SpecialRegisters.Cr3 );

// CR4
// 包含了一些高级控制和特征寄存器(Advanced Controls and Features Register),比如支持硬件虚拟化技术、支持大页面等。
__vmx_vmwrite( HOST_CR4, state->SpecialRegisters.Cr4 );
__vmx_vmwrite( GUEST_CR4, state->SpecialRegisters.Cr4 );
__vmx_vmwrite( CR4_GUEST_HOST_MASK, 0x2000 );
__vmx_vmwrite( CR4_READ_SHADOW, state->SpecialRegisters.Cr4 & ~0x2000 );

// Debug MSR and DR7
// 包含有关调试状态和断点设置的信息。通过这些寄存器,可以在 CPU 上设置断点和调试相关信息。
__vmx_vmwrite( GUEST_IA32_DEBUGCTL, state->SpecialRegisters.DebugControl );
__vmx_vmwrite( GUEST_DR7, state->SpecialRegisters.KernelDr7 );

/*
最后,加载客户机的堆栈、指令指针和标志寄存器,
这与 RtlCaptureContext 在 VmxInitializeCPU 中返回的位置完全对应。
这段代码的作用是将保存在特定位置的客户机上下文数据加载到相应的寄存器中,
以便恢复客户机的执行状态。具体来说,它加载了客户机的堆栈指针、指令指针和标志寄存器值,
使得当控制权返回到 VmxInitializeCPU 时,客户机可以继续执行。
*/
__vmx_vmwrite( GUEST_RSP, state->ContextFrame.Rsp );
__vmx_vmwrite( GUEST_RIP, state->ContextFrame.Rip );
__vmx_vmwrite( GUEST_RFLAGS, state->ContextFrame.EFlags );

/*
加载 hypervisor 的入口地址和 hypervisor 栈。我们为自己分配了一个标准大小的内核栈(24KB),
并偏移指向上下文结构的指针,从而避免在 entrypoint 中需要修改叠加寄存器 RSP 的指令。
注意 CONTEXT 指针和栈本身必须按照 16 字节对齐,以保证与 AMD64 架构的 ABI 兼容性。
否则,诸如 RtlCaptureContext 将执行的 XMM 操作等操作将会失败。
*/
NT_ASSERT( (KERNEL_STACK_SIZE - sizeof( CONTEXT )) % 16 == 0 );
__vmx_vmwrite( HOST_RSP, (ULONG_PTR)VpData->VMMStack + KERNEL_STACK_SIZE - sizeof( CONTEXT ) );
// 将当前状态存储在栈中,并跳转到一个 C 函数 VmxpExitHandler 中去处理 VM 的 exit 事件
__vmx_vmwrite( HOST_RIP, (ULONG_PTR)VmxVMEntry );
}
将GDT(全局描述符表)中的段信息转换为VMX(虚拟机扩展)所需的格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/// <summary>
/// Fill segment data
/// </summary>
/// <param name="GdtBase">GDTR base</param>
/// <param name="Selector">Segment selector value</param>
/// <param name="VmxGdtEntry">Resulting entry</param>
VOID VmxpConvertGdtEntry( IN PVOID GdtBase, IN USHORT Selector, OUT PVMX_GDTENTRY64 VmxGdtEntry )
{
/*
段选择子(segment selector)是用来唯一地标识一个内存段的16位或64位数字。
在x86体系结构中,每个段选择子由两个部分组成:
索引(index)和请求特权级别(requested privilege level,RPL)。

索引用于查找全局描述符表(GDT)或局部描述符表(LDT)中的段描述符,
而RPL则指定了访问该段所需的最低权限级别(0到3)。

RPL可以通过对段选择子进行屏蔽(掩码)来获取。具体来说,将段选择子与0b11(即二进制的3)进行按位与运算,可以得到RPL的值。

例如,如果段选择子为0x08,那么其二进制表示为0000 1000,其中低两位(00)即为RPL的值。如果需要将RPL屏蔽掉,
可以使用按位与运算符&和~操作数取反运算符来进行屏蔽:0x08 & ~0b11 = 0x08 & 0xFC = 0x08。
需要注意的是,在64位模式下,只有GDT被使用,因此只需要考虑索引部分,而RPL位会被忽略。
*/
/*
在x64 Windows操作系统内核中,每个进程都有自己的全局描述符表(GDT),
用于保存其自身内存分段的信息。在对内存进行分段时,可以使用一个16位的段选择子来唯一地标识一个内存段。

读取给定选择子(segment selector)的GDT条目,并屏蔽掉请求特权级别(requested privilege level,RPL)位。
因为x64Windows在内核态下已经不使用局部描述符表(LDT),所以不需要考虑TI位(Table Indicator),它用于区分是引用GDT还是LDT。
TI位(Table Indicator)是段选择子的一个位,用于指示该选择子所引用的描述符表是GDT还是LDT。TI位的取值为0或1,其中0表示引用GDT,1表示引用LDT。
TI位则占据了选择子的第2位,因此选择子的最高有效位为TI位。

在x64模式下,每个选择子由两个部分组成:索引部分和RPL部分。索引部分用于查找GDT中的相应段描述符,
而RPL则指定了访问该段的权限级别。RPL通常会和当前运行代码的特权级别进行比较,以确定访问是否被允许。

在Windows内核中,0和3级特权级别都可以引用内核GDT中的所有段描述符,因此不需要进行RPL屏蔽。
但是,在某些情况下,为了安全起见,可能会使用引用GDT的低特权级别来访问只有高特权级别才能访问的内存段。
在这种情况下,需要进行RPL屏蔽来确保代码可以访问相应的内存段。
*/
// 屏蔽RPL位
PKGDTENTRY64 gdtEntry = NULL;
NT_ASSERT( (Selector & SELECTOR_TABLE_INDEX) == 0 );
gdtEntry = (PKGDTENTRY64)((ULONG_PTR)GdtBase + (Selector & ~RPL_MASK));

// 将传入的段选择子赋值给VMX_GDTENTRY64结构体的Selector字段
VmxGdtEntry->Selector = Selector;

// 使用LSL指令(Load Segment Limit)读取段限制(segment limit)。
VmxGdtEntry->Limit = __segmentlimit( Selector );

/*
在x86体系结构中,段描述符中的“System”位(也称为“S”位)用于区分系统段和代码/数据段。
当“System”位被清除时(即为0),表示这是一个代码段或数据段,需要对相对虚拟地址(RVA)进行地址计算。
将段基址与RVA相加,构建完整的64位有效地址。

对于非系统段,需要进行以下步骤来构建完整的64位线性地址:
将段基址与RVA相加,得到32位的线性地址。
将线性地址的高32位与段选择子中的基地址合并,构建出64位的线性地址。
*/
/*
需要注意的是,在Windows操作系统中,KGDTENTRY64结构中的“System”字段位置被定义错误了,
实际上,“System”位应该编码在“Type”字段的最高位。这意味着在Windows内核中计算64位地址时需要特别小心,
以确保不会使用错误的字段。
*/
/*
根据GDT中的字段信息,构建完整的64位有效地址。需要注意的是,只有当Type字段中的System位为0时才进行构建。
构建过程分为三部分:将BaseHigh字段左移24位,将BaseMiddle字段左移16位,将BaseLow字段保持不变,
并使用逻辑或运算符组合它们。如果Type字段中的最高位为0,
则还需要将BaseUpper字段左移32位并与之前的结果进行逻辑或运算。
*/
VmxGdtEntry->Base = ((gdtEntry->Bytes.BaseHigh << 24) | (gdtEntry->Bytes.BaseMiddle << 16) | (gdtEntry->BaseLow)) & MAXULONG;
VmxGdtEntry->Base |= ((gdtEntry->Bits.Type & 0x10) == 0) ? ((ULONG_PTR)gdtEntry->BaseUpper << 32) : 0;

// 加载访问权限信息
VmxGdtEntry->AccessRights = 0;
VmxGdtEntry->Bytes.Flags1 = gdtEntry->Bytes.Flags1;
VmxGdtEntry->Bytes.Flags2 = gdtEntry->Bytes.Flags2;

// 处理VMX相关的位
VmxGdtEntry->Bits.Reserved = 0;
VmxGdtEntry->Bits.Unusable = !gdtEntry->Bits.Present;
}
将当前状态存储在栈中,并跳转到一个 C 函数 VmxpExitHandler 中去处理 VM 的 exit 事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
VmxVMEntry PROC
push rcx ; save RCX, as we will need to orverride it
lea rcx, [rsp+8h] ; store the context in the stack, bias for
; the return address and the push we just did.
call RtlCaptureContext ; save the current register state.
; note that this is a specially written function
; which has the following key characteristics:
; 1) it does not taint the value of RCX
; 2) it does not spill any registers, nor
; expect home space to be allocated for it

jmp VmxpExitHandler ; jump to the C code handler. we assume that it
; compiled with optimizations and does not use
; home space, which is true of release builds.
VmxVMEntry ENDP
初始化EPT,创建Guest到Host的页映射(Page Mappings)

在这里插入图片描述

为了解决GVA-GPA-HPA的转换关系,在没有硬件辅助的时代,Hypervisor通过影子页表,很巧妙的将GVA-GPA映射到GVA-HPA, 功能虽然达成,但是在很多实际场景下,如进程频繁切换,内存频繁分配释放等,性能损耗会非常大;

EPT在硬件的帮助下,实现内存虚拟化简单直接,传统页表继续负责GVA-GPA, 而EPT负责GPA-HPA; 虽然内存访问延时可能会增加一些,但是大幅减少了因为页表更新带来的vmexit, 综合性价比提升巨大, 所以现代内存虚拟化,基本都被EPT统一了。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// <summary>
/// Create Guest to Host page mappings
/// </summary>
/// <param name="pEPT">CPU EPT data</param>
/// <returns>Status code</returns>
NTSTATUS EptBuildIdentityMap( IN PEPT_DATA pEPT )
{
if (pEPT->PML4Ptr != NULL)
return STATUS_SUCCESS;// 已经创建过

/*
PML4(Page Map Level 4)是 x86 架构中分页机制的一级页表。
在 x86 架构的分页机制中,物理内存被划分为固定大小的页面(通常为4KB)。
为了管理这些页面,操作系统使用页表来建立虚拟地址与物理地址之间的映射关系。
页表被组织为一个多级结构,其中 PML4 是最高级的页表。

PML4 是一个包含512个表项的数组,每个表项(PML4 Entry)的大小为8字节。
每个表项对应着一个 PDP(Page Directory Pointer)表,PDP 表进一步将虚拟地址映射到下一级的页表,
直到最后一级页表(称为页表 PDT)。

通过多级的页表结构,操作系统可以灵活地管理大量的内存页面,并实现虚拟地址到物理地址的映射。
PML4 负责管理最高级的页表,它的物理地址必须在处理器的控制寄存器(CR3)中设置,
以使处理器能够正确进行地址转换。
*/
pEPT->PML4Ptr = (PEPT_PML4_ENTRY)EptpAllocatePage( pEPT );// 分配一个页面作为PML4表表项
if (pEPT->PML4Ptr == NULL)
return STATUS_INSUFFICIENT_RESOURCES;// 错误则表示内存资源不足

NTSTATUS status = EptpFillTable( pEPT, pEPT->PML4Ptr );// 填充PML4表,创建其他级别的页表和映射关系
if (!NT_SUCCESS( status ))
EptFreeIdentityMap( pEPT );// 创建失败则释放资源

//DPRINT( "HyperBone: CPU %d: %s: Used pages %d\n", CPU_IDX, __FUNCTION__, pEPT->TotalPages );
return status;
}
分配EPT页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/// <summary>
/// Allocate page for PTE table
/// </summary>
/// <param name="pEPT">CPU EPT data</param>
/// <returns>Allocated page or NULL</returns>
PEPT_MMPTE EptpAllocatePage( IN PEPT_DATA pEPT )
{
/*
IRQL(Interrupt Request Level)是Windows操作系统中用于表示中断请求的优先级的概念。
它用于控制不同中断的处理顺序,以确保系统正常运行并防止冲突。
在Windows内核中,存在多个不同的IRQL级别,从最低的PASSIVE_LEVEL到最高的HIGH_LEVEL。
较低的IRQL级别表示较低的中断优先级,而较高的IRQL级别表示较高的中断优先级。

例程(Routine)是指一段代码或函数,通常用于执行特定的任务或功能。
*/
/*
检查当前IRQL是否大于DISPATCH_LEVEL,如果大于意味着在此级别以上的中断处理程序中,
使用内存分配例程会发生错误,因此直接调用高IRQL情况下的页面分配函数。
*/
if (KeGetCurrentIrql() > DISPATCH_LEVEL)
return EptpAllocatePageHighIRQL( pEPT );

PHYSICAL_ADDRESS Highest = { 0 }, Lowest = { 0 };// 物理地址
Highest.QuadPart = ~0;// 全部置为1,设为最大物理地址
// 分配连续的、指定缓存类型的内存页面,页面大小为PAGE_SIZE(4kb)
PEPT_MMPTE ptr = (PEPT_MMPTE)MmAllocateContiguousMemorySpecifyCache( PAGE_SIZE, Lowest, Highest, Lowest, MmNonCached );

// 成功分配 Save page ptr in array
if (ptr)
{
pEPT->TotalPages++;// 增加已分配页面数量
RtlZeroMemory( ptr, PAGE_SIZE );

BOOLEAN allocEntry = FALSE;
PEPT_PAGES_ENTRY pEntry = NULL;
if (IsListEmpty( &pEPT->PageList ))// 检查EPT页链表是否为空
{
allocEntry = TRUE;// 需要分配一个新的列表入口
}
else
{
pEntry = CONTAINING_RECORD( pEPT->PageList.Flink, EPT_PAGES_ENTRY, link );
if (pEntry->count >= PAGES_PER_ENTRY)// 检查当前列表入口是否已满
allocEntry = TRUE;
}

if (allocEntry)// 需要分配一个新的列表入口
{
// 从指定NonPagedPoolNx内存池中分配内存,大小为EPT_PAGES_ENTRY结构体大小
pEntry = ExAllocatePoolWithTag( NonPagedPoolNx, sizeof( EPT_PAGES_ENTRY ), HB_POOL_TAG );
if (pEntry == NULL)
{
DPRINT( "HyperBone: CPU %d: %s: Failed to allocate EPT_PAGES_ENTRY struct\n", CPU_IDX, __FUNCTION__ );
return ptr;
}

RtlZeroMemory( pEntry, sizeof( EPT_PAGES_ENTRY ) );
pEntry->pages[pEntry->count] = ptr;
pEntry->count++;

InsertHeadList( &pEPT->PageList, &pEntry->link );// 将新的列表入口插到EPT页链表头部
}
else
{
// 不需要分配,直接记录
pEntry->pages[pEntry->count] = ptr;
pEntry->count++;
}
}
else
{
DPRINT( "HyperBone: CPU %d: %s: Failed to allocate EPT page\n", CPU_IDX, __FUNCTION__ );
ASSERT( FALSE );
}

return ptr;
}
-在更高级的IRQL申请页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// Allocate page at IRQL > DISPATCH_LEVEL
/// </summary>
/// <param name="pEPT">CPU EPT data</param>
/// <returns>Allocated page or NULL</returns>
PEPT_MMPTE EptpAllocatePageHighIRQL( IN PEPT_DATA pEPT )
{
// 检查预分配页面数量是否小于预分配页面数量上限
if (pEPT->Preallocations < EPT_PREALLOC_PAGES)
{
PEPT_MMPTE ptr = pEPT->Pages[pEPT->Preallocations];// 将预分配页面数组中的下一个页面指针赋给ptr
pEPT->Preallocations++;
return ptr;
}

// 没有页面可以分配,触发蓝屏
KeBugCheckEx( HYPERVISOR_ERROR, BUG_CHECK_EPT_NO_PAGES, pEPT->Preallocations, EPT_PREALLOC_PAGES, 0 );
}
根据已使用的物理区域填充 PML4 表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/// <summary>
/// Fill PML4 table accordingly to used physical regions
/// </summary>
/// <param name="pEPT">CPU EPT data</param>
/// <param name="PML4Ptr">EPT PML4 pointer</param>
/// <returns>Status code</returns>
NTSTATUS EptpFillTable( IN PEPT_DATA pEPT, IN PEPT_PML4_ENTRY PML4Ptr )
{
NT_ASSERT( PML4Ptr != NULL );// 断言PML4Ptr不为空
if (PML4Ptr == NULL)
return STATUS_INVALID_PARAMETER;
// 遍历已使用的物理内存区域填充PML4表
for (ULONG i = 0; i < g_Data->Memory->NumberOfRuns; i++)
{
// 计算该物理区域可用于填充表的页数,取总页数和EPT表项数的最小值
ULONG64 first = g_Data->Memory->Run[i].BasePage;
ULONG64 total = g_Data->Memory->Run[i].PageCount;
ULONG64 count = min( total, EPT_TABLE_ENTRIES - (first & (EPT_TABLE_ENTRIES - 1)) );
// 初始化物理页帧号为first
ULONG64 hostPFN = first;
for (ULONG64 pfn = first; total > 0;)
{
// 嵌套调用EptUpdateTableRecursive函数,填充PML4表
if (!NT_SUCCESS( EptUpdateTableRecursive( pEPT, PML4Ptr, EPT_TOP_LEVEL, pfn, EPT_ACCESS_ALL, hostPFN, (ULONG)count ) ))
return STATUS_UNSUCCESSFUL;
// 更新pfn、hostPFN和total
pfn += count;
hostPFN += count;
total -= count;
count = min( total, EPT_TABLE_ENTRIES - (pfn & (EPT_TABLE_ENTRIES - 1)) );
}
}

/*for (ULONG64 pfn = 0; pfn <= 0xFEE00; pfn += EPT_TABLE_ENTRIES, hostPFN += EPT_TABLE_ENTRIES)
{
if (!NT_SUCCESS( EptUpdateTableRecursive( PML4Ptr, 3, pfn, EPT_ACCESS_ALL, hostPFN, EPT_TABLE_ENTRIES ) ))
return STATUS_UNSUCCESSFUL;
}*/

return STATUS_SUCCESS;
}
-递归更新EPT表项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/// <summary>
/// Update EPT entry
/// </summary>
/// <param name="pEPTData">CPU EPT data</param>
/// <param name="pTable">EPT table</param>
/// <param name="level">EPT table level</param>
/// <param name="pfn">Page frame number to update</param>
/// <param name="access">New PFN access</param>
/// <param name="hostPFN">New hot PFN</param>
/// <param name="count">Number of entries to update</param>
/// <returns>Status code</returns>
NTSTATUS EptUpdateTableRecursive(
IN PEPT_DATA pEPTData,
IN PEPT_MMPTE pTable,
IN EPT_TABLE_LEVEL level,
IN ULONG64 pfn,
IN UCHAR access,
IN ULONG64 hostPFN,
IN ULONG count
)
{
if (level == EPT_LEVEL_PTE)
{
// PTE表示已递归到最底层
// 避免越界
ULONG64 first = EptpTableOffset( pfn, level );
ASSERT( first + count <= EPT_TABLE_ENTRIES );

// 根据访问权限设置对应字段
PEPT_PTE_ENTRY pPTE = (PEPT_PTE_ENTRY)pTable;
for (ULONG64 i = first; i < first + count; i++, hostPFN++)
{
pPTE[i].Fields.Read = (access & EPT_ACCESS_READ) != 0;
pPTE[i].Fields.Write = (access & EPT_ACCESS_WRITE) != 0;
pPTE[i].Fields.Execute = (access & EPT_ACCESS_EXEC) != 0;
pPTE[i].Fields.MemoryType = VMX_MEM_TYPE_WRITEBACK;
pPTE[i].Fields.PhysAddr = hostPFN;// 物理地址字段为hostPFN
}

return STATUS_SUCCESS;
}

ULONG64 offset = EptpTableOffset( pfn, level );// 计算要更新的 EPT 表项在指定表中的偏移量
PEPT_MMPTE pEPT = &pTable[offset];// 要更新的 EPT 表中的具体表项
PEPT_MMPTE pNewEPT = 0;

if (pEPT->Fields.PhysAddr == 0)
{
// 表示该表项未分配页面,需要分配一个页面
pNewEPT = (PEPT_MMPTE)EptpAllocatePage( pEPTData );
if (pNewEPT == NULL)
return STATUS_INSUFFICIENT_RESOURCES;
// 设置表示存在,可写,可执行和物理地址
pEPT->Fields.Present = 1;
pEPT->Fields.Write = 1;
pEPT->Fields.Execute = 1;
pEPT->Fields.PhysAddr = PFN( MmGetPhysicalAddress( pNewEPT ).QuadPart );
}
else
{
// 表示该表项已分配页面,通过物理地址获取其虚拟地址
PHYSICAL_ADDRESS phys = { 0 };
phys.QuadPart = pEPT->Fields.PhysAddr << 12;
pNewEPT = MmGetVirtualForPhysical( phys );
}

return EptUpdateTableRecursive( pEPTData, pNewEPT, level - 1, pfn, access, hostPFN, count );
}
–计算 EPT 表项索引(Table Index)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// EPT entry index in table
/// </summary>
/// <param name="pfn">EPT PFN</param>
/// <param name="level">EPT level</param>
/// <returns>Table index</returns>
inline ULONG64 EptpTableOffset( IN ULONG64 pfn, IN CHAR level )
{
/*
首先定义一个掩码(mask),用于提取指定层级的表项索引。
这里使用位运算来生成掩码,具体生成方式为 1 左移 ((level + 1) * EPT_TABLE_ORDER) 位,然后减去 1,
得到了一个所有位都置为 1 的掩码。
将掩码与 PFN 进行按位与操作,目的是提取出指定层级的表项索引。
按位与操作会将掩码中相应位为 0 的位置上的 PFN 位也设置为 0,从而提取出表项索引。
最后,将提取出的表项索引右移 (level * EPT_TABLE_ORDER) 位,将其放置在正确的位置上,并作为函数返回值。
*/
ULONG64 mask = (1ULL << ((level + 1) * EPT_TABLE_ORDER)) - 1;
return (pfn & mask) >> (level * EPT_TABLE_ORDER);
}
在CPU上开启EPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/// <summary>
/// Enable EPT for CPU
/// </summary>
/// <param name="PML4">PML4 pointer to use</param>
VOID EptEnable( IN PEPT_PML4_ENTRY PML4 )
{
/*
基本CPU执行控制是一组CPU执行控制位,用于控制处理器在VMX操作期间的行为。
这些控制位包括启用VMX操作、允许或阻止对VMCS的加载或存储、中断虚拟化、启用或禁用NMI虚拟化、
读取或屏蔽虚拟-NMI信息等。这些控制位可通过VMXON指令设置,
也可以通过VMClear和VMLaunch指令清除并重新加载来更新。

二级CPU执行控制是一组掩码,用于启用或禁用一组次要的、特殊的、可选的处理器功能。
这些控制位包括启用或禁用EPT、启用或禁用VPID、启用或禁用虚拟处理器退出控制、启用或禁用VMFUNC指令、
CR3传递、IO位图中断虚拟化、APIC虚拟化等。
这些控制位可以通过设置MSR IA32_VMX_PROCBASED_CTLS2寄存器来更新。
*/
VMX_CPU_BASED_CONTROLS primary = { 0 };// 基本CPU执行控制
VMX_SECONDARY_CPU_BASED_CONTROLS secondary = { 0 };// 二级CPU执行控制
EPT_TABLE_POINTER EPTP = { 0 };// EPT指针

// 读取相应寄存器的值
__vmx_vmread( SECONDARY_VM_EXEC_CONTROL, (size_t*)&secondary.All );
__vmx_vmread( CPU_BASED_VM_EXEC_CONTROL, (size_t*)&primary.All );

// 设置EPTP(EPT表指针)的字段值。
// 将EPTP的物理地址字段设置为PML4的物理地址(MmGetPhysicalAddress(PML4)),并将其右移12位(相当于除以4096),
// 以获取正确的页帧号。将页表的长度设置为3,表示EPT有4级表(PML4、PDPT、PD和PT)。
EPTP.Fields.PhysAddr = MmGetPhysicalAddress( PML4 ).QuadPart >> 12;
EPTP.Fields.PageWalkLength = 3;

// 将EPTP的值写入EPT_POINTER VMCS字段中,以启用EPT。
__vmx_vmwrite( EPT_POINTER, EPTP.All );
// 将虚拟处理器标识符(VPID)写入VMCS中,以启用VPID。
__vmx_vmwrite( VIRTUAL_PROCESSOR_ID, VM_VPID );

primary.Fields.ActivateSecondaryControl = TRUE;// 激活二级CPU执行控制
secondary.Fields.EnableEPT = TRUE;// 启用EPT
if(g_Data->Features.VPID)
secondary.Fields.EnableVPID = TRUE;// 启用VPID

// 将更新后的值写入VMCS中
__vmx_vmwrite( SECONDARY_VM_EXEC_CONTROL, secondary.All );
__vmx_vmwrite( CPU_BASED_VM_EXEC_CONTROL, primary.All );

// 刷新EPT上下文,并从ctx拿到上下文信息
EPT_CTX ctx = { 0 };
__invept( INV_ALL_CONTEXTS, &ctx );

//DPRINT( "HyperBone: CPU %d: %s: EPT enabled\n", CPU_NUM, __FUNCTION__ );
}
刷新EPT上下文
1
2
3
4
__invept PROC
invept rcx, OWORD PTR [rdx]
ret
__invept ENDP

AMD CPU 不支持

1
2
3
4
5
6
inline VOID AMDSubvertCPU( IN PVCPU Vcpu, IN PVOID arg )
{
UNREFERENCED_PARAMETER( Vcpu );
UNREFERENCED_PARAMETER( arg );
DPRINT( "HyperBone: CPU %d: %s: AMD-V not yet supported\n", CPU_IDX, __FUNCTION__ );
}

释放IntelCPU

1
2
3
4
5
6
inline VOID IntelRestoreCPU( IN PVCPU Vcpu )
{
// Prevent execution of VMCALL on non-vmx CPU
if (Vcpu->VmxState > VMX_STATE_OFF)
VmxShutdown( Vcpu );
}

从根模式切换回非根模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// <summary>
/// Revert CPU to non-root mode
/// </summary>
/// <param name="Vcpu">Virtual CPU data</param>
VOID VmxShutdown( IN PVCPU Vcpu )
{
//DPRINT( "HyperBone: CPU %d: %s: CR3 load count %d\n", CPU_IDX, __FUNCTION__, Vcpu->Cr3Loads );

__vmx_vmcall( HYPERCALL_UNLOAD, 0, 0, 0 );// 通知VMM卸载所加载的虚拟机
VmxVMCleanup( KGDT64_R3_DATA | RPL_MASK, KGDT64_R3_CMTEB | RPL_MASK );// 清除VMX相关设置

// 释放EPT的身份映射
EptFreeIdentityMap( &Vcpu->EPT );

// 释放数据结构VMXON、VMCS和VMM堆栈内存
if (Vcpu->VMXON)
MmFreeContiguousMemory( Vcpu->VMXON );
if (Vcpu->VMCS)
MmFreeContiguousMemory( Vcpu->VMCS );
if (Vcpu->VMMStack)
MmFreeContiguousMemory( Vcpu->VMMStack );

Vcpu->VMXON = NULL;
Vcpu->VMCS = NULL;
Vcpu->VMMStack = NULL;
}
通知VMM
1
2
3
4
__vmx_vmcall PROC
vmcall
ret
__vmx_vmcall ENDP
清除VMX相关设置
1
2
3
4
5
6
VmxVMCleanup PROC
mov ds, cx ; set DS to parameter 1
mov es, cx ; set ES to parameter 1
mov fs, dx ; set FS to parameter 2
ret ; return
VmxVMCleanup ENDP
释放EPT的身份映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/// <summary>
/// Release Guest to Host page mappings
/// </summary>
/// <param name="pEPT">CPU EPT data</param>
/// <returns>Status code</returns>
NTSTATUS EptFreeIdentityMap( IN PEPT_DATA pEPT )
{
// 检查PML4表是否为空
if (pEPT->PML4Ptr == NULL)
return STATUS_SUCCESS;

// 释放PML4表
pEPT->PML4Ptr = NULL;
// 循环释放EPT页链表中每个表项的所有页面
while (!IsListEmpty( &pEPT->PageList ))
{
PLIST_ENTRY pListEntry = pEPT->PageList.Flink;
// 获取列表入口的地址 从结构的成员指针得到结构的起始地址
PEPT_PAGES_ENTRY pEntry = CONTAINING_RECORD( pListEntry, EPT_PAGES_ENTRY, link );
for (ULONG i = 0; i < pEntry->count; i++)
// 释放表项的每个页面
if (pEntry->pages[i] != NULL)
MmFreeContiguousMemory( pEntry->pages[i] );
// 移除当前表项,并释放内存空间
RemoveEntryList( pListEntry );
ExFreePoolWithTag( pListEntry, HB_POOL_TAG );
}

// 表示释放了预分配的内存空间
pEPT->Preallocations = 0;
return STATUS_SUCCESS;
}

AMD不支持

1
2
3
4
5
inline VOID AMDRestoreCPU( IN PVCPU Vcpu )
{
UNREFERENCED_PARAMETER( Vcpu );
DPRINT( "HyperBone: CPU %d: %s: AMD-V not yet supported\n", CPU_IDX, __FUNCTION__ );
}

停止虚拟化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/// <summary>
/// Devirtualize each CPU
/// </summary>
/// <returns>Status code</returns>
NTSTATUS StopHV()
{
// Unknown CPU
if (g_Data->CPUVendor == CPU_Other)
return STATUS_NOT_SUPPORTED;

/*
如果在当前的上下文中调用 KeGenericCallDpc 并将当前线程切换到另一个处理器上执行回调函数,可能会导致死锁。

死锁是指两个或多个线程互相等待对方所持有的资源。在这种情况下,如果回调函数依赖于当前线程的某些资源,
并且该资源在其他处理器上被另一个线程持有,那么当前线程会一直等待资源释放,从而导致死锁。

为了避免潜在的死锁风险,作者决定屏蔽调用 KeGenericCallDpc 函数的代码,
并采用其他方式来停止虚拟化的每个CPU。
*/
// KeGenericCallDpc( HvmpHVCallbackDPC, NULL ); there will be Dead Lock


// 获取系统中活动处理器的数量,并循环依次处理每个处理器
ULONG number_of_processors = KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS);
for (ULONG processor_index = 0; processor_index < number_of_processors; processor_index++) {
PROCESSOR_NUMBER processor_number;
RtlZeroMemory(&processor_number, sizeof(PROCESSOR_NUMBER));
// 获取当前处理器的PROCESSOR_NUMBER处理器编号结构体
NTSTATUS status = KeGetProcessorNumberFromIndex(processor_index, &processor_number);
if (!NT_SUCCESS(status))
{
DbgBreakPoint();
}

// Switch the current processor
GROUP_AFFINITY affinity;
RtlZeroMemory(&affinity, sizeof(GROUP_AFFINITY));
// 表示将当前处理器作为目标处理器
affinity.Group = processor_number.Group;
affinity.Mask = 1ull << processor_number.Number;
/*
系统组亲和性(System Group Affinity)是一种将线程限制在特定处理器组上运行的机制。
在支持 NUMA(非统一内存访问)的系统中,处理器分为多个组,每个组具有自己的本地内存和其他资源。
通过设置线程的系统组亲和性,可以使线程优先在特定的处理器组上运行,以提高性能和资源的局部性。

KeSetSystemGroupAffinityThread 函数用于设置线程的系统组亲和性。它接受一个参数affinity,
表示要设置的系统组亲和性信息。affinity 结构体中的字段包括 Mask 和 Group,
分别表示处理器组掩码和组索引。通过调用该函数,可以将线程限制在指定的处理器组上运行。
将原本的信息返回到previous_affinity

KeRevertToUserGroupAffinityThread 函数用于恢复线程的用户组亲和性。当线程的系统组亲和性被设置后,
如果希望将其恢复为默认的用户组亲和性,则可以调用该函数。
*/
GROUP_AFFINITY previous_affinity;
RtlZeroMemory(&affinity, sizeof(GROUP_AFFINITY));
// 将当前线程切换到目标处理器
KeSetSystemGroupAffinityThread(&affinity, &previous_affinity);

// 获取指向当前处理器的指针,停止该处理器的虚拟化
PVCPU pVCPU = &g_Data->cpu_data[processor_index];
IntelRestoreCPU(pVCPU);

// 将当前线程切换回原来的处理器
KeRevertToUserGroupAffinityThread(&previous_affinity);
if (!NT_SUCCESS(status))
{
DbgBreakPoint();
}
}

return STATUS_SUCCESS;
}

开始测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
VOID TestStart( IN BOOLEAN SyscallHook, IN BOOLEAN PageHook1, IN IN BOOLEAN PageHook2 )
{
if (PageHook1)
{
TestPageHook();// 测试页面钩子
}
// 尝试获取 NtClose 系统调用函数的地址,并将其保存到全局变量 g_NtClose 中。
g_NtClose = (PVOID)UtilSSDTEntry( SSDTIndex( &ZwClose ) );
if (g_NtClose)
{
// 如果获取到
if (SyscallHook)
{
if (NT_SUCCESS( SHInitHook() ))// 系统调用入口钩子
SHHookSyscall( SSDTIndex( &ZwClose ), (PVOID)hkNtClose, 1 );// 将系统调用函数的地址与自定义hkNtClose进行钩子绑定
else
DPRINT( "HyperBone: CPU %d: %s: SHInitHook() failed\n", CPU_IDX, __FUNCTION__ );
}

if (PageHook2)
{
if (g_NtClose)
{
if (!NT_SUCCESS( PHHook( g_NtClose, (PVOID)hkNtClose2 ) ))// 将全局系统调用函数的地址与自定义hkNtClose2进行钩子绑定
DPRINT( "HyperBone: CPU %d: %s: PHHook() failed\n", CPU_IDX, __FUNCTION__ );
}
else
DPRINT( "HyperBone: CPU %d: %s: NtClose not found\n", CPU_IDX, __FUNCTION__ );
}
}
else
DPRINT( "HyperBone: CPU %d: %s: NtClose not found\n", CPU_IDX, __FUNCTION__ );
}

测试页面钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
VOID TestPageHook()
{
// 缓冲区
UCHAR buf[32] = { 0 };
// TestFn函数地址
PVOID pFn = (PVOID)TestFn;

// 复制函数地址前16字节到缓冲区,调用TestFn函数
RtlCopyMemory( buf, pFn, 16 );
DPRINT( "HyperBone: CPU %d: %s: Buffer contents: 0x%p, Call result 0x%X\n", CPU_IDX, __FUNCTION__, *(PULONG64)buf, TestFn( 100, 5 ) );

// 将pFn地址与hkTestFn函数进行钩子绑定
PHHook( pFn, (PVOID)hkTestFn );

// 再次复制函数地址前16字节到缓冲区
// 此时钩子生效,调用TestFn函数时,实际上调用的是hkTestFn函数
RtlCopyMemory( buf, pFn, 16 );
DPRINT( "HyperBone: CPU %d: %s: Buffer contents: 0x%p, Call result 0x%X\n", CPU_IDX, __FUNCTION__, *(PULONG64)buf, TestFn( 100, 5 ) );

// 恢复钩子
PHRestore( pFn );

// 再次复制函数地址前16字节到缓冲区,查看结果
RtlCopyMemory( buf, pFn, 16 );
DPRINT( "HyperBone: CPU %d: %s: Buffer contents: 0x%p, Call result 0x%X\n", CPU_IDX, __FUNCTION__, *(PULONG64)buf, TestFn( 100, 5 ) );
}

TestFn

1
2
3
4
5
6
7
8
9
10
// 指示放在.text0段中
#pragma alloc_text(".text0", TestFn)
ULONG64 TestFn( ULONG64 in1, ULONG64 in2 )
{
// 对传入的参数进行一些数学运算
ULONG64 data1 = 0x500;
data1 += in1;
in2 -= 0x10;
return in1 + in2 * 3 - in1 / in2 + data1;
}

hkTestFn

1
2
3
4
5
6
7
8
9
10
11
#pragma alloc_text(".text1", hkTestFn)
ULONG64 hkTestFn( ULONG64 in1, ULONG64 in2 )
{
// 获取 TestFn 函数的 hook entry
PPAGE_HOOK_ENTRY pEntry = PHGetHookEntry( (PVOID)(ULONG_PTR)TestFn );
if (pEntry)
// 将其 OriginalData 字段转换为函数指针,并调用原始函数,传入参数 in1 和 in2。
((ULONG64( *)(ULONG64, ULONG64))(ULONG_PTR)pEntry->OriginalData)(in1, in2);
// 返回固定值
return 0xDEADBEEF;
}

获取对应函数的HookEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// Get hook data by function pointer
/// </summary>
/// <param name="ptr">Function address</param>
/// <returns>Found entry or NULL</returns>
PPAGE_HOOK_ENTRY PHGetHookEntry( IN PVOID ptr )
{
if (g_PageList.Flink == NULL || IsListEmpty( &g_PageList ))
return NULL;

// 遍历g_PageList查找匹配的HookEntry钩子入口数据结构
for (PLIST_ENTRY pListEntry = g_PageList.Flink; pListEntry != &g_PageList; pListEntry = pListEntry->Flink)
{
// 计算PAGE_HOOK_ENTRY结构体的地址
PPAGE_HOOK_ENTRY pEntry = CONTAINING_RECORD( pListEntry, PAGE_HOOK_ENTRY, Link );
// 判断是否找到了匹配的PAGE_HOOK_ENTRY
if (pEntry->OriginalPtr == ptr)
return pEntry;
}

return NULL;
}

页面钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/// <summary>
/// Hook function
/// </summary>
/// <param name="pFunc">Function address</param>
/// <param name="pHook">Hook address</param>
/// /// <param name="Type">Hook type</param>
/// <returns>Status code</returns>
NTSTATUS PHHook( IN PVOID pFunc, IN PVOID pHook )
{
PUCHAR CodePage = NULL;
BOOLEAN Newpage = FALSE;
PHYSICAL_ADDRESS phys = { 0 };
phys.QuadPart = MAXULONG64;

// 检查EPT是否开启,权限是否为仅执行
if (!g_Data->Features.EPT || !g_Data->Features.ExecOnlyEPT)
return STATUS_NOT_SUPPORTED;

// 检查页面是否已经被钩子
PPAGE_HOOK_ENTRY pEntry = PHGetHookEntryByPage( pFunc, DATA_PAGE );
if (pEntry != NULL)
{
// 已经有钩子,获取HookEntry中的CodePageVA
CodePage = pEntry->CodePageVA;
}
else
{
// 没有钩子,分配一个连续页面
CodePage = MmAllocateContiguousMemory( PAGE_SIZE, phys );
Newpage = TRUE;
}

if (CodePage == NULL)
return STATUS_INSUFFICIENT_RESOURCES;

// 分配PAGE_HOOK_ENTRY结构体内存
PPAGE_HOOK_ENTRY pHookEntry = ExAllocatePoolWithTag( NonPagedPool, sizeof( PAGE_HOOK_ENTRY ), HB_POOL_TAG );
if (pHookEntry == NULL)
return STATUS_INSUFFICIENT_RESOURCES;

RtlZeroMemory( pHookEntry, sizeof( PAGE_HOOK_ENTRY ) );
// 将原始函数代码拷贝到CodePage所指向的内存页
RtlCopyMemory( CodePage, PAGE_ALIGN( pFunc ), PAGE_SIZE );

// 将原始函数代码拷贝到PAGE_HOOK_ENTRY的OriginalData缓冲区,并记录复制的代码长度
NTSTATUS status = PHpCopyCode( pFunc, pHookEntry->OriginalData, &pHookEntry->OriginalSize );
if (!NT_SUCCESS( status ))
{
ExFreePoolWithTag( pHookEntry, HB_POOL_TAG );
return status;
}

// 计算函数地址相对于页面的偏移
/*
掩码运算通过将一个页面大小减一的二进制数作为掩码,与待对齐地址进行位运算,
将指定位数之外的位都设置为0,从而达到向4kb对齐的目的。
*/
ULONG_PTR page_offset = (ULONG_PTR)pFunc - (ULONG_PTR)PAGE_ALIGN( pFunc );

// 初始化JUMP_THUNK结构体,用于跳转到Hook函数
JUMP_THUNK thunk = { 0 };
PHpInitJumpThunk( &thunk, (ULONG64)pHook );
memcpy( CodePage + page_offset, &thunk, sizeof( thunk ) );// 将JUMP_THUNK结构体拷贝到CodePage的偏移处

pHookEntry->OriginalPtr = pFunc;// 原始函数地址
pHookEntry->DataPageVA = PAGE_ALIGN( pFunc );// 数据页面虚拟地址
/*
物理帧号是指在操作系统中用来管理物理内存的一种编号方式。
物理内存由一系列连续的物理页面(或称为物理帧)组成,每个物理页面的大小通常是固定的,
比如4KB或者更大的幂次方。
物理帧号是用来标识这些物理页面的唯一数字编号。它与物理内存地址相对应,可以用于将逻辑地址映射到物理地址。

操作系统通过维护一个数据结构(如页表、段表等)来管理和跟踪物理内存的使用情况。
这些数据结构中的条目使用物理帧号来标识每个物理页面的状态,如被使用、空闲、保留等。

通过使用物理帧号,操作系统可以实现内存分页机制、内存保护、虚拟内存管理等功能。
它使得操作系统能够有效地管理和控制物理内存资源,同时提供了对进程的内存访问控制和地址映射的支持。
*/
pHookEntry->DataPagePFN = PFN( MmGetPhysicalAddress( pFunc ).QuadPart );// 数据页面物理帧号
pHookEntry->CodePageVA = CodePage;// 代码页面虚拟地址
pHookEntry->CodePagePFN = PFN( MmGetPhysicalAddress( CodePage ).QuadPart );// 代码页面物理帧号

// 检查全局链表是否为空,如果为空则初始化链表
if (g_PageList.Flink == NULL)
InitializeListHead( &g_PageList );
InsertTailList( &g_PageList, &pHookEntry->Link );// 将PAGE_HOOK_ENTRY结构体插入全局链表

// 新分配的页面
if (Newpage)
{
// 创建HOOK_CONTEXT结构体,设置对应的值
HOOK_CONTEXT ctx = { 0 };
ctx.Hook = TRUE;
ctx.DataPagePFN = pHookEntry->DataPagePFN;
ctx.CodePagePFN = pHookEntry->CodePagePFN;

// 执行PHpHookCallbackDPC回调创建EPT页面映射
KeGenericCallDpc( PHpHookCallbackDPC, &ctx );
}

return STATUS_SUCCESS;
}

计算物理帧

1
2
3
4
5
6
/*
这里的PFN是一个宏函数,接受一个地址作为参数,使用位移操作符>>将地址右移PAGE_SHIFT位,并将结果强制转换为ULONG64类型,作为物理帧号返回。

PAGE_SHIFT是一个常量或宏,表示页面大小与页内偏移之间的位移量。通常,页面大小是2的幂次方,例如4KB页面大小对应的PAGE_SHIFT值为12,即右移12位。位移操作实际上是对地址进行除法运算,将高位的部分消除,得到一个较小的数值,该数值就是物理帧号。
*/
#define PFN(addr) (ULONG64)((addr) >> PAGE_SHIFT)

获取页面函数钩子信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
/// Get hook data by page address
/// </summary>
/// <param name="ptr">Function pointer</param>
/// <param name="Type">Page type</param>
/// <returns>Found hook entry or NULL</returns>
PPAGE_HOOK_ENTRY PHGetHookEntryByPage( IN PVOID ptr, IN PAGE_TYPE Type )
{
if (g_PageList.Flink == NULL || IsListEmpty( &g_PageList ))
return NULL;

PVOID page = PAGE_ALIGN( ptr );// 确保页面对齐
// 遍历g_PageList查找匹配的PAGE_HOOK_ENTRY结构体
for (PLIST_ENTRY pListEntry = g_PageList.Flink; pListEntry != &g_PageList; pListEntry = pListEntry->Flink)
{
// 将节点转换为PAGE_HOOK_ENTRY结构体初始位置
PPAGE_HOOK_ENTRY pEntry = CONTAINING_RECORD( pListEntry, PAGE_HOOK_ENTRY, Link );
// 判断钩子的页面类型、数据是否匹配
if ((Type == DATA_PAGE && pEntry->DataPageVA == page) || (Type == CODE_PAGE && pEntry->CodePageVA == page))
return pEntry;
}

return NULL;
}

计算给定成员的结构体初始位置

1
2
3
4
5
6
7
8
9
10
11
12
/*
接受三个参数:address是指向某个成员的指针,type是结构体类型,field是结构体中的成员名。

宏定义使用了一些指针运算和转换来计算结构体的起始地址。首先,将address强制转换为PCHAR类型(字符指针),然后通过减去(&((type *)0)->field)的结果,得到结构体中成员field相对于结构体起始地址的偏移量(以字节计)。

接下来,将该偏移量加到address的地址上,得到结构体的起始地址,并将其强制转换为目标的type类型指针,最终返回该指针作为结果。

这个宏定义可以用于在已知结构体的某个成员地址的情况下,快速地获取整个结构体的起始地址。这种技巧在处理数据结构时非常有用。
*/
#define CONTAINING_RECORD(address, type, field) ((type *)( \
(PCHAR)(address) - \
(ULONG_PTR)(&((type *)0)->field)))

通过LDASM工具将原始字节复制到指定的缓冲区中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/// <summary>
/// Copy original bytes using LDASM
/// </summary>
/// <param name="pFunc">Original function ptr</param>
/// <param name="OriginalStore">Buffer to store bytes</param>
/// <param name="pSize">Lenght of copied data</param>
/// <returns>Status code</returns>
NTSTATUS PHpCopyCode( IN PVOID pFunc, OUT PUCHAR OriginalStore, OUT PULONG pSize )
{
/*
LDASM是一个函数解析工具,用于解析x86和x64指令的长度和操作码。它可以读取给定地址上的指令,
并提供有关指令的详细信息,例如指令长度、操作码、操作数等。

通过使用LDASM,开发人员可以分析和处理二进制代码,包括动态修改和重写函数的字节码。
它是基于汇编和机器码规范的解析库,它能够准确地分析各种指令,并提供相关信息以供后续处理。
*/
// 追踪已复制的总字节数
PUCHAR src = pFunc;// 指向原始函数地址
PUCHAR old = OriginalStore;// 指向目标地址OriginalStore缓冲区
ULONG all_len = 0;// 已复制的总字节数
ldasm_data ld = { 0 };// ldasm_data结构体,用于存储指令信息

do
{
ULONG len = ldasm( src, &ld, TRUE );// 解析指令,返回指令长度

// Determine code end
if (ld.flags & F_INVALID
|| (len == 1 && (src[ld.opcd_offset] == 0xCC || src[ld.opcd_offset] == 0xC3))
|| (len == 3 && src[ld.opcd_offset] == 0xC2)
|| len + all_len > 128)
{
// 解析失败或者指令长度超过128字节或者指令为单字节返回(0xcc\0xc3)、3字节返回(0xc2),RET、RETN、RETF
break;
}

// 将原始函数代码拷贝到OriginalStore缓冲区
memcpy( old, src, len );

// 如果指令中有相对偏移,需要修复
if (ld.flags & F_RELATIVE)
{
LONG diff = 0;
const uintptr_t ofst = (ld.disp_offset != 0 ? ld.disp_offset : ld.imm_offset);
const uintptr_t sz = ld.disp_size != 0 ? ld.disp_size : ld.imm_size;

memcpy( &diff, src + ofst, sz );
// 检查跳转目标地址是否超过了2GB的范围。如果跳转目标地址大于2GB,则跳转会导致整数溢出和错误的行为。
if (_abs64( src + len + diff - old ) > INT_MAX)
{
break;
}
else
{
diff += (LONG)(src - old);
memcpy( old + ofst, &diff, sz );
}
}

src += len;
old += len;
all_len += len;

} while (all_len < sizeof( JUMP_THUNK ));

// 检查是否成功复制了JUMP_THUNK结构体
if (all_len < sizeof( JUMP_THUNK ))
{
return STATUS_UNSUCCESSFUL;
}
else
{
// 初始化JUMP_THUNK结构体,用于跳转到Hook函数
PHpInitJumpThunk( (PJUMP_THUNK)old, (ULONG64)src );
*pSize = all_len;
}

return STATUS_SUCCESS;
}
LDASM 返回指令长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
unsigned int __fastcall ldasm( void *code, ldasm_data *ld, ULONG is64 )
/*
Description:
Disassemble one instruction

Arguments:
code - pointer to the code for disassemble
ld - pointer to structure ldasm_data
is64 - set this flag for 64-bit code, and clear for 32-bit

Return:
length of instruction
*/
{
UCHAR *p = (UCHAR*)code;
UCHAR s, op, f;
UCHAR rexw, pr_66, pr_67;

s = rexw = pr_66 = pr_67 = 0;

/* dummy check */
if (!code || !ld)
return 0;

/* init output data */
memset( ld, 0, sizeof( ldasm_data ) );

/* phase 1: parse prefixies */
while (cflags( *p ) & OP_PREFIX) {
if (*p == 0x66)
pr_66 = 1;
if (*p == 0x67)
pr_67 = 1;
p++; s++;
ld->flags |= F_PREFIX;
if (s == 15) {
ld->flags |= F_INVALID;
return s;
}
}

/* parse REX prefix */
if (is64 && *p >> 4 == 4) {
ld->rex = *p;
rexw = (ld->rex >> 3) & 1;
ld->flags |= F_REX;
p++; s++;
}

/* can be only one REX prefix */
if (is64 && *p >> 4 == 4) {
ld->flags |= F_INVALID;
s++;
return s;
}

/* phase 2: parse opcode */
ld->opcd_offset = (UCHAR)(p - (UCHAR*)code);
ld->opcd_size = 1;
op = *p++; s++;

/* is 2 byte opcode? */
if (op == 0x0F) {
op = *p++; s++;
ld->opcd_size++;
f = cflags_ex( op );
if (f & OP_INVALID) {
ld->flags |= F_INVALID;
return s;
}
/* for SSE instructions */
if (f & OP_EXTENDED) {
op = *p++; s++;
ld->opcd_size++;
}
}
else {
f = cflags( op );
/* pr_66 = pr_67 for opcodes A0-A3 */
if (op >= 0xA0 && op <= 0xA3)
pr_66 = pr_67;
}

/* phase 3: parse ModR/M, SIB and DISP */
if (f & OP_MODRM) {
UCHAR mod = (*p >> 6);
UCHAR ro = (*p & 0x38) >> 3;
UCHAR rm = (*p & 7);

ld->modrm = *p++; s++;
ld->flags |= F_MODRM;

/* in F6,F7 opcodes immediate data present if R/O == 0 */
if (op == 0xF6 && (ro == 0 || ro == 1))
f |= OP_DATA_I8;
if (op == 0xF7 && (ro == 0 || ro == 1))
f |= OP_DATA_I16_I32_I64;

/* is SIB byte exist? */
if (mod != 3 && rm == 4 && !(!is64 && pr_67)) {
ld->sib = *p++; s++;
ld->flags |= F_SIB;

/* if base == 5 and mod == 0 */
if ((ld->sib & 7) == 5 && mod == 0) {
ld->disp_size = 4;
}
}

switch (mod) {
case 0:
if (is64) {
if (rm == 5) {
ld->disp_size = 4;
if (is64)
ld->flags |= F_RELATIVE;
}
}
else if (pr_67) {
if (rm == 6)
ld->disp_size = 2;
}
else {
if (rm == 5)
ld->disp_size = 4;
}
break;
case 1:
ld->disp_size = 1;
break;
case 2:
if (is64)
ld->disp_size = 4;
else if (pr_67)
ld->disp_size = 2;
else
ld->disp_size = 4;
break;
}

if (ld->disp_size) {
ld->disp_offset = (UCHAR)(p - (UCHAR *)code);
p += ld->disp_size;
s += ld->disp_size;
ld->flags |= F_DISP;
}
}

/* phase 4: parse immediate data */
if (rexw && f & OP_DATA_I16_I32_I64)
ld->imm_size = 8;
else if (f & OP_DATA_I16_I32 || f & OP_DATA_I16_I32_I64)
ld->imm_size = 4 - (pr_66 << 1);

/* if exist, add OP_DATA_I16 and OP_DATA_I8 size */
ld->imm_size += f & 3;

if (ld->imm_size) {
s += ld->imm_size;
ld->imm_offset = (UCHAR)(p - (UCHAR *)code);
ld->flags |= F_IMM;
if (f & OP_RELATIVE)
ld->flags |= F_RELATIVE;
}

/* instruction is too long */
if (s > 15)
ld->flags |= F_INVALID;

return s;
}
构造跳转指令字节码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
/// Construct jump
/// </summary>
/// <param name="pThunk">Data to initialize</param>
/// <param name="To">Address of jump</param>
VOID PHpInitJumpThunk( IN OUT PJUMP_THUNK pThunk, IN ULONG64 To )
{
PULARGE_INTEGER liTo = (PULARGE_INTEGER)&To;// 无符号64位整数

pThunk->PushOp = 0x68;// 推入32位立即数
pThunk->AddressLow = liTo->LowPart;// 低32位地址
pThunk->MovOp = 0x042444C7;// 将32位常数移动到指定内存
pThunk->AddressHigh = liTo->HighPart;// 高32位地址
pThunk->RetOp = 0xC3;// 返回指令
}

每个CPU页面钩子和解钩的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// Per-CPU page hook/unhook routine
/// </summary>
/// <param name="Dpc">Unused</param>
/// <param name="Context">Valid PHOOK_CONTEXT</param>
/// <param name="SystemArgument1">Unused</param>
/// <param name="SystemArgument2">Unused</param>
VOID PHpHookCallbackDPC( IN PRKDPC Dpc, IN PVOID Context, IN PVOID SystemArgument1, IN PVOID SystemArgument2 )
{
UNREFERENCED_PARAMETER( Dpc );
PHOOK_CONTEXT pCTX = (PHOOK_CONTEXT)Context;// hook上下文

if (pCTX != NULL)// 进行hook或unhook
__vmx_vmcall( pCTX->Hook ? HYPERCALL_HOOK_PAGE : HYPERCALL_UNHOOK_PAGE, pCTX->DataPagePFN, pCTX->CodePagePFN, 0 );

KeSignalCallDpcSynchronize( SystemArgument2 );// 发出同步信号
KeSignalCallDpcDone( SystemArgument1 );// 通知DPC执行完成
}

解除页面钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/// <summary>
/// Restore hooked function
/// </summary>
/// <param name="pFunc">Function address</param>
/// <returns>Status code</returns>
NTSTATUS PHRestore( IN PVOID pFunc )
{
// 不支持执行保护,无法恢复
if (!g_Data->Features.ExecOnlyEPT)
return STATUS_NOT_SUPPORTED;

// 获取要恢复函数的PAGE_HOOK_ENTRY结构体
PPAGE_HOOK_ENTRY pHookEntry = PHGetHookEntry( pFunc );
if (pHookEntry == NULL)
return STATUS_NOT_FOUND;

// 恢复原始字节,根据页面钩子数量
if (PHPageHookCount( pFunc, DATA_PAGE ) > 1)
{
// 存在其他CPU正在使用页面钩子,需要进行原子性的内存补丁操作
// 将原始的字节数据从钩子页中复制回原始函数所在的页面。这样可以确保其他CPU在执行期间的一致性。
ULONG_PTR page_offset = (ULONG_PTR)pFunc - (ULONG_PTR)PAGE_ALIGN( pFunc );
memcpy( (PUCHAR)pHookEntry->CodePageVA + page_offset, pHookEntry->OriginalData, pHookEntry->OriginalSize );
}
// 只被一个CPU钩子的函数,将钩子所在页面与原始函数所在的页面进行交换,以还原函数的内容
else
{
HOOK_CONTEXT ctx = { 0 };// 临时HOOK_CONTEXT结构体
ctx.Hook = FALSE;
ctx.DataPagePFN = pHookEntry->DataPagePFN;
ctx.CodePagePFN = pHookEntry->CodePagePFN;;

// 使用Dpc回调Unhook模式
KeGenericCallDpc( PHpHookCallbackDPC, &ctx );
}

// 释放钩子信息缓存
MmFreeContiguousMemory( pHookEntry->CodePageVA );
RemoveEntryList( &pHookEntry->Link );
ExFreePoolWithTag( pHookEntry, HB_POOL_TAG );

return STATUS_SUCCESS;
}

计算当前函数有多少个hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// <summary>
/// Get number of hooks in one page
/// </summary>
/// <param name="ptr">Function address</param>
/// <param name="Type">Page type</param>
/// <returns>Number of hooks</returns>
ULONG PHPageHookCount( IN PVOID ptr, IN PAGE_TYPE Type )
{
ULONG count = 0;
// 检查是否为空
if (g_PageList.Flink == NULL || IsListEmpty( &g_PageList ))
return count;

// 获取页面地址
PVOID page = PAGE_ALIGN( ptr );
// 遍历g_PageList查找匹配的PAGE_HOOK_ENTRY结构体
for (PLIST_ENTRY pListEntry = g_PageList.Flink; pListEntry != &g_PageList; pListEntry = pListEntry->Flink)
{
PPAGE_HOOK_ENTRY pEntry = CONTAINING_RECORD( pListEntry, PAGE_HOOK_ENTRY, Link );
// 判断钩子的页面类型、数据是否匹配
if ((Type == DATA_PAGE && pEntry->DataPageVA == page) || (Type == CODE_PAGE && pEntry->CodePageVA == page))
count++;
}

return count;
}

系统调用入口钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/// <summary>
/// Perform LSTAR hooking
/// </summary>
/// <returns>Status code</returns>
NTSTATUS SHInitHook()
{
/*
LSTAR是一个MSR(Machine Specific Register),它保存了系统调用(system call)的入口地址,
hooking LSTAR就可以实现对所有系统调用的监控和拦截。
*/
NTSTATUS status = STATUS_SUCCESS;

// 没有SSDT
if (!UtilSSDTBase())
{
DPRINT( "HyperBone: CPU %d: %s: SSDT base not found\n", CPU_IDX, __FUNCTION__ );
return STATUS_NOT_FOUND;
}

// KiSystemServiceCopyEnd
// 这个函数用于将系统调用的参数从用户空间复制到内核空间并调用具体的系统调用处理函数
// F7 05 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0F 85 ? ? ? ? ? ? ? ? 41 FF D2
if (KiServiceCopyEndPtr == 0)
{
// 搜索KiSystemServiceCopyEnd函数特征码找到地址
CHAR pattern[] = "\xF7\x05\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x0F\x85\xcc\xcc\xcc\xcc\x41\xFF\xD2";
status = UtilScanSection( ".text", (PCUCHAR)pattern, 0xCC, sizeof( pattern ) - 1, (PVOID)&KiServiceCopyEndPtr );
if (!NT_SUCCESS( status ))
{
DPRINT( "HyperBone: CPU %d: %s: KiSystemServiceCopyEnd not found\n", CPU_IDX, __FUNCTION__ );
return status;
}
}

// Hook LSTAR
if (KiSystemCall64Ptr == 0)
{
KiSystemCall64Ptr = __readmsr( MSR_LSTAR );// 原始LSTAR地址

// Something isn't right
if (KiSystemCall64Ptr == 0)
return STATUS_UNSUCCESSFUL;

// 在DPC回调中hook,修改LSTAR寄存器的值为SyscallEntryPoint定义的函数
KeGenericCallDpc( SHpHookCallbackDPC, (PVOID)(ULONG_PTR)SyscallEntryPoint );
return STATUS_SUCCESS;
}

return STATUS_SUCCESS;
}

在内核PE文件的指定section中查找指定的pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary>
/// Find pattern in kernel PE section
/// </summary>
/// <param name="section">Section name</param>
/// <param name="pattern">Pattern data</param>
/// <param name="wildcard">Pattern wildcard symbol</param>
/// <param name="len">Pattern length</param>
/// <param name="ppFound">Found address</param>
/// <returns>Status code</returns>
NTSTATUS UtilScanSection( IN PCCHAR section, IN PCUCHAR pattern, IN UCHAR wildcard, IN ULONG_PTR len, OUT PVOID* ppFound )
{
NT_ASSERT( ppFound != NULL );
if (ppFound == NULL)
return STATUS_INVALID_PARAMETER;

PVOID base = UtilKernelBase( NULL );// 获取NTOS内核的基址
if (!base)
return STATUS_NOT_FOUND;

PIMAGE_NT_HEADERS64 pHdr = RtlImageNtHeader( base );// 获取NTOS内核的PE头
if (!pHdr)
return STATUS_INVALID_IMAGE_FORMAT;

PIMAGE_SECTION_HEADER pFirstSection = (PIMAGE_SECTION_HEADER)(pHdr + 1);// 获取NTOS内核的第一个节表
// 遍历所有节表,进行模式匹配
for (PIMAGE_SECTION_HEADER pSection = pFirstSection; pSection < pFirstSection + pHdr->FileHeader.NumberOfSections; pSection++)
{
ANSI_STRING s1, s2;
RtlInitAnsiString( &s1, section );
RtlInitAnsiString( &s2, (PCCHAR)pSection->Name );
if (RtlCompareString( &s1, &s2, TRUE ) == 0)
return UtilSearchPattern( pattern, wildcard, len, (PUCHAR)base + pSection->VirtualAddress, pSection->Misc.VirtualSize, ppFound );
}

return STATUS_NOT_FOUND;
}

LSTAR Hook 回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// Per-CPU LSTAR hook/unhook routine
/// </summary>
/// <param name="Dpc">Unused</param>
/// <param name="Context">New LASTAR value if hooking, 0 if unhooking</param>
/// <param name="SystemArgument1">Unused</param>
/// <param name="SystemArgument2">Unused</param>
VOID SHpHookCallbackDPC( PRKDPC Dpc, PVOID Context, PVOID SystemArgument1, PVOID SystemArgument2 )
{
UNREFERENCED_PARAMETER( Dpc );

// 判断Context确定hook或unhook操作,进行VMCALL调用
__vmx_vmcall( Context != NULL ? HYPERCALL_HOOK_LSTAR : HYPERCALL_UNHOOK_LSTAR, (ULONG64)Context, 0, 0 );
KeSignalCallDpcSynchronize( SystemArgument2 );// 通知等待同步
KeSignalCallDpcDone( SystemArgument1 );// 通知DPC执行完成
}

SyscallEntryPoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
EXTERN HookEnabled:DB
EXTERN ArgTble:DB
EXTERN HookTable:DQ

EXTERN KiSystemCall64Ptr:DQ
EXTERN KiServiceCopyEndPtr:DQ

USERMD_STACK_GS = 10h
KERNEL_STACK_GS = 1A8h

MAX_SYSCALL_INDEX = 1000h

.CODE

; *********************************************************
;
; Determine if the specific syscall should be hooked
;
; if (SyscallHookEnabled[EAX & 0xFFF] == TRUE)
; jmp KiSystemCall64_Emulate
; else (fall-through)
; jmp KiSystemCall64
;
; *********************************************************
SyscallEntryPoint PROC
;cli ; Disable interrupts
swapgs ; 将GS寄存器切换为内核PCR
mov gs:[USERMD_STACK_GS], rsp ; 将当前用户栈保存,以便调用返回时恢复

cmp rax, MAX_SYSCALL_INDEX ; 检查rax寄存器的值是否大于系统调用数组的大小
jge KiSystemCall64 ; 大于,表示该调用不存在,跳转到KiSystemCall64

lea rsp, offset HookEnabled ; 检查Hook是否被启用
cmp byte ptr [rsp + rax], 0 ; 检查Hook是否被启用
jne KiSystemCall64_Emulate ; 跳转进行KiSystemCall64_Emulate,否则进行KiSystemCall64
SyscallEntryPoint ENDP

; *********************************************************
;
; 返回到原始NTOSKRNL系统调用处理程序
; (Restore all old registers first)
;
; *********************************************************
KiSystemCall64 PROC
mov rsp, gs:[USERMD_STACK_GS] ; 取出用户模式RSP加载到RSP,切换到用户模式栈
swapgs ; 切换到用户模式GS寄存器,恢复原先寄存器
jmp [KiSystemCall64Ptr] ; 跳转到KiSystemCall64Ptr所指向的地址,64位指针
KiSystemCall64 ENDP

; *********************************************************
;
; 在SYSCALL之后直接执行的例程
; (See: MSR_LSTAR)
;
; *********************************************************
KiSystemCall64_Emulate PROC
; NOTE:
; First 2 lines are included in SyscallEntryPoint

mov rsp, gs:[KERNEL_STACK_GS] ; 加载内核模式RSP
push 2Bh ; push 虚拟的SS选择子
push qword ptr gs:[10h] ; push 用户模式栈指针
push r11 ; push 先前的 EFLAGS
push 33h ; push 虚拟 64位 CS 选择子
push rcx ; push 返回地址
mov rcx, r10 ; 在rcx中保存第一个参数

sub rsp, 8h ; 分配8字节作为虚拟错误码
push rbp ; 保存标准寄存器的值
sub rsp, 158h ; 分配一个固定大小的框架
lea rbp, [rsp+80h] ; 设置RBP为当前帧的基地址
mov [rbp+0C0h], rbx ; 保存一些非易失性的寄存器的值
mov [rbp+0C8h], rdi ;
mov [rbp+0D0h], rsi ;
mov byte ptr [rbp-55h], 2h ; 标志位,表示服务活动
mov rbx, gs:[188h] ; 获取当前线程地址
prefetchw byte ptr [rbx+90h] ; 对线程地址进行预取操作
stmxcsr dword ptr [rbp-54h] ; 保存当前MXCSR
ldmxcsr dword ptr gs:[180h] ; 设置默认MXCSR
cmp byte ptr [rbx+3], 0 ; 检查调试是否启用
mov word ptr [rbp+80h], 0 ; 如果调试未启用
jz KiSS05 ; 跳转到KiSS05标签处
mov [rbp-50h], rax ; 保存服务参数寄存器
mov [rbp-48h], rcx ;
mov [rbp-40h], rdx ;
mov [rbp-38h], r8 ;
mov [rbp-30h], r9 ;

int 3 ; 触发一个中断
align 10h

KiSS05:
;sti ; 启动中断
mov [rbx+88h], rcx
mov [rbx+80h], eax

KiSystemCall64_Emulate ENDP

EFLAGS

EFLAGS是x86架构中特有的寄存器,用于存储和控制处理器的状态标志位(flags)。它记录了CPU执行指令过程中产生的各种状态信息。

EFLAGS寄存器的位布局如下:

1
2
3
4
5
6
31      23           15          7    0
┌───────┬───────────┬───────────┬────┐
| | | | |
| RFU | VM, VIP | ID, VIP | AC |
| | | | |
└───────┴───────────┴───────────┴────┘

各个标志位的含义如下:

  • AC (Alignment Check):对齐检查标志位,用于检测内存操作的对齐情况。
  • ID (ID Flag):识别标志位,用于表示CPU是否支持CPUID指令。
  • VIP (Virtual Interrupt Pending):虚拟中断挂起标志位,在虚拟8086模式下使用。
  • VIF (Virtual Interrupt Flag):虚拟中断标志位,在虚拟8086模式下使用。
  • OF (Overflow Flag):溢出标志位,用于检测有符号整数运算结果是否溢出。
  • DF (Direction Flag):方向标志位,用于控制字符串操作指令的方向。
  • IF (Interrupt Flag):中断标志位,用于控制CPU是否响应外部中断。
  • TF (Trap Flag):陷阱标志位,用于控制CPU是否进入单步执行模式。
  • SF (Sign Flag):符号标志位,用于表示有符号整数运算结果的符号。
  • ZF (Zero Flag):零标志位,用于表示算术或逻辑运算结果是否为零。
  • AF (Auxiliary Carry Flag):辅助进位标志位,用于检测无符号整数运算时的进位情况。
  • PF (Parity Flag):奇偶标志位,用于表示运算结果的低8位中1的个数的奇偶性。
  • CF (Carry Flag):进位标志位,用于检测无符号整数运算结果是否产生进位。

这些标志位的状态可以通过指令读取或修改,用于判断和控制程序的执行流程。例如,通过检查ZF标志位可以判断某个运算结果是否为零,从而进行条件分支或循环的控制。

MXCSR

MXCSR是x86架构中的一种控制寄存器,全称为”Floating-Point Control and Status Register”,它用于管理和控制浮点运算的行为和状态。

MXCSR寄存器是一个32位的寄存器,其位布局如下:

1
2
3
4
Copy Code31        15       7      0
┌─────────┬───────┬───────┬─────┐
│ RFU │ RC │ PC │ PM │
└─────────┴───────┴───────┴─────┘

各个标志位的含义如下:

  • PM (Precision Mask):精度掩码位,用于控制浮点运算结果的舍入精度。
  • PC (Precision Control):精度控制位,用于设置浮点运算结果的默认舍入精度模式。
  • RC (Rounding Control):舍入控制位,用于设置浮点运算结果的舍入方式。
  • RFU (Reserved for Future Use):保留字段,暂未使用。

通过对MXCSR寄存器的设置,可以控制浮点运算的舍入方式、精度和异常处理等行为。例如,可以设置精度掩码位PM来屏蔽或允许特定类型的浮点异常,或者通过设置精度控制位PC来指定浮点运算结果的默认舍入精度模式。

MXCSR寄存器可以通过指令进行读取和修改,例如LDMXCSR和STMXCSR指令。它对于进行精确的浮点计算和处理浮点异常非常重要,能够提供更好的浮点运算控制和性能优化。

CS选择子

在x86架构中,CS(Code Segment)是一种代码段寄存器,主要用于存储当前程序正在执行的代码所在的段的信息。CS选择子则是对CS寄存器进行访问的一种方式。

CS选择子是一个16位的数据结构,包含了以下信息:

  • 段选择子:用于指向代码段的段描述符,其中包含了代码段的起始地址、大小、特权级等信息。
  • 请求特权级(RPL):用于指定代码段的访问权限,取值为0~3。

CS选择子可以通过一些指令读取或修改,例如LAR指令、LDS指令、LSS指令等。在进行指令跳转时,CPU会使用CS选择子来确定下一条指令的地址。因此,CS选择子的正确设置非常重要,对程序的执行具有至关重要的影响。

需要注意的是,CS选择子只是用于访问CS寄存器的一种方式,它并不是CS寄存器本身。CS寄存器还可以通过其他方式进行访问,例如POP CS指令、IRET指令等。

系统调用钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/// <summary>
/// Hook specific SSDT entry
/// </summary>
/// <param name="index">SSDT index</param>
/// <param name="hookPtr">Hook address</param>
/// <param name="argCount">Number of function arguments</param>
/// <returns>Status code</returns>
NTSTATUS SHHookSyscall( IN ULONG index, IN PVOID hookPtr, IN CHAR argCount )
{
NTSTATUS status = STATUS_SUCCESS;
if (index > MAX_SYSCALL_INDEX || hookPtr == NULL)// 超过最大系统调用索引或者hook地址为空
return STATUS_INVALID_PARAMETER;

KIRQL irql = KeGetCurrentIrql();// 获取当前IRQL(中断请求级别)
if (irql < DISPATCH_LEVEL)
// 如果小于调度级别,提升到调度级别
irql = KeRaiseIrqlToDpcLevel();

// 保存原始hook地址和参数个数
// 保证多线程环境下对HookTable、ArgTble、HookEnabled的操作的原子性
InterlockedExchange64( (PLONG64)&HookTable[index], (LONG64)hookPtr );
InterlockedExchange8( &ArgTble[index], argCount );
InterlockedExchange8( &HookEnabled[index], TRUE );

// 恢复权限
if (KeGetCurrentIrql() > irql)
KeLowerIrql( irql );

return status;
}

hkNtClose

1
2
3
4
5
NTSTATUS hkNtClose( HANDLE handle )
{
calls1++;// 记录hkNtClose函数被调用的次数
return ((pfnNtClose)g_NtClose)(handle);// 使用函数指针类型pfnNtClose调用了全局变量g_NtClose所指向的原始NtClose系统调用函数,并将handle作为参数传递给原始函数
}

hkNtClose2

1
2
3
4
5
6
7
8
9
10
11
12
NTSTATUS hkNtClose2( HANDLE handle )
{
PPAGE_HOOK_ENTRY pEntry = PHGetHookEntry( g_NtClose );// 获取全局系统调用函数的hook entry
if (pEntry)
{
calls2++;// 记录hkNtClose2函数被调用的次数
// 使用函数指针类型pfnNtClose调用了hook entry的OriginalData字段所指向的原始NtClose系统调用函数,并将handle作为参数传递给原始函数
return ((pfnNtClose)pEntry->OriginalData)(handle);
}

return STATUS_SUCCESS;
}

驱动卸载

1
2
3
4
5
6
7
8
9
10
11
VOID HBUnload( IN PDRIVER_OBJECT DriverObject )
{
UNREFERENCED_PARAMETER( DriverObject );

TestPrintResults();// 打印测试结果
TestStop();// 停止测试

NTSTATUS status = StopHV();// 停止虚拟化
DPRINT( "HyperBone: CPU %d: %s: Unload %s\n", CPU_IDX, __FUNCTION__, NT_SUCCESS( status ) ? "SUCCEDED" : "FAILED" );
FreeGlobalData( g_Data );// 释放全局变量
}

打印测试结果

1
2
3
4
5
VOID TestPrintResults()
{
DPRINT( "HyperBone: CPU %d: %s: SyscallHook Calls made %d\n", CPU_IDX, __FUNCTION__, calls1 );
DPRINT( "HyperBone: CPU %d: %s: PageHook Calls made %d\n", CPU_IDX, __FUNCTION__, calls2 );
}

停止测试

1
2
3
4
5
6
VOID TestStop()
{
PHRestore( g_NtClose );// 恢复系统调用钩子
SHRestoreSyscall( SSDTIndex( &ZwClose ) );// 恢复原始SSDT表入口
SHDestroyHook();// 释放LSTAR钩子
}

恢复SSDT表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// <summary>
/// Restore original SSDT entry
/// </summary>
/// <param name="index">SSDT index</param>
/// <returns>Status code</returns>
NTSTATUS SHRestoreSyscall( IN ULONG index )
{
// 检查最大系统调用索引
if (index > MAX_SYSCALL_INDEX)
return STATUS_INVALID_PARAMETER;

// 检查权限
KIRQL irql = KeGetCurrentIrql();
if (irql < DISPATCH_LEVEL)
irql = KeRaiseIrqlToDpcLevel();

// 恢复原始系统调用表
InterlockedExchange8( &HookEnabled[index], 0 );
InterlockedExchange8( &ArgTble[index], 0 );
InterlockedExchange64( (PLONG64)&HookTable[index], 0 );

// 还原权限
if( KeGetCurrentIrql() > irql )
KeLowerIrql( irql );

return STATUS_SUCCESS;
}

释放LSTAR钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// Unhook LSTAR
/// </summary>
/// <returns>Status code</returns>
NTSTATUS SHDestroyHook()
{
NTSTATUS status = STATUS_SUCCESS;
if (KiSystemCall64Ptr != 0)
// 有hook,DPC回调恢复原始LSTAR地址
KeGenericCallDpc( SHpHookCallbackDPC, NULL );

if (NT_SUCCESS( status ))
// 取消钩子
KiSystemCall64Ptr = 0;

return status;
}