【CTF对抗-腾讯游戏安全技术竞赛2026决赛题解】此文章归类为:CTF对抗。
从三环程序中可以直接提取出驱动,我个人比较习惯先分析驱动,因此直接开始

发现没有导入表,膨胀严重且驱动入口加了虚拟化,先选择hook一下常见api跑一下,但是导入表无了,这说明驱动是自己解析内核导出来调用的,不过一般驱动都会自己用MmGetSystemRoutineAddress解析几个导出来用,所以先hook一下跑一下日志来收集证据。
t!DbgBreakPointWithStatus:
fffff805`47c28610 cc int 3
0: kd> bp MmGetSystemRoutineAddress
0: kd> g
Breakpoint 0 hit
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx ss:0018:fffff88c`37a5f5a0=0000000000040286
1: kd> dt _UNICODE_STRING fffff88c37a5f5d0
nt!_UNICODE_STRING
"KdDebuggerEnabled"
+0x000 Length : 0x22
+0x002 MaximumLength : 0x24
+0x008 Buffer : 0xfffff88c`37a5f614 "KdDebuggerEnabled"
1: kd> g
Breakpoint 0 hit
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx ss:0018:fffff88c`37a5f5a0=9be7f70164f3dbf0
1: kd> dt _UNICODE_STRING fffff88c37a5f5d0
nt!_UNICODE_STRING
"KdDebuggerNotPresent"
+0x000 Length : 0x28
+0x002 MaximumLength : 0x2a
+0x008 Buffer : 0xfffff88c`37a5f63a "KdDebuggerNotPresent"
1: kd> g
Breakpoint 0 hit
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx ss:0018:fffff88c`37a5f5a0=9be7f70164f3dbf0
1: kd> dt _UNICODE_STRING fffff88c37a5f5d0
nt!_UNICODE_STRING
"KdDisableDebugger"
+0x000 Length : 0x22
+0x002 MaximumLength : 0x24
+0x008 Buffer : 0xfffff88c`37a5f5ee "KdDisableDebugger"
1: kd> g
Breakpoint 0 hit
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx ss:0018:fffff88c`37a5f5a0=9be7f70164f3dbf0
1: kd> dt _UNICODE_STRING fffff88c37a5f5d0
nt!_UNICODE_STRING
"KdEnableDebugger"
+0x000 Length : 0x20
+0x002 MaximumLength : 0x22
+0x008 Buffer : 0xfffff88c`37a5f666 "KdEnableDebugger"
1: kd> g
Breakpoint 0 hit
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx
nt!MmGetSystemRoutineAddress:
fffff805`47f56d20 48895c2408 mov qword ptr [rsp+8],rbx ss:0018:fffff88c`37a5f530=0000000000040282
1: kd> dt _UNICODE_STRING fffff88c37a5f5d0
nt!_UNICODE_STRING
"IoDriverObjectType"
+0x000 Length : 0x24
+0x002 MaximumLength : 0x26
+0x008 Buffer : 0xfffff88c`37a5f5f2 "IoDriverObjectType"
[HyperCharge-debug]:D:\UGit\HyperCharge2\src\kernelsys\ddma.cpp:ddma::Init:91 :failed to find disk,status : 0xc0000225
[HyperCharge-error]:D:\UGit\HyperCharge2\src\kernelsys\main.cpp:DriverEntry:36 :#1,status : 0xc0000225
对这里面和反调试相关的api下断点再跑一遍
Breakpoint 0 hit
nt!KdDisableDebugger:
fffff801`1f1673b0 4883ec28 sub rsp,28h
发现驱动确实在调用 KdDisableDebugger
将其ret后发现驱动调用 IoDriverObjectType ,因此可能是在和驱动对象通信
hook RtlInitUnicodeString 找一下字符串
ffff980b`b99975b6 5c 00 44 00 72 00 69 00-76 00 65 00 72 00 5c 00 \.D.r.i.v.e.r.\.
ffff980b`b99975c6 44 00 69 00 73 00 6b 00-00 00 20 00 22 00 00 00 D.i.s.k... ."...
ffff980b`b99975d6 00 00 66 76 99 b9 0b 98-ff ff 18 00 1a 00 00 00 ..fv............
ffff980b`b99975e6 00 00 b6 75 99 b9 0b 98-ff ff 37 44 49 00 6f 00 ...u......7DI.o.
ffff980b`b99975f6 44 00 72 00 69 00 76 00-65 00 72 00 4f 00 62 00 D.r.i.v.e.r.O.b.
ffff980b`b9997606 6a 00 65 00 63 00 74 00-54 00 79 00 70 00 65 00 j.e.c.t.T.y.p.e.
ffff980b`b9997616 00 00 a2 95 45 ac d9 23-28 7f 00 00 00 00 00 00 ....E..#(.......
ffff980b`b9997626 00 00 e2 05 00 00 00 00-00 00 a2 95 45 ac d9 23 ............E..#
很可能是在用 IoEnumerateDeviceObjectList 扫其下的设备

果然发现命中,执行完就返回了失败逻辑,猜测其是做了某些校验
hook RtlCompareMemory 发现了比较逻辑一共比较两次,综合起来预期的返回值应当是Msft Virtual Disk

伪造返回值,让其成功通过校验试试
大量MmCopy 和 MmMapVideoDisplay(其实就是MmMapIOSpace) 后蓝屏了,传入的地址可以看出是物理地址,参数不断+0x1000扫页,驱动在拿到合适的磁盘设备后,进行了爆搜物理地址的操作
总结一下
驱动手动解析出一堆api,尝试获取一个 Msft Virtual Disk 的磁盘设备,成功后进行爆搜内核,现在给了驱动的一个fake返回值而蓝屏,目前不清楚驱动真正想要的设备是什么样的
根据以上的证据我找到了一个老项目
af7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1N6r3u0V1i4K6u0r3k6r3c8E0j5b7`.`.
和 在运行时修改 Hyper-V 和SLAT绕过有关,先把题目往这方面想
获取 Msft Virtual Disk 的磁盘设备 可能是为了 访问/修改 Hyper-V 的某些函数,劫持某些功能,结合后面爆搜内核地址的逻辑来看,很可能存在特征码,因为固定偏移就不需要爆扫内核了。
在驱动中找相关的逻辑
发现明显的特征码扫描特征分析一下看看命中的是什么组件
直接用我的分析系统 22631
pattern
66 83 FE 01 75 0A E8 00 00 00 00 48 8B 4C 24 00 FB 8B D6 0B 54 24 00 E8 00 00 00 00 E9
mask
xxxxxxx????xxxx?xxxxxx?x????x
patch_offset
0x17
命中了 hvix64 .text 的 RVA 0x23E407


里面似乎是VmExithandle
结合ida中有关扫内核和windbg的hook日志

不难判断出驱动使用了一种类似 1e1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1N6r3u0V1i4K6u0r3k6r3c8E0j5b7`.`. 的技术来绕过 Hyper-v SLAT 搜索并定位相关 VmExithandle,因为现在还没有分析三环程序 我只猜测可能改了 VmExithandle 的hypercall之类的把校验逻辑藏在里面
既然大概的逻辑可以分析出来,接下来就是定位它往VmExithandle里面到底改了什么
尝试伪造磁盘信息骗过驱动
eb 56 69 72 74 75 61 6c 20 44 69 73 6b 20 20 20 20
之后却发现驱动总是失败退出或蓝屏,定位不到目标区域,在这里卡了好长一段时间
后来想到既然三环程序总归是能独立加载并拉起驱动的,说明三环程序有着让驱动能真正找到patch点的能力
因此转到三环进行分析
从驱动入手可以基本分析得到可能是一个 Hyper-v SLAT bypass 因此首先想到三环程序可能是检测了 Microsoft Hv
搜一下 cpuid 果然如此

sub_144A06650

xor恢复结果
ModuleHandleA = GetModuleHandleA("ntdll.dll");
result = GetProcAddress(ModuleHandleA, "NtQuerySystemInformation");
逻辑如下
通过动态解密字符串后获取 firmware table 相关 API
枚举 ACPI / firmware table
扫描表项签名
若发现:
DMAR
或 IVRS则返回成功
上述的两个函数检查了
是否运行在 Microsoft Hyper-V
是否存在 IOMMU / VT-d / AMD-Vi 相关 ACPI 表
开启 Hyper-V 关掉 iommu 虚拟化 ,程序运行成功且成功加载了驱动

根据前面的分析 三环程序大概率 是有创建 VHD 的功能,不过既然能成功加载驱动并正确使用其功能,我们可以继续进行驱动的分析

字符串定位到加载驱动的逻辑,这里再system32/driver下面落地了一个随机名字的驱动,给他patch成固定的ACEDriver.sys方便我们在windbg调试时少打一条lm
能成功启动三环程序说明对 Hyper-V 的修改是已经发生了,我们逆向一下对应的 读/写 函数所在位置,准备打断点分析


通过查找手动解析的发io请求的导入的调用位置 我们可以恢复出未被虚拟化的驱动读写函数
地址位于140003E60
其中 ddma_sptd_transfer_page 是底层函数,其调用的api如下图所示
[I][GenericMonitorHandler():2948] Function: IoEnumerateDeviceObjectList
Hooked Address: fffff8011efc64b0
RCX: ffffaf0a39f9f960, RDX: ffffaf0a425fd2d0, R8: 8, R9: ffff980bb82371e4
Return Address: fffff8012de91bca
[I][GenericMonitorHandler():2948] Function: KeInitializeEvent
Hooked Address: fffff8011eeaf840
RCX: ffff980bb8236fb8, RDX: 0, R8: 0, R9: 67b35c77cbf19e2b
Return Address: fffff8012dea8788
[I][GenericMonitorHandler():2948] Function: IoBuildDeviceIoControlRequest
Hooked Address: fffff8011ee51430
RCX: 4d014, RDX: ffffaf0a39fa1060, R8: ffff980bb8236fd0, R9: 60
Return Address: fffff8012deaa6f3
[I][GenericMonitorHandler():2948] Function: IofCallDriver
Hooked Address: fffff8011ee2ef10
RCX: ffffaf0a39fa1060, RDX: ffffaf0a46767080, R8: ffffaf0a42469040, R9: f9ba63ab8ae24c95
Return Address: fffff8012deaaf53
[I][GenericMonitorHandler():2948] Function: KeWaitForSingleObject
其中构造
IOCTL_SCSI_PASS_THROUGH_DIRECT = 0x4D014
TransferLength = 0x1000
opcode:
0x28 = READ(10)
0x2A = WRITE(10)
发 IOCTL_SCSI 请求虚拟磁盘按页读写,加载驱动后发现多了 vhdmp 设备对象

设备是 vhdmp 驱动就是用这个设备对象发 IOCTL_SCSI_PASS_THROUGH_DIRECT 进行类ddma读写

驱动爆搜内核找 hvix64的 vmexithandle
如果 MmCopyMemory 读不到 就走 MmMapIoSpace+
ddma_disk_copy_page 的类ddma读写
查引用发现还有一个使用ddma_disk_copy_page 类ddma读写 的函数

一共找到如下的ddma写入函数
bp ACEDriver+0x3E4C(单页读)
bp ACEDriver+0x5001(10页写)
bp ACEDriver+0x96BB (底层)
之后对于驱动的ddma读写函数都进行了hook来分析驱动是怎样劫持vmexit的

dump出来一堆看似有用的垃圾数据既然驱动要劫持vmexit 必然会写hyper-v地址空间
因此hook全部的ddma写函数来分析驱动ddma写入行为

找到一个看起来像是pe的页写入,先保存下来

经过长时间的调试 终于在 HyperCharge-debug 释放完日志后的一次ddma写入函数调用中找到了目标hyperv进程

原本是0xcc的代码洞被驱动写入了一个stub
ffff9600`8ff78406 50 push rax
ffff9600`8ff78407 53 push rbx
ffff9600`8ff78408 48bb00e0dfbf7fffffff mov rbx, 0FFFFFF7FBFDFE000h
ffff9600`8ff78412 48b80030150001000000 mov rax, 100153000h
ffff9600`8ff7841c 4883c803 or rax, 3
ffff9600`8ff78420 48898320030000 mov qword ptr [rbx+320h], rax
ffff9600`8ff78427 0f20db mov rbx, cr3
ffff9600`8ff7842a 0f22db mov cr3, rbx
ffff9600`8ff7842d 48bb0000e0ff7f320000 mov rbx, 327FFFE00000h
ffff9600`8ff78437 488b03 mov rax, qword ptr [rbx]
ffff9600`8ff7843a e800000000 call FFFF96008FF7843F
ffff9600`8ff7843f 58 pop rax
ffff9600`8ff78440 482500f0ffff and rax, 0FFFFFFFFFFFFF000h
ffff9600`8ff78446 4805a038fdff add rax, 0FFFFFFFFFFFD38A0h
ffff9600`8ff7844c 4881c380400000 add rbx, 4080h
ffff9600`8ff78453 488903 mov qword ptr [rbx], rax
ffff9600`8ff78456 5b pop rbx
ffff9600`8ff78457 58 pop rax
ffff9600`8ff78458 e84334fdff call FFFF96008FF4B8A0
ffff9600`8ff7845d e982ffffff jmp FFFF96008FF783E4
对比winddbg中原逻辑 发现他 call FFFF96008FF4B8A0 后 jmp FFFF96008FF783E4 jmp到了
FFFF96008FF4B8A0 的下一条指令处
这说明 call FFFF96008FF4B8A0 应该会被修改为 jmp ffff9600`8ff78406
FFFFFF7FBFDFE000当前没法访问 stub把
生成出来的 PDPT 物理地址 0x100153000 写到它的 +0x320
FFFFFF7FBFDFE000 无法访问,可能是hyper-v私有页表之类的
mov cr3后立刻去访问 0x327FFFE00000 这块隐藏虚拟窗口
0x327FFFE00000= PML4[100] / PDPT[511] / PD[511] / PT[0]
0x320 / 8 = 100是 PML4 第 100 项
这说明 327FFFE00000 很可能就是这段stub带起来的逻辑,解题的关键就在 327FFFE00000 附近的可执行逻辑里面
call FFFF96008FF7843F
pop rax
and rax, 0FFFFFFFFFFFFF000h
add rax, 0FFFFFFFFFFFD38A0h
随后解析了 原来Hyper-v的handle所在的地址 传到 0x327FFFE00000+0x4080

