1. 为什么需要多视角检测

前面的文章分别讨论过模块列表、对象目录和内存特征扫描。把这些内容放在一起后,可以得到一个更清晰的结论:驱动检测的难点不是读取某一个列表,而是判断多个独立视角是否能够相互解释。

正常加载的驱动通常同时具有映像、驱动对象和运行时行为等信息。攻击者如果只修改其中一处登记信息,其他视角仍可能留下痕迹。因此,SnowSword 的设计目标不是寻找一个“绝对可信”的 API,而是把多个来源关联起来,降低单一数据源被篡改带来的影响。

这里的“隐藏驱动”是检测语境中的结果标签,不等于已经完成恶意归因。一个条目被标记为异常后,还需要结合系统版本、加载方式、对象关系和调试器结果进行人工复核。

2. 威胁模型:驱动可能藏在哪里

本文只讨论防御性检测和实验验证,不实现或复现隐藏驱动代码。为了理解检测边界,可以把攻击者可能修改的登记位置抽象成下面几类:

视角 可能的异常现象 检测盲区
模块加载器 映像不再出现在 PsLoadedModuleList 对应的模块结果中 只查询 SystemModuleInformation 时无法发现摘链对象
对象管理器 驱动对象未出现在预期目录,或对象与映像信息无法关联 只枚举 \\Driver 时无法覆盖不注册对象的场景
内存映像 内核可执行区域存在 PE 特征,但找不到已知模块归属 PE 头被清除、扫描范围不完整时可能漏检
运行时行为 存在可疑回调、设备通信或异常内存操作 单凭行为难以确定对应的驱动映像

这几类现象并不互相等价。例如,模块列表中没有某个基址,可能是摘链,也可能是结构读取失败或版本适配问题。检测程序应该输出证据和置信等级,而不是直接输出“恶意”。

3. SnowSword 的总体架构

SnowSword 的检测流程可以拆成三个层次:

1
2
3
4
5
6
7
8
9
10
11
12
用户态界面与调度
│
│ IOCTL
▼
内核态采集与安全读取
│
├─ 模块列表集合
├─ DRIVER_OBJECT 集合
└─ 内存 PE 候选集合
│
▼
用户态按基址关联、分级展示和人工复核

内核态适合访问对象管理器和内核地址空间,但需要严格处理 IRQL、对象引用和异常读取;用户态适合做集合运算、路径格式化、结果排序和界面展示。把这两类职责分开,既能减少驱动中的复杂逻辑,也方便后续替换某一种采集方式。

一个统一的检测结果至少应包含以下字段:

  • 映像基址和大小;
  • 驱动名称或路径(如果能够获得);
  • 来源视角:模块列表、对象目录或内存扫描;
  • 是否能与其他视角按基址关联;
  • 当前状态:已知模块、对象关联、游离 PE 或需要人工复核;
  • 目标系统版本和扫描时间。

4. 第一视角:枚举内核模块列表

SnowSword 已有的模块枚举使用 SystemModuleInformation。这类信息通常来自内核维护的已加载模块链表,可以得到驱动路径、映像基址和映像大小。

由于返回缓冲区大小不固定,常见实现是两阶段查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 伪代码:只展示查询流程,不代表完整的错误处理
status = ZwQuerySystemInformation(
SystemModuleInformation,
NULL,
0,
&requiredSize
);

if (status != STATUS_INFO_LENGTH_MISMATCH || requiredSize == 0) {
return status;
}

buffer = AllocateNonPagedPool(requiredSize);
if (buffer == NULL) {
return STATUS_INSUFFICIENT_RESOURCES;
}

status = ZwQuerySystemInformation(
SystemModuleInformation,
buffer,
requiredSize,
&requiredSize
);

实际代码还需要处理大小变化、内存释放、返回结构版本和路径编码。用户态拿到结果后,可以将 ImageName 的最后一段作为显示名,但不能只依赖名称进行关联,因为名称可能被截断、重复或格式化不同。

4.1 这一视角的优点和局限

优点是接口相对稳定,能够快速得到已加载映像的基址和大小,适合建立系统基线。局限也很明显:如果模块链表中的记录被摘除,查询结果就不会包含它;因此,模块列表更适合当作一个参考集合,而不是唯一事实来源。

5. 第二视角:对象管理器中的驱动对象

驱动加载后,通常会在对象管理器命名空间中留下 DRIVER_OBJECT。普通驱动常见于 \\Driver,文件系统相关驱动还需要关注 \\FileSystem 及其可能的子目录。漏掉子目录会使检测视野不完整。

5.1 枚举流程

