-
-
[原创][原创]2026腾讯游戏安全PC决赛Writeup
-
发表于: 10小时前 424
-
记录一下今年 2026 决赛过程
题目描述
(1) 「影」核心系统「根」需要在一些开启了特殊特性的机器上才能部署成功,逆向找到成功部署条件!成功部署「影」核心系统,即成功运行shadow_panel.exe,控制台程序成功运行进入至输入终止密码的终端。(满分0.5分)
(2) 「根」使用特殊方法,对操作系统底层进行了攻击,并借此将关键核心代码隐藏了起来,分析其完整实现流程。(满分2.5分)
(3) 编写检测代码,检测(2)中「影」核心系统攻击操作系统底层的特殊方法。(满分2分)
(4) 计算出正确的终止密码,输入到shadow_panel.exe中,使得其返回成功。(满分1.0分)
(5) 编写keygen,使得在任意机器,任何一次运行shadow_panel.exe,都可以正确计算出终止密码。(满分2.5分)
(6) 详细描述完整的解题过程和思路,提供所有编写的程序的源代码。做到清晰易懂,操作可以复现结果。编码工整风格优雅、注释详尽。相同实现方法下,提交时间靠前者得分更高,AI可用于辅助分析,其产出的内容(代码/文档等)需明确标注并提交完整提示词和**聊天记录,**赛事方针对此项的判断具有最终解释权。(满分1.5分
(1)成功部署条件
有三个:
- 开启 VT-x 虚拟化,从它针对 Intel 加初赛的flag就能看出来,一定要开这个。
- 开启 Hyper-V,应用命令
bcdedit /set {current} hypervisorlaunchtype auto即可。 - 管理员运行。
这三个条件满足之后,成功进入系统:

(2,4)找出隐藏的代码&计算Key
结论
「根」系统采用 EPT Shadow Page(EPT影子页) 技术攻击操作系统底层。该技术利用Hyper-V hypervisor提供的Extended Page Tables(EPT,扩展页表)机制,在CPU的内存地址翻译层面实现"读/执行分离"——同一个虚拟地址的读操作和执行操作被映射到不同的物理内存页。这使得PatchGuard等内核完整性校验机制读取到的是未修改的原始代码,而CPU实际执行的却是被篡改后的恶意代码。攻击完成后,驱动自行卸载销毁,但EPT修改作为hypervisor层的配置将持续生效,实现了真正意义上的"代码隐藏"。
分析
根据题目要求,sub_144A07540 检测环境是否满足要求:

通过两次 cpuid 的调用,第一次检测 ECX.bit31(Hypervisor Present位),第二次检测Hypervisor厂商字符串为 "Microsoft Hv"(Hyper-V),经过这里的分析,第一问的配置就出来了,需要开启 hyper-v。
把驱动提取一下,断 ControlService,然后找到驱动文件即可。

通过 pdb 文件可知,驱动原始名字为 hypercharge.sys,加载的时候会把名字随机化。
lumina可以识别少量函数,0x140001060 检测了 CPU 的特性。

0x14000BEB0 检查了当前 CPU 是 Intel 还是 AMD,决定后续EPT/NPT操作的代码路径,我的 CPU 是 Intel。

继续往下跟,驱动本身没有导入表,很多 API 都是通过 FNV-1a hash 动态解析的。比较关键的几个 hash 如下:
| hash | api | 作用 |
|---|---|---|
0x2F0FCEED6FC55D71 |
RtlGetVersion |
获取系统版本 |
0x8CD9141D23428B07 |
MmAllocateContiguousMemory |
分配连续物理内存 |
0x8A0AB57E2BF51C65 |
MmGetPhysicalMemoryRanges |
获取物理内存范围 |
0x306328EE8E049A39 |
MmCopyMemory |
读取物理内存 |
0x40ED8EA987B20683 |
MmMapIoSpace |
映射物理页 |
0x47E1CA0E288956C0 |
MmUnmapIoSpace |
取消映射 |
0x0338042AB15F61CC |
MmGetPhysicalAddress |
获取物理地址 |
0x0D9FE78EA5EE16A1 |
KeInitializeEvent |
初始化事件 |
0x1D9B1572A04955D7 |
IoBuildDeviceIoControlRequest |
构造 IOCTL 请求 |
0xC26886D76545A61B |
IofCallDriver |
下发 IRP |
0x11BDFAD31435CB3C |
KeWaitForSingleObject |
等待 IOCTL 完成 |
这里可以看出来它不是简单的驱动 hook,而是在内核里继续调用 Hyper-V 相关的设备接口。
sub_140009530 是一个很关键的函数,它封装了一次操作,大概流程如下:
KeInitializeEvent(&event, 1, 0);
irp = IoBuildDeviceIoControlRequest(
0x4D014,
device_object,
&input,
0x38,
0,
0,
0,
&event,
&iosb);
status = IofCallDriver(device_object, irp);
if (status == STATUS_PENDING)
KeWaitForSingleObject(&event, 0, 0, 0, 0);
其中 0x4D014 是发给 Hyper-V 设备对象的控制码,输入结构大小是 0x38。里面有页大小 0x1000,目标物理地址,还有一个操作码。
操作码这里比较有意思:
op = (2 * (a2 == 42)) | 0x28;
也就是:
a2 == 42,最后 op 为0x2A。a2 != 42,最后 op 为0x28。
因此它操作码只有两种,一个是 0x2A,一个是 0x28
这个就是后面隐藏代码的基础,根据逻辑:

可以大概得到 IoBuildDeviceIoControlRequest 的输入参数结构体:
#pragma pack(push, 1)
typedef struct _HYPER_EPT_IOCTL_INPUT {
uint16_t Size0; // +0x00 = 0x38
uint8_t unk02[4]; // +0x02
uint8_t Type; // +0x06 = 0x0a
uint8_t unk07[5]; // +0x07
uint32_t PageSize; // +0x0c = 0x1000
uint32_t CountOrMode; // +0x10 = 5
uint32_t unk14; // +0x14
uint64_t PageOrPhys; // +0x18 = old_phys / new_phys / qword_141D0FD18+8
uint32_t Size1; // +0x20 = 0x38
uint8_t Operation; // +0x24 = 0x28 or 0x2a
uint8_t unk25[7]; // +0x25 mostly zero
uint8_t Flags; // +0x2c = 8
uint8_t unk2d[0x0b]; // +0x2d
} HYPER_EPT_IOCTL_INPUT;
#pragma pack(pop)
找到hook页面
既然已经分析出了 hook 的函数,那么直接上内核调试器断下看参数即可,应用层调试器断 StartServiceW,内核调试器下一个断点
VOID LoadImageNotifyRoutine(
PUNICODE_STRING FullImageName,
HANDLE ProcessId,
PIMAGE_INFO ImageInfo
)
{
if (!ProcessId && FullImageName && wcsstr(FullImageName->Buffer, L"DC0VBM4HKO"))
{
DBG_PRINT("\n> ============= Driver %ws ================\n", FullImageName->Buffer);
HANDLE hThread;
Hooks::Base = ImageInfo->ImageBase;
Hooks::Size = ImageInfo->ImageSize;
DBG_PRINT("ImageBase: 0x%p\n", (UINT64)ImageInfo->ImageBase);
DBG_PRINT("Breakpoint: 0x%p\n", (UINT64)ImageInfo->ImageBase + 0x9530);
DbgBreakPoint();
count = 0;
}
}
断 StartServiceW 的时候驱动名已经是固定的了,所以直接写死。

分析第一次命中断点

先看看 0x28 操作的表示,rcx 指向了一个 DriverObject,对应了 vhdmp 设备,之前在用户层有见到过 create_vhd 之类的,感觉有一些关联。
不过第三个参数给了一个 nonepaged 内存,上面描述了一些字符信息。

Msft Virtual Disk 1.0,跟到 API 去看,基本也坐实是 vhd 的初始化

后面紧跟着断了一个 0x2a 的操作。
kd> g
8957812500 - STORMINI: StorNVMe - POWER: IDLE
Breakpoint 0 hit
DC0VBM4HKO+0x9530:
fffff804`80789530 4157 push r15
kd> r
rax=ffff9b8f92808000 rbx=ffffb280780af000 rcx=ffff9b8f93cd1060
rdx=ffffe883b4b8732a rsi=ffffb280780ac000 rdi=0004000ffffffffd
rip=fffff80480789530 rsp=ffffe883b4b873a8 rbp=ffffb280780af000
r8=ffffb280780af000 r9=1004000ffffffffd r10=0000000000000001
r11=ffffabd5eaf57000 r12=0000000000000000 r13=40ed8ea987b20683
r14=fffff801153440b9 r15=0000000000001000
iopl=0 nv up ei ng nz na pe nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040282
DC0VBM4HKO+0x9530:
fffff804`80789530 4157 push r15
kd> dq rcx
ffff9b8f`93cd1060 00000000`07c80003 ffff9b8f`8c19e8f0
ffff9b8f`93cd1070 ffff9b8f`8ab56060 ffff9b8f`952cfae0
ffff9b8f`93cd1080 00000000`00000000 00000000`00000000
ffff9b8f`93cd1090 00000100`01002050 ffff9b8f`910d0520
ffff9b8f`93cd10a0 ffff9b8f`93cd11b0 00000002`00000007
ffff9b8f`93cd10b0 00000000`00000000 00000000`00000000
ffff9b8f`93cd10c0 00000000`00000000 00000000`00000000
ffff9b8f`93cd10d0 00000000`00000000 00000000`00000000
kd> dq ffffb280780af000
ffffb280`780af000 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780af010 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780af020 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780af030 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780af040 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780af050 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780af060 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780af070 ffffffff`ffffffff ffffffff`ffffffff
kd> !devobj ffff9b8f93cd1060
Device object (ffff9b8f93cd1060) is for:
DR1 \Driver\disk DriverObject ffff9b8f8c19e8f0
Current Irp 00000000 RefCount 0 Type 00000007 Flags 01002050
Vpb 0xffff9b8f910d0520 SecurityDescriptor ffffc28935bfcaa0 DevExt ffff9b8f93cd11b0 DevObjExt ffff9b8f93cd1828 Dope ffff9b8f910d0440
ExtensionFlags (0000000000)
Characteristics (0x00000100) FILE_DEVICE_SECURE_OPEN
AttachedDevice (Upper) ffff9b8f952cfae0 \Driver\hrdevmon
AttachedTo (Lower) ffff9b8f91e77060 \Driver\vhdmp
Device queue is not busy.
kd> kv
# Child-SP RetAddr : Args to Child : Call Site
00 ffffe883`b4b873a8 fffff804`80783e56 : ffffd3f2`0ddf7517 f1a315f9`d31d2634 00000000`00001000 fffff801`15524300 : DC0VBM4HKO+0x9530
01 ffffe883`b4b873b0 fffff804`8078c485 : 00000000`00040246 ffffe883`b4b87540 fffff804`82718461 00000100`000001b3 : DC0VBM4HKO+0x3e56
02 ffffe883`b4b87400 fffff804`82558a09 : ffffffff`8000191c fffff801`15343f3b 7f2823d9`ac4595a2 fffff804`825556a4 : DC0VBM4HKO+0xc485
03 ffffe883`b4b87790 fffff804`82577026 : 00000000`00000000 00000000`00000000 ffff9b8f`937c7650 ffffe883`b4b87a20 : DC0VBM4HKO+0x1dd8a09
04 ffffe883`b4b87820 fffff804`824b4084 : 00000000`00000016 fffff801`155634f6 00000000`00000002 ffffffff`8000191c : DC0VBM4HKO+0x1df7026
05 ffffe883`b4b87890 fffff801`15961a2c : ffff9b8f`953e6000 00000000`00000000 ffff9b8f`90bf48d0 00000000`00000000 : DC0VBM4HKO+0x1d34084
06 ffffe883`b4b878c0 fffff801`1592d1bd : 00000000`00000016 00000000`00000000 00000000`00000000 00000000`00001000 : nt!PnpCallDriverEntry+0x4c
07 ffffe883`b4b87920 fffff801`159724c7 : 00000000`00000000 00000000`00000000 fffff801`15f25440 00000000`00000000 : nt!IopLoadDriver+0x4e5
08 ffffe883`b4b87af0 fffff801`15452b65 : ffff9b8f`00000000 ffffffff`8000191c ffff9b8f`8abf9040 ffff9b8f`00000000 : nt!IopLoadUnloadDriver+0x57
09 ffffe883`b4b87b30 fffff801`15471d25 : ffff9b8f`8abf9040 00000000`00000080 ffff9b8f`8a076200 000fe47f`b19bbdff : nt!ExpWorkerThread+0x105
0a ffffe883`b4b87bd0 fffff801`15600628 : fffff801`109e2180 ffff9b8f`8abf9040 fffff801`15471cd0 00000000`00000000 : nt!PspSystemThreadStartup+0x55
0b ffffe883`b4b87c20 00000000`00000000 : ffffe883`b4b88000 ffffe883`b4b81000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x28
从调用栈来说,是从 sub_140003E10 调过来的
__int64 __fastcall sub_140003E10(__int64 a1, __int64 a2)
{
__int64 v4; // rdi
__int64 result; // rax
unsigned int v6; // esi
__int64 *v7; // [rsp+28h] [rbp-20h] BYREF
if ( !byte_141D0FD10 )
return 0xC00000A3LL;
v7 = 0;
v4 = sub_140003EB0(a2, &v7);
result = EPT_Operation(*(_QWORD *)qword_141D0FD18, 0x2A, a2);
if ( v7 )
*v7 = v4;
if ( (int)result >= 0 )
{
v6 = EPT_Operation(*(_QWORD *)qword_141D0FD18, 0x28, a1);
EPT_Operation(*(_QWORD *)qword_141D0FD18, 0x2A, qword_141D0FD18 + 8);
return v6;
}
return result;
}
对应的也就是第一次的 EPT_Operation。
第二次调用 sub_140003E10
kd> dq rcx
ffffb280`780ac000 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac010 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac020 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac030 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac040 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac050 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac060 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac070 ffffffff`ffffffff ffffffff`ffffffff
kd> dq rdx
ffffb280`78d9a000 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9a010 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9a020 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9a030 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9a040 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9a050 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9a060 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9a070 ffffffff`ffffffff ffffffff`ffffffff
Breakpoint 3 hit
DC0VBM4HKO+0x3e10:
fffff804`80783e10 56 push rsi
kd> dq rcx
ffffb280`780ac000 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac010 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac020 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac030 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac040 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac050 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac060 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`780ac070 ffffffff`ffffffff ffffffff`ffffffff
kd> dq rdx
ffffb280`78d9c000 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9c010 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9c020 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9c030 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9c040 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9c050 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9c060 ffffffff`ffffffff ffffffff`ffffffff
ffffb280`78d9c070 ffffffff`ffffffff ffffffff`ffffffff
一次一次的比较慢,选择直接 inline hook 3E10和3EB0,找到关键的LOG,dump关键的页。

日志推断,它应该是 hook 了 vmexit 的 handler,中间是在做扫秒页表用特征码判断 vmexit 的操作。

从dump的结果来看,基本可以确定就是 Terminate Code 算法的页面了。
代码直接让ai分析,可以分析得到一些结果

也是终于看到了验证成功的输出。
随后发现,dump的页面中存在 payload.sys 字符,那么无疑是驱动里面应该是释放了一个 PE 文件,找PE标志位,发现 14000E630 是一个 PE loader,下断,dump,拿到完整 PE 文件。

这个注册机判定比较简单,基本就是直接算出明文去比对的,借助一下ai的神力,得到keygen。
MASK = 0xFFFFFFFFFFFFFFFF
K = 0x9E3779B97F4A7C15
A = 0x5348414430574E54
B = 0x4859504552564D58
C = 0x40A7B892E31B1A47
def rol64(x, n):
x &= MASK
return ((x << n) | (x >> (64 - n))) & MASK
def keygen(seed16: bytes):
assert len(seed16) == 16
seed0 = int.from_bytes(seed16[0:8], "little")
seed1 = int.from_bytes(seed16[8:16], "little")
x = seed0 ^ A
y = seed1 ^ B
for _ in range(8):
x = (x + K * rol64(y, 13)) & MASK
y = ((rol64(x, 29) - C) & MASK) ^ y
y &= MASK
x = (x ^ (y >> 17)) & MASK
y = (y + ((x << 7) & MASK)) & MASK
return x, y
seed = bytes.fromhex("") # 16 bytes memory
key0, key1 = keygen(seed)
print(f"{key0:016x}{key1:016x}".upper())
现在最主要的是 payload1的地址不知道如何获取,陷入了僵局,只要拿到这 16 字节的数据,就可以完美还原出 key。
几经辗转还是认为特征码的判断这边有大说法,最后也认定了,AI 给的特征码是判断 NT 的结论是错误的,因为 NT 就在内存中,没必要通过 vmhd 设备去读物理内存再去比较特征码,经过一些资料的查找,最终认定关键文件 hvix64.exe,这个是 hyper-v 的关键文件。
特征码函数 sub_140018260:

根据对自己虚拟机的windows版本判断,最终可以扫描序列 65 C6 04 25 6D 00 00 00 00 48 8B 4C 24 ?? 48 8B 54 24 ?? E8 ?? ?? ?? ?? E9 得到一个结果。

找到结果,它的 SIG 扫描传了一个 len 一个 offset,我系统的分支中,len 和 offset 分别是 0x1D 和 0x13,刚好对应了一个 CALL 的指令,如图所示

最终经过一些尝试,在 call 指向的内存就是 payload1 的值。

如图选中的就是 keygen 所需的内存。
MASK = 0xFFFFFFFFFFFFFFFF
K = 0x9E3779B97F4A7C15
A = 0x5348414430574E54
B = 0x4859504552564D58
C = 0x40A7B892E31B1A47
def rol64(x, n):
x &= MASK
return ((x << n) | (x >> (64 - n))) & MASK
def keygen(seed16: bytes):
assert len(seed16) == 16
seed0 = int.from_bytes(seed16[0:8], "little")
seed1 = int.from_bytes(seed16[8:16], "little")
x = seed0 ^ A
y = seed1 ^ B
for _ in range(8):
x = (x + K * rol64(y, 13)) & MASK
y = ((rol64(x, 29) - C) & MASK) ^ y
y &= MASK
x = (x ^ (y >> 17)) & MASK
y = (y + ((x << 7) & MASK)) & MASK
return x, y
seed = bytes.fromhex("FA 01 0F 8C 70 77 03 00 83 FA 03 0F 8D 67 77 03")
key0, key1 = keygen(seed)
print(f"{key0:016x}{key1:016x}".upper())
# 89EB8A97F689F19012FFA10AFB37791A
该 Key 就是对应我系统上正确的 Key 了。

隐藏流程总结
上述分析的流程比较杂乱,因为是边调边写的,这里做一个完整的总结。
- exe 首先通过
CPUID检查运行环境是否满足要求(需要Hyper-V),随后将驱动释放到C:\Windows\System32\下并随机命名加载。驱动加载后会立即删除服务和驱动文件,因此磁盘上很难留下稳定文件名。 - 驱动层基本没有正常导入表,主要通过
FNV-1a hash动态解析内核 API,包括后续访问设备栈和物理内存相关的关键 API。 - 程序创建/挂载一个 VHD,使系统产生
vhdmp -> disk设备栈。这个 VHD 本身内容几乎为空,更像是为了得到一条可控的存储设备路径。 - 驱动找到
vhdmp/disk相关设备对象后,通过IoBuildDeviceIoControlRequest构造IOCTL_SCSI_PASS_THROUGH_DIRECT请求,并通过IofCallDriver将 IRP 发送到磁盘设备栈中。该请求内部使用SCSI READ(10) / WRITE(10),以 4KB 页面为单位对目标页面进行读写/触发。 - 在页面读写和地址转换过程中,驱动还会使用
MmMapIoSpace将物理地址映射到内核虚拟地址空间,从而读取或修改对应页面内容。结合vhdmp/disk路径,这部分效果上类似绕过普通 guest 视角去访问 Hyper-V 相关内存,因为hvix64.exe/hvax64.exe对普通 guest 调试视角并不可见,Windbg 也无法读相关的内存。 - 驱动根据 CPU 厂商选择目标模块:Intel 对应
hvix64.exe,AMD 对应hvax64.exe。随后根据系统 build 选择对应特征码,在目标模块中匹配 VM-exit 路径附近的 call site。例如在hvix64.exe 10.0.19041.2006中,特征命中hvix64+0x23D42D,描述符 offset 为0x13,最终定位到hvix64+0x23D440这条call指令。 - 驱动会解析该
call rel32,得到原始目标函数地址,并结合已经编译好的payload.sys导出表,把原 handler、payload handler 以及若干偏移写入 payload 的.bss数据槽中。其中 ordinal 1 对应payload+0x4080,用于保存原始处理路径,payload 在不处理的 VM-exit 上会跳回该地址。 - 完成上述准备后,驱动通过页面级替换/EPT hook 一类方式将 Hyper-V 的 CPUID VM-exit 处理路径接到 payload 中。驱动自身随后退出,但 Hyper-V 层的 CPUID handler 已经被 payload 接管。
以上就是关于隐藏代码的具体方式。
Key流程分析
首先在应用层通过 search pattern 给所有可能的 CPUID 指令下断点,定位关键校验位置。

输入 0x61626364656667687172737475767778,观察断点处的寄存器上下文。

此时大概可以猜出它的数据传递方式:
RAX放Magic Number 0xDEADBEEF,用于让 Hyper-V 层的 payload 判断这是题目的私有 CPUID 通信请求。RBX放 flag 前 16 位 hex。RDX放 flag 后 16 位 hex。RCX放另一个Magic Number,作为命令控制码。
在 payload.sys 中可以找到对应 hook 的 VM-exit handler。

继续查看处理函数,通信逻辑会更加清楚。

由此可以得出结论:
- payload 只额外处理
RAX = 0xDEADBEEF的CPUIDVM-exit,其余 CPUID 或其它 VM-exit 一律转发给原来的 Hyper-V handler。 RCX作为命令控制码,处理四种命令:0x114514:握手命令,不校验 flag,将GUEST_RAX设置为0x1919810后返回。0x1919810:校验 flag 正确性,根据结果生成小 banner。0xB16B00B5:校验 flag 正确性,根据结果生成剧情相关提示文字(核心被摧毁 / 访问拒绝)。0xCAFED00D:校验 flag 正确性,根据结果生成最终提示文字(当前阶段成功 / 再次尝试)。
- 提示性文字会按顺序写回到
RBX, RCX, RDX中。 sub_140001000是 flag 判断的关键校验函数。
flag 校验和 CPUID 通信逻辑至此分析完毕。
(5) 编写正确的 KeyGen
将上面手动分析出的流程用代码实现即可。需要注意的是,seed 来自 Hyper-V 原 handler 所在页的固定偏移,因此需要根据 CPU 厂商区分目标模块:
- Intel 平台对应
hvix64.exe - AMD 平台对应
hvax64.exe
驱动根据系统版本选择对应特征码,定位 Hyper-V 中的目标 call site,再解析原 handler 地址。sub_140001000 会取该地址所在页的 +0x500 和 +0x508 两个 qword 作为 seed,经过 8 轮 64 位混合运算后得到最终需要传入的两个 64 位值。
因为我的主机是 Intel 的,所以 Intel 的分支不需要重复分析,唯一需要找一下 AMD 的,经过寻找找到了对应的逻辑。

根据代码产生的特征码应该是 E8 ?? ?? ?? ?? 48 89 04 24 E9,可以在虚拟机对应的版本找到唯一的匹配。

AMD 平台似乎没有对于 Build Number 的判断,尝试一下本机(Win11 25H2)能否命中,经过检测发现也是可以的,看来逻辑就是这样了。
那么据此写出全平台通用 Keygen:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MASK_PAGE 0xFFFFF000u
typedef struct {
const char* name;
DWORD min_build;
const unsigned char* pattern;
const char* mask;
DWORD length;
DWORD call_offset;
} SIGNATURE;
typedef struct {
DWORD va;
DWORD vsize;
DWORD raw;
DWORD raw_size;
} PE_SECTION;
typedef struct {
unsigned char* data;
DWORD size;
uint64_t image_base;
PE_SECTION* sections;
WORD section_count;
} PE_IMAGE;
typedef struct {
int has_cpu;
int is_intel;
int has_build;
DWORD build;
const char* file_path;
} OPTIONS;
static const unsigned char kIntel22621[] = {
0x66, 0x83, 0xFE, 0x01, 0x75, 0x0A, 0xE8, 0x00,
0x00, 0x00, 0x00, 0x48, 0x8B, 0x4C, 0x24, 0x00,
0xFB, 0x8B, 0xD6, 0x0B, 0x54, 0x24, 0x00, 0xE8,
0x00, 0x00, 0x00, 0x00, 0xE9
};
static const unsigned char kIntel19041[] = {
0x65, 0xC6, 0x04, 0x25, 0x6D, 0x00, 0x00, 0x00,
0x00, 0x48, 0x8B, 0x4C, 0x24, 0x00, 0x48, 0x8B,
0x54, 0x24, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00,
0xE9
};
static const unsigned char kIntel17763[] = {
0x48, 0x8B, 0x4C, 0x24, 0x00, 0xEB, 0x07, 0xE8,
0x00, 0x00, 0x00, 0x00, 0xEB, 0xF2, 0x48, 0x8B,
0x54, 0x24, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00,
0xE9
};
static const unsigned char kIntel17134[] = {
0xF2, 0x80, 0x3D, 0xFC, 0x12, 0x46, 0x00, 0x00,
0x0F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8B,
0x54, 0x24, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00,
0xE9
};
static const unsigned char kIntel10586[] = {
0xD0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8B,
0x54, 0x24, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00,
0xE9
};
static const unsigned char kIntel10240[] = {
0x60, 0xC0, 0x0F, 0x29, 0x68, 0xD0, 0x80, 0x3D,
0x7E, 0xAF, 0x49, 0x00, 0x01, 0x0F, 0x84, 0x00,
0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00,
0xE9
};
static const unsigned char kAmdFixed[] = {
0xE8, 0x00, 0x00, 0x00, 0x00, 0x48, 0x89, 0x04, 0x24, 0xE9
};
static const SIGNATURE kIntelSignatures[] = {
{ "intel_ge_22621", 22621, kIntel22621, "xxxxxxx????xxxx?xxxxxx?x????x", sizeof(kIntel22621), 0x17 },
{ "intel_ge_19041", 19041, kIntel19041, "xxxxxxxxxxxxx?xxxx?x????x", sizeof(kIntel19041), 0x13 },
{ "intel_ge_17763", 17763, kIntel17763, "xxxx?xxx????xxxxxx?x????x", sizeof(kIntel17763), 0x13 },
{ "intel_ge_17134", 17134, kIntel17134, "xxxxxxx?xx????xxxx?x????x", sizeof(kIntel17134), 0x13 },
{ "intel_ge_10586", 10586, kIntel10586, "xx????x?xx????xxxx?x????x", sizeof(kIntel10586), 0x13 },
{ "intel_ge_10240", 10240, kIntel10240, "xxxxxxxxxxxxxxx????x????x", sizeof(kIntel10240), 0x13 },
};
static const SIGNATURE kAmdSignature = {
"amd_fixed", 0, kAmdFixed, "x????xxxxx", sizeof(kAmdFixed), 0x00
};
static uint64_t rol64(uint64_t value, unsigned int bits) {
return (value << bits) | (value >> (64 - bits));
}
static void calc_key(uint64_t q500, uint64_t q508, uint64_t* key1, uint64_t* key2) {
uint64_t x = q500 ^ 0x5348414430574E54ULL;
uint64_t y = q508 ^ 0x4859504552564D58ULL;
for (int i = 0; i < 8; ++i) {
x = x + 0x9E3779B97F4A7C15ULL * rol64(y, 13);
y = (rol64(x, 29) - 0x40A7B892E31B1A47ULL) ^ y;
x = x ^ (y >> 17);
y = y + (x << 7);
}
*key1 = x;
*key2 = y;
}
static void print_le64(uint64_t value) {
for (int i = 0; i < 8; ++i) {
printf("%02X", (unsigned int)((value >> (8 * i)) & 0xFF));
}
}
static void print_hex64(uint64_t value) {
printf("%08lX%08lX",
(unsigned long)((value >> 32) & 0xFFFFFFFFu),
(unsigned long)(value & 0xFFFFFFFFu));
}
static int contains_ci(const char* s, const char* needle) {
size_t nlen = strlen(needle);
if (nlen == 0) {
return 1;
}
for (; *s; ++s) {
size_t i = 0;
while (i < nlen && s[i] &&
tolower((unsigned char)s[i]) == tolower((unsigned char)needle[i])) {
++i;
}
if (i == nlen) {
return 1;
}
}
return 0;
}
static int equals_ci(const char* a, const char* b) {
while (*a && *b) {
if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) {
return 0;
}
++a;
++b;
}
return *a == 0 && *b == 0;
}
static void print_usage(const char* program) {
printf("Usage: %s [--cpu intel|amd] [--build number] [--file path]\n", program);
printf(" %s [-c intel|amd] [-b number] [-f path]\n", program);
printf("\n");
printf("No arguments: detect current CPU, current Windows build, and System32 hvix64/hvax64 automatically.\n");
printf("Supported by challenge scope: Intel build 10240..22631, AMD build 10240..19041.\n");
}
static int parse_options(int argc, char** argv, OPTIONS* options) {
memset(options, 0, sizeof(*options));
for (int i = 1; i < argc; ++i) {
const char* arg = argv[i];
if (equals_ci(arg, "--help") || equals_ci(arg, "-h") || equals_ci(arg, "/?")) {
print_usage(argv[0]);
exit(0);
}
else if (equals_ci(arg, "--cpu") || equals_ci(arg, "-c")) {
const char* value = NULL;
if (++i >= argc) {
fprintf(stderr, "missing value for %s\n", arg);
return 0;
}
value = argv[i];
if (equals_ci(value, "intel")) {
options->has_cpu = 1;
options->is_intel = 1;
}
else if (equals_ci(value, "amd")) {
options->has_cpu = 1;
options->is_intel = 0;
}
else {
fprintf(stderr, "invalid CPU type: %s\n", value);
return 0;
}
}
else if (equals_ci(arg, "--build") || equals_ci(arg, "-b")) {
char* end = NULL;
unsigned long value = 0;
if (++i >= argc) {
fprintf(stderr, "missing value for %s\n", arg);
return 0;
}
value = strtoul(argv[i], &end, 10);
if (!end || *end != 0 || value == 0 || value > 0xFFFFFFFFul) {
fprintf(stderr, "invalid build number: %s\n", argv[i]);
return 0;
}
options->has_build = 1;
options->build = (DWORD)value;
}
else if (equals_ci(arg, "--file") || equals_ci(arg, "-f")) {
if (++i >= argc) {
fprintf(stderr, "missing value for %s\n", arg);
return 0;
}
options->file_path = argv[i];
}
else {
fprintf(stderr, "unknown argument: %s\n", arg);
print_usage(argv[0]);
return 0;
}
}
return 1;
}
static int get_cpu_vendor(char* vendor, DWORD vendor_size, int* is_intel) {
HKEY key = NULL;
DWORD type = 0;
DWORD size = vendor_size;
vendor[0] = 0;
*is_intel = -1;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0",
0, KEY_READ, &key) != ERROR_SUCCESS) {
return 0;
}
if (RegQueryValueExA(key, "VendorIdentifier", NULL, &type, (LPBYTE)vendor, &size) != ERROR_SUCCESS ||
type != REG_SZ) {
RegCloseKey(key);
return 0;
}
RegCloseKey(key);
if (contains_ci(vendor, "GenuineIntel")) {
*is_intel = 1;
}
else if (contains_ci(vendor, "AuthenticAMD")) {
*is_intel = 0;
}
return *is_intel != -1;
}
static DWORD get_current_build(void) {
HKEY key = NULL;
char value[64];
DWORD type = 0;
DWORD size = sizeof(value);
DWORD build = 0;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
0, KEY_READ, &key) != ERROR_SUCCESS) {
return 0;
}
if (RegQueryValueExA(key, "CurrentBuild", NULL, &type, (LPBYTE)value, &size) == ERROR_SUCCESS &&
type == REG_SZ) {
build = (DWORD)strtoul(value, NULL, 10);
}
if (build == 0) {
size = sizeof(value);
if (RegQueryValueExA(key, "CurrentBuildNumber", NULL, &type, (LPBYTE)value, &size) == ERROR_SUCCESS &&
type == REG_SZ) {
build = (DWORD)strtoul(value, NULL, 10);
}
}
RegCloseKey(key);
return build;
}
static int find_hyperv_file(int is_intel, char* path, DWORD path_size) {
char windows_dir[MAX_PATH];
const char* filename = is_intel ? "hvix64.exe" : "hvax64.exe";
if (!GetWindowsDirectoryA(windows_dir, sizeof(windows_dir))) {
return 0;
}
_snprintf_s(path, path_size, _TRUNCATE, "%s\\Sysnative\\%s", windows_dir, filename);
if (GetFileAttributesA(path) != INVALID_FILE_ATTRIBUTES) {
return 1;
}
_snprintf_s(path, path_size, _TRUNCATE, "%s\\System32\\%s", windows_dir, filename);
if (GetFileAttributesA(path) != INVALID_FILE_ATTRIBUTES) {
return 1;
}
return 0;
}
static int read_file_all(const char* path, unsigned char** data, DWORD* size) {
HANDLE file = INVALID_HANDLE_VALUE;
LARGE_INTEGER file_size;
unsigned char* buffer = NULL;
DWORD total = 0;
*data = NULL;
*size = 0;
file = CreateFileA(path, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) {
return 0;
}
if (!GetFileSizeEx(file, &file_size) || file_size.QuadPart <= 0 || file_size.QuadPart > 0x7FFFFFFF) {
CloseHandle(file);
return 0;
}
buffer = (unsigned char*)malloc((size_t)file_size.QuadPart);
if (!buffer) {
CloseHandle(file);
return 0;
}
while (total < (DWORD)file_size.QuadPart) {
DWORD got = 0;
DWORD want = (DWORD)file_size.QuadPart - total;
if (!ReadFile(file, buffer + total, want, &got, NULL) || got == 0) {
free(buffer);
CloseHandle(file);
return 0;
}
total += got;
}
CloseHandle(file);
*data = buffer;
*size = total;
return 1;
}
static int parse_pe(PE_IMAGE* pe, const char* path) {
IMAGE_DOS_HEADER* dos = NULL;
IMAGE_NT_HEADERS64* nt64 = NULL;
IMAGE_FILE_HEADER* file = NULL;
IMAGE_SECTION_HEADER* sec = NULL;
DWORD nt_off = 0;
memset(pe, 0, sizeof(*pe));
if (!read_file_all(path, &pe->data, &pe->size)) {
return 0;
}
if (pe->size < sizeof(IMAGE_DOS_HEADER)) {
return 0;
}
dos = (IMAGE_DOS_HEADER*)pe->data;
if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0) {
return 0;
}
nt_off = (DWORD)dos->e_lfanew;
if (nt_off + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) > pe->size) {
return 0;
}
nt64 = (IMAGE_NT_HEADERS64*)(pe->data + nt_off);
if (nt64->Signature != IMAGE_NT_SIGNATURE) {
return 0;
}
file = &nt64->FileHeader;
if (nt_off + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) + file->SizeOfOptionalHeader > pe->size) {
return 0;
}
if (nt64->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
pe->image_base = nt64->OptionalHeader.ImageBase;
}
else if (nt64->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
IMAGE_NT_HEADERS32* nt32 = (IMAGE_NT_HEADERS32*)(pe->data + nt_off);
pe->image_base = nt32->OptionalHeader.ImageBase;
}
else {
return 0;
}
pe->section_count = file->NumberOfSections;
pe->sections = (PE_SECTION*)calloc(pe->section_count, sizeof(PE_SECTION));
if (!pe->sections) {
return 0;
}
sec = (IMAGE_SECTION_HEADER*)((unsigned char*)&nt64->OptionalHeader + file->SizeOfOptionalHeader);
if ((unsigned char*)(sec + pe->section_count) > pe->data + pe->size) {
return 0;
}
for (WORD i = 0; i < pe->section_count; ++i) {
pe->sections[i].va = sec[i].VirtualAddress;
pe->sections[i].vsize = sec[i].Misc.VirtualSize;
pe->sections[i].raw = sec[i].PointerToRawData;
pe->sections[i].raw_size = sec[i].SizeOfRawData;
}
return 1;
}
static void free_pe(PE_IMAGE* pe) {
free(pe->sections);
free(pe->data);
memset(pe, 0, sizeof(*pe));
}
static int raw_to_rva(const PE_IMAGE* pe, DWORD raw, DWORD* rva) {
for (WORD i = 0; i < pe->section_count; ++i) {
const PE_SECTION* s = &pe->sections[i];
if (s->raw_size == 0) {
continue;
}
if (raw >= s->raw && raw < s->raw + s->raw_size) {
*rva = s->va + (raw - s->raw);
return 1;
}
}
return 0;
}
static int rva_to_raw(const PE_IMAGE* pe, DWORD rva, DWORD* raw) {
for (WORD i = 0; i < pe->section_count; ++i) {
const PE_SECTION* s = &pe->sections[i];
DWORD span = s->vsize > s->raw_size ? s->vsize : s->raw_size;
if (span == 0) {
continue;
}
if (rva >= s->va && rva < s->va + span) {
DWORD candidate = s->raw + (rva - s->va);
if (candidate >= pe->size) {
return 0;
}
*raw = candidate;
return 1;
}
}
return 0;
}
static int read_u64_rva(const PE_IMAGE* pe, DWORD rva, uint64_t* value) {
DWORD raw = 0;
if (!rva_to_raw(pe, rva, &raw) || raw + sizeof(uint64_t) > pe->size) {
return 0;
}
memcpy(value, pe->data + raw, sizeof(uint64_t));
return 1;
}
static const SIGNATURE* choose_signature(int is_intel, DWORD build) {
if (!is_intel) {
if (build < 10240 || build > 19041) {
return NULL;
}
return &kAmdSignature;
}
if (build < 10240 || build > 22631) {
return NULL;
}
for (size_t i = 0; i < sizeof(kIntelSignatures) / sizeof(kIntelSignatures[0]); ++i) {
if (build >= kIntelSignatures[i].min_build) {
return &kIntelSignatures[i];
}
}
return NULL;
}
static int match_at(const unsigned char* data, DWORD pos, const SIGNATURE* sig) {
for (DWORD i = 0; i < sig->length; ++i) {
if (sig->mask[i] == 'x' && data[pos + i] != sig->pattern[i]) {
return 0;
}
}
return 1;
}
static DWORD find_signature_hits(const PE_IMAGE* pe, const SIGNATURE* sig, DWORD* first_hit) {
DWORD hits = 0;
*first_hit = 0;
if (pe->size < sig->length) {
return 0;
}
for (DWORD pos = 0; pos <= pe->size - sig->length; ++pos) {
if (match_at(pe->data, pos, sig)) {
if (hits == 0) {
*first_hit = pos;
}
++hits;
}
}
return hits;
}
int main(int argc, char** argv) {
OPTIONS options;
char vendor[128];
char path[MAX_PATH];
DWORD build = 0;
int is_intel = -1;
PE_IMAGE pe;
const SIGNATURE* sig = NULL;
DWORD match_raw = 0;
DWORD hits = 0;
DWORD match_rva = 0;
DWORD call_site_raw = 0;
DWORD call_site_rva = 0;
int32_t rel32 = 0;
DWORD handler_rva = 0;
DWORD seed_page_rva = 0;
DWORD q500_rva = 0;
DWORD q508_rva = 0;
uint64_t q500 = 0;
uint64_t q508 = 0;
uint64_t key1 = 0;
uint64_t key2 = 0;
if (!parse_options(argc, argv, &options)) {
return 1;
}
vendor[0] = 0;
path[0] = 0;
if (options.has_cpu) {
is_intel = options.is_intel;
strcpy_s(vendor, sizeof(vendor), is_intel ? "manual:intel" : "manual:amd");
}
else if (!get_cpu_vendor(vendor, sizeof(vendor), &is_intel)) {
fprintf(stderr, "failed to detect CPU vendor\n");
return 1;
}
build = options.has_build ? options.build : get_current_build();
if (build == 0) {
fprintf(stderr, "failed to read Windows build number\n");
return 1;
}
if (options.file_path) {
strcpy_s(path, sizeof(path), options.file_path);
}
else if (!find_hyperv_file(is_intel, path, sizeof(path))) {
fprintf(stderr, "failed to find %s in Windows system directory\n", is_intel ? "hvix64.exe" : "hvax64.exe");
return 1;
}
sig = choose_signature(is_intel, build);
if (!sig) {
if (is_intel) {
fprintf(stderr, "unsupported Intel Windows build: %lu, expected 10240..22631\n", (unsigned long)build);
}
else {
fprintf(stderr, "unsupported AMD Windows build: %lu, expected 10240..19041\n", (unsigned long)build);
}
return 1;
}
if (!parse_pe(&pe, path)) {
fprintf(stderr, "failed to parse PE: %s\n", path);
return 1;
}
hits = find_signature_hits(&pe, sig, &match_raw);
if (hits == 0) {
fprintf(stderr, "signature not found: %s\n", sig->name);
free_pe(&pe);
return 1;
}
call_site_raw = match_raw + sig->call_offset;
if (call_site_raw + 5 > pe.size || pe.data[call_site_raw] != 0xE8) {
fprintf(stderr, "selected call site is not E8\n");
free_pe(&pe);
return 1;
}
if (!raw_to_rva(&pe, match_raw, &match_rva) || !raw_to_rva(&pe, call_site_raw, &call_site_rva)) {
fprintf(stderr, "failed to convert raw offset to RVA\n");
free_pe(&pe);
return 1;
}
memcpy(&rel32, pe.data + call_site_raw + 1, sizeof(rel32));
handler_rva = call_site_rva + 5 + rel32;
seed_page_rva = handler_rva & MASK_PAGE;
q500_rva = seed_page_rva + 0x500;
q508_rva = seed_page_rva + 0x508;
if (!read_u64_rva(&pe, q500_rva, &q500) || !read_u64_rva(&pe, q508_rva, &q508)) {
fprintf(stderr, "failed to read seed qwords\n");
free_pe(&pe);
return 1;
}
calc_key(q500, q508, &key1, &key2);
printf("cpu : %s\n", is_intel ? "intel" : "amd");
printf("cpu vendor : %s\n", vendor);
if (build != 0) {
printf("build : %lu\n", (unsigned long)build);
}
printf("file : %s\n", path);
printf("signature : %s\n", sig->name);
printf("signature hits : %lu (using index 0)\n", (unsigned long)hits);
printf("match raw/rva/va : 0x%lX / 0x%lX / 0x",
(unsigned long)match_raw,
(unsigned long)match_rva);
print_hex64(pe.image_base + match_rva);
printf("\n");
printf("call site raw/rva/va: 0x%lX / 0x%lX / 0x",
(unsigned long)call_site_raw,
(unsigned long)call_site_rva);
print_hex64(pe.image_base + call_site_rva);
printf("\n");
printf("call rel32 : 0x%08lX (%ld)\n",
(unsigned long)((uint32_t)rel32),
(long)rel32);
printf("handler rva/va : 0x%lX / 0x", (unsigned long)handler_rva);
print_hex64(pe.image_base + handler_rva);
printf("\n");
printf("seed page rva/va : 0x%lX / 0x", (unsigned long)seed_page_rva);
print_hex64(pe.image_base + seed_page_rva);
printf("\n");
printf("q500 rva/va/value : 0x%lX / 0x", (unsigned long)q500_rva);
print_hex64(pe.image_base + q500_rva);
printf(" / 0x");
print_hex64(q500);
printf("\n");
printf("q508 rva/va/value : 0x%lX / 0x", (unsigned long)q508_rva);
print_hex64(pe.image_base + q508_rva);
printf(" / 0x");
print_hex64(q508);
printf("\n");
printf("key part1 (RBX) : 0x");
print_hex64(key1);
printf("\n");
printf("key part2 (RDX) : 0x");
print_hex64(key2);
printf("\n");
printf("key hex : ");
print_hex64(key1);
print_hex64(key2);
printf("\n");
printf("key little-endian : ");
print_le64(key1);
print_le64(key2);
printf("\n");
free_pe(&pe);
return 0;
}
(3)检测hook方法
我想了几种方案
- cpuid 观察时间进行检测
- cpuid 观察协议进行检测
- 对设备进行检测
- 扫描物理内存进行检测
R3检测方法
时间检测
由于 hyperv 本质也是进行了一个hook,因此再次做 hook 比较时间差异不显著,而且因为系统机器的差异,我们所有的数据都只能从同一个环境获得。
在同样的环境,我们可以检测有控制码对应的分支和无控制码对应的分支,检测之间的比值是否低于正常值即可。
正常情况下,跑100w条 cpuid 指令,cpuid(0,0) 和 cpuid(0xdeadbeef,0) 和 cpuid(deadbeef,cafed00d) 之间的差异不会过于显著,基本上不会超过 2% 的水平。

开启 root 之后,这个值显然被拉大了,虽然依然在 5% 附近,但是多次测试排除偶然性之后认定这个方案是可行的。

协议探测检测
这个很容易,RCX=0x114514 RAX=0xDEADBEEF 即可通过 RAX 的值判断是否开启了 root 系统,当然其它的控制码也可以检测。
VHD文件检测
ShadowRoot 运行之后,会生成一个 C:\Windows\Temp\WdiServiceHost\{GUID}.vhd 文件,通过检测该文件也可以判断 Shadow Root 的存活。
检测代码
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#if !defined(_M_X64) && !defined(__x86_64__)
#error This detector requires a Windows x64 build.
#endif
typedef struct {
uint64_t rax;
uint64_t rbx;
uint64_t rcx;
uint64_t rdx;
} CPUID_REGS64;
typedef void (*CPUID_FULL_FN)(
uint64_t in_rax,
uint64_t in_rbx,
uint64_t in_rcx,
uint64_t in_rdx,
CPUID_REGS64* out_regs
);
static const unsigned char kCpuidStubWin64[] = {
0x53, /* push rbx */
0x48, 0x89, 0xC8, /* mov rax, rcx */
0x48, 0x89, 0xD3, /* mov rbx, rdx */
0x4C, 0x89, 0xC1, /* mov rcx, r8 */
0x4C, 0x89, 0xCA, /* mov rdx, r9 */
0x0F, 0xA2, /* cpuid */
0x4C, 0x8B, 0x54, 0x24, 0x30, /* mov r10, [rsp+30h] */
0x49, 0x89, 0x02, /* mov [r10+00h], rax */
0x49, 0x89, 0x5A, 0x08, /* mov [r10+08h], rbx */
0x49, 0x89, 0x4A, 0x10, /* mov [r10+10h], rcx */
0x49, 0x89, 0x52, 0x18, /* mov [r10+18h], rdx */
0x5B, /* pop rbx */
0xC3 /* ret */
};
static int equals_ci(const char* a, const char* b) {
while (*a && *b) {
if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) {
return 0;
}
++a;
++b;
}
return *a == 0 && *b == 0;
}
static void print_hex64(uint64_t value) {
printf("%08lX%08lX",
(unsigned long)((value >> 32) & 0xFFFFFFFFu),
(unsigned long)(value & 0xFFFFFFFFu));
}
static void print_dec_u64(uint64_t value) {
char buf[32];
size_t pos = sizeof(buf);
buf[--pos] = 0;
if (value == 0) {
buf[--pos] = '0';
}
else {
while (value != 0 && pos > 0) {
buf[--pos] = (char)('0' + (value % 10));
value /= 10;
}
}
printf("%s", &buf[pos]);
}
static void print_fixed_3(uint64_t value_x1000) {
unsigned long frac = (unsigned long)(value_x1000 % 1000);
print_dec_u64(value_x1000 / 1000);
printf(".%03lu", frac);
}
static void print_regs(const char* tag, const CPUID_REGS64* regs) {
printf("%-18s RAX=0x", tag);
print_hex64(regs->rax);
printf(" RBX=0x");
print_hex64(regs->rbx);
printf(" RCX=0x");
print_hex64(regs->rcx);
printf(" RDX=0x");
print_hex64(regs->rdx);
printf("\n");
}
static void regs_to_ascii(const CPUID_REGS64* regs, char* out, size_t out_size) {
uint64_t values[3];
size_t pos = 0;
values[0] = regs->rbx;
values[1] = regs->rcx;
values[2] = regs->rdx;
if (out_size == 0) {
return;
}
for (int r = 0; r < 3; ++r) {
for (int i = 0; i < 8 && pos + 1 < out_size; ++i) {
unsigned char c = (unsigned char)((values[r] >> (8 * i)) & 0xFF);
out[pos++] = isprint(c) ? (char)c : '.';
}
if (r != 2 && pos + 1 < out_size) {
out[pos++] = '|';
}
}
out[pos] = 0;
}
static CPUID_FULL_FN create_cpuid_stub(void) {
void* mem = VirtualAlloc(NULL, sizeof(kCpuidStubWin64),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!mem) {
return NULL;
}
memcpy(mem, kCpuidStubWin64, sizeof(kCpuidStubWin64));
FlushInstructionCache(GetCurrentProcess(), mem, sizeof(kCpuidStubWin64));
return (CPUID_FULL_FN)mem;
}
static void destroy_cpuid_stub(CPUID_FULL_FN fn) {
if (fn) {
VirtualFree((void*)fn, 0, MEM_RELEASE);
}
}
static uint64_t qpc_elapsed_us(LARGE_INTEGER start, LARGE_INTEGER end, LARGE_INTEGER freq) {
uint64_t delta = (uint64_t)(end.QuadPart - start.QuadPart);
return (delta * 1000000ULL) / (uint64_t)freq.QuadPart;
}
static uint64_t measure_cpuid_batch_us(
CPUID_FULL_FN cpuid_full,
uint64_t in_rax,
uint64_t in_rbx,
uint64_t in_rcx,
uint64_t in_rdx,
size_t batch_count
) {
CPUID_REGS64 tmp;
LARGE_INTEGER freq;
LARGE_INTEGER start;
LARGE_INTEGER end;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);
for (size_t i = 0; i < batch_count; ++i) {
cpuid_full(in_rax, in_rbx, in_rcx, in_rdx, &tmp);
}
QueryPerformanceCounter(&end);
return qpc_elapsed_us(start, end, freq);
}
static void harden_measurement_thread(void) {
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)1);
Sleep(50);
}
static int has_vhd_extension_ci(const char* name) {
const char* dot = strrchr(name, '.');
if (!dot) {
return 0;
}
return equals_ci(dot, ".vhd") || equals_ci(dot, ".vhdx") ||
equals_ci(dot, ".avhd") || equals_ci(dot, ".avhdx");
}
static int detect_wdi_vhd_files(void) {
char windows_dir[MAX_PATH];
char search_path[MAX_PATH * 2];
WIN32_FIND_DATAA find_data;
HANDLE find_handle;
DWORD attr;
int found = 0;
if (!GetWindowsDirectoryA(windows_dir, (UINT)sizeof(windows_dir))) {
printf("[wdi vhd probe]\n");
printf("GetWindowsDirectoryA failed: %lu\n", GetLastError());
return 0;
}
_snprintf_s(search_path, sizeof(search_path), _TRUNCATE,
"%s\\Temp\\WdiServiceHost", windows_dir);
printf("\n[wdi vhd probe]\n");
printf("directory : %s\n", search_path);
attr = GetFileAttributesA(search_path);
if (attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
printf("directory verdict : not present\n");
return 0;
}
_snprintf_s(search_path, sizeof(search_path), _TRUNCATE,
"%s\\Temp\\WdiServiceHost\\*.vhd*", windows_dir);
find_handle = FindFirstFileA(search_path, &find_data);
if (find_handle == INVALID_HANDLE_VALUE) {
DWORD err = GetLastError();
if (err == ERROR_FILE_NOT_FOUND) {
printf("vhd verdict : no vhd file found\n");
}
else {
printf("FindFirstFileA failed: %lu\n", err);
}
return 0;
}
do {
if ((find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 &&
has_vhd_extension_ci(find_data.cFileName)) {
ULARGE_INTEGER size;
size.HighPart = find_data.nFileSizeHigh;
size.LowPart = find_data.nFileSizeLow;
printf("vhd file : %s size=", find_data.cFileName);
print_dec_u64(size.QuadPart);
printf(" bytes attrs=0x%08lX\n", find_data.dwFileAttributes);
found = 1;
}
} while (FindNextFileA(find_handle, &find_data));
FindClose(find_handle);
printf("vhd verdict : %s\n", found ? "present" : "no vhd file found");
return found;
}
static int parse_iterations(int argc, char** argv, size_t* batch_count) {
*batch_count = 1000000;
for (int i = 1; i < argc; ++i) {
if (equals_ci(argv[i], "--help") || equals_ci(argv[i], "-h") || equals_ci(argv[i], "/?")) {
printf("Usage: %s [--batch cpuid_count]\n", argv[0]);
printf("\n");
printf("Detects the Hyper-V CPUID hook by protocol probing and total CPUID time in microseconds.\n");
printf("Default is --batch 1000000.\n");
return 0;
}
if (equals_ci(argv[i], "--batch") || equals_ci(argv[i], "-B")) {
char* end = NULL;
unsigned long value;
if (++i >= argc) {
fprintf(stderr, "missing value for --batch\n");
return -1;
}
value = strtoul(argv[i], &end, 10);
if (!end || *end != 0 || value < 1 || value > 10000000ul) {
fprintf(stderr, "invalid batch count: %s\n", argv[i]);
return -1;
}
*batch_count = (size_t)value;
}
else {
fprintf(stderr, "unknown argument: %s\n", argv[i]);
return -1;
}
}
return 1;
}
int main(int argc, char** argv) {
CPUID_FULL_FN cpuid_full = NULL;
CPUID_REGS64 leaf0;
CPUID_REGS64 magic_ping;
CPUID_REGS64 magic_bad_cmd;
CPUID_REGS64 magic_try_again;
uint64_t normal_us = 0;
uint64_t bad_cmd_us = 0;
uint64_t complex_us = 0;
size_t batch_count = 1000000;
int parse_result;
char ascii[64];
int protocol_hit;
int timing_suspicious;
int wdi_vhd_hit;
uint64_t ratio_complex_normal_x1000;
uint64_t ratio_complex_badcmd_x1000;
parse_result = parse_iterations(argc, argv, &batch_count);
if (parse_result <= 0) {
return parse_result == 0 ? 0 : 1;
}
cpuid_full = create_cpuid_stub();
if (!cpuid_full) {
fprintf(stderr, "failed to allocate CPUID stub\n");
return 1;
}
harden_measurement_thread();
cpuid_full(0, 0, 0, 0, &leaf0);
cpuid_full(0xDEADBEEFULL, 0, 0x114514ULL, 0, &magic_ping);
cpuid_full(0xDEADBEEFULL, 0, 0, 0, &magic_bad_cmd);
cpuid_full(0xDEADBEEFULL, 0, 0xCAFED00DULL, 0, &magic_try_again);
printf("[protocol probe]\n");
print_regs("cpuid(0,0)", &leaf0);
print_regs("magic ping", &magic_ping);
print_regs("magic bad cmd", &magic_bad_cmd);
print_regs("magic cafed00d", &magic_try_again);
regs_to_ascii(&magic_try_again, ascii, sizeof(ascii));
printf("magic cafed00d ascii(RBX|RCX|RDX) = \"%s\"\n", ascii);
protocol_hit = ((uint32_t)magic_ping.rax == 0x01919810u);
printf("protocol verdict : %s\n",
protocol_hit ? "HOOK PRESENT, RAX changed to 0x1919810" : "not observed");
for (int i = 0; i < 2000; ++i) {
cpuid_full(0, 0, 0, 0, &leaf0);
cpuid_full(0xDEADBEEFULL, 0, 0, 0, &leaf0);
cpuid_full(0xDEADBEEFULL, 0, 0xCAFED00DULL, 0, &leaf0);
}
normal_us = measure_cpuid_batch_us(cpuid_full, 0, 0, 0, 0, batch_count);
bad_cmd_us = measure_cpuid_batch_us(cpuid_full, 0xDEADBEEFULL, 0, 0, 0, batch_count);
complex_us = measure_cpuid_batch_us(cpuid_full, 0xDEADBEEFULL, 0, 0xCAFED00DULL, 0, batch_count);
printf("\n[timing total, unit=us, batch=");
print_dec_u64((uint64_t)batch_count);
printf("]\n");
printf("cpuid(0,0) total_us=");
print_dec_u64(normal_us);
printf("\n");
printf("deadbeef,cmd=0 total_us=");
print_dec_u64(bad_cmd_us);
printf("\n");
printf("deadbeef,cafed00d total_us=");
print_dec_u64(complex_us);
printf("\n");
ratio_complex_normal_x1000 = normal_us ? (complex_us * 1000) / normal_us : 0;
ratio_complex_badcmd_x1000 = bad_cmd_us ? (complex_us * 1000) / bad_cmd_us : 0;
printf("timing ratio complex/normal = ");
print_fixed_3(ratio_complex_normal_x1000);
printf("\n");
printf("timing ratio complex/badcmd = ");
print_fixed_3(ratio_complex_badcmd_x1000);
printf("\n");
timing_suspicious = ratio_complex_badcmd_x1000 < 980;
printf("timing detected : %s\n", timing_suspicious ? "yes" : "no");
wdi_vhd_hit = detect_wdi_vhd_files();
printf("\n[final verdict]\n");
if (protocol_hit) {
printf("Hyper-V CPUID hook detected by private CPUID protocol.\n");
}
if (timing_suspicious) {
printf("same-leaf timing is suspicious. Re-run and consider kernel physical scan.\n");
}
if (wdi_vhd_hit) {
printf("WdiServiceHost VHD artifact detected. Treat as a Shadow Root environment indicator.\n");
}
if (!protocol_hit && !timing_suspicious && !wdi_vhd_hit) {
printf("No Shadow Root indicator detected by user-mode checks.\n");
}
destroy_cpuid_stub(cpuid_full);
return protocol_hit ? 2 : ((timing_suspicious || wdi_vhd_hit) ? 1 : 0);
}
R0检测方法
设备检测
Shadow Root 在运行过程中会创建并挂载一个 vhd 文件,随后驱动层通过 vhdmp 设备栈与该虚拟磁盘交互。前面的调试中可以看到,只有在 Shadow Root 运行后,系统里才会出现一条 \Driver\disk -> \Driver\vhdmp 的设备栈。因此,可以通过枚举 \Driver\disk 下的设备对象,并检查其 lower/upper device stack 中是否存在 \Driver\vhdmp,将其作为检测 Shadow Root 是否运行过的一种侧信号。
未开启 Shadow Root:

开启 Shadow Root:

代码:
#include <ntddk.h>
#define DBG_PRINT(...) DbgPrintEx( DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[2026ACE_Final]" __VA_ARGS__);
extern "C" POBJECT_TYPE* IoDriverObjectType;
extern "C"
NTKERNELAPI
NTSTATUS
ObReferenceObjectByName(
_In_ PUNICODE_STRING ObjectName,
_In_ ULONG Attributes,
_In_opt_ PACCESS_STATE AccessState,
_In_opt_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_TYPE ObjectType,
_In_ KPROCESSOR_MODE AccessMode,
_Inout_opt_ PVOID ParseContext,
_Out_ PVOID* Object
);
typedef PDEVICE_OBJECT(*PFN_IO_GET_LOWER_DEVICE_OBJECT)(
_In_ PDEVICE_OBJECT DeviceObject
);
static PFN_IO_GET_LOWER_DEVICE_OBJECT
ResolveIoGetLowerDeviceObject()
{
static PFN_IO_GET_LOWER_DEVICE_OBJECT getLower = NULL;
static BOOLEAN resolved = FALSE;
UNICODE_STRING routineName;
if (!resolved) {
RtlInitUnicodeString(&routineName, L"IoGetLowerDeviceObject");
getLower = (PFN_IO_GET_LOWER_DEVICE_OBJECT)MmGetSystemRoutineAddress(&routineName);
resolved = TRUE;
}
return getLower;
}
static BOOLEAN
IsVhdmpDevice(
_In_ PDEVICE_OBJECT DeviceObject
)
{
UNICODE_STRING vhdmpName;
RtlInitUnicodeString(&vhdmpName, L"\\Driver\\vhdmp");
return DeviceObject &&
DeviceObject->DriverObject &&
RtlEqualUnicodeString(&DeviceObject->DriverObject->DriverName, &vhdmpName, TRUE);
}
static PDEVICE_OBJECT
FindVhdmpUpperDeviceInStack(
_In_ PDEVICE_OBJECT DeviceObject
)
{
PDEVICE_OBJECT current = DeviceObject;
while (current) {
if (IsVhdmpDevice(current)) {
return current;
}
current = current->AttachedDevice;
}
return NULL;
}
static PDEVICE_OBJECT
FindVhdmpLowerDeviceInStack(
_In_ PDEVICE_OBJECT DeviceObject
)
{
PFN_IO_GET_LOWER_DEVICE_OBJECT getLower;
PDEVICE_OBJECT current;
getLower = ResolveIoGetLowerDeviceObject();
if (!getLower) {
DBG_PRINT("IoGetLowerDeviceObject unavailable\n");
return NULL;
}
current = getLower(DeviceObject);
while (current) {
PDEVICE_OBJECT next;
if (IsVhdmpDevice(current)) {
return current;
}
next = getLower(current);
ObDereferenceObject(current);
current = next;
}
return NULL;
}
static NTSTATUS
FindVhdmpBackedDiskDevice(
_Outptr_ PDRIVER_OBJECT* DiskDriverObject,
_Outptr_ PDEVICE_OBJECT* DiskDeviceObject,
_Outptr_result_maybenull_ PDEVICE_OBJECT* VhdmpDeviceObject,
_Out_ BOOLEAN* VhdmpDeviceReferenced
)
{
UNICODE_STRING diskName;
PDRIVER_OBJECT diskDriverObject = NULL;
NTSTATUS status;
*DiskDriverObject = NULL;
*DiskDeviceObject = NULL;
*VhdmpDeviceObject = NULL;
*VhdmpDeviceReferenced = FALSE;
RtlInitUnicodeString(&diskName, L"\\Driver\\disk");
status = ObReferenceObjectByName(
&diskName,
OBJ_CASE_INSENSITIVE,
NULL,
0,
*IoDriverObjectType,
KernelMode,
NULL,
(PVOID*)&diskDriverObject);
if (!NT_SUCCESS(status)) {
return status;
}
for (PDEVICE_OBJECT diskDevice = diskDriverObject->DeviceObject;
diskDevice;
diskDevice = diskDevice->NextDevice) {
PDEVICE_OBJECT vhdmpDevice;
DBG_PRINT("check disk_device=%p attached_upper=%p\n",
diskDevice,
diskDevice->AttachedDevice);
vhdmpDevice = FindVhdmpLowerDeviceInStack(diskDevice);
if (vhdmpDevice) {
*DiskDriverObject = diskDriverObject;
*DiskDeviceObject = diskDevice;
*VhdmpDeviceObject = vhdmpDevice;
*VhdmpDeviceReferenced = TRUE;
return STATUS_SUCCESS;
}
vhdmpDevice = FindVhdmpUpperDeviceInStack(diskDevice);
if (vhdmpDevice) {
*DiskDriverObject = diskDriverObject;
*DiskDeviceObject = diskDevice;
*VhdmpDeviceObject = vhdmpDevice;
*VhdmpDeviceReferenced = FALSE;
return STATUS_SUCCESS;
}
}
ObDereferenceObject(diskDriverObject);
return STATUS_NOT_FOUND;
}
static VOID
DetectVhdmpRootUnload(
_In_ PDRIVER_OBJECT DriverObject
)
{
UNREFERENCED_PARAMETER(DriverObject);
DBG_PRINT("unload\n");
}
extern "C"
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
PDRIVER_OBJECT diskDriverObject = NULL;
PDEVICE_OBJECT diskDeviceObject = NULL;
PDEVICE_OBJECT vhdmpDeviceObject = NULL;
BOOLEAN vhdmpReferenced = FALSE;
NTSTATUS status;
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DetectVhdmpRootUnload;
DBG_PRINT( "load\n");
status = FindVhdmpBackedDiskDevice(
&diskDriverObject,
&diskDeviceObject,
&vhdmpDeviceObject,
&vhdmpReferenced);
if (NT_SUCCESS(status)) {
DBG_PRINT("Found Shadw Root Device\n");
} else {
DBG_PRINT("Shadow Root not found\n")
}
if (vhdmpReferenced && vhdmpDeviceObject) {
ObDereferenceObject(vhdmpDeviceObject);
}
if (diskDriverObject) {
ObDereferenceObject(diskDriverObject);
}
return STATUS_SUCCESS;
}
DMA
运行之后,如果能够从 Hyper-V 侧视图读取到目标物理页,就可以在内存中找到 payload.sys 留下的特征字符串。

