Skip to content

关于驱动 31 的报告 #156

Description

@Felix3322

我把当前 main 里的 DriverEntry、控制设备创建、两个 WDF 队列、五类内核回调、前置 HVM/RXPF 初始化、INF/KMDF 配置以及最终签名脚本都追了一遍。

当前最合理的判断:少数机器返回 31,首要嫌疑是 KswordARKCallbackInitialize 的“一票否决”设计;而从 31 这个数值本身看,WdfIoQueueCreate 返回 STATUS_UNSUCCESSFUL 是最直接的对应路径。

现在无法在二者之间定案,因为 KSword 没有把“失败阶段 + 原始 NTSTATUS”持久化下来,而且回调启动期的内部日志实际上会被自己吞掉。

真正能让 KSword 整体加载失败的路径只有三段

当前 DriverEntry 中,下面三步失败才会直接把失败状态返回给系统:

  1. WdfDriverCreate
  2. KswordARKDriverCreateControlDevice
  3. KswordARKCallbackInitialize

HVM、IDT baseline、RX/PF、Driver Communication、Dispatch Editor、Driver Image、DynData、Redirect、Network、File Monitor 的初始化失败全部只是记录 warning,随后继续加载。因此,PDB、动态偏移、HVM 不支持、minifilter 注册失败都不是 31 的直接原因;除非其中存在尚未发现的内存破坏,这一点目前没有静态证据。

首要嫌疑:五类回调里任何一个失败,整个驱动都死

KswordARKCallbackInitialize 当前的顺序是:

创建 callback runtime
创建 AskUser 手动等待队列
注册表回调
进程回调
线程回调
映像加载回调
对象句柄回调
发布全局 runtime

注册表、进程、线程、映像回调只要有一个注册失败,代码就立即:

释放全局锁
撤销此前已经注册的所有回调
删除运行时
把原始失败状态返回 DriverEntry
DriverEntry 删除控制设备并返回失败

对象回调也只有 STATUS_ACCESS_DENIED 和 STATUS_INVALID_IMAGE_HASH 被允许降级;若是 altitude collision、资源不足或参数错误,仍然让整个驱动加载失败。

这完全符合“绝大多数人正常,少数特定机器失败”的分布:

  • 注册表回调固定使用 altitude 385201.5141;另一个已加载的 KSword、分叉版本或碰巧使用同 altitude 的驱动会让 CmRegisterCallbackEx 返回 STATUS_FLT_INSTANCE_ALTITUDE_COLLISION。Microsoft 也明确说明 altitude 由 Microsoft 管理分配。
  • 对象回调固定使用 385201.5142;同样可能发生 altitude collision。当前代码没有把这种冲突作为可降级状态。
  • 进程创建回调在重复注册或系统达到回调数量限制时返回 STATUS_INVALID_PARAMETER。
  • 映像加载回调在现代系统最多同时注册 64 个;达到限制时返回 STATUS_INSUFFICIENT_RESOURCES。装有多套 EDR、杀毒、反作弊和调试驱动的机器更容易命中这一条件。
  • 线程回调注册也可能因资源不足失败。

项目链接参数已经设置了 /INTEGRITYCHECK,所以干净构建上的进程回调不应因为缺少 IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 而返回 STATUS_ACCESS_DENIED。这让“回调数量限制、固定 altitude 冲突、资源分配失败”比“缺少完整性标志”更值得优先检查。

现在为什么完全不知道是哪一个回调失败

这里有一个很明确的可观测性 bug。

KswordArkCallbackLogFrame 首先通过 g_KswordArkCallbackRuntime 找 runtime;全局 runtime 为空时,它直接丢弃日志。

但 g_KswordArkCallbackRuntime = runtime 是在五类回调全部注册成功之后才执行的。也就是说,注册表、进程、线程、映像和对象回调在初始化阶段输出的成功或失败日志都无法进入 KSword 自己的日志队列。

所以目前用户端只能看到:

StartService failed: 31

驱动内部即使知道原始 NTSTATUS,也没有可靠地把“在哪一步失败”带出来。

这个要先修,不然继续猜没有意义。

第二嫌疑:控制设备的默认 WDF 队列

控制设备创建过程中,下列步骤也是致命的:

WdfControlDeviceInitAllocate
WdfDeviceInitAssignName
WdfDeviceCreate
WdfSpinLockCreate
调试输出初始化
WdfDeviceCreateSymbolicLink
WdfIoQueueCreate(默认队列)

其中 KswordARKDriverQueueInitialize 创建了一个默认并行队列。Microsoft 文档明确列出:如果设备已经存在默认队列,或者 WDF 发生内部错误,WdfIoQueueCreate 可以返回 STATUS_UNSUCCESSFUL。STATUS_UNSUCCESSFUL 是通向 Win32 ERROR_GEN_FAILURE = 31 最直接的状态。

