驱动程序中的每一个漏洞本质上都是Windows内核中的一个漏洞,因为每个驱动程序都共享内核的内存空间。拥有了在内核中运行代码、从模型寄存器读写或复制特权访问令牌的能力实际上是拥有了系统。本文将介绍在WDM驱动程序中发现漏洞的方法,然后通过kAFL利用内核模糊。大多数漏洞似乎都在WDM或KMDF中。
在本博客的每一部分中,我们都将从基础开始,比如熟悉相关的API和数据结构。
WDM
Windows驱动程序模型(WDM)是最古老的,也是最常用的驱动程序框架。每个驱动本质上都是一个WDM驱动;较新的框架WDF (Windows Driver framework)封装了WDM,简化了WDM的开发过程,解决了WDM的多种技术难题。在检查WDM驱动程序时,我们关心的主要事情是如何与它们通信;几乎驱动程序中的每个漏洞都涉及到一些从非特权用户到驱动程序本身的通信。
在示例中,这是名为“testy”的驱动程序的入口点:
经典的DriverEntry代码,注意,对IoCreateDevice的调用没有FILE_DEVICE_SECURE_OPEN标志
这段代码是每个WDM驱动都有的DriverEntry函数的普通架构。第一个参数是DriverObject结构指针,用于设备创建和调度例程初始化。接下来,驱动程序有MajorFunction成员,它是一个函数指针数组,用于为不同的事件分配调度例程。此外,我们还有将在下一节中介绍的关键设备创建例程。
设备创建和初始化
驱动程序首先通过调用 IoCreateDevice 创建设备,这将在对象管理器中创建一个 DEVICE_OBJECT。在 Windows 中,设备对象表示驱动程序处理 I/O 请求的逻辑、虚拟或物理设备。所有这些听起来都不错,但如果我们希望它从普通用户的角度进行交流,这还不够;为此,我们调用 IoCreateSymbolicLink,它将在对象管理器中创建一个 DoS 设备名称,使用户能够通过该设备与驱动程序进行通信。但是,有些设备没有正常的名称;它们具有自动生成的名称(在 PDO 中完成)。对于没有经验的检测人员来说,它们可能看起来很奇怪,所以如果你在你最喜欢的设备中第一次看到它们,请查看软件,并在设备名称列中查看 8 位十六进制。这些设备可以像其他所有命名设备一样进行交互。
展示 WinObjEx 设备命名空间
在设备创建例程中要注意的最重要的事情是程序员是否为设备分配了 ACL 以及 DeviceCharacteristics 的值。
不幸的是,IoCreateDevice方法不允许程序员指定任何ACL,这是不好的。因此,开发人员必须在注册表或驱动程序的ini文件中定义一个ACL。如果他们不能这样做,任何用户都可以访问设备。然而,使用IoCreateDeviceSecure方法可以缓解这种情况。
除此之外,我们还需要查看第五个参数,即 DeviceCharacteristics 。如果 DeviceCharacteristics 的值没有与 0x00000100 或 FILE_DEVICE_SECURE_OPEN 进行 OR 运算,我们可能会面临安全漏洞(除非我们讨论文件系统驱动程序或任何支持名称结构的驱动程序)。这背后的原因是 Windows 对待设备的方式;每个设备都有自己的命名空间。设备命名空间中的名称是以设备名称开头的路径。对于名为 \Device\DeviceName 的设备,其命名空间由“\Device\DeviceName\anyfile”形式的任何名称组成。
如图1所示,没有FILE_DEVICE_SECURE_OPEN标志的IoCreateDevice调用意味着设备ACL不应用于打开设备命名空间内文件的文件请求。换句话说,即使我们在通过IoCreateDeviceSecure或其他方式创建设备时指定了强ACL,该ACL也不会应用于打开文件请求。结果,我们并没有真正得到我们想要的,所以使用 \Device\testydrv 调用 CreateFile 会失败,但使用“\device\testydrv\anyfile”调用会成功,因为 IoManager 没有应用设备 ACL到创建请求(因为它假设它是一个文件系统驱动程序)。对于初学者来说,它被认为是一个值得修复的漏洞。此外,这将导致非管理员用户尝试读/写设备,执行 DeviceIoControl 请求等等,这通常是你不希望非管理员用户做的事情。
更好的用户保护
我们可以通过调用IoCreateDeviceSecure(或WdmlibIoCreateDeviceSecure;它是相同的函数),使用安全描述符防止非管理员用户打开设备句柄,并在创建例程中使用FILE_DEVICE_SECURE_OPEN值。这也将为我们省去在注册表中声明设备权限的麻烦,就像我们在 IoCreateDevice 中需要的那样。
我们应该如何创建设备
从寻找漏洞的角度来看,我们应该列举系统中每一个可能的设备,然后尝试用GENERIC_READ | GENERIC_WRITE打开它,这允许我们过滤掉不能与之通信的设备。
调度方法
创建设备很好,但仅仅与驱动程序通信是不够的,还需要 IRP。驱动程序代表 IoManager 接收 IRP、I/O 请求数据包以用于特定触发器。例如,如果应用程序尝试打开设备句柄,IoManager 将调用分配给驱动程序对象的相关调度方法。因此,它允许每个驱动程序为其创建的每个设备支持多个不同的 MajorFunction。大约有 30 种不同的 MajorFunction。如果算上已弃用的 IRP_MJ_PNP_POWER,每个都代表不同的事件。我们将只关注其中两个 MajorFunction 方法,并添加关于其余方法的简短描述,这是我们在寻找漏洞时应该注意的地方。
基本的驱动程序调度表分配
调用IRP_MJ_CREATE
在我们深入研究最有趣的目标之前,即 IRP_MJ_DEVICE_CONTROL,我们将从 IRP_MJ_CREATE 开始。每个内核模式驱动程序都必须在驱动程序调度回调函数中处理 IRP_MJ_CREATE。驱动程序必须实现 IRP_MJ_CREATE,因为没有它,你将无法打开设备或文件对象的句柄。
正如你可能猜到的,当你调用 NtCreateFile 或 ZwCreateFile 时会调用 IRP_MJ_CREATE 调度例程。在大多数情况下,它将是一个空存根,并根据设备的 ACL 返回一个带有请求的 DesiredAccess 的句柄。
典型的DistpachCreate强制方法
但是,在某些情况下,会涉及更复杂的代码,即使你满足设备的 ACL 标准,你也可能会收到类似 STATUS_INVALID_PARAMETER 的状态漏洞,因为你在调用 NtCreateFile 时使用了不正确的参数。
不幸的是,这表明你不能盲目打开设备,希望通过DeviceIoControl与驱动程序通信;你首先需要了解它的预期参数。通常,DispatchCreate 需要一些 ExtendedAttributes(不能为此使用常规 CreateFile)或特定文件名(除了设备名称)。因此,我们必须访问 DispatchCreate 方法。
显示检查是否存在名为“StorVsp-v2”的扩展属性以及值字段的长度是否为 0x19 字节长。因此,驱动程序是 StorVsp.sys
除了打开句柄之外,你还可以在DispatchCreate中查找漏洞。函数变得越复杂,内存分配和释放漏洞的可能性就越高,特别是因为DispatchCreate并不经常被检查。
我们在寻找驱动程序中的漏洞时采取的一般方法是:
枚举每个设备对象:
尝试使用最宽松的 DesiredAccess 打开它;
如果失败,检查状态码;如果不是 STATUS_ACCESS_DENIED,你可能仍然可以通过做一些手动工作并更改一些参数来打开句柄;
通过遵循这个简单的算法,我们将拥有一个包含大约 70 个设备的列表,我们可以从非管理员的角度与之交谈。当然,这个数字会因不同的 Windows 设备而异,因为 OEM 驱动程序和许多类型的软件也会安装驱动程序。
使用ioctls控制设备
虽然ioctls很少让你完全控制设备/驱动程序,但它实际上是应用程序与驱动程序通信的方式。驱动程序可以创建两种ioctl调度例程:
设备控制方法的典型用法
唯一重要的方法是TestyDispatchIoctl,因为我们不能用任意参数发起对IoBuildDeviceIoControlRequest或IIoAllocateIrp的调用,这是触发IRP_MJ_INTERNAL_DEVICE_CONTROL主函数的函数。如果是,那是因为内部调度方法很少经过适当的测试。
与DriverObject的任何调度方法一样,它从IoManager接收两个参数。
WDM驱动程序中的每个调度方法共享相同的函数签名
第一个是我们对其执行 CreateFile 操作的设备对象,第二个是指向 IRP 的指针。从漏洞研究的角度来看,IRP 封装了用户数据和我们并不真正关心的许多其他内容。我们在这里关心的主要是从用户模式发送哪些参数。如果我们看一下 NtDeviceIoControlFile 的签名,我们可以猜测在寻找驱动程序中的漏洞时我们关心哪些字段:
DeviceIoControl API
这种方法的主要问题是输入/输出缓冲区、它们的长度和Ioctl代码本身。我们从Ioctl代码开始,它是一个充当说明符的32位数字;它描述了缓冲区和长度如何被使用/复制到内核,所需的DesiredAccess(当你打开一个设备句柄时)和一个函数指示器。示例如下:
FileTest.exe工具的图像,显示了32 Ioctl编号的位域
我们可以看到ioctl代码是0x1000,翻译过来就是:
DeviceType: FileDevice_0:它与我们无关;
Function: 0:与我们无关;
Method: METHOD_NEITHER:它与我们相关,因为它描述了imanager如何将数据传输到内核;
Access: FILE_ANY_ACCESS:它与我们相关,因为它定义了你需要对句柄拥有的所需访问权限。如果你没有正确的访问权限,那么 IoManager 将不允许调用发生并返回 AccessDenied。有四个不同的值:
FILE_ANY_ACCESS:无论 DesiredAccess 参数如何,你始终拥有设备句柄;
FILE_READ_DATA:你使用 GENERIC_READ 请求了一个句柄并获得了一个有效的句柄;
FILE_WRITE_DATA:你使用 GENERIC_WRITE 请求了一个句柄并获得了一个有效的句柄;FILE_READ_DATA | FILE_WRITE_DATA:不言自明;你需要这两种权利;
在 \Device\VfpExt 的句柄上运行此 DeviceIoControl 请求将导致 BSoD,无论你的权限级别如何,在理解了图3中的Method字段之后,我们将看到其中的原因。
Method/TransferType
Method/TransferType被称为万恶之母,这听起来有些夸大其词,但不幸的是,事实确实如此。传输类型的方法,即ioctl 32位数中的两个最低有效位,指示 IoManager 在内核中引用参数(缓冲区和长度)的方式。与访问字段一样,有四个不同的选项:
(1) METHOD_NEITHER,两个位都是打开的:IoManager 是惰性的,不对缓冲区及其长度进行检查。缓冲区不会复制到驱动程序并驻留在用户模式下。因此,用户可以随意操纵缓冲区的长度并释放/分配他们的页面,这将导致许多糟糕的事情:系统崩溃和权限提升,除非正确探测缓冲区。如果你看到一个驱动程序没有探测缓冲区,而是使用METHOD_NEITHER,那肯定存在漏洞。
(2) METHOD_BUFFERED,没有一个位是打开的:IoManager将输入/输出缓冲区及其长度复制到内核,这使得它更加安全,因为用户不能随意换出缓冲区或更改它们的内容和长度。之后,输入/输出缓冲区指针被分配给IRP。
(3) METHOD_IN_DIRECT和(4)METHOD_OUT_DIRECT两个位中的一个是打开的:这两个非常相似;imanager会像METHOD_BUFFERED那样分配输入缓冲区。对于输出缓冲区,IoManager探测缓冲区并检查虚拟地址在当前访问模式下是否可写或可读。然后,它锁定内存页并将指针传递给 IRP。
让我们看看驱动程序如何访问用户模式缓冲区并查看一个快速漏洞,它说明了在驱动程序中没有进行适当的安全检查的漏洞。
在这里我们可以看到我们应该如何关联驱动程序中的每个缓冲区关于描述方法和传输类型的 Ioctl 代码
由于驱动程序通常可以支持多个 ioctl 代码,因此对于每个不同的 ioctl 代码,它都有一个大的 switch case,影响缓冲区在内存中的存储位置。在下一节中,我们将看到如果我们不注意会发生什么。
本文翻译自:https://www.cyberark.com/resources/threat-research-blog/finding-bugs-in-windows-drivers-part-1-wdm如若转载,请注明原文地址