7.4 应用程序与驱动程序的通信

虽然应用程序处在操作系统的用户态,而驱动程序处在操作系统的内核态,但是它们之间是必须通信的,应用程序的执行都是依赖于操作系统内核的。

7.4.1 创建设备

在编写驱动程序的时候,主函数的入口函数是DriverEntry()函数,该函数有一个参数为 PDRIVER_OBJECT类型,是指向驱动对象的指针的。为了能够使驱动程序被卸载掉,我们为DRIVER_OBJECT结构体中的DriverUnload成员进行了赋值。DriverUnload是驱动对象中的一个程序,其保存了驱动卸载例程(Routine,也就是函数的意思)的指针,其实保存的就是一个函数指针。在DRIVER_OBJECT中,除了 DriverUnload以外,还有一个数组也存放了函数指针,这个数组名是MajorFunction,该数组的大小为IRP_MJ_MAXIMUM_FUNCTION + 1,IRP_MJ_MAXIMUM_FUNCTION是一个宏,该宏的定义为:

7.4 应用程序与驱动程序的通信 - 图1

在MajorFunction中可以指定非常多的例程指针,比如IRP_MJ_CREATE、IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_CLOSE……这些例程都有一个非常相似的定义,定义如下:

7.4 应用程序与驱动程序的通信 - 图2

这些是摘自WDK提供的文件中的定义,函数名可以随意修改,但是函数的定义是不能修改的,也就是说函数的返回值和参数是已经被确定的。在这些函数的定义中,第一个参数是PDEVICE_OBJECT的类型,是一个指向设备对象的指针。第二个参数是一个指向IRP结构体的指针,IRP是I/O请求包。应用程序与驱动程序的通信就是依靠IRP进行的。因此,要在DriverEntry()下指定MajorFunction数组中的各种例程。在没有指定MajorFunction的情况下,有默认的例程来执行。

既然在所有的例程中都需要设备对象指针,那么自然要创建一个设备,创建设备的函数为IoCreateDevice(),其定义如下:

7.4 应用程序与驱动程序的通信 - 图3

7.4 应用程序与驱动程序的通信 - 图4

该函数用来创建一个设备,参数说明如下。

(1)DriverObject:该参数是一个指向驱动对象的指针,指定设备所属的驱动,使用 DriverEntry()的第一个参数即可。

(2) DeviceExtensionSize:指定设备扩展的大小,如果没有设备扩展的话该参数为0。

(3) DeviceName:指定设备的名称,设备名通常是\Device\xxx。

(4) DeviceType:指定设备的类型,由于创建的设备没有具体对应的设备类型,因此此处指定 FILE_DEVICE_UNKNOWN。

(5) DeviceCharacteristics:设置设备对象的特征,这里赋值为0即可。

(6) Exclusive:该参数指定设备是否为互斥设备,这里赋值为TRUE即可。

(7) DeviceObject:该参数是一个指向设备对象的指针。

为了能与设备对象进行通信,必须为设备对象指定一个符号链接,用来在应用程序中打开。创建符号链接的函数为IoCreateSymbolicLink(),其定义如下:

7.4 应用程序与驱动程序的通信 - 图5

该函数的第一个参数用来指定符号链接的名称,该名称通常的格式为\??\xxx,第二个参数为设备名称。

7.4.2 应用程序与驱动程序的通信方式

应用程序与驱动程序的通信方式通常有打开、关闭、读、写、控制这5种。这5种方式对应的 Win32 API函数分别为 CreateFile()、CloseHandle()、ReadFile()、WriteFile()和 DeviceIoControl()。而这5个Win32 API函数分别对应着5个派遣例程,分别是 IRPMJ_CREATE、IRP_MJ_CLOSE、IRP_MJ_READ、IRP_MJ_WRITE和 IRP_MJ_DEVICE CONTROL。下面主要来介绍一下 DeviceIoControl()函数与 IRP_MJ_DEVICE_CONTROL派遣例程。

DeviceIoControl()函数是用来向指定设备发送控制码的,当指定的设备接收到 DeviceIoControl()发来的控制码后会调用IRP_MJ_DEVICE_CONTROL对应的派遣例程,针对不同的控制码进行不同的处理。DeviceIoControl()函数的定义如下:

7.4 应用程序与驱动程序的通信 - 图6

(1) hDevice:该参数指明设备句柄。

(2) dwIoControlCode:该参数是控制码,该值由CTL_CODE生成。

(3) lpInBuffer:该参数是输入的缓冲区。

(4) nInBufferSize:该参数是输入缓冲区大小。

(5) IpOutBuffer:该参数是输出的缓冲区。

(6) nOutBufferSize:该参数是输出缓冲区大小。

(7) lpBytesReturned:该参数是返回字节的个数。

(8) lpOverlapped:该参数针对重叠方式使用,通常为0即可。

7.4.3 应用程序与驱动程序的通信实例

应用程序与驱动程序之间的通信实例以前面写的HOOK NtCreateProcessEx()来进行介绍。在应用程序中通过发送控制码来启动和关闭HOOK,对NtCreateProcessEx()的HOOK进行有效地控制。下面直接给出完整的源代码,代码如下:

7.4 应用程序与驱动程序的通信 - 图7

7.4 应用程序与驱动程序的通信 - 图8

7.4 应用程序与驱动程序的通信 - 图9

7.4 应用程序与驱动程序的通信 - 图10

下面来看一下应用层的代码,在于驱动进行通信时,首先要打开驱动程序:

7.4 应用程序与驱动程序的通信 - 图11

在使用CreateFile()函数时,该函数的第一个参数为驱动程序的符号链接。定义驱动的符号链接为:L“\??\HookNtCreateProcessEx”,当在应用层中使用时应改为: \\.\HookNtCreateProcessEx。

打开驱动程序后,通过DeviceIoControl()来对驱动程序发送控制码,使驱动程序开启和关闭HOOK。代码如下:

7.4 应用程序与驱动程序的通信 - 图12

在应用程序中使用CTL_CODE宏时应包含头文件winioctl.h。运行看一下效果,当开启 HOOK时NtCreateProcessEx()会直接返回,也就是不进行进程的创建。当关闭HOOK后, NtCreateProcessEx()又会正常地创建进程。用KmdManager加载驱动,然后用我们编写的应用程序开启HOOK后运行的任意程序,创建进程都会失败,如图7-12所示。

7.4 应用程序与驱动程序的通信 - 图13

图7-12 开启HOOK后创建进程失败

关于驱动的开发有非常多的东西需要学习,驱动的开发时下也非常流行,很多安全厂商(比如,反病毒、反外挂公司)都对驱动开发人员有大量的需求。