继续找下一个写入点 果然stub被改了,这说明我们之前的分析是正确的

FFFFFF7FBFDFE000这个地址我识别,在 Voyager 这个项目中也有使用Voyager 是一个旨在为 AMD 和 Intel 版本的 Hyper-V 提供模块注入和 vmexit 钩子的项目
这说明我们分析的驱动逻辑基本上正确

从voyager的源码来看 驱动应该也有一个映射器,这和我们前面观察到的一个类pe页相符合,驱动很可能早就将最终payload映射到了内存中
重新分析下驱动 果然在 14000E630 找到了对应的映射函数此函数 构造了新的 PT/PD/PDPT 三页页表,有明显识别pe的特征,并且映射后抹去了pe头,这解释了之前为什么只看到像pe但没找到pe头

也能通过地址确定这就是我们找的释放最终payload的函数


打断点dump下来分析一下,可以看dump的驱动和我们在运行态抓到的写入操作完全匹配
payload1偏移是 4080 与 前面提到过的 handle所在的地址 传到 0x327FFFE00000+0x4080 符合,确实有这么一个全局变量
驱动入口
只拦 CPUID VM-exit
命中 magic leaf 就自己处理
不命中就回原始 handler

分析下140001000 逻辑如下
orig = payload_1; //前面stub传的原Hyper-V的vmexit handle指针
page = orig & ~0xFFF;
q500 = *(u64 *)(page + 0x500);
q508 = *(u64 *)(page + 0x508);
r8 = q500 ^ 0x5348414430574E54;
r9 = q508 ^ 0x4859504552564D58;
循环 8 轮 mixer
return (in1 == r8 && in2 == r9);

