《Windows驱动开发技术详解》之读写操作
- 缓冲区方式读写操作
设置缓冲区读写方式:
读写操作一般是由ReadFile和WriteFile函数引起的,这里先以WriteFile函数为例进行介绍。WriteFile要求用户提供一段缓冲区,并且说明缓冲区的大小,然后WriteFile将这段内存的数据传入到驱动程序中。这种方法,操作系统将应用程序提供缓冲区数据直接复制到内核模式的地址中。这样做,比较简单的解决了将用户地址传入驱动的问题,而缺点是需要在用户模式和内核模式之间复制数据,影响了效率。在少量内存操作时,可以采用这种方法。拷贝到内核模式下的地址由WriteFile创建的IRP的AssociatedIrp.SystemBuffer子域记录。
下面的代码演示了如何利用缓冲区方式读取设备,这个例子中,驱动程序负责向缓冲区中填入了数据:
应用层调用ReadFile,想驱动传送一个读IRP请求:
1 int main(){ 2 HANDLE hDevice = 3 CreateFile("\\\\.\\HelloDDK", 4 GENERIC_READ | GENERIC_WRITE, 5 0, NULL, 6 OPEN_EXISTING, 7 FILE_ATTRIBUTE_NORMAL, 8 NULL); 9 if (hDevice == INVALID_HANDLE_VALUE){ 10 printf("Open device failed!\n"); 11 } 12 else{ 13 printf("Open device succeed!\n"); 14 } 15 UCHAR buffer[10]; 16 ULONG ulRead; 17 BOOL bRet = ReadFile(hDevice, buffer, 10, &ulRead, NULL); 18 if (bRet){ 19 printf("Read %d bytes!", ulRead); 20 for (int i = 0; i < (int)ulRead; i++){ 21 printf("%02X", buffer[i]); 22 } 23 printf("\n"); 24 } 25 CloseHandle(hDevice); 26 system("pause"); 27 return 0; 28 }
运行之后的结果如下:
创建一个虚拟设备模拟文件读写:
读、写派遣函数如下:
1 NTSTATUS HelloDDKDispatchRead(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 2 UNREFERENCED_PARAMETER(pDevObj); 3 DbgPrint("Enter dispach read!\n"); 4 PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; 5 NTSTATUS status = STATUS_SUCCESS; 6 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); 7 //得到要读取的数据的长度 8 ULONG ulReadLength = stack->Parameters.Read.Length; 9 ULONG ulReadOffset = (ULONG)stack->Parameters.Read.ByteOffset.QuadPart; 10 if (ulReadOffset + ulReadLength > MAX_FILE_LENGTH){ 11 status = STATUS_FILE_INVALID; 12 ulReadLength = 0; 13 } 14 else{ 15 memcpy(pIrp->AssociatedIrp.SystemBuffer, pDevExt->buffer + ulReadOffset, ulReadLength); 16 status = STATUS_SUCCESS; 17 } 18 pIrp->IoStatus.Status = status; 19 pIrp->IoStatus.Information = ulReadLength; 20 //memset(pIrp->AssociatedIrp.SystemBuffer, 0x68, ulReadLength); 21 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 22 return status; 23 } 24 25 NTSTATUS HelloDDKDispatchWrite(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 26 UNREFERENCED_PARAMETER(pDevObj); 27 NTSTATUS status = STATUS_SUCCESS; 28 PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; 29 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); 30 ULONG ulWriteLength = stack->Parameters.Write.Length; 31 ULONG ulWriteOffset = (ULONG)stack->Parameters.Write.ByteOffset.QuadPart; 32 if (ulWriteOffset + ulWriteLength > MAX_FILE_LENGTH){ 33 status = STATUS_FILE_INVALID; 34 ulWriteLength = 0; 35 } 36 else{ 37 memcpy(pDevExt->buffer + ulWriteOffset, pIrp->AssociatedIrp.SystemBuffer, ulWriteLength); 38 status = STATUS_SUCCESS; 39 if (ulWriteLength + ulWriteOffset > pDevExt->file_length){ 40 pDevExt->file_length = ulWriteLength + ulWriteOffset; 41 } 42 } 43 pIrp->IoStatus.Status = status; 44 pIrp->IoStatus.Information = ulWriteLength; 45 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 46 47 return status; 48 }
再在R3添加入代码:
1 UCHAR buffer[10]; 2 memset(buffer, 0x66, 10); 3 ULONG ulRead; 4 ULONG ulWrite; 5 BOOL bRet = WriteFile(hDevice, buffer, 10, &ulWrite, NULL); 6 if (bRet){ 7 printf("Write %d bytes!\n", ulWrite); 8 } 9 10 bRet = ReadFile(hDevice, buffer, 10, &ulRead, NULL); 11 if (bRet){ 12 printf("Read %d bytes!", ulRead); 13 for (int i = 0; i < (int)ulRead; i++){ 14 printf("%02X", buffer[i]); 15 } 16 } 17 printf("\n");
运行,得到结果:
如果我们要查询文件信息,没有注册IRP_MY_QUERY_INFORMATION的派遣函数时,GetFileSize会正常返回读到的文件的大小:
但是,因为GetFileSize读取的是文件的大小,而这里传递的是设备对象的句柄,本来是读不到大小的,但是如果利用驱动对IRP进行修改,再返回给R3层,就可以得到了:
其派遣函数代码如下:
R3层添加代码:
1 bRet = GetFileSizeEx(hDevice, &dwFileSize); 2 printf("File size is %u\n", dwFileSize);
- 直接方式读写操作
与缓冲区方式读写设备不同,直接方式读写设备,操作系统会将用户模式下的缓冲区锁住。然后,操作系统将这段缓冲区在内核模式地址再次映一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理内存。无论操作系统如何切换进程,内核模式地址都保持不变。
//这里锁住的意思就是建立一个虚拟内存到物理内存的映射固定不变。如果不锁住内存,那么这个页被交换到硬盘,等到再重新交换到内存时,虚拟地址就可能对应到了其它物理地址。操作系统先将用户模式的地址锁住后,操作系统用内存描述符表(MDL)记录这段内存。用户模式的这段缓冲区在虚拟内存上是连续的,但是在物理内存上可能是离散的。
设置直接读写方式:
这里说明一下,如果你设置的是pDevObj->Flags |= DO_DIRECT_IO,而你在派遣函数中的读函数采用的是Buffer形式去读,就会蓝屏。
示例代码如下:
1 NTSTATUS HelloDDKDispatchRead_Direct(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 2 UNREFERENCED_PARAMETER(pDevObj); 3 DbgPrint("Enter HelloDDKDispatchRead_Direct!"); 4 //PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; 5 NTSTATUS status = STATUS_SUCCESS; 6 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); 7 ULONG ulReadLength = stack->Parameters.Read.Length; 8 //得到锁定缓冲区长度 9 ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress); 10 //得到锁定缓冲区的首地址 11 PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress); 12 //得到锁定缓冲区的偏移 13 ULONG mdl_offset = MmGetMdlByteOffset(pIrp->MdlAddress); 14 DbgPrint("mdl_address:0x%016X\n", mdl_address); 15 DbgPrint("mdl_offset:%d\n", mdl_offset); 16 DbgPrint("mdl_length:%d\n", mdl_length); 17 18 if (mdl_length!= ulReadLength){ 19 pIrp->IoStatus.Information = 0; 20 status = STATUS_SUCCESS; 21 } 22 else{ 23 PVOID64 kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, 24 NormalPagePriority); 25 DbgPrint("kernel_address:0x%016X\n", kernel_address); 26 memset(kernel_address, 0x66, ulReadLength); 27 pIrp->IoStatus.Information = ulReadLength; 28 } 29 pIrp->IoStatus.Status = status; 30 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 31 return status; 32 }
一开始输出的kernel地址也是应用层地址:
我不知道是哪里错了,然后用windbg跟踪下发现,是输出方式有问题:
所以输出要进行一下修改:
更改之后就没有问题了:
- 其它方式读写操作:
在使用其它方式读写设备时,派遣函数直接读写应用程序提供的缓冲区地址。对于驱动程序编程,这样做是很危险的。只有在驱动程序与应用程序运行在相同线程上下文的情况下,才能使用这种方式。
示例代码如下:
1 NTSTATUS HelloDDKDispatchRead_Other(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 2 UNREFERENCED_PARAMETER(pDevObj); 3 DbgPrint("Enter HelloDDKDispatchRead_Other!\n"); 4 // PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; 5 NTSTATUS status = STATUS_SUCCESS; 6 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); 7 ULONG ulReadLength = stack->Parameters.Read.Length; 8 // ULONG ulReadOffset = (ULONG)stack->Parameters.Read.ByteOffset.QuadPart; 9 PVOID user_address = pIrp->UserBuffer; 10 DbgPrint("User address:0x%016llX", user_address); 11 __try{ 12 ProbeForWrite(user_address, ulReadLength, 4); 13 memset(user_address, 0x67, ulReadLength); 14 } 15 __except(EXCEPTION_EXECUTE_HANDLER){ 16 DbgPrint("Catch exception!\n"); 17 status = STATUS_UNSUCCESSFUL; 18 } 19 pIrp->IoStatus.Status = status; 20 pIrp->IoStatus.Information = ulReadLength; 21 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 22 return status; 23 }
显示应用层缓冲区地址:
读取到用户态地址:
- IO设备控制操作
DeviceIoControl内部会使操作系统创建一个IRP_MJ_DEVICE_CONTROL类型的IRP,然后操作系统会将这个IRP转发到派遣函数中。程序员可以用DeviceIoControl定义除了读写之外的其它操作,它可以让应用程序和驱动程序进行通信。
缓冲区内存模式IOCTL,示例如下:
1 NTSTATUS HelloDDKDeviceIoControlTest(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 2 UNREFERENCED_PARAMETER(pDevObj); 3 DbgPrint("Enter DeviceIoControl dispatch!\n"); 4 NTSTATUS status = STATUS_SUCCESS; 5 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); 6 //这三个都是从上层传下来的 7 ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength; 8 ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength; 9 ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; 10 ULONG info = 0; 11 switch (code){ 12 case IOCTL_TEST:{ 13 //DeviceIoControl中的第三个参数指向的数据被复制到底层SystemBuffer所指位置 14 UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer; 15 for (ULONG i = 0; i < cbin; i++){ 16 DbgPrint("%c\n", InputBuffer[i]); 17 } 18 UCHAR*OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer; 19 memset(OutputBuffer, 0x68, cbout-1); 20 OutputBuffer[cbout-1] = 0; 21 info = cbout; 22 break; 23 } 24 default: 25 status = STATUS_INVALID_VARIANT; 26 27 } 28 pIrp->IoStatus.Status = status; 29 pIrp->IoStatus.Information = info; 30 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 31 return status; 32 }
应用层代码如下:
并没有得到预期的数据:
利用windbg附加到这个进程进行调试:
加载user程序的pdb文件:
非侵入式切换进程空间:
我们发现这里的lpInBuffer地址不正确。于是我突然想到这里是64位系统,所以不应该在R3中将lpInBuffer定义为32位指针,而应该定义64位指针。(注意,CHAR*和WCHAR*都是32位指针,只是他们指向的数据一个是多字节、一个是宽字符型)修改如下:
输出结果正确:
如果改为如下代码:
则输出结果也正确:
这里lpInBuffer表示一个数组,&lpInBuffer就是取这个数组的地址
但是如果改为这样,也会有输出错误:
输出结果如下:
我们尝试在HelloDDKDeviceIoControlTest派遣函数中下断点,发现这时断不下来,说明这样修改以后,根本就没有进入到这个派遣函数中。
这里发现的现象就是,就DeviceIoControl而言,如果缓冲区指针有问题,并不会报错,而是不进入到DeviceIoControl对应的派遣函数中。具体为什么、怎么实现的还没搞懂。
我之前还尝试跟踪调试了一下DeviceIoControl,但是没什么特别的发现,这里把简要步骤写下:
push了所有的参数
输入的应用层缓冲区地址都是32位的:
比较控制码:
传递上层的八个同样的参数:
最底层这个函数DbgPrint相关数据,这里边都是wow64cpu中的函数调用:
直接内存模式IOCTL,示例代码如下:
1 NTSTATUS HelloDDKDeviceIoControl_Direct(PDEVICE_OBJECT pDevObj, PIRP pIrp){
2 UNREFERENCED_PARAMETER(pDevObj);
3 NTSTATUS status = STATUS_SUCCESS;
4 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
5 ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;
6 ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;
7 ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
8 ULONG info = 0;
9 switch (code){
10 case IOCTL_TEST2:
11 {
12 UCHAR*InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
13 for (ULONG i = 0; i < cbin; i++){
14 DbgPrint("%c\n", InputBuffer[i]);
15 }
16 UCHAR*OutputBuffer = (UCHAR*)MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
17 memset(OutputBuffer, 0x68, cbout - 1);
18
19 OutputBuffer[cbout - 1] = 0;
20 DbgPrint("Dircet Kernel address:%llx\n", OutputBuffer);
21 DbgPrint("Dircet Kernel content:%s\n", OutputBuffer);
22 info = cbout;
23 break;
24 }
25 default:
26 status = STATUS_INVALID_VARIANT;
27 }
28 pIrp->IoStatus.Status = status;
29 pIrp->IoStatus.Information = info;
30 IoCompleteRequest(pIrp, IO_NO_INCREMENT);
31 return status;
32 }
输出结果:
如果不对OutputBuffer进行修改,这输出的结果是:
说明确实可以从一个固定的某个地址上取到应用层映射的数据。
还有一种就是其他内存模式IOCTL,和之前讲过的内容类似,这里不进行赘述。