crewCTF 2025 -- Hypervison
Hyper-V is a well known, well documented virtualisation framework with excellent tooling. Wait, no that's not it.
首先创建虚拟机和处理器,映射内存。都是一些 API 调用。

接下来是初始化寄存器。
从 WHvSetVirtualProcessorRegisters 看出,一共有 20 个寄存器。动态调试看看有那些。

其中前 10 个寄存器是固定的,后 10 个寄存器用输入的 flag 值来填充。

验证一下,导出 RegisterValues


从图中可以看出,flag 前 64 个字填充到 r8 - r15,正好是 8 个 64 位寄存器(8 * 64 = 512)。后面的内容忽略,最后两个寄存器 rbx 和 rdx 是 0。
前 10 个寄存器大部分是控制寄存器,写个程序。
点击查看代码
#include <stdio.h>
#include <stdint.h>
#define FIELD(r, x, len) (((r)>>(x)) & ((1ull<<(len))-1))
#define BIT(r, x) FIELD(r, x, 1)
#define CR0_PE(r) BIT(r, 0)
#define CR0_MP(r) BIT(r, 1)
#define CR0_EM(r) BIT(r, 2)
#define CR0_TS(r) BIT(r, 3)
#define CR0_ET(r) BIT(r, 4)
#define CR0_NE(r) BIT(r, 5)
#define CR0_WP(r) BIT(r, 16)
#define CR0_AM(r) BIT(r, 18)
#define CR0_NW(r) BIT(r, 29)
#define CR0_CD(r) BIT(r, 30)
#define CR0_PG(r) BIT(r, 31)
#define CR3_PWT(r) BIT(r, 3)
#define CR3_PCD(r) BIT(r, 4)
#define CR3_PML4_BASE_ADDRESS(r) FIELD(r, 12, 40)
#define CR4_VME(r) BIT(r, 0)
#define CR4_PVI(r) BIT(r, 1)
#define CR4_TSD(r) BIT(r, 2)
#define CR4_DE(r) BIT(r, 3)
#define CR4_PSE(r) BIT(r, 4)
#define CR4_PAE(r) BIT(r, 5)
#define CR4_MCE(r) BIT(r, 6)
#define CR4_PGE(r) BIT(r, 7)
#define CR4_PCE(r) BIT(r, 8)
#define CR4_OSFXSR(r) BIT(r, 9)
#define CR4_OSXMMEXCEPT(r) BIT(r, 10)
#define CR4_OSXSAVE(r) BIT(r, 18)
#define EFER_SCE(r) BIT(r, 0)
#define EFER_LME(r) BIT(r, 8)
#define EFER_LMA(r) BIT(r, 10)
#define EFER_NXE(r) BIT(r, 11)
#pragma pack(push, 8)
typedef struct _WHV_X64_SEGMENT_REGISTER
{
uint64_t Base;
uint32_t Limit;
uint16_t Selector;
#pragma pack(push, 8)
union
{
#pragma pack(push, 8)
struct
{
uint16_t SegmentType : 4;
uint16_t NonSystemSegment : 1;
uint16_t DescriptorPrivilegeLevel : 2;
uint16_t Present : 1;
uint16_t Reserved : 4;
uint16_t Available : 1;
uint16_t Long : 1;
uint16_t Default : 1;
uint16_t Granularity : 1;
};
#pragma pack(pop)
uint16_t Attributes;
};
#pragma pack(pop)
}WHV_X64_SEGMENT_REGISTER;
#pragma pack(pop)
int main()
{
FILE* fp = fopen("reg_value.bin","rb");
uint8_t buffer[16];
fread(buffer, 1, 16, fp);
uint64_t cr0 = *(uint64_t*)buffer;
printf("CR0 value: 0x%016llx\n", cr0);
printf("bit 0 : %d - PE (Protection Enable)\n", CR0_PE(cr0));
printf("bit 1 : %d - MP (Monitor Coprocessor)\n", CR0_MP(cr0));
printf("bit 2 : %d - EM (Emulation)\n", CR0_EM(cr0));
printf("bit 3 : %d - TS (Task Switched)\n", CR0_TS(cr0));
printf("bit 4 : %d - ET (Extension Type)\n", CR0_ET(cr0));
printf("bit 5 : %d - NE (Numeric Error)\n", CR0_NE(cr0));
printf("bit 16 : %d - WP (Write Protect)\n", CR0_WP(cr0));
printf("bit 18 : %d - AM (Alignment Mask)\n", CR0_AM(cr0));
printf("bit 29 : %d - NW (Not Write-through)\n", CR0_NW(cr0));
printf("bit 30 : %d - CD (Cache Disable)\n", CR0_CD(cr0));
printf("bit 31 : %d - PG (Paging)\n", CR0_PG(cr0));
printf("\n");
fread(buffer, 1, 16, fp);
uint64_t cr3 = *(uint64_t*)buffer;
printf("CR3 value: 0x%016llx\n", cr3);
printf("bit 3 : %d - PWT (Page Write Through)\n", CR3_PWT(cr3));
printf("bit 4 : %d - PCD (Page Cache Disable)\n", CR3_PCD(cr3));
printf("PML4 base Address : %x000\n", CR3_PML4_BASE_ADDRESS(cr3));
printf("\n");
fread(buffer, 1, 16, fp);
uint64_t cr4 = *(uint64_t*)buffer;
printf("CR4 value: 0x%016llx\n", cr4);
printf("bit 0 : %d - VME (Virtual-8086 Mode Extensions)\n", CR4_VME(cr4));
printf("bit 1 : %d - PVI (Protected-Mode Virtual Interrupts)\n", CR4_PVI(cr4));
printf("bit 2 : %d - TSD (Time Stamp Disable)\n", CR4_TSD(cr4));
printf("bit 3 : %d - DE (Debugging Extensions)\n", CR4_DE(cr4));
printf("bit 4 : %d - PSE (Page Size Extensions)\n", CR4_PSE(cr4));
printf("bit 5 : %d - PAE (Physical Address Extension)\n", CR4_PAE(cr4));
printf("bit 6 : %d - MCE (Machine Check Enable)\n", CR4_MCE(cr4));
printf("bit 7 : %d - PGE (Page-Global Enable)\n", CR4_PGE(cr4));
printf("bit 8 : %d - PCE (Performance-Monitoring Counter Enable)\n", CR4_PCE(cr4));
printf("bit 9 : %d - OSFXSR (OS FXSAVE/FXRESTOR Support)\n", CR4_OSFXSR(cr4));
printf("bit 10 : %d - OSXMMEXCEPT (OS Unmasked Exception Support)\n", CR4_OSXMMEXCEPT(cr4));
printf("bit 18 : %d - OSXSAVE (XSAVE and Processor Extended States Enable Bit)\n", CR4_OSXSAVE(cr4));
printf("\n");
fread(buffer, 1, 16, fp);
uint64_t efer = *(uint64_t*)buffer;
printf("EFER value: 0x%016llx\n", efer);
printf("bit 0 : %d - SCE (SYSCALL Enable)\n", EFER_SCE(efer));
printf("bit 8 : %d - LME (IA-32e Mode Enable)\n", EFER_LME(efer));
printf("bit 10 : %d - LMA (IA-32e Mode Active)\n", EFER_LMA(efer));
printf("bit 11 : %d - NXE (Execution Disable Bit Enable)\n", EFER_NXE(efer));
printf("\n");
WHV_X64_SEGMENT_REGISTER seg;
fread(&seg, 16, 1, fp);
printf("CS\n");
printf("base = %016llx, limit = %08x, selector = %04x\n", seg.Base, seg.Limit, seg.Selector);
printf("SegmentType = %d\n", seg.SegmentType);
printf("NonSystemSegment = %d\n", seg.NonSystemSegment);
printf("DescriptorPrivilegeLevel = %d\n", seg.DescriptorPrivilegeLevel);
printf("Present = %d\n", seg.Present);
printf("Available = %d\n", seg.Available);
printf("Long = %d\n", seg.Long);
printf("Default = %d\n", seg.Default);
printf("Granularity = %d\n", seg.Granularity);
printf("\n");
fread(&seg, 16, 1, fp);
printf("SS\n");
printf("base = %016llx, limit = %08x, selector = %04x\n", seg.Base, seg.Limit, seg.Selector);
printf("SegmentType = %d\n", seg.SegmentType);
printf("NonSystemSegment = %d\n", seg.NonSystemSegment);
printf("DescriptorPrivilegeLevel = %d\n", seg.DescriptorPrivilegeLevel);
printf("Present = %d\n", seg.Present);
printf("Available = %d\n", seg.Available);
printf("Long = %d\n", seg.Long);
printf("Default = %d\n", seg.Default);
printf("Granularity = %d\n", seg.Granularity);
printf("\n");
fread(&seg, 16, 1, fp);
printf("DS\n");
printf("base = %016llx, limit = %08x, selector = %04x\n", seg.Base, seg.Limit, seg.Selector);
printf("SegmentType = %d\n", seg.SegmentType);
printf("NonSystemSegment = %d\n", seg.NonSystemSegment);
printf("DescriptorPrivilegeLevel = %d\n", seg.DescriptorPrivilegeLevel);
printf("Present = %d\n", seg.Present);
printf("Available = %d\n", seg.Available);
printf("Long = %d\n", seg.Long);
printf("Default = %d\n", seg.Default);
printf("Granularity = %d\n", seg.Granularity);
printf("\n");
fread(&seg, 16, 1, fp);
printf("ES\n");
printf("base = %016llx, limit = %08x, selector = %04x\n", seg.Base, seg.Limit, seg.Selector);
printf("SegmentType = %d\n", seg.SegmentType);
printf("NonSystemSegment = %d\n", seg.NonSystemSegment);
printf("DescriptorPrivilegeLevel = %d\n", seg.DescriptorPrivilegeLevel);
printf("Present = %d\n", seg.Present);
printf("Available = %d\n", seg.Available);
printf("Long = %d\n", seg.Long);
printf("Default = %d\n", seg.Default);
printf("Granularity = %d\n", seg.Granularity);
printf("\n");
fread(buffer, 1, 16, fp);
uint64_t rip = *(uint64_t*)buffer;
printf("RIP = 0x%016llx\n", rip);
printf("\n");
fread(buffer, 1, 16, fp);
uint64_t rsp = *(uint64_t*)buffer;
printf("RSP = 0x%016llx\n", rsp);
printf("\n");
fclose(fp);
return 0;
}
输出结果太长了,比较重要的大概是这些:
CR3 value: 0x00000000001fc000
bit 3 : 0 - PWT (Page Write Through)
bit 4 : 0 - PCD (Page Cache Disable)
PML4 base Address : 1fc000
RIP = 0x0000000000000008
RSP = 0x000000000badc0de
运行结束后,首先检查退出必须是 hlt 指令。其次查询脏页(被修改的内存页)位图,位图要刚好和输入的 flag 相同。

主程序分析完了,我们来看看虚拟机程序。把 SourceAddress 导出,长度为 0x200000(2MB),保存为 code.bin
用 ida 打开(MetaPC,64-bit mode),发现每个页(4KB)几乎一模一样,结构如图

写个脚本批量设置函数
for ea in range(0,0x200000,0x1000):
idc.del_items(ea)
idc.del_items(ea + 8)
idc.create_insn(ea + 8)
if not idc.add_func(ea + 8,ea + 0x70):
print(f'0x{ea:x} add_func failed')
if not idc.create_align(ea + 0x71, 0, 12):
print(f'0x{ea:x} create_align failed')
分析函数的功能。首先检查 r15 某些位是否是 1。如果是,执行一个 push;否则进入循环。
循环体相当于把 r8 ~ r15 组成一个大寄存器,每次左移 1 位,通过溢出的位决定 cr3 的赋值


浙公网安备 33010602011771号