sub_1400013A0 分析出来是 cpuid handle
0x114514分支
直接返回:
*a1 = 0x01919810; return 1;
0x1919810分支
调 sub_140001000(*a2, *a4)
成功返回 [OK]
失败返回 [!!]
0xb16b00b5分支
调 sub_140001000(*a2, *a4)
成功时输出一个解码后的 token
失败时输出 "Access Denied"
成功 token 是:
ShadowCore Destroyed
0xCAFED00D分支
调 sub_140001000(*a2, *a4)
失败时输出 "TRY AGAIN"
成功 token 是:
MISSION COMPLETE
虽然现在还没分析到对应三环最终校验证据,但是可以先保存下来,之后找三环逻辑会轻松不少
现在已经能完整解答题目 (2) 了
「根」的实现不是把核心逻辑直接放在 Windows 驱动里,而是先由shadow_panel.exe加载驱动,驱动先找shadow_panel.exe创建的vhd之后用vhdmp进行读写,然后驱动解析内核导出并扫描 hvix 隐藏页,依据版本寻找不同特征码,在我分析机上通过vhdmp进行类 DDMA 的 SCSI 读过掉slat爆扫内核物理地址定位到hvix64!0x23E41E 这一 VM-exit 关键 call 点;
随后驱动通过vhdmp进行类 DDMA 的 SCSI 读写链,搬运隐藏页正文与payload代码,并由在相同函数中构造新的PT/PD/PDPT三页页表。之后驱动在 hvix 目标页中植入 stub,并再次使用通过vhdmp进行的类 DDMA写入把原始call改成jmp stub。当 Hyper-V 后续自然执行到该 VM-exit 路径时,执行流即被导向 stub;
stub 再将0xFFFFFF7FBFDFE000+0x320(是Hyper-v在510处的自映射)处的 PML4 项改为驱动准备好的 PDPT,刷新 CR3,使0x327FFFE00000隐藏映射窗口生效,并进一步call原handle把执行流切到真正的payload。最终,核心功能不再依赖原始驱动,而是在 hvix / VMX-root 的匿名驻留页中长期存在并运行,因此驱动卸载后功能仍在,模块枚举也难以发现,从而完成了对操作系统底层的攻击与关键核心代码的隐藏。
分析驱动已经知道是用的特殊cpuid做的隐蔽通信,因此直接找就行