但是当前源码中,每个控制设备只创建了一次默认队列,没有明显的重复调用。因此:

  • 若拿到的原始状态确实是 0xC0000001,优先断在 WdfIoQueueCreate。
  • 若源码与实际二进制完全一致,“默认队列已经存在”不太成立;剩下的是 WDF 内部失败、实际运行的不是当前构建,或者此前初始化破坏了 WDF/device 状态。
  • AskUser 队列是手动队列,不是第二个默认队列,而且已经显式设置 PowerManaged = WdfFalse。

控制设备默认队列也建议显式写成:

WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(
&queueConfig,
WdfIoQueueDispatchParallel
);
queueConfig.PowerManaged = WdfFalse;
queueConfig.EvtIoDeviceControl = KswordARKDriverEvtIoDeviceControl;
queueConfig.EvtIoRead = KswordARKDriverEvtIoRead;
queueConfig.EvtIoStop = NULL;

控制设备不支持电源管理队列;当前 WdfUseDefault 理应被 WDF 解析为非电源管理,但显式设为 WdfFalse 可以排除版本和配置歧义。

还有一个结构问题:当前代码在 KswordARKCallbackInitialize 之前已经执行 WdfControlFinishInitializing。也就是控制设备先对外可见,再开始建立回调运行时。应当把“创建设备”和“发布设备”拆开,等队列与 callback runtime 都完成后再 WdfControlFinishInitializing。

设备名或符号链接冲突也是实际候选

KSword 使用固定控制设备名和固定 DOS 符号链接。如果机器上还有一个旧 KSword 驱动以其他服务名加载,或者旧版本没有正确卸载,下面两步可能失败:

WdfDeviceCreate
WdfDeviceCreateSymbolicLink

这类情况会集中出现在升级过多个版本、使用过测试服务名或手动加载过不同副本的人身上,也符合“少数机器固定失败”。

复现后可立即检查:

sc.exe query type= driver state= all | findstr /i "Ksword"
reg.exe query HKLM\SYSTEM\CurrentControlSet\Services /s /f "KswordARK.sys"

同时用 WinObj 查看:

\Device\KswordARKLog
\GLOBAL??\KswordARKLog

具体名字以 KswordArkLogProtocol.h 当前定义为准。驱动启动失败后这些对象仍存在,就不是普通资源不足,而是另一实例或清理不完整。

签名和最终二进制不能排除,但应当用“是否进入 DriverEntry”判断

当前构建不是简单地对 .sys 签一次名。MSBuild 会:

  1. 复制 clean driver;
  2. 调用 CSignTool;
  3. 再添加 cross-certificate;
  4. 通过 AuthenticodeVariantGUI 生成最终变体;
  5. 用变体替换原始 KswordARK.sys。

这里有个发布风险:脚本最终只是用 Get-AuthenticodeSignature 打印摘要,不会因为最终状态无效而失败;变体流程失败时还会进入 non-fatal legacy test-sign fallback,即使 fallback 也失败,构建仍可能继续。

因此最终发布必须增加硬校验:

signtool.exe verify /kp /all /v .\KswordARK.sys
if ($LASTEXITCODE -ne 0) {
throw "Final kernel signature verification failed."
}
Get-FileHash .\KswordARK.sys -Algorithm SHA256

判断边界非常清楚:

  • DriverEntry 的第一条 breadcrumb 都没有出现:优先查最终 .sys、签名、Code Integrity、导入函数、KMDF 绑定和系统版本。
  • 已经进入 DriverEntry:签名已经通过主要加载检查,应转查 WDF、控制设备和 callback registration。

项目使用 KMDF 1.15,并且 INF 的声明范围从 Windows 10 build 16299 开始。低于 10.0.16299 的系统应直接判定为不受支持,而不是继续分析 callback。

应立即加入的启动 breadcrumb

不要只记:

"KswordARKCallbackInitialize failed"

应当保存到:

HKLM\SYSTEM\CurrentControlSet\Services\KswordARK\Parameters
LastStartStage
LastStartStatus
LastStartBuild

建议阶段编号:

typedef enum _KSWORD_START_STAGE {
KswordStartEnteredDriverEntry = 1,
KswordStartWdfDriverCreate,
KswordStartControlInitAllocate,
KswordStartDeviceAssignName,
KswordStartDeviceCreate,
KswordStartLogChannel,
KswordStartDebugOutput,
KswordStartSymbolicLink,
KswordStartDefaultQueue,
KswordStartCallbackRuntimeAllocate,
KswordStartCallbackWaitQueue,
KswordStartRegistryCallback,
KswordStartProcessCallback,
KswordStartThreadCallback,
KswordStartImageCallback,
KswordStartObjectCallback,
KswordStartReady
} KSWORD_START_STAGE;

每次调用前写 stage,失败时写原始 NTSTATUS:

static NTSTATUS
KswordStartFailure(
In KSWORD_START_STAGE Stage,
In NTSTATUS Status
)
{
DbgPrintEx(
DPFLTR_IHVDRIVER_ID,
DPFLTR_ERROR_LEVEL,
"[KswordARK] startup failure: stage=%lu status=0x%08lX\n",
(ULONG)Stage,
(ULONG)Status
);
KswordPersistStartupResult(Stage, Status); // 失败也不得影响原始返回值
return Status;
}