工程上可以按以下步骤组织对象枚举:

  1. 打开目标目录对象;
  2. 将句柄转换为内核对象引用;
  3. 遍历目录项并取得对象指针;
  4. 用 ObGetObjectType 判断对象是否为驱动对象;
  5. 在确认对象有效后读取 DriverSection 和 DllBase;
  6. 将基址、名称和对象来源写入结果集合;
  7. 严格配对所有对象引用。

某些实现会直接遍历 OBJECT_DIRECTORY 的哈希桶,以取得对象指针。需要强调的是,OBJECT_DIRECTORY、目录项布局和哈希桶数量都不是适合无条件硬编码的公开稳定接口。已有研究中常见的 37 个桶不能直接视为所有 Windows 版本的通用结论;目标系统必须通过符号、调试器或版本适配逻辑确认布局。

5.2 基址关联

DRIVER_OBJECT 的 DriverSection 通常可以关联到加载器数据表项,再从 DllBase 得到映像基址。这个基址是连接两个视角的关键字段:

1
2
3
4
对象目录中的 DRIVER_OBJECT
│ DriverSection->DllBase
▼
模块列表中的 ImageBase

5.3 引用计数纪律

遍历对象时,引用生命周期比查找逻辑更容易出错:

1
2
3
4
5
6
ObReferenceObject
│
├─ 非目标对象:ObDereferenceObject
└─ 目标对象:把引用明确转交给调用者
│
└─ 使用结束后 ObDereferenceObject

漏释放会造成对象引用泄漏,多释放或在引用转交前释放则可能导致后续访问失效。驱动端应该为“本次遍历持有的引用”和“返回给上层的引用”建立清晰的所有权约定,并在错误路径中逐一回收。

6. 第三视角:内存中的 PE 特征

如果一个可执行映像不再出现在模块列表,且没有可枚举的驱动对象,前两种视角可能同时失效。作为补充,SnowSword 的设计还考虑在受控的内核内存范围内寻找 PE 镜像特征。

6.1 两级特征校验

驱动映像通常具有 DOS 头的 MZ 签名,并能通过 e_lfanew 定位到 PE\\0\\0 签名。扫描时应该先进行轻量检查,再读取后续头部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 伪代码:候选识别,不等于恶意判定
if (ReadSafely(address, &dosHeader, sizeof(dosHeader)) != STATUS_SUCCESS) {
return CandidateReadFailed;
}

if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) {
return NotPeImage;
}

if (!IsReasonableOffset(dosHeader.e_lfanew)) {
return InvalidPeOffset;
}

if (ReadSafely(address + dosHeader.e_lfanew,
&signature, sizeof(signature)) != STATUS_SUCCESS) {
return CandidateReadFailed;
}

return signature == IMAGE_NT_SIGNATURE ? PeCandidate : NotPeImage;

这里的 ReadSafely 代表安全的内核拷贝。

6.2 扫描范围不是“全内存”

全地址空间逐字节扫描既不现实,也会带来严重的性能和稳定性问题。当前设计优先考虑内核模块附近的受控范围,并把非分页池、高地址内存和已清除 PE 头列为盲区。扫描范围必须页对齐,地址和结果字段必须使用足够宽的 64 位类型。

早期实现可以由用户态按页发起 IOCTL,但这会产生大量上下文切换。正式版本应考虑批量传递地址区间,让驱动一次处理一批页,并只返回候选结果。无论采用哪种方式,都不能把“命中 PE 头”直接等同于隐藏驱动:正常模块、残留映像或数据误匹配都可能产生误报。

7. 结果关联与分级判定

三个集合可以抽象为:

1
2
3
A = 模块列表中的基址集合
B = 对象目录关联到的 DRIVER_OBJECT 基址集合
C = 内存扫描得到的 PE 候选基址集合

按基址关联后,可以采用更保守的状态分类:

状态 典型条件 建议处理
已知模块 A 中存在,且能与 B 关联 作为正常基线记录
对象可见但模块缺失 B 中存在而 A 中不存在 标记为疑似摘链,人工复核
游离 PE 候选 C 中存在,但无法归属于 A 标记为疑似游离映像,不直接判恶意
模块存在但对象未关联 A 中存在而 B 中不存在 结合启动阶段、驱动类型和系统基线判断
读取或版本不确定 结构读取失败、布局未确认或信息不完整 标记为无法判断,不强行归类