cpuid(EAX=0xDEADBEEF, ECX=0x114514) 的 EAX 必须等于:0x01919810
剩下两个cpuid似乎在虚拟化路径中,但是不影响解题,因为驱动失败提示已知 第一次 Access Denied对应B16B00B5 第二次自然对应0xCAFED00D
因此恢复处三环程序cpuid校验过程
0x114514 -> 0x01919810 -> 0xB16B00B5 -> 0xCAFED00D
这里三环程序的成功部署条件也全部分析出来了
1.运行在 Microsoft Hyper-V
2.关掉 iommu 虚拟化,因为如果启用了 IOMMU,Hyper-V 也会通过它隐藏自身,使其免受运行时 DMA 攻击。
3.管理员运行以加载驱动
4.cpuid(EAX=0xDEADBEEF, ECX=0x114514) 的 EAX 必须等于:0x01919810
orig = payload_1;
page = orig & ~0xFFF;
q500 = *(u64 *)(page + 0x500);
q508 = *(u64 *)(page + 0x508);
r8 = q500 ^ 0x5348414430574E54;
r9 = q508 ^ 0x4859504552564D58;
循环 8 轮 mixer
return (in1 == r8 && in2 == r9);
自己手动按分析出的逻辑从hvix64提出payload_1
handle地址 + 0x500 q500 = 0x33574D8B48000000ULL;
handle地址 + 0x508 q508 = 0x8B4C2824448948D2ULL;
写个脚本
MASK = (1 << 64) - 1
q500 = 0x33574D8B48000000
q508 = 0x8B4C2824448948D2
X1 = 0x5348414430574E54
X2 = 0x4859504552564D58
K1 = 0x9E3779B97F4A7C15
K2 = 0x40A7B892E31B1A47
def rol64(x, n):
return ((x << n) | (x >> (64 - n))) & MASK
print(f"q500 = 0x{q500:016X}")
print(f"q508 = 0x{q508:016X}")
r8 = q500 ^ X1
r9 = q508 ^ X2
print(f"init r8 = q500 ^ X1 = 0x{r8:016X}")
print(f"init r9 = q508 ^ X2 = 0x{r9:016X}")
print()
for i in range(8):
t1 = (rol64(r9, 13) * K1) & MASK
r8 = (r8 + t1) & MASK
t2 = (rol64(r8, 29) - K2) & MASK
r9 ^= t2
r8 ^= (r9 >> 17)
r9 = (r9 + ((r8 << 7) & MASK)) & MASK
print(f"round {i+1}:")
print(f" r8 = 0x{r8:016X}")
print(f" r9 = 0x{r9:016X}")
print()
password = f"{r8:016X}{r9:016X}"
print(f"secret1 = 0x{r8:016X}")
print(f"secret2 = 0x{r9:016X}")
print(f"PASSWORD = {password}")
成功得到密码
PASSWORD = C453CA26984755FD16FA037F03D99807
成功截图

