chosenny
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
介绍
在花费了整个周末的时间编写一个专门的 Windows 驱动程序,以便允许我从用户模式与 Hyper-V 虚拟机监控程序以及安全内核进行通信后,我意识到,关于非 PnP 驱动程序开发的简洁技术内容匮乏,尤其是关于 Windows 驱动程序框架(WDF)如何从根本上改变此类驱动程序的开发方式。
尽管我最终会在 GitHub 上发布我的完整工具(经过更好的打磨),但我想先分享一些我在开发过程中采取的步骤。与我平时发布的那些更为低级和技术性的文章不同,这篇文章更多是作为一个入门介绍和教程,因此,如果你已经认为自己在 WDF 驱动开发方面有经验,完全可以等待高阶部分的发布。
编写传统的非 PnP 驱动程序
我已经编写了将近二十年的非 PnP Windows 驱动模型(WDM)风格驱动程序,这几乎已经成为一种机械化技能,使我能够在不到 15 分钟的时间里编写出一个基础驱动程序(以及相关的用户模式客户端应用程序),每次我都发现自己在遵循相同的基本步骤:
编写 DriverEntry 和 DriverUnload 函数,包括创建设备对象、命名它、正确设置 ACL,并在 \DosDevices 下创建一个 Win32 符号链接
为 IRP_MJ_CREATE、IRP_MJ_CLOSE 和 IRP_MJ_DEVICE_CONTROL 处理程序编写空的占位函数
定义一些 IOCTLs 为 METHOD_BUFFERED,以确保安全
实现 IRP_MJ_CREATE/IRP_MJ_CLOSE 处理程序,使其始终返回成功并完成 IRP
在 IOCTL 处理程序中进行正确的 IO_STACK_LOCATION 操作,使用适当的 WDM 宏来读取、解析和完成请求
编写一个用户模式工具,调用 CreateFile 和 DeviceIoControl 与驱动程序进行通信
使用命令行中的 Sc.exe 创建内核模式服务条目,然后根据需要启动/停止驱动程序,或者使用 CreateService、StartService 和 StopService 编写等效的 C 代码
无数的在线教程和来自微软 GitHub 上的更新示例(如 Windows 驱动程序工具包的一部分)详细解释了这些步骤,供有兴趣的人参考 —— 但这篇文章并不是要重述这些内容,而是要探讨新的方法。
我早就听说过 Windows 驱动程序框架(WDF)应该为编写真正的硬件设备驱动程序提供更简单的模型 —— 包括在 Windows 10 中,允许大多数可跨编译的用户模式驱动程序(与旧的框架相比,后者要求使用 C++/COM 编写驱动程序)。
但我错误地认为,对于编写非 PnP 研究/学术驱动程序(甚至是生产环境中的驱动程序),WDM 仍然是更好、更容易的选择。例如,我不知道有哪款反恶意软件工具的过滤器驱动程序是基于 WDF 的 —— 实际上,WDF 框架并不适合这种使用场景(或 NDIS 轻量级过滤器(LWF)、Windows 过滤平台(WFP)回调驱动程序等)。
事实是,除非你确实在某个仍专注于 WDM 的操作系统过滤栈中插入驱动程序,否则编写一个简单的“处理一些 IOCTLs”的驱动程序,可以更容易地使用 WDF 来实现,并且通过遵循某些原则,安装和卸载过程也可以变得更加健壮。
编写现代的“非 PnP”驱动程序
使用 WDF —— 更具体地说,是其内核对应物,内核模式驱动程序框架(KMDF) —— 你将有两个选项来编写一个简单的非硬件驱动程序,它可以与用户空间客户端进行通信(请注意,KMDF 从 Windows 2000 起就已经被回溯支持,因此这并不是 Windows 10 新增的功能):
你可以通过在 WDF 驱动程序对象上设置正确的标志来编写一个真正的非 PnP 驱动程序,手动创建一个作为“控制设备”的 WDF 设备,命名它,创建 Win32 符号链接,并使用 ACL 对其进行安全保护。你不需要 IRP_MJ_CREATE 或 IRP_MJ_CLOSE 处理程序,而是可以直接编写你的 IOCTL 处理程序。你仍然需要提供一个 DriverUnload 例程。
你也可以通过提供一个简单的 .INF 文件来开发一个“根总线枚举”的 PnP 驱动程序,提供一个 AddDevice 例程,并让 WDF 在检测到你的“设备”时自动调用该例程。在你的 AddDevice 例程中,构造一个未命名的 WDF 设备,注册一个自定义 GUID 的接口(你也可以在 INF 文件中执行此操作,避免写额外的代码行),并提供你的 IOCTL 处理程序。你不需要 IRP_MJ_CREATE、IRP_MJ_CLOSE 或 IRP_MJ_PNP/IRP_MJ_POWER 处理程序,但仍需提供一个 DriverUnload 例程。
这两种选项看起来很相似,但有一个至关重要的区别 —— 在第一个实现中,你必须每次希望从用户模式与驱动程序进行交互时手动注册、加载和卸载驱动程序。如果你将它保持加载状态,用户唯一的选择就是手动运行像 Sc.exe 或 Net.exe 这样的命令行工具来卸载它。如果没有使用取证工具、调试器或高级工具,用户无法知道你的驱动程序是否已加载。你必须为设备选择一个静态名称,并希望它不会与其他人的设备名称冲突。
在第二个实现中,你的驱动程序作为一个 PnP 驱动程序注册到系统中,并且由于它位于“根总线”上,系统会自动检测到它。这意味着用户可以在设备管理器中看到它,并且可以轻松地查询它的信息、禁用它,甚至卸载它。要与驱动程序进行通信,应用程序使用你定义的自定义 GUID,并枚举与之关联的接口 —— 这是一个比依赖字符串更加严格和独特的协议。这样的驱动程序还可以更容易地通过微软的 Windows 硬件质量实验室(WHQL)基础设施进行签名,并且可以比没有 .INF 或 .CAT 文件的原始非 PnP 驱动程序更好地证明其安全性。
显然,对于一个纯粹的“概念验证”驱动程序,第二种实现的好处可能看起来不值得写一个额外的 INF 文件并学习一些新的 SetupAPI 函数,而不是使用 CreateService。但对于一个更精致、更加通用的驱动程序,我个人认为,选择一个根总线枚举驱动程序是更好的选择。
值得注意的是,WDF 并没有发明这个概念,但使它(在我看来)能够被研究者广泛接触到的是,与 WDM 中不同,在 WDM 中,使用此选项需要 1500 到 4000 行的样板代码来正确地枚举、安装、卸载、禁用、查询等,而在 WDF 中,实际上不需要任何额外的工作 —— 再次强调,不需要 IRP_MJ_PNP 处理程序、没有 WMI、没有电源管理,甚至没有你曾经在过去尝试过的那些东西。
实际上,严格来说,编写一个根总线枚举的 PnP 驱动程序所需的代码行数比编写一个非 PnP 驱动程序还要少,前提是后者需要一个 INF 文件。但坦率说,一旦你写过一次,你可以大部分直接复制粘贴 —— 如果你曾经想让你的驱动程序通过微软的签名,你无论如何都需要一个 INF 文件。
与根总线枚举的 PnP 驱动程序交互
由于你没有静态命名你的设备驱动程序,并且它必须是 PnP 枚举的,因此用户模式代码看起来与传统的安装和与非 PnP 驱动程序交互的方式有所不同。这里有两个可能对你
畅读付费文章
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)