在本章中,将使用我们在前几章中学到的许多概念,并构建一个简单但完整的驱动程序和客户机应用程序,同时填补一些缺失的细节。我们将部署驱动程序并使用它的功能——在内核模式下执行用户模式不可用的一些操作。
1.介绍
我们将要用一个简单的内核驱动来解决的问题是用Windows API设置线程优先级的不灵活性。在用户模式下,一个线程的优先级是由其进程PriorityClass与具有有限数量级别的每个线程基础上的偏移量的组合决定的。
改变进程/优先级:SetPriorityClass,该函数设置的优先级是线程的默认优先级,也可以使用SetThreadPriority设置线程优先级(都是用偏移量设置),下面的表格展示了基于进程优先级类和线程的优先级偏移量的可用线程优先级。
实时优先级类并不意味着Windows是一个实时操作系统;此外,由于实时优先级非常高,并且与许多做重要工作的内核线程竞争,这样的进程必须使用管理员特权运行;否则,试图将优先级类设置为实时会将值设置为“高”。
只有一小部分优先级可以直接设置。我们希望创建一个驱动程序,以规避这些限制,并允许将线程的优先级设置为任何数字,而不管其进程优先级类如何。
2.驱动初始化
创建一个新的项目命名为PriorityBooster并且删掉INF文件,新建一个cpp文件,大多数软件驱动程序需要在驱动器条目中执行以下操作:
设置一个卸载例程;
设置驱动程序所支持的调度例程;
创建一个设备对象;
创建一个到设备对象的符号链接。
一旦执行了所有这些操作,驱动程序就已经准备好接受请求了。第一步是添加一个卸载例程,并从驱动程序对象指向它。以下是新的具有卸载程序的DriverEntry,当然我们需要根据我们实际工作中的需要添加代码。
接下来我们需要设置我们要支持的调度例程,实际上,所有的驱动程序都必须支持IRP_MJ_CREATE和IRP_MJ_CLOSE,否则就无法为该驱动程序的任何设备打开手柄。所以我们将以下内容添加到DriverEntry中。
1 | DriverObject->MajorFunction[IRP_MJ_CREATE] = PriorityBoosterCreateClose; |
所有的major function都有同样的类型(它们是函数指针数组的一部分),因此我们需要给函数PriorityBoosterCreateClose加一个类型NTSTATUS
传递信息给驱动
我们的目的是为了设置现成的优先级,所以我们需要一种方式告诉驱动程序是哪个线程以及设置什么值。从我们的目的处罚,我们需要用到WriteFile或者 DeviceIoControl,一般的思维是使用前者,但是在这里,后者是一种向驱动程序传递数据的通用机制。由于更改线程的优先级不是纯粹的写操作,所以我们将使用DeviceIoControl。此函数具有以下原型(我们主要关注三个参数):
操作的控制代码标识要执行的特定操作以及执行该操作的设备的类型。每个控制代码的文档都提供了lpInBuffer,nInBufferSize,lpOutBuffer和nOutBufferSize参数的使用细节。
在驱动程序方面,DeviceIoControl对应于IRP_MJ_DEVICE_CONTROL的major function代码。我们将其添加到调度例程的初始化中:
客户端/驱动程序通信协议
前面提到我们需要ControlCode以及Inputbuffer(包括线程id以及设置的优先级)这些信息片段必须可由驱动程序和客户端同时使用。客户端将提供数据,而驱动程序将对其采取行动。这意味着这些定义必须包含在驱动程序和客户端代码必须包含的单独文件中。
基于此我需要创建一个头文件,这个头文件需要定义驱动程序期望从客户端获得的数据结构和用于更改线程优先级的控制代码。(请注意为什么不用DWORD,因为该类型只在用户模式下定义,ULONG在用户以及内核模式下都有定义)
ControlCode必须使用CTL_CODE宏构建,该宏接受组成最终ControlCode的四个参数。CTL_CODE的定义如下:
1 | #define CTL_CODE( DeviceType, Function, Method, Access ) ( |
DeviceType定义设备类型,可以是WDK头文件中的某个FILE_DEVICE_XXX常量,但是这个变量是针对于基于硬件的驱动程序。(我们的是基于软件的,尽管如此,微软的文档还是规定,第三方驱动程序应该从0x8000开始。
Function 一个表示特定操作的升序数字。如果没有其他内容,那么这个数字在同一驱动程序的不同控制代码之间必须有所不同。同样,任何数字都可以,但官方文件显示,第三方驱动程序应该从0x800开始。
Method ControlCode中最重要的部分。它指示Client提供的输入和输出缓冲区如何传递给驱动程序。对于我们的驱动程序,我们将使用最简单的值METHOD_NEITHER。
Access 指示此操作是来自FILE_WRITE_ACCESS、FILE_READ_ACCESS还是FILE_ANY_ACCESS。典型的驱动程序只使用FILE_ANY_ACCESS并处理IRP_MJ_DEVICE_CONTROL处理程序中的实际请求。
创建设备对象
一个典型的软件驱动程序只需要一个设备对象,其中有一个符号链接指向它,这样用户模式客户端就可以获得句柄。用IoCreateDevice实现该功能。
DeviceExtensionSize 除了sizeof(DEVICE_OBJECT)之外,还将分配的额外字节。可用于将某些数据结构与设备相关联。它对于仅创建一个设备对象的软件驱动程序不那么有用,因为设备所需的状态可以简单地由全局变量来管理。
Devicetype 与某些类型的基于硬件的驱动程序相关。对于软件驱动程序,应该使用值FILE_DEVICE_UNKNOWN。
Exclusive 是否应该允许多个文件对象打开同一设备?大多数驱动程序应该指定false,但在某些情况下,TRUE更合适;它强制单个客户端到设备。
DeviceObject 返回的指针,作为指针传递给一个指针。如果成功,Io创建device将从非页面池中分配结构,并将生成的指针存储在取消引用的参数中
在调用IoCreateDevice之前,我们必须创建一个UNICODE_STRING来保存内部设备的名称.
设备名称可以是任何东西,但是应该在设备对象管理器目录中。有两种方法可以用一个常量字符串来初始化一个UNICODE_STRING。第一个是使用RtlInitUnicodeString。但是RtlInitUnicode字符串必须计算字符串中的字符数,才能适当地初始化长度和最大长度。有一种更快的方法,使用RTL_CONSTANT_STRING宏,在编译时静态计算字符串的长度,这意味着它只能对常量字符串正确操作。
如果一切顺利,我们现在有了一个指向设备对象的指针。下一步是通过提供一个符号链接,使用户模式调用者可以访问此设备对象。以下行创建符号链接并将其连接到我们的设备对象:
IoCreateSymbolicLink通过接受符号链接和链接的目标来完成工作。注意,如果创建失败,我们必须通过调用IoDeleteDevice来撤销到这里我们所做的所有操作(目前我们只是创建了对象,所以删除就好了)。如果驱动器条目返回任何故障状态,则不调用卸载例程。如果我们有更多的初始化步骤要做,我们将必须记住在那之前撤销所有内容,直到出现失败。(第五章有更好的处理方式)
符号链接这一步成功后,DriverEntry就返回成功,驱动程序就可以接受请求了。
卸载程序:在我们的例子中,有两件事(设备对象创建和符号链接创建)。我们将按相反的顺序撤销它们:
客户端代码
客户端项目(控制台桌面项目)首先需要include由驱动端创建的头文件并且需要在主函数中接受命令行参数(线程ID和优先级),请求驱动程序将线程的优先级更改为给定的值。
然后需要打开设备句柄(note:如果驱动程序此时没有加载,这意味着没有设备对象,也没有符号链接,我们将得到一个错误号2)
打开设备对象后将参数传给ThreadData结构体,然后开始DeviceIoControl
该函数主要调用IRP_MJ_DEVICE_CONTROL主函数例程到达驱动程序。
创建和关闭派遣函数
所需要的只需以一个成功的状态完成请求。以下是完整的创建/关闭调度例行程序实现,IoCompleteRequest函数的第一个参数是将IRP返回给它的创建者(通常是I/O manager),第二个参数是驱动程序可以向其客户端提供的临时优先级提升值。在大多数情况下,零的值是最好的(IO_NO_INCREMENT被定义为零),因为请求同步完成,因此调用者不必获得优先级提升。详细可见第六章
DeviceToControl派遣函数
这一部分是项目的核心工作,首先需要IRP信息,关键是查看与当前设备层相关联的IO_STACK_LOCATION内部。调用IoGetCurrentIrpStackLocation将返回一个指向正确的IO_STACK_LOCATION的指针。IO_STACK_LOCATION的主要组成部分是一个名为Parameters的巨大联合成员,它包含一组结构,每种类型的IRP都一个。在IRP_MJ_DEVICE_CONTROL的情况下,要看的结构是DeviceIoControl。在该结构中,我们可以找到客户端所传递的信息,如控制代码、缓冲区及其长度。在我们的例子中,实际上只有一个IO_STACK_LOCATION.
其次需要检查控制代码是否是驱动程序支持的,如果没有,我们只将状态设置为成功以外的东西,我们需要的最后一段通用代码是在switch块之后完善IRP,无论它是否成功。
然后第一步是检查我们收到的缓冲区是否大到足以包含一个线程数据对象。
接下来让我们看看优先级是否在1到31的范围内,如果没有,则中止:
设置优先级之前,我们需要获得内核空间中真是的线程对象的指针,用到PsLookupThreadByThreadId函数,通过线程Id获得(note:include ntfis.h)。
函数的第一个参数是一个句柄,这个ID它是一个作为句柄输入的ID。其原因与进程和线程id的生成方式有关。这些都是由全局私有内核句柄表生成的,所以句柄“值”是实际的id。ULongToHandle宏提供了必要的转换。(请记住,64位系统上的句柄是64位,但客户端提供的线程ID总是32位。)
KeSetPriorityThread需要的参数是一个PKTHREAD,但是PETHREAD的第一个成员就是前者类型,所以两者可以相互转换
安装和测试
1.管理员打开cmd
sc create booster type= kernel binPath= c:\PriorityBooster.sys
booster是注册表键值,所以必须是独一无二的
2.加载驱动
sc start booster(note:生成文件后缀没有recipe只有sys)
然后采用ProcessExplorer查看cmd.exe的一个线程,将其作为实践对象
3.使用线程ID和所需的优先级运行客户端:
booster 3384 25