记录一下今年 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 是一个很关键的函数,它封装了一次操作,大概流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,目标物理地址,还有一个操作码。

操作码这里比较有意思:

1
op = (2 * (a2 == 42)) | 0x28;

也就是:

  • a2 == 42,最后 op 为 0x2A
  • a2 != 42,最后 op 为 0x28

因此它操作码只有两种,一个是 0x2A,一个是 0x28

这个就是后面隐藏代码的基础,根据逻辑:

可以大概得到 IoBuildDeviceIoControlRequest 的输入参数结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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,内核调试器下一个断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 的操作。

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
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 调过来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__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

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
第二次调用 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 了 vmexithandler,中间是在做扫秒页表用特征码判断 vmexit 的操作。

从dump的结果来看,基本可以确定就是 Terminate Code 算法的页面了。

代码直接让ai分析,可以分析得到一些结果

也是终于看到了验证成功的输出。

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

这个注册机判定比较简单,基本就是直接算出明文去比对的,借助一下ai的神力,得到keygen。

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
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,我系统的分支中,lenoffset 分别是 0x1D0x13,刚好对应了一个 CALL 的指令,如图所示

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

如图选中的就是 keygen 所需的内存。

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
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,观察断点处的寄存器上下文。

此时大概可以猜出它的数据传递方式:

  • RAXMagic 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 = 0xDEADBEEFCPUID VM-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

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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
#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 的存活。

检测代码

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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
#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

代码:

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
#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 页,并输出检测结果。

代码:

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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
#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。