先把驱动中全部特征码匹配逻辑 找出来,然后写keygen先获取版本号,匹配特征码匹配逻辑,寻找磁盘上hvix64.exe并处理偏移问题,之后计算出正确终止密钥。

后面还有一段amd的 ida没识别出来

特征码逻辑
intelcpu hvix64.exe
1)build >= 22621
分支入口:
xxxxxxx????xxxx?xxxxxx?x????x
对应 pattern bytes:66 83 FE 01 75 0A E8 00 00 00 00 48 8B 4C 24 00 FB 8B D6 0B 54 24 00 E8 00 00 00 00 E9
2)19041 <= build < 22621
pattern:0x140021070
长度:0x19
patch offset:0x13
mask:
xxxxxxxxxxxxx?xxxx?x????x
pattern:
65 C6 04 25 6D 00 00 00 00 48 8B 4C 24 00 48 8B 54 24 00 E8 00 00 00 00 E9
3)17763 <= build < 19041
pattern:0x140021090
长度:0x19
patch offset:0x13
mask:
xxxx?xxx????xxxxxx?x????x
pattern:
48 8B 4C 24 00 EB 07 E8 00 00 00 00 EB F2 48 8B 54 24 00 E8 00 00 00 00 E9
4)17134 <= build < 17763
pattern:0x1400210B0
长度:0x19
patch offset:0x13
mask:
xxxxxxx?xx????xxxx?x????x
pattern:
F2 80 3D FC 12 46 00 00 0F 84 00 00 00 00 48 8B 54 24 00 E8 00 00 00 00 E9
5)10586 <= build < 17134
pattern:0x1400210D0
长度:0x19
patch offset:0x13
mask:
xx????x?xx????xxxx?x????x
pattern:
D0 80 00 00 00 00 00 00 0F 84 00 00 00 00 48 8B 54 24 00 E8 00 00 00 00 E9
6)10240 <= build < 10586
pattern:0x1400210F0
长度:0x19
patch offset:0x13
mask:
xxxxxxxxxxxxxxx????x????x
pattern:
60 C0 0F 29 68 D0 80 3D 7E AF 49 00 01 0F 84 00 00 00 00 E8 00 00 00 00 E9
7)build < 10240
amd分支 hvax64.exe
E8 00 00 00 00 48 89 04 24 E9
"x????xxxxx"
直接用ai
prompt:
我找出了全部特征码匹配逻辑并落到了readme.md 将现在脚本改成根据版本进行特征码匹配找seed 要满足 使得在任意机器,任何一次运行shadow_panel.exe,都可以正确计算出终止密码。用py编写
完整代码见keygen.py
def main(argv: Optional[Iterable[str]] = None) -> int:
args = parse_args(argv)
if args.cpu:
cpu = args.cpu
vendor_text = f"manual:{cpu}"
else:
cpu, vendor_text = detect_cpu_from_registry()
build = args.build if args.build is not None else detect_build_from_registry()
if args.file_path:
file_path = Path(args.file_path)
else:
file_path = get_windows_hv_file(cpu)
sig = select_signature(cpu, build)
try:
pe = parse_pe(file_path)
except ValueError:
print(f"failed to parse PE: {file_path}", file=sys.stderr)
return 1
hits = mask_search_all(pe.data, sig)
if not hits:
print(f"signature not found: {sig.name}", file=sys.stderr)
return 1
match_raw = hits[0]
patch_raw = match_raw + sig.patch_offset
if patch_raw + 5 > pe.size or pe.data[patch_raw] != 0xE8:
print("selected call site is not E8", file=sys.stderr)
return 1
try:
match_rva = pe.raw_to_rva(match_raw)
patch_rva = pe.raw_to_rva(patch_raw)
except ValueError:
print("failed to convert raw offset to RVA", file=sys.stderr)
return 1
rel32 = struct.unpack_from("<i", pe.data, patch_raw + 1)[0]
handler_rva = (patch_rva + 5 + rel32) & 0xFFFFFFFF
seed_page_rva = handler_rva & 0xFFFFF000
try:
q500_raw = pe.rva_to_raw(seed_page_rva + 0x500)
q508_raw = pe.rva_to_raw(seed_page_rva + 0x508)
except ValueError:
print("failed to read seed qwords", file=sys.stderr)
return 1
if q500_raw >= pe.size or q500_raw + 8 > pe.size or q508_raw >= pe.size or q508_raw + 8 > pe.size:
print("failed to read seed qwords", file=sys.stderr)
return 1
q500 = struct.unpack_from("<Q", pe.data, q500_raw)[0]
q508 = struct.unpack_from("<Q", pe.data, q508_raw)[0]
part1, part2 = derive_key_parts(q500, q508)
key_hex = f"{part1:016X}{part2:016X}"
key_le = (part1.to_bytes(8, "little") + part2.to_bytes(8, "little")).hex().upper()
运行结果 driver:19041+