不过,普通的 Guest R0 物理内存扫描并不稳定。因为 Shadow Root 使用的是 EPT 读写/执行视图分离,某些页在普通物理读视角下可能表现为全 FF 或不可见。因此检测代码可以参考 Shadow Root 自身的做法:先遍历物理内存范围,筛选普通物理读视角下全 FF 的候选页,再通过 vhdmp 设备和 SCSI WRITE(10) / READ(10) 通道,把 Hyper-V 侧 backing page bounce 出来进行扫描。
扫描时检测 payload 中的特征字符串:
now start to hook vm exit handler.
为了避免扫描到检测驱动自身 .sys 中保存的明文特征,可以在代码中对该字符串做简单异或编码,运行时再解码比较。实际测试中,该方法可以在开启 Shadow Root 的机器上命中 payload 页,并输出检测结果。

代码:
#include <ntddk.h>
#include <ntddscsi.h>
#include <intrin.h>
#define DBG_PRINT(...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[2026ACE_Final]" __VA_ARGS__)
#define TAG_SCAN 'nScP'
#define TAG_SPTD 'dSpP'
#define LOG_PREFIX "[PhysScanPayload] "
#define MAX_HITS 1
#define SCRATCH_LBA 0
#define SCSIOP_READ10 0x28
#define SCSIOP_WRITE10 0x2A
#define SECTOR_SIZE 512ULL
#define PAGE_SECTORS (PAGE_SIZE / SECTOR_SIZE)
#define PAGE_MASK_ULL (~((ULONGLONG)PAGE_SIZE - 1))
#define ALIGN_DOWN_PAGE_ULL(x) ((x) & PAGE_MASK_ULL)
#define ALIGN_UP_PAGE_ULL(x) (((x) + PAGE_SIZE - 1) & PAGE_MASK_ULL)
#define PATTERN_XOR_KEY 0xA7
#define MIN_BOUNCE_PHYSICAL_ADDRESS 0x100000000ULL
#define MAX_BOUNCE_PHYSICAL_ADDRESS 0x100200000ULL
#define MAX_BOUNCE_PROBES 512
#define REQUIRE_CPUID_PROTOCOL_BEFORE_BOUNCE 1
static const UCHAR g_EncodedPayloadPattern[] = {
0xC9, 0xC8, 0xD0, 0x87, 0xD4, 0xD3, 0xC6, 0xD5,
0xD3, 0x87, 0xD3, 0xC8, 0x87, 0xCF, 0xC8, 0xC8,
0xCC, 0x87, 0xD1, 0xCA, 0x87, 0xC2, 0xDF, 0xCE,
0xD3, 0x87, 0xCF, 0xC6, 0xC9, 0xC3, 0xCB, 0xC2
};
typedef struct _SPTD_WITH_SENSE {
SCSI_PASS_THROUGH_DIRECT Sptd;
UCHAR Sense[32];
} SPTD_WITH_SENSE, *PSPTD_WITH_SENSE;
typedef struct _SCAN_STATS {
ULONG Hits;
ULONG BounceProbes;
BOOLEAN Stop;
} SCAN_STATS, *PSCAN_STATS;
extern "C" POBJECT_TYPE* IoDriverObjectType;
extern "C"
NTKERNELAPI
NTSTATUS
ObReferenceObjectByName(
_In_ PUNICODE_STRING ObjectName,
_In_ ULONG Attributes,
_In_opt_ PACCESS_STATE AccessState,
_In_opt_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_TYPE ObjectType,
_In_ KPROCESSOR_MODE AccessMode,
_Inout_opt_ PVOID ParseContext,
_Out_ PVOID* Object
);
typedef PDEVICE_OBJECT(*PFN_IO_GET_LOWER_DEVICE_OBJECT)(
_In_ PDEVICE_OBJECT DeviceObject
);
static BOOLEAN
IsPageFilledWith(
_In_reads_bytes_(PAGE_SIZE) const UCHAR* Page,
_In_ UCHAR Value
)
{
for (SIZE_T i = 0; i < PAGE_SIZE; ++i) {
if (Page[i] != Value) {
return FALSE;
}
}
return TRUE;
}
static NTSTATUS
CopyPhysicalPage(
_In_ ULONGLONG PhysicalAddress,
_Out_writes_bytes_(PAGE_SIZE) PUCHAR PageBuffer
)
{
MM_COPY_ADDRESS sourceAddress;
SIZE_T copied = 0;
NTSTATUS status;
if ((PhysicalAddress & (PAGE_SIZE - 1)) != 0) {
return STATUS_INVALID_PARAMETER;
}
RtlZeroMemory(&sourceAddress, sizeof(sourceAddress));
sourceAddress.PhysicalAddress.QuadPart = PhysicalAddress;
status = MmCopyMemory(
PageBuffer,
sourceAddress,
PAGE_SIZE,
MM_COPY_MEMORY_PHYSICAL,
&copied);
if (!NT_SUCCESS(status) || copied != PAGE_SIZE) {
return NT_SUCCESS(status) ? STATUS_PARTIAL_COPY : status;
}
return STATUS_SUCCESS;
}
static SIZE_T
MinSizeT(
_In_ SIZE_T A,
_In_ SIZE_T B
)
{
return (A < B) ? A : B;
}
static UCHAR
GetPatternByte(
_In_reads_(Index + 1) const UCHAR* EncodedPattern,
_In_ SIZE_T Index
)
{
return (UCHAR)(EncodedPattern[Index] ^ PATTERN_XOR_KEY);
}
static BOOLEAN
ProbeShadowCpuidProtocol(VOID)
{
int regs[4] = { 0 };
__cpuidex(regs, 0xDEADBEEF, 0x114514);
if ((ULONG)regs[0] == 0x01919810) {
DBG_PRINT(LOG_PREFIX "cpuid protocol probe: present eax=0x%08X\n", (ULONG)regs[0]);
return TRUE;
}
DBG_PRINT(LOG_PREFIX "cpuid protocol probe: absent eax=0x%08X\n", (ULONG)regs[0]);
return FALSE;
}
static VOID
LogBytesFromOffset(
_In_reads_bytes_(Length) const UCHAR* Bytes,
_In_ SIZE_T Length,
_In_ SIZE_T Offset
)
{
static const CHAR hex[] = "0123456789ABCDEF";
CHAR line[3 * 64 + 1];
SIZE_T count;
SIZE_T pos = 0;
RtlZeroMemory(line, sizeof(line));
if (!Bytes || Length == 0 || Offset >= Length) {
DBG_PRINT(LOG_PREFIX "bytes: <unavailable>\n");
return;
}
count = MinSizeT(Length - Offset, 64);
for (SIZE_T i = 0; i < count && pos + 3 < sizeof(line); ++i) {
UCHAR value = Bytes[Offset + i];
line[pos++] = hex[(value >> 4) & 0xF];
line[pos++] = hex[value & 0xF];
line[pos++] = ' ';
}
DBG_PRINT(LOG_PREFIX "bytes[%Iu:%Iu]: %s\n", Offset, Offset + count, line);
}
static VOID
BruteForceScanBuffer(
_In_reads_bytes_(Length) const UCHAR* Buffer,
_In_ SIZE_T Length,
_In_ ULONGLONG BufferPhysicalAddress,
_In_reads_bytes_(PatternLength) const UCHAR* EncodedPattern,
_In_ SIZE_T PatternLength,
_Inout_ PSCAN_STATS Stats
)
{
if (!Buffer || !EncodedPattern || !Stats || PatternLength == 0 || Length < PatternLength) {
return;
}
for (SIZE_T offset = 0; offset + PatternLength <= Length; ++offset) {
BOOLEAN matched = TRUE;
for (SIZE_T index = 0; index < PatternLength; ++index) {
if (Buffer[offset + index] != GetPatternByte(EncodedPattern, index)) {
matched = FALSE;
break;
}
}
if (!matched) {
continue;
}
ULONGLONG hitPa = BufferPhysicalAddress + offset;
ULONGLONG matchEndPa = hitPa + PatternLength - 1;
++Stats->Hits;
DBG_PRINT(
LOG_PREFIX "HIT[%lu] hit_pa=%I64x page_pa=%I64x page_off=%I64x match_end=%I64x\n",
Stats->Hits,
hitPa,
ALIGN_DOWN_PAGE_ULL(hitPa),
hitPa & (PAGE_SIZE - 1),
matchEndPa);
LogBytesFromOffset(Buffer, Length, offset);
if (Stats->Hits >= MAX_HITS) {
Stats->Stop = TRUE;
return;
}
}
}
static PFN_IO_GET_LOWER_DEVICE_OBJECT
ResolveIoGetLowerDeviceObject(VOID)
{
static PFN_IO_GET_LOWER_DEVICE_OBJECT getLower = NULL;
static BOOLEAN resolved = FALSE;
UNICODE_STRING routineName;
if (!resolved) {
RtlInitUnicodeString(&routineName, L"IoGetLowerDeviceObject");
getLower = (PFN_IO_GET_LOWER_DEVICE_OBJECT)MmGetSystemRoutineAddress(&routineName);
resolved = TRUE;
}
return getLower;
}
static BOOLEAN
IsVhdmpDevice(
_In_ PDEVICE_OBJECT DeviceObject
)
{
UNICODE_STRING vhdmpName;
RtlInitUnicodeString(&vhdmpName, L"\\Driver\\vhdmp");
return DeviceObject &&
DeviceObject->DriverObject &&
RtlEqualUnicodeString(&DeviceObject->DriverObject->DriverName, &vhdmpName, TRUE);
}
static PDEVICE_OBJECT
FindVhdmpUpperDeviceInStack(
_In_ PDEVICE_OBJECT DeviceObject
)
{
PDEVICE_OBJECT current = DeviceObject;
while (current) {
if (IsVhdmpDevice(current)) {
return current;
}
current = current->AttachedDevice;
}
return NULL;
}
static PDEVICE_OBJECT
FindVhdmpLowerDeviceInStack(
_In_ PDEVICE_OBJECT DeviceObject
)
{
PFN_IO_GET_LOWER_DEVICE_OBJECT getLower;
PDEVICE_OBJECT current;
getLower = ResolveIoGetLowerDeviceObject();
if (!getLower) {
DBG_PRINT(LOG_PREFIX "MmGetSystemRoutineAddress(IoGetLowerDeviceObject) failed\n");
return NULL;
}
current = getLower(DeviceObject);
while (current) {
PDEVICE_OBJECT next;
if (IsVhdmpDevice(current)) {
return current;
}
next = getLower(current);
ObDereferenceObject(current);
current = next;
}
return NULL;
}
static NTSTATUS
FindVhdmpBackedDiskDevice(
_Outptr_ PDRIVER_OBJECT* DiskDriverObject,
_Outptr_ PDEVICE_OBJECT* DeviceObject
)
{
UNICODE_STRING diskName;
PDRIVER_OBJECT diskDriverObject = NULL;
NTSTATUS status;
*DiskDriverObject = NULL;
*DeviceObject = NULL;
RtlInitUnicodeString(&diskName, L"\\Driver\\disk");
status = ObReferenceObjectByName(
&diskName,
OBJ_CASE_INSENSITIVE,
NULL,
0,
*IoDriverObjectType,
KernelMode,
NULL,
(PVOID*)&diskDriverObject);
if (!NT_SUCCESS(status)) {
return status;
}
for (PDEVICE_OBJECT deviceObject = diskDriverObject->DeviceObject;
deviceObject;
deviceObject = deviceObject->NextDevice) {
PDEVICE_OBJECT vhdmpDevice;
vhdmpDevice = FindVhdmpLowerDeviceInStack(deviceObject);
if (vhdmpDevice) {
*DiskDriverObject = diskDriverObject;
*DeviceObject = deviceObject;
DBG_PRINT(LOG_PREFIX "selected disk device=%p vhdmp_lower_device=%p\n", deviceObject, vhdmpDevice);
ObDereferenceObject(vhdmpDevice);
return STATUS_SUCCESS;
}
vhdmpDevice = FindVhdmpUpperDeviceInStack(deviceObject);
if (vhdmpDevice) {
*DiskDriverObject = diskDriverObject;
*DeviceObject = deviceObject;
DBG_PRINT(LOG_PREFIX "selected disk device=%p vhdmp_upper_device=%p\n", deviceObject, vhdmpDevice);
return STATUS_SUCCESS;
}
}
ObDereferenceObject(diskDriverObject);
return STATUS_NOT_FOUND;
}
static NTSTATUS
ScsiTransferPageAtLba(
_In_ PDEVICE_OBJECT DeviceObject,
_In_ UCHAR Operation,
_In_ UCHAR DataDirection,
_In_ ULONG Lba,
_Inout_updates_bytes_(PAGE_SIZE) PVOID PageBuffer,
_Out_ SIZE_T* BytesCopied,
_Out_opt_ PUCHAR ScsiStatus
)
{
PSPTD_WITH_SENSE request;
KEVENT event;
IO_STATUS_BLOCK iosb;
PIRP irp;
NTSTATUS status;
USHORT sectors = (USHORT)PAGE_SECTORS;
*BytesCopied = 0;
if (ScsiStatus) {
*ScsiStatus = 0xFF;
}
request = (PSPTD_WITH_SENSE)ExAllocatePool2(
POOL_FLAG_NON_PAGED,
sizeof(SPTD_WITH_SENSE),
TAG_SPTD);
if (!request) {
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlZeroMemory(request, sizeof(SPTD_WITH_SENSE));
request->Sptd.Length = sizeof(SCSI_PASS_THROUGH_DIRECT);
request->Sptd.CdbLength = 10;
request->Sptd.SenseInfoLength = sizeof(request->Sense);
request->Sptd.DataIn = DataDirection;
request->Sptd.DataTransferLength = PAGE_SIZE;
request->Sptd.TimeOutValue = 5;
request->Sptd.DataBuffer = PageBuffer;
request->Sptd.SenseInfoOffset = FIELD_OFFSET(SPTD_WITH_SENSE, Sense);
request->Sptd.Cdb[0] = Operation;
request->Sptd.Cdb[2] = (UCHAR)((Lba >> 24) & 0xFF);
request->Sptd.Cdb[3] = (UCHAR)((Lba >> 16) & 0xFF);
request->Sptd.Cdb[4] = (UCHAR)((Lba >> 8) & 0xFF);
request->Sptd.Cdb[5] = (UCHAR)(Lba & 0xFF);
request->Sptd.Cdb[7] = (UCHAR)((sectors >> 8) & 0xFF);
request->Sptd.Cdb[8] = (UCHAR)(sectors & 0xFF);
KeInitializeEvent(&event, NotificationEvent, FALSE);
RtlZeroMemory(&iosb, sizeof(iosb));
irp = IoBuildDeviceIoControlRequest(
IOCTL_SCSI_PASS_THROUGH_DIRECT,
DeviceObject,
request,
sizeof(SPTD_WITH_SENSE),
request,
sizeof(SPTD_WITH_SENSE),
FALSE,
&event,
&iosb);
if (!irp) {
ExFreePoolWithTag(request, TAG_SPTD);
return STATUS_INSUFFICIENT_RESOURCES;
}
status = IoCallDriver(DeviceObject, irp);
if (status == STATUS_PENDING) {
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
status = iosb.Status;
}
if (NT_SUCCESS(status)) {
status = iosb.Status;
}
if (ScsiStatus) {
*ScsiStatus = request->Sptd.ScsiStatus;
}
if (NT_SUCCESS(status) && request->Sptd.ScsiStatus == 0) {
*BytesCopied = request->Sptd.DataTransferLength;
}
ExFreePoolWithTag(request, TAG_SPTD);
return status;
}
static NTSTATUS
ReadScratchPage(
_In_ PDEVICE_OBJECT DeviceObject,
_Out_writes_bytes_(PAGE_SIZE) PUCHAR PageBuffer,
_Out_ SIZE_T* BytesCopied,
_Out_opt_ PUCHAR ScsiStatus
)
{
RtlZeroMemory(PageBuffer, PAGE_SIZE);
return ScsiTransferPageAtLba(
DeviceObject,
SCSIOP_READ10,
SCSI_IOCTL_DATA_IN,
SCRATCH_LBA,
PageBuffer,
BytesCopied,
ScsiStatus);
}
static NTSTATUS
WriteScratchPage(
_In_ PDEVICE_OBJECT DeviceObject,
_In_reads_bytes_(PAGE_SIZE) PVOID PageBuffer,
_Out_ SIZE_T* BytesCopied,
_Out_opt_ PUCHAR ScsiStatus
)
{
return ScsiTransferPageAtLba(
DeviceObject,
SCSIOP_WRITE10,
SCSI_IOCTL_DATA_OUT,
SCRATCH_LBA,
PageBuffer,
BytesCopied,
ScsiStatus);
}
static NTSTATUS
BounceReadPhysicalPageViaVhdmp(
_In_ PDEVICE_OBJECT DeviceObject,
_In_ ULONGLONG PhysicalAddress,
_Out_writes_bytes_(PAGE_SIZE) PUCHAR OutputPage,
_Out_ SIZE_T* BytesCopied
)
{
PVOID mappedPage;
PHYSICAL_ADDRESS physicalAddress;
SIZE_T written = 0;
UCHAR scsiStatus = 0xFF;
NTSTATUS status;
*BytesCopied = 0;
if ((PhysicalAddress & (PAGE_SIZE - 1)) != 0) {
return STATUS_INVALID_PARAMETER;
}
physicalAddress.QuadPart = PhysicalAddress;
mappedPage = MmMapIoSpace(physicalAddress, PAGE_SIZE, MmNonCached);
if (!mappedPage) {
return STATUS_CONFLICTING_ADDRESSES;
}
status = WriteScratchPage(DeviceObject, mappedPage, &written, &scsiStatus);
MmUnmapIoSpace(mappedPage, PAGE_SIZE);
if (!NT_SUCCESS(status) || scsiStatus != 0 || written < PAGE_SIZE) {
return NT_SUCCESS(status) ? STATUS_IO_DEVICE_ERROR : status;
}
status = ReadScratchPage(DeviceObject, OutputPage, BytesCopied, &scsiStatus);
if (!NT_SUCCESS(status) || scsiStatus != 0 || *BytesCopied < PAGE_SIZE) {
return NT_SUCCESS(status) ? STATUS_IO_DEVICE_ERROR : status;
}
return STATUS_SUCCESS;
}
static VOID
ScanVhdmpBounceForPayload(VOID)
{
const SIZE_T patternLength = sizeof(g_EncodedPayloadPattern);
PPHYSICAL_MEMORY_RANGE ranges = NULL;
PDRIVER_OBJECT diskDriverObject = NULL;
PDEVICE_OBJECT deviceObject = NULL;
PUCHAR pageBuffer = NULL;
PUCHAR physicalView = NULL;
PUCHAR scanBuffer = NULL;
PUCHAR originalScratch = NULL;
SCAN_STATS stats;
NTSTATUS status;
BOOLEAN cpuidProtocolPresent = FALSE;
RtlZeroMemory(&stats, sizeof(stats));
DBG_PRINT(
LOG_PREFIX "load\n"
LOG_PREFIX "scan start, mode=vhdmp-bounce-write10-read10-bruteforce-xor pattern_size=0x%Ix scratch_lba=%lu pa=%I64x-%I64x max_probes=%lu\n",
patternLength,
(ULONG)SCRATCH_LBA,
(ULONGLONG)MIN_BOUNCE_PHYSICAL_ADDRESS,
(ULONGLONG)MAX_BOUNCE_PHYSICAL_ADDRESS,
(ULONG)MAX_BOUNCE_PROBES);
if (KeGetCurrentIrql() != PASSIVE_LEVEL) {
DBG_PRINT(LOG_PREFIX "unexpected IRQL=%lu, skip scan\n", KeGetCurrentIrql());
goto Exit;
}
cpuidProtocolPresent = ProbeShadowCpuidProtocol();
if (REQUIRE_CPUID_PROTOCOL_BEFORE_BOUNCE && !cpuidProtocolPresent) {
DBG_PRINT(LOG_PREFIX "skip vhdmp bounce scan because private cpuid protocol was not observed\n");
goto Exit;
}
pageBuffer = (PUCHAR)ExAllocatePool2(POOL_FLAG_NON_PAGED, PAGE_SIZE, TAG_SCAN);
physicalView = (PUCHAR)ExAllocatePool2(POOL_FLAG_NON_PAGED, PAGE_SIZE, TAG_SCAN);
scanBuffer = (PUCHAR)ExAllocatePool2(POOL_FLAG_NON_PAGED, PAGE_SIZE, TAG_SCAN);
originalScratch = (PUCHAR)ExAllocatePool2(POOL_FLAG_NON_PAGED, PAGE_SIZE, TAG_SCAN);
if (!pageBuffer || !physicalView || !scanBuffer || !originalScratch) {
DBG_PRINT(LOG_PREFIX "ExAllocatePool2 failed\n");
goto Exit;
}
status = FindVhdmpBackedDiskDevice(&diskDriverObject, &deviceObject);
if (!NT_SUCCESS(status)) {
DBG_PRINT(LOG_PREFIX "FindVhdmpBackedDiskDevice failed: 0x%08X\n", status);
goto Exit;
}
{
SIZE_T copied = 0;
UCHAR scsiStatus = 0xFF;
status = ReadScratchPage(deviceObject, originalScratch, &copied, &scsiStatus);
if (!NT_SUCCESS(status) || scsiStatus != 0 || copied < PAGE_SIZE) {
DBG_PRINT(
LOG_PREFIX "backup scratch LBA failed: status=0x%08X scsi=0x%02X copied=0x%Ix\n",
status,
scsiStatus,
copied);
goto Exit;
}
}
ranges = MmGetPhysicalMemoryRanges();
if (!ranges) {
DBG_PRINT(LOG_PREFIX "MmGetPhysicalMemoryRanges failed\n");
goto Exit;
}
for (PPHYSICAL_MEMORY_RANGE range = ranges;
range->BaseAddress.QuadPart || range->NumberOfBytes.QuadPart;
++range) {
ULONGLONG rawStart = (ULONGLONG)range->BaseAddress.QuadPart;
ULONGLONG rawSize = (ULONGLONG)range->NumberOfBytes.QuadPart;
ULONGLONG rawEnd = rawStart + rawSize;
ULONGLONG start = ALIGN_UP_PAGE_ULL(rawStart);
ULONGLONG end = ALIGN_DOWN_PAGE_ULL(rawEnd);
if (rawEnd <= rawStart || end <= start) {
continue;
}
if (end <= MIN_BOUNCE_PHYSICAL_ADDRESS) {
continue;
}
if (start < MIN_BOUNCE_PHYSICAL_ADDRESS) {
start = MIN_BOUNCE_PHYSICAL_ADDRESS;
}
if (start >= MAX_BOUNCE_PHYSICAL_ADDRESS) {
continue;
}
if (end > MAX_BOUNCE_PHYSICAL_ADDRESS) {
end = MAX_BOUNCE_PHYSICAL_ADDRESS;
}
if (end <= start) {
continue;
}
DBG_PRINT(LOG_PREFIX "range pa=%I64x-%I64x\n", start, end);
for (ULONGLONG pa = start; pa < end; pa += PAGE_SIZE) {
SIZE_T copied = 0;
status = CopyPhysicalPage(pa, physicalView);
if (!NT_SUCCESS(status) || !IsPageFilledWith(physicalView, 0xFF)) {
continue;
}
if (stats.BounceProbes >= MAX_BOUNCE_PROBES) {
DBG_PRINT(LOG_PREFIX "stop scan after max bounce probes=%lu\n", stats.BounceProbes);
stats.Stop = TRUE;
break;
}
++stats.BounceProbes;
status = BounceReadPhysicalPageViaVhdmp(deviceObject, pa, pageBuffer, &copied);
if (!NT_SUCCESS(status)) {
continue;
}
RtlCopyMemory(scanBuffer, pageBuffer, copied);
BruteForceScanBuffer(
scanBuffer,
copied,
pa,
g_EncodedPayloadPattern,
patternLength,
&stats);
if (stats.Stop) {
break;
}
}
if (stats.Stop) {
break;
}
}
Exit:
if (deviceObject && originalScratch) {
SIZE_T restored = 0;
UCHAR scsiStatus = 0xFF;
status = WriteScratchPage(deviceObject, originalScratch, &restored, &scsiStatus);
DBG_PRINT(
LOG_PREFIX "restore scratch LBA status=0x%08X scsi=0x%02X copied=0x%Ix\n",
status,
scsiStatus,
restored);
}
if (stats.Hits != 0) {
DBG_PRINT(LOG_PREFIX "Detected Shadow Root\n");
} else {
DBG_PRINT(LOG_PREFIX "Shadow Root not detected\n");
}
if (ranges) {
ExFreePool(ranges);
}
if (diskDriverObject) {
ObDereferenceObject(diskDriverObject);
}
if (originalScratch) {
ExFreePoolWithTag(originalScratch, TAG_SCAN);
}
if (scanBuffer) {
ExFreePoolWithTag(scanBuffer, TAG_SCAN);
}
if (physicalView) {
ExFreePoolWithTag(physicalView, TAG_SCAN);
}
if (pageBuffer) {
ExFreePoolWithTag(pageBuffer, TAG_SCAN);
}
}
static VOID
ScanMemoryUnload(
_In_ PDRIVER_OBJECT DriverObject
)
{
UNREFERENCED_PARAMETER(DriverObject);
DBG_PRINT(LOG_PREFIX "unload\n");
}
extern "C"
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = ScanMemoryUnload;
ScanVhdmpBounceForPayload();
return STATUS_SUCCESS;
}
附件说明
- Source/HookEPT:调试用的驱动源码
- Source/Keygen:TerminateCode生成源码
- Source/DetectShadow:R3层三种检测代码源码
- Source/ScanMemory:扫描内存检测代码
- Source/DetectVhdmp:检测设备代码
- Bin/:对应的二进制文件
- Conversation:对应的AI问答记录,使用了 claude 和 codex。
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。