与用户程序进行交互
编写一个用户程序能够使用CreateFile进行交互的驱动。
首先我们的设备的符号连接名称是:"\\\\.\\HelloDDK"
,由于取消转义,所以\比较多。
用户层程序:
1 2 3 4 5 6 7 8 9 10 11 12
| int _tmain(int argc, _TCHAR* argv[]) { HANDLE hDevcie = CreateFile("\\\\.\\HelloDDK",GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if (hDevcie == INVALID_HANDLE_VALUE) { printf("error"); return 1; } CloseHandle(hDevcie); system("pause"); return 0; }
|
就是一个简单的CreateFile得到句柄的程序,最后在退出的时候关闭句柄。
在驱动层首先我们要先创建一个设备,并且将设备关联上符号连接名,我们创建一个常用的CreateDevice函数,用来创建我们的设备,并且关联连接名。
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
| #pragma INITCODE NTSTATUS CreateDevice(IN PDRIVER_OBJECT pDriverObject) { NTSTATUS status; PDEVICE_OBJECT pDevObj; PDEVICE_EXTENSION pDevExt; UNICODE_STRING devName; RtlInitUnicodeString(&devName, L"\\Device\\WkerDDKDevice"); status = IoCreateDevice(pDriverObject, sizeof(DEVICE_EXTENSION), &devName, FILE_DEVICE_UNKNOWN, 0, TRUE, &pDevObj); if (!NT_SUCCESS(status)) { return status; } pDevObj->Flags |= DO_BUFFERED_IO; pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; pDevExt->pDevice = pDevObj; pDevExt->ustrDeviceName = devName; UNICODE_STRING symLinkName; RtlInitUnicodeString(&symLinkName, L"\\??\\HelloDDK"); pDevExt->ustrSymLinkName = symLinkName; status = IoCreateSymbolicLink(&symLinkName, &devName); if (!NT_SUCCESS(status)) { IoDeleteDevice(pDevObj); return status; } return STATUS_SUCCESS;
}
|
由于这个函数是我们在进入驱动使用的,所以我们最好也加上INITCODE,节省一部分的内存,我们首先先是创建一个名字为WkerDDKDevice的设备程序,这个名称是我们不可见的。参数也比较简单,在此之前我们 还需要定义一个设备扩展对象的结构体:
1 2 3 4 5 6
| typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT pDevice; UNICODE_STRING ustrDeviceName; UNICODE_STRING ustrSymLinkName; }DEVICE_EXTENSION, *PDEVICE_EXTENSION;
|
我们创建完设备对象之后,我们将我们的设备对象相关的属性进行晚上,首先是我们需要支持缓冲区类型的读写,然后我们晚上设备扩展对象,这个东西比我们的全局变量是好用许多的,添加我们需要常用的字段信息,然后初始化我们的符号名,最后将设备名称和符号链接名进行连接,这样我们简单的设备就创建完毕了。
在DriverEntry中我们这样子写:
1 2 3 4 5 6 7 8 9 10 11
| NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath) { pDriverObject->DriverUnload = Unload; NTSTATUS status; pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = HelloDDKDispatchRoutin; status = CreateDevice(pDriverObject); return status; }
|
将我们的Create、close、cleanup的irp派遣函数设置成一个。
我们接下来继续写我们的派遣函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| NTSTATUS HelloDDKDispatchRoutin(IN PDEVICE_OBJECT pDevObj, IN PIRP pirp) { KdPrint(("进入IRP\n")); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pirp); KdPrint(("%d", stack->MajorFunction));
NTSTATUS status = STATUS_SUCCESS; pirp->IoStatus.Status = status; pirp->IoStatus.Information = 0; IoCompleteRequest(pirp, IO_NO_INCREMENT);
KdPrint(("离开IRP\n")); return status; }
|
参数的话呢是DDK给我们定义的,我们首先获取当前设备栈的stack对象,这个对象里面保存了我们当前级别的信息,里面有一个MajorFunction是定义到底是什么类型的操作调用了这个派遣函数(这个我不是用char定义了,有点麻烦),得到之后我门将返回值设为真,并且将设备栈的状态也设置为真,information为我们操作的字节数,这里给0吧,然后使用IoCompleteRequest
函数返回我们的应用程序。
最终的效果:
其实我们也可以发现,close其实就是先调用了cleanup,然后调用了close。
模拟读写操作(缓冲区)
在应用程序使用ReadFile类型的函数的时候都是使用的CreateFile得到的handle,在驱动中都有相对的IRP处理。
例如我们使用ReadFile的时候相对应的驱动会调用IRP_MJ_READ
的派遣函数。
我们只需在这个IRP中处理我们想要处理的内容就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| NTSTATUS HelloDDKDispatchRoutin(IN PDEVICE_OBJECT pDevObj, IN PIRP pirp) { KdPrint(("进入IRP\n")); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pirp);
KdPrint(("%d", stack->MajorFunction));
NTSTATUS status = STATUS_SUCCESS; pirp->IoStatus.Status = status; pirp->IoStatus.Information = 0;
if (stack->MajorFunction == 3) { ULONG ulReadLength = stack->Parameters.Read.Length; pirp->IoStatus.Information = ulReadLength; memset(pirp->AssociatedIrp.SystemBuffer, 0xAA, ulReadLength); } IoCompleteRequest(pirp, IO_NO_INCREMENT);
KdPrint(("离开IRP\n")); return status; }
|
为了方便起见我还是将Read的派遣函数放在了这个通用的函数里面,但是我会判断设备栈的类型,如果是3也就是ReadFile的话呢我就做ReadFile的操作,首先我先获取他要获取的字节数,这个在stack->Parameters.Read.Length
这个域里面保存着,我们只需要获取出来就可以了,然后热值IRP的操作字节数(其实也就是我们ReadFile里面真实读取个数的OUT参数),然后我们将缓冲区使用memset拷贝是个0xAA进去。
这样就完成了模拟一个读驱动,那么我们只需要在应用程序中执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| UCHAR buffer[10]; ULONG ulRead; bool bRet = ReadFile(hDevcie,buffer,10,&ulRead,NULL); if (bRet) { printf("共读取字节数:%d\n",ulRead); for (int i=0;i<(int)ulRead;i++) { printf("%02X",buffer[i]); } }else {
printf("读取数据出错"); }
|
打印出来是个0xAA。
写的话呢类似,只是我们写到的其实是我们设备扩展属性的一个buffer里面,需要注意的是不要超出界限,否则你懂的。
再写一个获取文件长度的函数吧。
首先我们先设置IRP:
1
| pDriverObject->MajorFunction[IRP_MJ_QUERY_INFORMATION] = HelloDDKQueryInformation;
|
然后编写派遣函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| NTSTATUS HelloDDKQueryInformation(IN PDEVICE_OBJECT pDevObj, IN PIRP pirp) { KdPrint(("进入IRP查询文件长度\n")); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pirp); FILE_INFORMATION_CLASS info = stack->Parameters.QueryFile.FileInformationClass; if (info == FileStandardInformation) { PFILE_STANDARD_INFORMATION file_info = (PFILE_STANDARD_INFORMATION)pirp->AssociatedIrp.SystemBuffer; file_info->EndOfFile = RtlConvertLongToLargeInteger(1314); } NTSTATUS status = STATUS_SUCCESS; pirp->IoStatus.Status = status; pirp->IoStatus.Information = stack->Parameters.QueryFile.Length; IoCompleteRequest(pirp, IO_NO_INCREMENT);
KdPrint(("离开IRP查询文件长度\n")); return status; }
|
其实还是比较简单的,首先我们先获取设备栈,然后获取查询文件的类型,如果是标准的文件属性查询的话呢我们就获取IRP的系统buffer,这个字段有一个属性就是EndOfFile,表明读取的长度,是一个大整数,这里其实应该读取扩展设备的字段的,但是为了简单我就直接写了一个1314,然后下面的操作就是常规的操作,在最后的时候我们需要注意的是要将操作的字节数设置类设备战中查询文件的长度。
我们在应用程序中写:
1 2 3 4
| DWORD fileSize; DWORD low = GetFileSize(hDevcie,&fileSize); printf("\n文件长度%d\n",low); printf("文件长度%d\n",fileSize);
|
最后程序的运行效果:
需要注意的是这个大小返回给应用程序的时候是有低32和高32的区别的。
!!!这里有一点需要注意。我搞了一个小时才搞明白的一个东西,就是运行驱动的时候突然提示文件找不到了,这个解释我一直不知道,但是直到我想到我的设备对象没有销毁的时候我才知道为啥,我们一定要在Unload的时候将我们的设备对象进行销毁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #pragma PAGECODE VOID Unload(IN PDRIVER_OBJECT pDriverObject) { KdPrint(("Unload")); PDEVICE_OBJECT pNextObj; pNextObj = pDriverObject->DeviceObject; while (pNextObj != NULL) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pNextObj->DeviceExtension; UNICODE_STRING pLinkName = pDevExt->ustrSymLinkName; IoDeleteSymbolicLink(&pLinkName); pNextObj = pNextObj->NextDevice; IoDeleteDevice(pDevExt->pDevice); }
}
|
删除设备和设备对象,并且是整个链表都删除!
而且需要注意的额是,我们在设置设备对象的Flags属性的时候我们要加上DO_BUFFER_IO属性。
直接读取设备
原理也是比较简单的,就是找到用户给的缓冲区的真是物理地址,然后映射到内核模式下的内存里面,这样子内核模式操作的内存其实也就是缓冲区真是的内存,如果不这样直接操作缓冲去的话呢很有可能导致蓝屏。
我们直接读取设备得到的真是内存的方法是MsGetSystemAddressPorMdlSage这个函数,参数有两个,第一个是我们的pIrp的MdlAddress结构,这个结构主要记录了用户缓冲区的信息,第二个参数是内存也得信息,一般是:NormalPagePriority
,同样的我们需要知道操作的大小,其实在MDLAddress结构体里面都有存储,但是DDK给了我们一个宏,方便操作,都是MmGetMdlXXX开头的,又回去长度,首地址,偏移量。其他的操作和缓冲区读写是一样的,这里不做记录了。
其他IO操作
其他的话呢就只有一个完全读写,就是那种不安全的,我们要放在try_except中,并且使用ProbeForWrite这类的函数去检查他的可读写程度。
DeviceControl
IRP是:IRP MJ_DEVICE_CONTROL
这个是其他操作方式,其实这里相对于读写操作来说的话呢就是多了一个IOCTL的判断,这个就是一个用CTL_CODE宏生成的代码:
1 2 3 4 5 6 7 8 9
| CTL_CODE:用于创建一个唯一的32位系统I/O控制代码,这个控制代码包括4部分组成:
DeviceType(设备类型,高16位(16-31位)),
Function(功能2-13 位),
Method(I/O访问内存使用方式),
Access(访问限制,14-15位)。
|
第一个是设备类型,第三个是操作模式(缓冲区,直接),第四个是访问权限。
第二个的话呢是一个0X800-0XFFF的数值,0-0X07FFF是系统的,我们只能定义0X800到0XFFF的。
与read和write区别的就是,我们通过stack->Parameters.DeviceControl.xxx获取相关的信息:
- IoControlCode是IOCTL码
- OutBufferLegth是输出buffer的长度
- 同样的,操作的数据还是在pIrp->AssociatedIrp.SystemBuffer里面
- 直接读数据和缓冲区设备读取的却别就在于设置FLAG和DeviceType,并且是否要用MnGetSystemAddressForMdlSafe函数获得真正的物理地址映射的内核模式地址
看看这个就想起来了,缓冲区的话呢是不用使用内核地址的。
其他操作方式访问的话呢去直接和缓冲区的区别就在于要检查内存的可用性和获取输入输出地址的方式:
1 2 3
| 对于 DeviceIOControl提供的输入缓冲区的地址,派遣函数可以通过LO堆栈(IO_ STACK_LOCATION)的stack->Parameters.DeviceloControl.Type3InputBuffer得到。同时, DeviceIOControl提供的输出缓冲区的地址,派遣函数可以由IRP的pIrp->UserBuffer得到。
|
一般我们是通过switch语句来判断IoControlCode码进行case操作。