不同机器运行结果

确实能兼容驱动适配的对应版本
是对类 DDMa 读写的检测,自然想到监控三环设备创建虚拟磁盘和驱动向虚拟磁盘发请求等
可在内核中监控以下行为:
枚举设备检测
Hook \Driver\Disk 的 IRP_MJ_DEVICE_CONTROL,识别:
IOCTL_SCSI_PASS_THROUGH_DIRECT (0x4D014),CDB0 == 0x28 / 0x2A,DataTransferLength == 0x1000,LBA == 0,blocks == 8
vhdmp读写检测
Hook \Driver\vhdmp 的 IRP_MJ_INTERNAL_DEVICE_CONTROL,识别:
vendor='Msft' , product='Virtual Disk',READ10 / WRITE10,xfer == 0x1000,LBA == 0,blocks == 8
告警规则
若短时间内在同一 Msft Virtual Disk 上出现大量:
READ10/WRITE10 4KB LBA=0 blocks=8
则可判定为样本使用虚拟盘执行页级运输/DDMA-like 访问。
测试结果

identity via='vhdmp' dev=FFFFDC0C43028050 vendor='Msft' product='Virtual Disk' exactMsftVirtualDisk=1
可以发现探测到了Msft Virtual Disk 这一vhd的选中
在 nt!KdDisableDebugger 调用前还没进入爆扫内核路径,在调用后立马出现
[ALERT] DDMA-like translated 4KB page transport detected via='vhdmp' dev=FFFFDC0C43028050 vendor='Msft' product='Virtual Disk' exactMsftVirtualDisk=1
说明确实探测到了驱动发大量 IRP_MJ_INTERNAL_DEVICE_CONTROL来读写的操作
部分代码
static VOID TryHookKnownStorageDrivers(VOID)
{
InstallHookByName(L"\\Driver\\Disk", TRUE, FALSE);
InstallHookByName(L"\\Driver\\vhdmp", FALSE, TRUE);
}
static VOID ImageLoadNotify(
_In_opt_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId,
_In_ PIMAGE_INFO ImageInfo
)
{
CHAR baseName[MAX_ID_LEN];
UNREFERENCED_PARAMETER(ProcessId);
if (!ImageInfo || !ImageInfo->SystemModeImage) {
return;
}
CopyUnicodeBaseNameToAnsi(baseName, sizeof(baseName), FullImageName);
if (_stricmp(baseName, "disk.sys") == 0) {
InstallHookByName(L"\\Driver\\Disk", TRUE, FALSE);
} else if (_stricmp(baseName, "vhdmp.sys") == 0) {
InstallHookByName(L"\\Driver\\vhdmp", FALSE, TRUE);
}
}
抓:
CreateVirtualDisk OpenVirtualDisk AttachVirtualDisk
方式是:
Frida 注入目标进程
监控 virtdisk.dll 加载对这 3 个导出函数挂钩,打印调用参数和返回值
结果

可以看到确实有初始化 Virtual Disk 的api调用
更多【CTF对抗-腾讯游戏安全技术竞赛2026决赛题解】相关视频教程:www.yxfzedu.com