1.驱动入口
#include <ntddk.h>
NTSTATUS DriverEntry (
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath ) {
NTSTATUS status = STATUS_UNSUCCESSFUL;
return status;
}
函数DriverEntry是每个驱动程序中必须的。如同Win32应用程序里的WinMain。DriverEntry的第一个参数就是一个DRIVER_OBJECT的指针。这个DRIVER_OBJECT结构就对应当前编写的驱动程序。其内存是Windows系统已经分配的。
第二个参数RegistryPath是一个字符串。代表一个注册表子键。这个子键是专门分配给这个驱动程序使用的。用于保存驱动配置信息到注册表中。
DriverEntry的返回值决定这个驱动的加载是否成功。如果返回为STATUS_SUCCESS,则驱动将成功加载。否则,驱动加载失败。
2.分发函数和卸载函数
DRIVER_OBJECT中含有分发函数指针。这些函数用来处理发到这个驱动的各种请求。Windows总是自己调用DRIVER_OBJECT下的分发函数来处理这些请求。所以编写一个驱动程序,本质就是自己编写这些处理请求的分发函数。
DRIVER_OBJECT下的分发函数指针的个数为IRP_MJ_MAXIMUM_FUNCTION。
NTSTATUS DriverEntry (
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
) {
ULONG i;
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;++i)
{
DriverObject->MajorFunctions[i] = MyDispatchFunction;
}
}
下面是分发函数的原型,这个原型是Windows驱动编程的规范,不能更改。
NTSTATUS MyDispatchFunction(PDEVICE_OBJECT device,PIRP irp) {
}
//device本驱动生成的设备对象、irp系统请求
卸载函数:卸载时该函数会被执行。
VOID MyDriverUnload(PDRIVER_OBJECT driver) {
}
3.设备与符号链接
驱动程序和系统其他组件之间的交互是通过给设备发送或者接受发给设备的请求来交互的。换句话说,一个没有任何设备的驱动是不能按规范方式和系统交互的。
但并不意味着这样的驱动程序不存在。如果一个驱动程序只是想写写日志文件、Hook某些内核函数或者是做一些其他的小动作,也可以不生成任何设备,也不需要关心分发函数的设置。
如果驱动程序要和应用程序之间通信,则应该生成设备。此外还必须为设备生成应用程序可以访问的符号链接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #include <ntifs.h> NTSTATUS DriverEntry( PDRIVER_OBJECT driver, PUNICODE_STRING reg_path) { NTSTATUS status; PDEVICE_OBJECT device; //设备名 UNICODE_STRING device_name = RTL_CONSTANT_STRING("\\Device\\MyCDO"); // 符号链接名 UNICODE_STRING symb_link = RTL_CONSTANT_STRING("\\DosDevices\\MyCDOSL"); //生成设备对象 status = IoCreateDevice( driver, 0, device_name, FILE_DEVICE_UNKNOWN, 0, FALSE, &device); // 如果不成功,就返回。 if(!NT_SUCCESS(status)) return status; // 生成符号链接 status = IoCreateSymbolicLink( &symb_link, &device_name); if(!NT_SUCCESS(status)) { IoDeleteDevice(device); return status; } // 设备生成之后,打开初始化完成标记 device->Flags &= ~DO_DEVICE_INITIALIZING; return status; } |
这个驱动成功加载之后,生成一个名叫“\Device\MyCDO”的设备。然后在给这个设备生成了一个符号链接名字叫做“\DosDevices\MyCDOSL”。应用层可以通过打开这个符号链接来打开设备。应用层可以调用CreateFile就像打开文件一样打开。只是路径应该是“”\\.\ MyCDOSL”。前面的“\\.\”意味后面是一个符号链接名,而不是一个普通的文件。请注意,由于C语言中斜杠要双写,所以正确的写法应该是“\\\\.\\”。
4.设备的安全创建
NTSTATUS IoCreateDevice(
_In_ PDRIVER_OBJECT DriverObject,
_In_ ULONG DeviceExtensionSize,
_In_opt_ PUNICODE_STRING DeviceName,
_In_ DEVICE_TYPE DeviceType,
_In_ ULONG DeviceCharacteristics,
_In_ BOOLEAN Exclusive,
_Out_ PDEVICE_OBJECT *DeviceObject
);
第一个参数是生成这个设备的驱动对象。
第二个参数DeviceExtensionSize非常重要。由于分发函数中得到的总是设备的指针。当用户需要在每个设备上记录一些额外的信息(比如用于判断这个设备是哪个设备的信息、以及不同的实际设备所需要记录的实际信息,比如网卡上数据包的流量、过滤器所绑定真实设备指针等等),需要指定的设备扩展区内存的大小。如果DeviceExtensionSize设置为非0,IoCreateDevice会分配这个大小的内存在DeviceObject->DeviceExtension中。以后用户就可以从根据DeviceObject-> DeviceExtension来获得这些预先保存的信息。
DeviceName,是设备的名字。目前生成设备,请总是生成在\Device\目录下。所以前面写的名字是“\Device\MyCDO”。其他路径也是可以的。
DeviceType表示设备类型。目前的范例无所谓设备类型,所以填写FILE_DEVICE_UNKNOWN即可。
DeviceCharacteristics目前请简单的填写0即可。
Exclusive这个参数必须设置FALSE。
文档没有做任何解释。 最后生成的设备对象指针返回到DeviceObject中。
这种设备生成之后,必须有系统权限的用户才能打开(比如管理员)。所以如果编程者写了一个普通的用户态的应用程序去打开这个设备进行交互,那么很多情况下可以(用管理员登录的时候)。但是偶尔又不行(用普通用户登录的时候)。
但是依然有时候必须用普通用户打开设备。为了这个目的,设备必须是对所有的用户开放的。此时不能用IoCreateDevice。必须用IoCreateDeviceSecure。
NTSTATUS IoCreateDeviceSecure(
_In_ PDRIVER_OBJECT DriverObject,
_In_ ULONG DeviceExtensionSize,
_In_opt_ PUNICODE_STRING DeviceName,
_In_ DEVICE_TYPE DeviceType,
_In_ ULONG DeviceCharacteristics,
_In_ BOOLEAN Exclusive,
_In_ PCUNICODE_STRING DefaultSDDLString,
_In_opt_ LPCGUID DeviceClassGuid,
_Out_ PDEVICE_OBJECT *DeviceObject
);
DefaultSDDLString。这个一个用于描述权限的字符串。字符串“D:P(A;;GA;;;WD)”将满足“人人皆可以打开”的需求。
另一个参数是一个设备的GUID。请随机手写一个GUID。不要和其他设备的GUID冲突。
//随机手写一个GUID
const GUID DECLSPEC_SELECTANY MYGUID_CLASS_MYCDO =
{0x26e0d1e0L, 0x8189, 0x12e0, {0x98,0x14, 0x08, 0x01, 0x22, 0x33, 0x19, 0x03}}; // 全用户可读写权限
UNICODE_STRING sddl = RLT_CONSTANT_STRING(L”D:P(A;;GA;;;WD)”); // 生成设备
status = IoCreateDeviceSecure(
DriverObject,
0,
&device_name,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE, &sddl,
(LPCGUID)&SFGUID_CLASS_MYCDO,
&device);
使用这个函数的时候,必须连接库wdmsec.lib。
5.符号链接的用户相关性
如果一个普通用户创建了符号链接“\DosDevices\MyCDOSL”,那么,其实其他的用户是看不见这个符号链接的。
如果在DriverEntry中生成符号链接,则所有用户都可以看见。原因是DriverEntry总是在进程“System”中执行。系统用户生成的符号链接是大家都可以看见的。 当前用户总是取决于当前启动当前进程的用户。实际编程中并不一定要在DriverEntry中生成符号链接。一旦在一个不明用户环境下生成符号链接,就可能出现注销然后换用户登录之后,符号链接“不见了”的严重错误。
解决的方案很简单,任何用户都可以生成全局符号链接,让所有其他用户都能看见。路径“\DosDevices\MyCDOSL”改为“\DosDevices\Global\MyCDOSL”即可。 但是在不支持符号链接用户相关性的系统上,生成“\DosDevices\Global\MyCDOSL”这样的符号链接是一种错误。为此必须先判断一下。
UNICODE_STRING device_name;
UNICODE_STRING symbl_name;
if (IoIsWdmVersionAvailable(1, 0x10)) {
// 如果是支持符号链接用户相关性的版本的系统,用\DosDevices\Global.
RtlInitUnicodeString(&symbl_name, L”\\DosDevices\\Global\\SymbolicLinkName”);
} else {
// 如果是不支持的,则用\DosDevices
RtlInitUnicodeString(&symbl, L”\\DosDevices\\SymbolicLinkName”);
}
// 生成符号链接
IoCreateSymbolicLink(&symbl_name, &device_name);
6.请求处理
应用程序为了和驱动通信,首先必须打开设备。然后发送或者接收信息。最后关闭它。这至少需要三个IRP:第一个是打开请求。第二个发送或者接收信息。第三个是关闭请求。
IRP的种类取决于主功能号。主功能号就是前面的说的DRIVER_OBJECT中的分发函数指针数组中的索引。打开请求的主功能号是IRP_MJ_CREATE,而关闭请求的主功能号是IRP_MJ_CLOSE。
IRP的主功能号在IRP的当前栈空间中。 IRP总是发送给一个设备栈。到每个设备上的时候拥有一个“当前栈空间”来保存在这个设备上的请求信息。
NTSTATUS MyDispatchFunction(PDEVICE_OBJECT device,PIRP irp) {
// 获得当前irp调用栈空间
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
NTSTATUS status = STATUS_UNSUCCESSFUL;
swtich(irpsp->MajorFunction) { // 处理打开请求
case IRP_MJ_CREATE: break; // 处理关闭请求
case IRP_MJ_CLOSE: break; // 处理设备控制信息
case IRP_MJ_DEVICE_CONTROL: break; // 处理读请求
case IRP_MJ_READ: break; // 处理写请求
case IRP_MJ_WRITE: break;
default: break;
}
return status;
}
用于与应用程序通信时,上面这些请求都由应用层API引发。对应的关系大致如下:
应用层调用的API 驱动层收到的IRP主功能号
CreateFile IRP_MJ_CREATE
CloseHandle IRP_MJ_CLOSE
DeviceIoControl IRP_MJ_DEVICE_CONTROL
ReadFile IRP_MJ_READ
WriteFile IRP_MJ_WRITE
7.打开关闭请求处理
简单的返回一个IRP成功(或者直接失败)是三部曲,如下:
1. 设置irp->IoStatus.Information为0。关于Information的描述,请联系前面关于IO_STATUS_BLOCK结构的解释。
2. 设置irp->IoStatus.Status的状态。如果成功则设置STATUS_SUCCESS,否则设置错误码。
3. 调用IoCompleteRequest (irp,IO_NO_INCREMENT)。这个函数完成IRP。
以上三步完成后,直接返回irp->IoStatus.Status即可。示例代码如下。
NTSTATUS MyCreateClose(
IN PDEVICE_OBJECT device,
IN PIRP irp) {
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
在应用层,打开和关闭这个设备的代码如下:
HANDLE device=CreateFile(“\\\\.\\MyCDOSL”,
GENERIC_READ|GENERIC_WRITE,
0,
0,
OPEN_EXISTING,
FILE_ATTRIBUTE_SYSTEM,
0);
if (device == INVALID_HANDLE_VALUE) {
// 打开失败,说明驱动没加载,报错即可
}
// 关闭
CloseHandle(device);
8.应用层信息传入
DeviceIoControl称为设备控制接口。其特点是可以发送一个带有特定控制码的IRP。同时提供输入和输出缓冲区。应用程序可以定义一个控制码,然后把相应的参数填写在输入缓冲区中。同时可以从输出缓冲区得到返回的更多信息。 当驱动得到一个DeviceIoControl产生的IRP的时候,需要了解的有当前的控制码、输入缓冲区的位置和长度,以及输出缓冲区的位置和长度。其中控制码必须预先用一个宏定义。
#define MY_DVC_IN_CODE \
(ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, \
0xa01, \
METHOD_BUFFERED, \
FILE_READ_DATA|FILE_WRITE_DATA)
其中0xa01这个数字是用户可以自定义的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | NTSTATUS MyDeviceIoControl( PDEVICE_OBJECT dev, PIRP irp){ // 得到irpsp的目的是为了得到功能号、输入输出缓冲长度等信息。 PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp); // 首先要得到功能号 ULONG code = irpsp->Parameters.DeviceIoControl.IoControlCode; // 得到输入输出缓冲长度 ULONG in_len = irpsp->Parameters.DeviceIoControl.InputBufferLength; ULONG out_len = irpsp->Parameters.DeviceIoControl.OutputBufferLength; // 请注意输入输出缓冲是公用内存空间的 PVOID buffer = irp->AssociatedIrp.SystemBuffer; // 如果是符合定义的控制码,处理完后返回成功 if(code == MY_DVC_IN_CODE) { // 在这里进行需要的处理动作 //因为不返回信息给应用,所以直接返回成功即可。 // 没有用到输出缓冲 irp->IoStatus.Information = 0; irp->IoStatus.Status = STATUS_SUCCESS; }else{ // 其他的请求不接受。直接返回错误。请注意这里返回错误和前面返回成功的区别。 irp->IoStatus.Information = 0; irp->IoStatus.Status = STATUS_INVALID_PARAMETER; } IoCompleteRequest (irp,IO_NO_INCREMENT); return irp->IoStatus.Status; } |
应用程序方面,进行DeviceIoControl的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | HANDLE device=CreateFile("\\\\.\\MyCDOSL", GENERIC_READ|GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, 0); BOOL ret; DWORD length = 0; // 返回的长度 if (device == INVALID_HANDLE_VALUE) { //打开失败,说明驱动没加载,报错即可 } BOOL ret = DeviceIoControl(device, MY_DVC_IN_CODE, // 功能号 in_buffer, // 输入缓冲,要传递的信息,预先填好 in_buffer_len, // 输入缓冲长度 NULL, // 没有输出缓冲 0, // 输出缓冲的长度为0 &length, // 返回的长度 NULL); if(!ret) { // DeviceIoControl失败。报错。 } // 关闭 CloseHandle(device); |
9.驱动层信息传出
驱动主动通知应用和应用通知驱动的通道是同一个。只是方向反过来。应用程序需要开启一个线程调用DeviceIoControl,(调用ReadFile亦可)。而驱动在没有消息的时候,则阻塞这个IRP的处理。等待有信息的时候返回。
也可以在应用层生成一个事件,然后把事件传递给驱动。驱动有消息要通知应用的时候,则设置这个事件。但是实际上这种方法和上述方法本质相同:应用都必须开启一个线程去等待(等待事件)。
让应用程序简单的调用DeviceIoControl就可以了。当没有消息的时候,这个调用不返回。应用程序自动等待(相当于等待事件)。有消息的时候这个函数返回。并从缓冲区中读到消息。 实际上,驱动内部要实现这个功能,还是要用事件的。只是不用在应用和驱动之间传递事件了。
驱动内部需要制作一个链表。当有消息要通知应用的时候,则把消息放入链表中,并设置事件在DeviceIoControl的处理中等待事件。下面是一个例子:这个例子展示的是驱动中处理DeviceIoControl的控制码为MY_DVC_OUT_CODE的部分。实际上应用如果有消息要通知驱动,必须把消息放入队列尾并设置事件g_my_notify_event。MyGetPendingHead获得第一条消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | NTSTATUS MyDeviceIoCtrlOut( PIRP irp, ULONG out_len) { MY_NODE *node; ULONG pack_len; // 获得输出缓冲区。 PVOID buffer = irp->AssociatedIrp.SystemBuffer; // 从队列中取得第一个。如果为空,则等待直到不为空。 while((node = MyGetPendingHead()) == NULL) { KeWaitForSingleObject( &g_my_notify_event,// 一个用来通知有请求的事件 Executive, KernelMode, FALSE, 0); } // 有请求了。此时请求是node。获得PACK要多长。 pack_len = MyGetPackLen(node); if(out_len < pack_len) { irp->IoStatus.Information = pack_len; // 这里写需要的长度 irp->IoStatus.Status = STATUS_INVALID_BUFFER_SIZE; IoCompleteRequest (irp,IO_NO_INCREMENT); return irp->IoStatus.Status; } // 长度足够,填写输出缓冲区。 MyWritePackContent(node,buffer); // 头节点被发送出去了,可以删除了 MyPendingHeadRemove (); // 返回成功 irp->IoStatus.Information = pack_len; // 这里写填写的长度 irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest (irp,IO_NO_INCREMENT); return irp->IoStatus.Status; } |
这个函数的处理要追加到MyDeviceIoControl中。如下:
NTSTATUS MyDeviceIoControl(
PDEVICE_OBJECT dev,
PIRP irp) {
if(code == MY_DVC_OUT_CODE)
return MyDeviceIoCtrlOut(dev,irp);
}
在这种情况下,应用可以循环调用DeviceIoControl,来取得驱动驱动通知它的信息。