A 有而 B 无并不天然表示隐藏。早期启动驱动、没有典型设备对象的驱动以及版本差异,都可能导致这种结果。相反,B有而A` 无也只能说明两个视角不一致,最终结论还需要 WinDbg、文件签名、加载日志和运行时行为等独立证据。

8. 工程实现中的几个关键问题

8.1 IOCTL 边界

用户态传入的地址、长度、线程数量和输出缓冲区都必须做范围检查。不能信任用户态传来的指针,也不能让单次请求覆盖过大的扫描区间。对于输出结果,要明确结构版本、元素数量和总字节数,避免整数溢出和越界写入。

8.2 安全读取与执行级别

访问内核地址前,需要确认当前代码运行在允许的 IRQL,并使用适合当前环境的安全拷贝方式。MmCopyMemory、异常保护和地址有效性检查各自解决的问题不同,不能把某一个检查当成“绝对安全”的保证。读取私有结构时还要考虑对象并发销毁和字段被修改的情况。

8.3 线程与资源管理

用户态多线程可以缩短实验扫描时间,但共享结果数组、停止事件和驱动句柄必须有明确的同步策略。驱动卸载时要停止工作线程、完成未决 IRP、释放池内存和对象引用。调试输出只能在命中或关键错误时记录,不能在逐页循环中无条件打印。

8.4 版本适配

OBJECT_DIRECTORY、LDR_DATA_TABLE_ENTRY 以及其他内核私有结构都可能随 Windows 版本变化。工程上应把版本识别、符号验证和字段读取失败作为正常分支,而不是假设所有系统都使用同一组偏移。对不确定的数据,宁可返回“未知”,也不要用危险的硬编码继续访问。

9. 当前实测边界

已有测试使用自写 DriverLoader 加载 TestHideDriver1.sys,并通过 DebugView 观察到 Driver is alive! 等输出。在当时的 SnowSword 界面中,该驱动显示了名称、基址 0xFFFFF80267140000、大小 0x1000 和桌面测试路径,同时被红色标记。

同一篇记录还保留了 OpenArk 的对比截图:截图中的 OpenArk 驱动列表没有显示 TestHideDriver1.sys,而 SnowSword 显示了该项。这个结果能够证明两个工具在该测试状态下的枚举结果不同,但不能单凭截图断言某个工具一定正确,也不能据此推出对所有隐藏方式都有效。

目前仍缺少以下数据:

  • 干净系统基线和多次重复测试;
  • 模块、对象目录和内存候选的逐项对照;
  • 扫描耗时、CPU 占用和 IOCTL 次数;
  • 非分页池、清除 PE 头和不同 Windows 版本的验证。

10. 后续实验计划

后续可以按照下面的顺序完善证据链:

阶段 实验内容 需要保存的证据
基线 干净快照扫描 系统版本、模块集合、对象集合、耗时
正常驱动 加载普通测试驱动 加载日志、基址、大小、三视角结果
摘链场景 在隔离虚拟机中使用已授权测试样本 前后集合差异、WinDbg 复核、SnowSword 日志
性能 比较逐页和批量 IOCTL 扫描时间、CPU、请求数量、候选数量
兼容性 对多个 Windows 版本重复 结构验证结果、误报和漏报记录

实现层面的 v2 方向包括:批量化内存扫描、研究非分页池定位、为 DRIVER_OBJECT 增加多特征交叉校验,以及把 WinDbg 验证信息纳入结果报告。

总结

SnowSword 隐藏驱动检测的核心不是某个特殊 API,而是把模块加载器、对象管理器和内存特征三个视角放到同一条证据链中:

1
2
3
4
5
6
7
不同来源采集
↓
按基址和名称关联
↓
输出差异与证据
↓
人工复核后再做结论

这种设计无法保证发现所有内核隐蔽技术,但能让攻击者必须同时处理更多独立痕迹,也让检测结果比单一列表更容易解释和复核。对于 ARK 工具,工程上的稳定性、版本适配和误报控制,与“能否扫到一个对象”同样重要。

参考

  • Windows Internals, 7th Edition(对象管理器、加载器、驱动对象和内存管理章节)
  • Microsoft Learn:ZwQuerySystemInformation、ZwOpenDirectoryObject、ObReferenceObjectByHandle、ObGetObjectType、MmCopyMemory
  • 本系列文章:隐藏驱动检测学习记录、内存暴力搜索隐藏驱动、SnowSword 隐藏驱动检测实测
  • SnowSword 项目地址:https://gitee.com/le-ziyi/SnowSword

相关项目与联系

项目SnowSword —— Windows 10/11 x64 内核分析与 Anti-Rootkit 工具,源码与发布包在 Gitee。

技术合作Windows 内核 / 驱动开发 / 逆向分析 / 兼容性开发,范围与边界见 技术合作。

联系2160752730@qq.com · QQ 2160752730 · 微信 lst_3258110