- 緩沖區方式讀寫操作
設置緩沖區讀寫方式:
讀寫操作一般是由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,和之前講過的內容類似,這里不進行贅述。