然后:

KswordPersistStartupStage(KswordStartDefaultQueue);
status = WdfIoQueueCreate(
Device,
&queueConfig,
WDF_NO_OBJECT_ATTRIBUTES,
&queue
);
if (!NT_SUCCESS(status)) {
return KswordStartFailure(
KswordStartDefaultQueue,
status
);
}

回调注册阶段也分别保存:

status = KswordArkRegistryCallbackRegister(runtime);
runtime->RegistryRegisterStatus = status;
if (NT_SUCCESS(status)) {
runtime->RegisteredCallbacksMask |=
KSWORD_ARK_CALLBACK_REGISTERED_REGISTRY;
}
else {
TraceEvents(
TRACE_LEVEL_WARNING,
TRACE_DRIVER,
"Registry callback unavailable %!STATUS!",
status
);
}

回调功能不应再阻止整个驱动加载

当前驱动已经有 RegisteredCallbacksMask,但初始化逻辑没有真正利用它做降级。正确结构是:

  • runtime 分配失败:禁用 callback 模块,核心驱动继续加载;
  • AskUser 队列失败:禁用 AskUser,其他 callback 仍可注册;
  • 注册表回调失败:只清除 Registry capability;
  • 进程回调失败:只清除 Process capability;
  • 线程回调失败:只清除 Thread capability;
  • 映像回调失败:只清除 Image capability;
  • 对象回调失败:只清除 Object capability;
  • UI 显示每项原始 NTSTATUS 和可用状态。

这样即使某台机器已经占满 image callback 槽位,用户仍然可以使用 KSword 的驱动、进程、线程、内存、句柄、内核审计等其他能力。

另外,初始化日志应改为显式接收正在构建的 runtime:

KswordArkCallbackLogFrameForRuntime(
runtime,
"Warn",
"Image callback registration failed."
);

不能在发布 g_KswordArkCallbackRuntime 之前再通过全局变量反查 runtime,否则启动日志仍会被丢掉。

我建议的定案方式

做三组诊断构建,在一台确定会返回 31 的机器上测试:

A:WdfDriverCreate + control device + 默认队列,不初始化 callback
B:A + callback manual wait queue
C:B + Registry / Process / Thread / Image / Object 逐个启用

结果会非常明确:

  • A 都失败:控制设备、默认队列、WDF 或最终二进制问题;
  • A 成功、B 失败:callback waiter 的 WdfIoQueueCreate;
  • B 成功、某个 C 失败:对应的内核 callback 注册 API;
  • 连 DriverEntry stage 1 都没有:签名、CI、导入或 KMDF/系统版本。

受影响机器同时收集:

$Since = (Get-Date).AddMinutes(-10)
Get-WinEvent -FilterHashtable @{
LogName = 'System'
StartTime = $Since
} |
Where-Object {
$_.ProviderName -in @(
'Service Control Manager',
'Microsoft-Windows-Kernel-PnP',
'Microsoft-Windows-Kernel-WDF'
)
} |
Format-List TimeCreated, Id, ProviderName, LevelDisplayName, Message
Get-WinEvent -LogName 'Microsoft-Windows-CodeIntegrity/Operational'
-MaxEvents 100 |
Format-List TimeCreated, Id, LevelDisplayName, Message
Get-FileHash .\KswordARK.sys -Algorithm SHA256
sc.exe qc KswordARK
sc.exe queryex KswordARK
Get-ComputerInfo |
Select-Object WindowsProductName, WindowsVersion, OsBuildNumber, OsArchitecture

内核调试时直接下这些断点:

bu KswordARK!DriverEntry
bu KswordARK!KswordARKDriverCreateControlDevice
bu KswordARK!KswordARKDriverQueueInitialize
bu KswordARK!KswordArkCallbackWaiterInitialize
bu KswordARK!KswordArkRegistryCallbackRegister
bu KswordARK!KswordArkProcessCallbackRegister
bu KswordARK!KswordArkThreadCallbackRegister
bu KswordARK!KswordArkImageCallbackRegister
bu KswordARK!KswordArkObjectCallbackRegister
g

在每个函数返回后看:

r rax
!error @Rax

我的最终排序是:

  1. callback registration 任一步失败,但当前被当作整个驱动的致命错误。
  2. 默认 WDF 队列创建返回 STATUS_UNSUCCESSFUL,这是数值上最贴近 31 的路径。
  3. 旧实例导致设备名、符号链接或固定 callback altitude 冲突。
  4. 最终 Authenticode 变体、签名 fallback 或实际分发二进制不一致。
  5. HVM、偏移、DynData 等非致命初始化产生了间接内存破坏;目前没有代码证据支持,应放最后查。

所以这不是“Windows 偶尔莫名其妙返回 31”。KSword 当前缺少启动阶段诊断,并且把本应可选的全局回调注册做成了全驱动启动门槛。先把 raw stage/status 持久化并改成 callback 降级运行,下一台复现机器一次启动就能把根因精确到某一个 API。

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions