SnowSword 隐藏驱动检测原理与工程实现:从多视角枚举到结果关联
1. 为什么需要多视角检测
前面的文章分别讨论过模块列表、对象目录和内存特征扫描。把这些内容放在一起后,可以得到一个更清晰的结论:驱动检测的难点不是读取某一个列表,而是判断多个独立视角是否能够相互解释。
正常加载的驱动通常同时具有映像、驱动对象和运行时行为等信息。攻击者如果只修改其中一处登记信息,其他视角仍可能留下痕迹。因此,SnowSword 的设计目标不是寻找一个“绝对可信”的 API,而是把多个来源关联起来,降低单一数据源被篡改带来的影响。
这里的“隐藏驱动”是检测语境中的结果标签,不等于已经完成恶意归因。一个条目被标记为异常后,还需要结合系统版本、加载方式、对象关系和调试器结果进行人工复核。
2. 威胁模型:驱动可能藏在哪里
本文只讨论防御性检测和实验验证,不实现或复现隐藏驱动代码。为了理解检测边界,可以把攻击者可能修改的登记位置抽象成下面几类:
| 视角 | 可能的异常现象 | 检测盲区 |
|---|---|---|
| 模块加载器 | 映像不再出现在 PsLoadedModuleList 对应的模块结果中 |
只查询 SystemModuleInformation 时无法发现摘链对象 |
| 对象管理器 | 驱动对象未出现在预期目录,或对象与映像信息无法关联 | 只枚举 \\Driver 时无法覆盖不注册对象的场景 |
| 内存映像 | 内核可执行区域存在 PE 特征,但找不到已知模块归属 | PE 头被清除、扫描范围不完整时可能漏检 |
| 运行时行为 | 存在可疑回调、设备通信或异常内存操作 | 单凭行为难以确定对应的驱动映像 |
这几类现象并不互相等价。例如,模块列表中没有某个基址,可能是摘链,也可能是结构读取失败或版本适配问题。检测程序应该输出证据和置信等级,而不是直接输出“恶意”。
3. SnowSword 的总体架构
SnowSword 的检测流程可以拆成三个层次:
1 | 用户态界面与调度 |
内核态适合访问对象管理器和内核地址空间,但需要严格处理 IRQL、对象引用和异常读取;用户态适合做集合运算、路径格式化、结果排序和界面展示。把这两类职责分开,既能减少驱动中的复杂逻辑,也方便后续替换某一种采集方式。
一个统一的检测结果至少应包含以下字段:
- 映像基址和大小;
- 驱动名称或路径(如果能够获得);
- 来源视角:模块列表、对象目录或内存扫描;
- 是否能与其他视角按基址关联;
- 当前状态:已知模块、对象关联、游离 PE 或需要人工复核;
- 目标系统版本和扫描时间。
4. 第一视角:枚举内核模块列表
SnowSword 已有的模块枚举使用 SystemModuleInformation。这类信息通常来自内核维护的已加载模块链表,可以得到驱动路径、映像基址和映像大小。
由于返回缓冲区大小不固定,常见实现是两阶段查询:
1 | // 伪代码:只展示查询流程,不代表完整的错误处理 |
实际代码还需要处理大小变化、内存释放、返回结构版本和路径编码。用户态拿到结果后,可以将 ImageName 的最后一段作为显示名,但不能只依赖名称进行关联,因为名称可能被截断、重复或格式化不同。
4.1 这一视角的优点和局限
优点是接口相对稳定,能够快速得到已加载映像的基址和大小,适合建立系统基线。局限也很明显:如果模块链表中的记录被摘除,查询结果就不会包含它;因此,模块列表更适合当作一个参考集合,而不是唯一事实来源。
5. 第二视角:对象管理器中的驱动对象
驱动加载后,通常会在对象管理器命名空间中留下 DRIVER_OBJECT。普通驱动常见于 \\Driver,文件系统相关驱动还需要关注 \\FileSystem 及其可能的子目录。漏掉子目录会使检测视野不完整。
5.1 枚举流程
工程上可以按以下步骤组织对象枚举:
- 打开目标目录对象;
- 将句柄转换为内核对象引用;
- 遍历目录项并取得对象指针;
- 用
ObGetObjectType判断对象是否为驱动对象; - 在确认对象有效后读取
DriverSection和DllBase; - 将基址、名称和对象来源写入结果集合;
- 严格配对所有对象引用。
某些实现会直接遍历 OBJECT_DIRECTORY 的哈希桶,以取得对象指针。需要强调的是,OBJECT_DIRECTORY、目录项布局和哈希桶数量都不是适合无条件硬编码的公开稳定接口。已有研究中常见的 37 个桶不能直接视为所有 Windows 版本的通用结论;目标系统必须通过符号、调试器或版本适配逻辑确认布局。
5.2 基址关联
DRIVER_OBJECT 的 DriverSection 通常可以关联到加载器数据表项,再从 DllBase 得到映像基址。这个基址是连接两个视角的关键字段:
1 | 对象目录中的 DRIVER_OBJECT |
5.3 引用计数纪律
遍历对象时,引用生命周期比查找逻辑更容易出错:
1 | ObReferenceObject |
漏释放会造成对象引用泄漏,多释放或在引用转交前释放则可能导致后续访问失效。驱动端应该为“本次遍历持有的引用”和“返回给上层的引用”建立清晰的所有权约定,并在错误路径中逐一回收。
6. 第三视角:内存中的 PE 特征
如果一个可执行映像不再出现在模块列表,且没有可枚举的驱动对象,前两种视角可能同时失效。作为补充,SnowSword 的设计还考虑在受控的内核内存范围内寻找 PE 镜像特征。
6.1 两级特征校验
驱动映像通常具有 DOS 头的 MZ 签名,并能通过 e_lfanew 定位到 PE\\0\\0 签名。扫描时应该先进行轻量检查,再读取后续头部:
1 | // 伪代码:候选识别,不等于恶意判定 |
这里的 ReadSafely 代表安全的内核拷贝。
6.2 扫描范围不是“全内存”
全地址空间逐字节扫描既不现实,也会带来严重的性能和稳定性问题。当前设计优先考虑内核模块附近的受控范围,并把非分页池、高地址内存和已清除 PE 头列为盲区。扫描范围必须页对齐,地址和结果字段必须使用足够宽的 64 位类型。
早期实现可以由用户态按页发起 IOCTL,但这会产生大量上下文切换。正式版本应考虑批量传递地址区间,让驱动一次处理一批页,并只返回候选结果。无论采用哪种方式,都不能把“命中 PE 头”直接等同于隐藏驱动:正常模块、残留映像或数据误匹配都可能产生误报。
7. 结果关联与分级判定
三个集合可以抽象为:
1 | A = 模块列表中的基址集合 |
按基址关联后,可以采用更保守的状态分类:
| 状态 | 典型条件 | 建议处理 |
|---|---|---|
| 已知模块 | 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 | 不同来源采集 |
这种设计无法保证发现所有内核隐蔽技术,但能让攻击者必须同时处理更多独立痕迹,也让检测结果比单一列表更容易解释和复核。对于 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
