《Windows驱动开发技术详解》之读写操作

2023-11-18

  • 缓冲区方式读写操作

设置缓冲区读写方式:

读写操作一般是由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,和之前讲过的内容类似,这里不进行赘述。

  • 缓冲区方式读写操作

设置缓冲区读写方式:

读写操作一般是由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,和之前讲过的内容类似,这里不进行赘述。


FROM: http://www.cnblogs.com/predator-wang/p/5532125.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

《Windows驱动开发技术详解》之读写操作 的相关文章

  • [1181]linux两台服务器之间传输文件和文件夹

    文章目录 scp 1 从服务器复制文件到本地 2 复制文件到本地并重命名 3 从服务器复制文件夹到本地 4 从本地复制文件到服务器 不包括文件夹本身 5 从本地复制文件夹到服务器 包括文件夹本身 rcp 命令使用 wget rsync 在日
  • Vue3父子组件通信,父子传参

    Vue3父子组件通信 父子传参 前言 Vue2的小伙伴应该该经历过转战Vue3过程的中的抓狂 好多地方使用都不太一样 这期就给大家讲一下近期我也在用vue3开发中遇到到的问题父子组件通信 父传子 在父组件中引入son vue子组件 为子组件
  • sql 2008 R2 修改数据库表编辑行200小技巧

    在使用sql server 2008 R2时 有时候要打开一个表 看里面的数据 发现只能编辑前面200行 如下图 如果我的数据库表的数据 超过200 怎么办呢 其实只要修改下配置 就可以了 如下图 点击选项 进入选项界面 如下图 在sql
  • nginx配置主域名跳转www域名并支持ssl

    server listen 80 listen 443 ssl server name xxxx com return 301 https www xxx com request uri server listen 80 server na
  • 视频转码后有色差要如何处理

    目录 视频转码后有色差要如何处理 KEY COLOR STANDARD KEY COLOR RANGE 视频转码后有色差要如何处理 以下是回答 欢迎大家留言讨论补充 1 色差是如何产生的 1 有损压缩产生的质量损失 解决方法为尽可能的提高码
  • 如何用计算机计算log除法,对数计算器_如何使用计算器计算对数

    如何使用计算器计算对数 示例 使用Windows自带的计算器 这理假设要计算的对数是logaN a 32 N 2 1 打开计算器 快捷键WIN R 输入calc 然后回车 怎样使用科学计算器计算对数 计算机上的log都是默认以10为底的对数
  • c语言实现面向对象编程(const * ,* const)

    c语言实现面向对象编程 面向对象思想 封装 继承 多态 代码实现 函数签名及返回值的约定 const 重载 参考了onlyshi的博客代码 orz传送门 参考了嵌入式实践一些代码 这里就不加了 面向对象思想 面向对象编程 OOP 并不是一种
  • 千年虫及UNIX时间

    转自 http hi baidu com dugucloud blog item b903ba803e5192c59123d99d html 千年虫何来 在上个世纪 许多计算机系统只用二进制7 8位数 足够十进制二位数使用 表示年份 比如说
  • ANR触发机制分析

    ANR是一套监控Android应用程序响应是否及时的机制 可以把发生ANR比作是引爆炸弹 那么整个流程包含三部分组成 埋定时炸弹 system server进程启动倒计时 在规定时间内如果目标应用进程没有干完所有的活 则system ser
  • MySQL的基本语法

    Welcome Huihui s Code World 接下来看看由辉辉所写的关于MySQL的相关操作吧 目录 Welcome Huihui s Code World 一 数据库 建立 查看 使用 删除 建库 查看数据库 使用数据库 删除数
  • decrypt()解密和encrypt()加密

    1 解密函数 select dbo decrypt StName from student 2 加密函数 select dbo encrypt StName from student
  • CxImage的编译及简单使用举例

    1 从http sourceforge net projects cximage 下载最新的CxImage 702源码 2 解压缩后 以管理员身份打开CxImageFull vc10 sln工程 在编译之前先将每个工程属性的Characte
  • Vue中props组件和slot标签的区别

    在 Vue 中 props 和 slot 都是组件之间进行通信的机制 它们的作用和应用场景有一些区别 props 是一种组件的数据传递机制 通过在父组件中以属性的形式向子组件传递数据 子组件接收这些数据 并可以进行相应的处理和渲染 prop

随机推荐