缓冲区处理

也许任何驱动程序中最常见的错误都与缓冲区处理有关,其中缓冲区无效或太小。 这些错误可能导致缓冲区溢出或导致系统崩溃,从而对系统造成安全威胁。

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

  • 分页缓冲区,不一定驻留在内存中。

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

当然,无效地址既不是分页地址,也不是非分页地址,但是当操作系统开始努力解决缓冲区导致的页面错误时,它会将无效地址隔离到“标准”地址范围之一 (分页内核地址、非分页内核地址或用户地址) ,并引发适当的错误类型。 缓冲区错误始终由 bug 检查 (PAGE_FAULT_IN_NONPAGED_AREA(例如) )或异常 (STATUS_ACCESS_VIOLATION(例如) )处理。 如果出现 bug 检查,系统将停止操作。 在发生异常时,将调用基于堆栈的异常处理程序,如果它们都无法处理异常,则将调用 bug 检查。

无论如何,应用程序调用的任何导致驱动程序导致 bug 检查的访问路径都是驱动程序内的安全违规。 这允许应用程序对整个系统造成拒绝服务攻击。

此领域最常见的问题之一是驱动程序编写者对操作环境有太多的假设。 这可能包括:

  • 检查地址中是否设置了高位。 这不适用于基于 x86 的计算机,其中系统通过设置 Boot.ini 文件中的 /3GB 选项使用 4 GB 优化 (4GT) 。 在这种情况下,用户模式地址为地址空间的第 3 GB (GB) 设置高位。

  • 使用 ProbeForReadProbeForWrite 验证地址。 虽然这将确保地址在探测时是有效的用户模式地址,但没有任何要求它在探测操作后保持有效。 因此,此技术引入了一种微妙的争用条件,可能导致周期性不可生成的崩溃。 ProbeForReadProbeForWrite 调用是出于不同原因所必需的:验证地址是否为用户模式地址以及缓冲区的长度是否在用户地址范围内。 如果省略探测,用户可以传入有效的内核模式地址,这些地址不会被__try捕获,__except块 (结构化异常处理) ,并会打开一个大安全漏洞。 因此 ,需要 ProbeForReadProbeForWrite 调用来确保对齐,并且用户模式地址加上长度在用户地址范围内。 但是,需要__try和__except块来防止访问。

    请注意,对于没有 4GT 的系统, ProbeForRead 仅验证地址和长度是否在可能的用户模式地址范围内, (略低于 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 函数使用 ProbeForReadProbeForWrite 来验证碎片整理 API 中的用户缓冲区。

  • cdfs\read.c 中的 CdCommonRead 函数使用__try,并将代码__except为零用户缓冲区。 请注意, CdCommonRead 中的示例代码似乎使用 try 和 ,但关键字除外。 在 WDK 环境中,C 中的这些关键字根据编译器扩展__try和__except进行定义。 任何使用 C++ 代码的人都必须使用本机编译器类型来正确处理异常,因为 __try 是 C++ 关键字 (keyword) ,而不是 C 关键字 (keyword) ,并且将提供一种对内核驱动程序无效的 C++ 异常处理形式。