缓冲区处理

任何驱动程序中最常见的错误之一与缓冲区处理相关,其中缓冲区无效或太小。 这些错误可能允许缓冲区溢出或导致系统崩溃,这可能会损害系统安全性。 本文讨论了缓冲区处理方面的一些常见问题以及如何避免这些问题。 它还标识了演示正确缓冲区处理技术的 WDK 示例代码。

缓冲区类型和无效地址

从驱动程序的角度来看,缓冲区分为以下两个品种之一:

  • 分页缓冲区(可能或可能驻留在内存中)。

  • 非分页缓冲区,该缓冲区必须驻留在内存中。

无效的内存地址未分页或未分页。 由于操作系统用于解决由不正确的缓冲区处理导致的页面错误,因此需要执行以下步骤:

  • 它将无效地址隔离为“标准”地址范围之一(分页内核地址、非分页内核地址或用户地址)。

  • 它引发适当的错误类型。 系统始终通过 bug 检查(如PAGE_FAULT_IN_NONPAGED_AREA)或异常(如STATUS_ACCESS_VIOLATION)处理缓冲区错误。 如果错误是 bug 检查,系统将停止操作。 对于异常,系统会调用基于堆栈的异常处理程序。 如果异常处理程序均未处理异常,则系统会调用 bug 检查。

不管怎样,应用程序程序可以调用的任何访问路径都会导致驱动程序导致 bug 检查是驱动程序中的安全冲突。 此类冲突允许应用程序对整个系统造成拒绝服务攻击。

常见假设和错误

此区域中最常见的问题之一是驱动程序编写者对操作环境承担太多。 一些常见的假设和错误包括:

  • 驱动程序只是检查地址中是否设置了高位。 依赖固定位模式来确定地址类型不适用于所有系统或方案。 例如,当系统使用 四千兆字节优化 (4GT)时,此检查不适用于基于 x86 的计算机。 使用 4GT 时,用户模式地址为地址空间的第三 GB 设置高位。

  • 仅使用 ProbeForReadProbeForWrite 验证地址的驱动程序。 这些调用可确保地址在探测时是有效的用户模式地址。 但是,不能保证此地址在探测操作后保持有效。 因此,此方法引入了一个微妙的争用条件,这可能导致定期不可修复的崩溃。

    ProbeForReadProbeForWrite 调用仍是必需的。 如果驱动程序省略探测,用户可以传入有效的内核模式地址,即__try__except块(结构化异常处理)不会捕获,从而打开一个大安全漏洞。

    底线是需要探测和结构化异常处理:

    • 探测将验证地址是否为用户模式地址,并且缓冲区的长度是否在用户地址范围内。

    • __try/__except阻止防止访问。

    请注意, ProbeForRead 仅验证地址和长度是否在可能的用户模式地址范围内(例如,对于没有 4GT 的系统,略低于 2 GB),而不是内存地址是否有效。 相比之下, ProbeForWrite 会尝试访问指定长度的每个页面中的第一个字节,以验证这些字节是否为有效的内存地址。

  • 依赖于内存管理器函数(如 MmIsAddressValid )的驱动程序,以确保地址有效。 如探测函数所述,这种情况引入了一种争用条件,这可能导致无法实现的崩溃。

  • 驱动程序无法使用结构化异常处理。 __try/except编译器中的函数使用操作系统级别的支持来处理异常。 通过调用 ExRaiseStatus 或其中一个相关函数,内核级异常将抛回到系统。 驱动程序未能围绕任何可能引发异常的调用使用结构化异常处理将导致 bug 检查(通常KMODE_EXCEPTION_NOT_HANDLED)。

    围绕预期不会引发错误的代码使用结构化异常处理是错误的错误。 此用法只会屏蔽将发现的真实 bug。 将包装器放在 __try/__except 例程的顶层不是解决此问题的正确解决方案,尽管有时是驱动程序编写器尝试的反射解决方案。

  • 一个驱动程序,假定用户内存的内容将保持稳定。 例如,假设驱动程序将一个值写入用户模式内存位置,然后在随后的同一例程中引用该内存位置。 恶意应用程序可能会在写入后主动修改该内存,因此会导致驱动程序崩溃。

对于文件系统,这些问题十分严重,因为文件系统通常依赖于直接访问用户缓冲区(METHOD_NEITHER传输方法)。 此类驱动程序直接操作用户缓冲区,因此必须包含缓冲区处理的预防措施,以避免操作系统级崩溃。 快速 I/O 始终传递原始内存指针,因此,如果支持快速 I/O,驱动程序需要防范类似的问题。

用于缓冲区处理的示例代码

WDK 包含 fastfat CDFS 文件系统驱动程序示例代码中的大量缓冲区验证示例,包括:

  • fastfat\deviosup.c 中的 FatLockUserBuffer 函数使用 MmProbeAndLockPages 锁定用户缓冲区后面的物理页,在 FatMapUserBuffer锁定 MmGetSystemAddressForMdlSafe,为锁定的页面创建虚拟映射。

  • fastfat\fsctl.c 中的 FatGetVolumeBitmap 函数使用 ProbeForRead ProbeForWrite 来验证碎片整理 API 中的用户缓冲区。

  • cdfs\read.c 中的 CdCommonRead 函数使用__try代码并将其__except周围的代码用于零用户缓冲区。 CdCommonRead 中的示例代码似乎使用tryexcept关键字。 在 WDK 环境中,C 中的这些关键字在编译器扩展 __try__except. 任何使用C++代码的人都必须使用本机编译器类型来正确处理异常,就像C++关键字一样 __try ,但不是 C 关键字,并且将提供一种对内核驱动程序无效的C++异常处理形式。