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_resolve0x144A3EAB0
  • api_resolve_init0x144A3ED50
  • resolve_kernel32_and_virtdisk_apis0x144A10B30
  • stage_result_reporter0x144A04160
  • thread_entry_stage_result_reporter0x1449FA120

作用:

  • 配置控制台输出
  • 动态解析一些 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_4k0x140009530
  • scsi_sfer_n_4k_pages0x140004FB0
  • ensure_pfn_entry_state_for_scsi_bounce0x140003EB0
  • scsi_xfer_1_page0x140003E10

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,如果两个都不是会急

image-20260502120914969

AMD 路径#
  • bytes: E8 ?? ?? ?? ?? 48 89 04 24 E9
  • mask: x????xxxxx

image-20260502121830371

image-20260502121813044

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 结构,失败输出日志

image-20260502124412943

后面就是节拷贝,拷贝失败会输出做题的时候看到的日志

      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 的位置,看附近的代码可以发现

image-20260504180706222

262 行这个位置替换了占位符 0xCAFEBABEDEADBEEF,在对应的地方断下来步过,发现 420000 被写入了

image-20260504173745877

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

image-20260504174111811

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

image-20260504181008080

image-20260504181157810

追加了一句 call 和一句 jmp

image-20260512185812074

现在的状况: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

image-20260504184313930

image-20260504184613328

之后又将这次修改回滚(140011349),因为此时这段 payload 已经跑完。那么不难想到在此之前一定调用了一次 cpuid 来跑这段 payload

image-20260504190155823

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)image-20260504190840746

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)

image-20260504192823504

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 镜像的逻辑是必须找到的

  1. .vmem 是虚拟机物理内存,所有痕迹都可以找到,包括 vmexit handler(通过特征码定位)、代码洞、payload.sys 的位置、题目中的自建页表。题目中的日志泄露了自建页表的物理地址,据此也能够找到 payload.sys 的位置。具体地说,先是要定位到 patch 点,然后拿到 0x327FFFE01720 这个目标地址,再用页表计算物理地址。
  2. .vmem 中扫题目中多次出现的魔数,分别 dump 上下文,交给 ai 分析,应该也能够发现蛛丝马迹,并且魔数本身就在 payload.sys 中出现过

基本概念#

  1. 将 vhd 用 AttachVirtualDisk 挂载后,会在系统内暴露出一个虚拟磁盘设备,之后可以按 LBA 访问
  2. Windows 存储栈:上层通常有 partmgr.sys, disk.sys,底层是 vhdmp.sys,为 IRP 传输提供一个可用的设备栈
  3. SCSI_PASS_THROUGH_DIRECT:Windows 提供的标准 pass-through 方式,调用者构造 CDB、DataBuffer、长度等字段,让存储栈来执行一个 SCSI 命令,比如 READ(10), WRITE(10)
  4. scsi bounce:hypervisor 利用 EPT 保护 Ring -1 页面,此时需要通过 DMA 访问真实内容
  5. 为什么需要 ensure_pfn_entry_state_for_scsi_bounce:通过 scsi_sfer_n_4k_pages 的调用场景中,src 是 hypervisor 保护页面由 MmMapIoSpace 映射的结果,这种页面存储栈可能不接受,需要修改 _MMPFN 的 u4 字段(具体来说,修改 PageIdentity 这一位)。但当前版本测试下来不修改它也能正常运行