轉 Windows串口過濾驅動程序的開發

在Windows系統上與安全軟件相關的驅動開發過程當中,「過濾(filter)」是極其重要的一個概念。過濾是在不影響上層和下層接口的狀況下,在Windows系統內核中加入新的層,從而不須要修改上層的軟件和下層的真實驅動,就加入了新的功能。算法

 

過濾的概念和基礎編程

 

1.設備綁定的內核API之一數組

進行過濾的最主要方法是對一個設備對象(Device Object)進行綁定。經過編程生成一個虛擬設備,並「綁定」(Attach)在一個真實的設備上。一旦綁定,則原本操做系統發送給真實設備的請求,就會首先發送的這個虛擬設備。安全

在WDK中,有多個內核API能實現綁定功能。下面是其中一個函數的原型:數據結構

[cpp]  view plain  copy
 
  1. NTSTATUS   
  2.    IoAttachDevice(  
  3.            IN PDEVICE_OBJECT SourceDevice,  
  4.            IN PUNICODE_STRING TargetDevice,  
  5.            OUT PDEVICE_OBJECT *AttachedDevice  
  6.           );  


IoAttachDevice的參數以下:函數

SouceDevice是調用者生成的用來過濾的虛擬設備;而TargetDevice是要被綁定的目標設備。請注意這裏的TargetDevice並非一個PDEVICE_OBJECT,而是設備的名字。測試

若是一個設備被其餘設備綁定了,他們在一塊兒的一組設備,被稱爲設備棧。實際上,IoAttachDevice總會綁定設備棧最上層的那個設備。spa

AttachedDevice是一個用來返回的指針的指針。綁定成功後,被綁定的設備的指針返回到這個地址。操作系統

下面這個例子綁定串口1。之因此這裏綁定很方便,是由於在Windows中,串口設備有固定的名字。第一個串口名字爲「\Device\Serial0」,第二個爲「\Device\Serial1」,以此類推。請注意實際編程時C語言中的「\」要寫成「\\」。.net

[cpp]  view plain  copy
 
  1. UNICODE_STRING com_name = RLT_CONSTANT_STRING(L"\\Device\\Serial0");  
  2. NTSTATUS status = IoAttachDevice(  
  3.     com_filter_device,   //生成的過濾設備  
  4.     &com_device_name,    //串口的設備名  
  5.     &attached_device     //被綁定的設備指針返回到這裏  
  6.     );  


2.綁定設備的內核API之二

並非全部設備都有設備名字,因此依靠IoAttachDevice沒法綁定沒有名字的設備。另外還有兩個API:一個是IoAttachDeviceToDeviceStack,另外一個是IoAttachDeivceToDeviceStackSafe。這兩個函數功能同樣,都是根據設備對象的指針(而不是名字)進行綁定;區別是後者更加安全,並且只有在Windows2000SP4和Windows XP以上的系統中才有。

[cpp]  view plain  copy
 
  1. NTSTATUS  
  2.   IoAttachDeviceToDeviceStackSafe(   
  3.                                       IN PDEVICE_OBJECT SourceDevice,     //過濾設備  
  4.                   IN PDEVICE_OBJECT TargetDevice,     //要被綁定的設備  
  5.                   IN OUT PDEVICE_OBJECT *AttachedToDeviceObject  //返回最終綁定的設備  
  6.                   );  


和第一個API相似,只是TargetDevice換成了一個指針。另外,AttachedToDeviceObject一樣也是返回最終被綁定的設備,實際上也就是以前設備棧上最頂端的那個設備。

 

3.生成過濾設備並綁定

在綁定一個設備以前,先要知道如何生成一個用於過濾的過濾設備。函數IoCreateDevice被用於生成設備:

[cpp]  view plain  copy
 
  1. NTSTATUS  
  2.   IoCreateDevice(  
  3.                    IN PDRIVER_OBJECT DriverObject,  
  4.          IN ULONG DeviceExtensionSize,  
  5.          IN PUNICODE_STRING DeviceName,  
  6.          IN DEVICE_TYPE DeviceType,   
  7.          IN ULONG DeviceCharacteristics,  
  8.          IN BOOLEAN Exclusive,   
  9.          OUT PDEVICE_OBJECT *DeviceObject  
  10.          );  

DriverObject:輸入參數,每一個驅動程序中會有惟一的驅動對象與之對應,但每一個驅動對象會有若干個設備對象。DriverObject指向的就是驅動對象指針。

DeviceExtensionSize:輸入參數,指定設備擴展的大小,I/O管理器會根據這個大小,在內存中建立設備擴展,並與驅動對象關聯。

DeviceName:輸入參數,設置設備對象的名字。一個規則是,過濾設備通常不須要設備名,傳入NULL便可

DeviceType:輸入參數,設備類型,保持和被綁定的設備類型一致便可。

DeviceCharacterristics:輸入參數,設備對象的特徵。

Exclusive:輸入參數,設置設備對象是否爲內核模式下使用,通常設置爲TRUE。

DeviceObject:輸出參數,I/O管理器負責建立這個設備對象,並返回設備對象的地址。

但值得注意的是,在綁定一個設備以前,應該把這個設備對象的多個子域設置成和要綁定的目標對象一致,包括標誌和特徵。下面是一個示例函數,這個函數能夠生成一個設備,並綁定到另外一個設備上。

[cpp]  view plain  copy
 
  1. NTSTATUS  
  2.  ccpAttachDevice(  
  3.      PDRIVER_OBJECT driver,  
  4.      PDEVICE_OBJECT oldobj,  
  5.      PDEVICE_OBJECT *fltobj,  
  6.      PDEVICE_OBJECT *next)  
  7. {  
  8.     NTSTATUS status;  
  9.     PDEVICE_OBJECT topdev = NULL;  
  10.   
  11.     //生成設備而後綁定  
  12.     status = IoCreateDevice(driver,  
  13.                             0,  
  14.                   NULL,  
  15.                           oldobj->DeviceType,  
  16.                   0,  
  17.                   FALSE,  
  18.                   fltobj);  
  19.     if (status != STATUS_SUCCESS)  
  20.     {  
  21.         return status;  
  22.     }  
  23.   
  24.     //拷貝重要標誌位  
  25.     if (oldobj->Flags & DO_BUFFERED_IO)  
  26.     {  
  27.         (*fltobj)->Flags |= DO_BUFFERED_IO;  
  28.     }  
  29.     if(oldobj->Flags & DO_DIRECT_IO)  
  30.     {  
  31.         (*fltobj)->Flags |= DO_DIRECT_IO;  
  32.     }  
  33.     if (oldobj->Characteristics & FILE_DEVICE_SECURE_OPEN)  
  34.     {  
  35.         (*fltobj)->Characteristics |= FILE_DEVICE_SECURE_OPEN;  
  36.     }  
  37.     (*fltobj)->Flags |= DO_POWER_PAGABLE;  
  38.     //將一個設備綁定到另外一個設備  
  39.     topdev = IoAttachDeviceToDeviceStack(*fltobj,oldobj);  
  40.     if (topdev == NULL)  
  41.     {  
  42.         //若是綁定失敗了,銷燬設備,返回錯誤  
  43.         IoDeleteDevice(*fltobj);  
  44.         *float = NULL;  
  45.         status = STATUS_UNSUCCESSFUL;  
  46.         return status;  
  47.     }  
  48.     *next = topdev;  
  49.   
  50.     //設置這個設備已經啓動  
  51.     (*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING;  
  52.     return STATUS_SUCCESS;  
  53. }  


 

4.從名字得到設備對象

在知道一個設備名字的狀況下,使用IoGetDeviceObjectPointer能夠得到這個設備對象的指針。這個函數的原型以下:

[cpp]  view plain  copy
 
  1. NTSTATUS   
  2.   IoGetDeviceObjectPointer(   
  3.                    IN PUNICODE_STRING ObjectName,  
  4.          IN ACCESS_MASK DesiredAccess,  
  5.          OUT PFILE_OBJECT *FileObject,  
  6.          OUT PDEVICE_OBJECT *DeviceObject   
  7.         );  


其中ObjectName就是設備名字。

DesireAccess是指望訪問的權限。實際使用時不要顧慮那麼多,直接填寫FILE_ACCESS_ALL便可。

FileObject是一個返回參數,即得到設備對象的同時會獲得一個文件對象(File Object)。就打開串口這件事而言,這個文件對象沒有什麼用處。但必須注意:在使用這個函數以後必須把這個文件對象「解除引用」,不然會引發內存泄露。

 

要獲得的設備對象就返回在參數DeviceObject中了。

[cpp]  view plain  copy
 
  1. //打開一個端口設備  
  2. PDEVICE_OBJECT ccpOpenCom(ULONG id,NTSTATUS *status)  
  3. {  
  4.     //外面輸入的是串口的id,這裏會改寫成字符串的形式  
  5.     UNICODE_STRING name_str;  
  6.     static WCHAR name[32] = {0};  
  7.     PFILE_OBJECT fileObj = NULL;  
  8.     PDEVICE_OBJECT devObj = NULL;  
  9.   
  10.     //根據id轉換成串口的名字  
  11.     memset(name,0,sizeof(WCHAR)*32);  
  12.     RtlStringCchPrintfW(  
  13.                   name,32,  
  14.                   L"\\Device\\Serial%d",id);  
  15.     RtlInitUnicodeString(&name_str,name);  
  16.   
  17.     //打開設備  
  18.     *status = IoGetDeviceObjectPointer(  
  19.                         &name_str,  
  20.                    FILE_ALL_ACCESS,  
  21.                    &fileObj,&devObj);  
  22.   
  23.     //若是打開成功了,記得必定要把文件對象解除引用  
  24.     if (*status == NT_SUCCESS)  
  25.     {  
  26.         ObDereferenceObject(fileObj);  
  27.     }  
  28.   
  29.     //返回設備對象  
  30.     return devObj;  
  31. }  

 

 

5.綁定全部端口

下面是一個簡單的函數,實現了綁定本機上全部串口的功能。這個函數用到了前面提供的ccpOpenCom和ccpAttachDevice這兩個函數

[cpp]  view plain  copy
 
  1. //計算機上最多隻有32個串口,這裏是筆者的假定  
  2. #define CCP_MAX_COM_IO 32  
  3. //保存全部過濾設備指針  
  4. static PDEVICE_OBJECT s_fltObj[CCP_MAX_COM_IO] = {0};  
  5. //保存全部真實設備指針  
  6. static PDEVICE_OBJECT s_nextObj[CCP_MAX_COM_IO] = {0};  
  7.   
  8. //這個函數綁定全部的串口  
  9. void ccpAttachAllComs(PDRIVER_OBJECT driver)  
  10. {  
  11.     ULONG i;  
  12.     PDEVICE_OBJECT com_ob;  
  13.     NTSTATUS status;  
  14.     for(i = 0 ; i < CCP_MAX_COM_IO ; i++)  
  15.     {  
  16.         //得到object引用  
  17.         com_ob = ccpOpenCom(i,&status);  
  18.         if (com_ob == NULL)  
  19.         {  
  20.             continue;  
  21.         }  
  22.         //在這裏綁定,並無論綁定是否成功  
  23.         ccpAttachDevice(driver,com_ob,&s_fltObj[i],&s_nextObj[i]);  
  24.     }  
  25. }  


不必關心這個綁定是否成功,就算失敗,看下一個s_fltObj便可。這個數組中不爲NULL的成員表示已經綁定,爲NULL的成員則是沒有綁定成功或者綁定失敗的。這個函數須要一個DRIVER_OBJECT的指針。

 

得到實際數據

 

1.請求的區分

Windows的內核開發者們肯定了不少數據結構,例如:驅動對象(DriverObject),設備對象(DeviceObject),文件對象(FileObject)等,須要瞭解的是:

(1)每一個驅動程序只有一個驅動對象

(2)每一個驅動程序能夠生成若干個設備對象,這些設備對象從屬於一個驅動對象

(3)若干個設備(他們能夠屬於不一樣的驅動)依次綁定造成一個設備棧,老是最頂端的設備先接收到請求。

(4)IRP是上層設備之間傳遞請求的常見數據結構,但不是惟一的數據結構

 

串口設備接收到的都是IRP,所以只要對全部IRP進行過濾,就能夠獲得串口流過的數據。請求能夠經過IRP的主功能號區分。例以下面代碼:

[cpp]  view plain  copy
 
  1. PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);  
  2. if (irpsp->MajorFunction == IRP_MJ_WRITE)  
  3. {  
  4.     //若是是寫....  
  5. }  
  6. else if (irpsp->MajorFunction == IRP_MJ_READ)  
  7. {  
  8.     //若是是讀....  
  9. }  


 

 

2.請求的結局

對請求的過濾,最終的結局有3種:

(1)請求被經過了,過濾不作任何事情,或者簡單的獲取請求的一些信息。可是請求自己不受干擾。

(2)請求直接被否決了,下層驅動根本收不到這個請求。

(3)過濾完成了這個請求。

 

串口過濾要捕獲兩種數據:一種是發送出的數據(也就是寫請求的數據),另外一種是接收的數據(也就是讀請求的數據)。爲了簡單起見,咱們只捕獲發送出去的請求。這樣,只須要採起第一種處理方法便可。

這種處理最爲簡單。首先調用IoSkipCurrentIrpStackLocation跳到當前棧空間;而後調用IoCallDriver把這個請求發送給真實的設備。請注意:由於真實的設備已經被過濾設備綁定,因此首先接收到IRP的是過濾設備對象。代碼以下:

[cpp]  view plain  copy
 
  1. //跳到當前棧空間  
  2. IoSkipCurrentIrpStackLocation(irp);  
  3. status = IoCallDriver(s_nextObj[i],irp);  


3.寫請求的數據

那麼,一個寫請求(也就是串口一次發送的數據)保存在哪呢?IRP的結構中有三個地方能夠描述緩衝區:一個是irp->MDLAddress,一個是irp->UserBuffer,一個是irp->AssociatedIrp.SystemBuffer.三種結構的具體區別參見(Windows驅動技術開發詳解__派遣函數)。

回到串口的問題,那麼串口的寫請求究竟是用哪一種方式呢?咱們不知道,可是能夠用下面方法得到:

[cpp]  view plain  copy
 
  1. PBYTE buffer = NULL;  
  2. if (IRP->MdlAddress != NULL)  
  3.    buffer = (PBYTE)MmGetSystemAddressForMdlSafe(IRP->MdlAddress);  
  4. else  
  5.    buffer = (PBYTE)IRP->UserBuffer;  
  6. if (buffer == NULL)  
  7.    buffer = (PBYTE)IRP->AssociatedIrp.SystemBuffer;  


 

完整的代碼

 

1.完整的分發函數(派遣函數)

[cpp]  view plain  copy
 
  1. NTSTATUS ccpDispatch(PDEVICE_OBJECT device,PIRP irp)  
  2. {  
  3.     PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);  
  4.     NTSTATUS status;  
  5.     ULONG i,j;  
  6.   
  7.     //首先得知道發送給哪一個設備,設備一共最多CCP_MAX_COM_ID個  
  8.     //是前面的代碼保存好的你都在s_fltObj中  
  9.     for (i = 0 ; i < CCP_MAX_COM_ID ; i++)  
  10.     {  
  11.         if (s_fltObj[i] == device)  
  12.         {  
  13.             //全部電源操做所有直接放過  
  14.             if (irpsp->MajorFunction == IRP_MJ_POWER)  
  15.             {  
  16.                 //直接發送,而後返回說已被處理  
  17.                 PoStartNextPowerIrp(irp);  
  18.                 IoSkipCurrentIrpStackLocation(irp);  
  19.                 return PoCallDriver(s_nextObj[i],irp);  
  20.             }  
  21.   
  22.         }  
  23.         //此外咱們只過濾寫請求  
  24.         if (irpsp->MajorFunction == IRP_MJ_WRITE)  
  25.         {  
  26.             //若是是寫,先得到長度  
  27.             ULONG len = irpsp->Parameters.Write.Length;  
  28.             //而後得到緩衝區  
  29.             PUCHAR buf = NULL;  
  30.             if(irp->MdlAddress != NULL)  
  31.                 buf = (PUCHAR)  
  32.                 MmGetSystemAddressForMdlSafe(irp->MdlAddress,NormalPagePriority);  
  33.             else  
  34.                 buf = (PUCHAR)irp->UserBuffer;  
  35.             if(buf == NULL)  
  36.                 buf = (PUCHAR)irp->AssociatedIrp.SystemBuffer;  
  37.   
  38.             //打印內容  
  39.             for (j = 0 ; j < len ; j++)  
  40.             {  
  41.                 DbgPrint("comcap:Send Data:%2x\r\n",buf[j]);  
  42.             }  
  43.         }  
  44.         //這些請求直接下發便可  
  45.         IoSkipCurrentIrpStackLocation(irp);  
  46.         return IoCallDriver(s_nextObj[i],irp);  
  47.   
  48.     }  
  49.     //若是根本就不在被綁定的設備中,那是有問題的,直接返回參數錯誤  
  50.     irp->IoStatus.Information = 0;  
  51.     irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;  
  52.     IoCompleteRequest(irp,IO_NO_INCREMENT);  
  53.     return STATUS_SUCCESS;  
  54.   
  55. }  


2.動態卸載

前面說了如何綁定,可是沒說如何解除綁定。若是要把這個模塊作成能夠動態卸載的模塊,則必須提供一個卸載函數。咱們應該在卸載函數中完成解除綁定的功能,不然,一旦卸載必定會藍屏。

[cpp]  view plain  copy
 
  1. #define  DELAY_ONE_MICROSECOND  (-10)  
  2. #define  DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)  
  3. #define  DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)  
  4.   
  5. VOID ccpUnload(PDRIVER_OBJECT drv)  
  6. {  
  7.     ULONG i;  
  8.     LARGE_INTEGER interval;  
  9.   
  10.     //首先解除綁定  
  11.     for (i = 0 ; i < CCP_MAX_COM_ID ; i++)  
  12.     {  
  13.         if(s_nextObj[i] != NULL)  
  14.             IoDeleteDevice(s_nextObj[i]);  
  15.     }  
  16.   
  17.     //睡眠5秒,等待全部IRP處理結束  
  18.     interval.QuadPart = (5*1000 *DELAY_ONE_MICROSECOND);  
  19.     KeDelayExecutionThread(KernelMode,FALSE,&interval);  
  20.   
  21.     //刪除這些設備  
  22.     for (i = 0 ; i < CCP_MAX_COM_ID ; i++)  
  23.     {  
  24.         if(s_fltObj[i] != NULL)  
  25.             IoDeleteDevice(s_fltObj[i]);  
  26.     }  
  27. }  


DriverEntry函數代碼:

[cpp]  view plain  copy
 
  1. NTSTATUS DriverEntry(  
  2.     IN OUT PDRIVER_OBJECT   DriverObject,  
  3.     IN PUNICODE_STRING      RegistryPath  
  4.     )  
  5. {  
  6.     DbgPrint("Enter Driver\r\n");  
  7.     size_t i;  
  8.     //全部分發函數都設置成同樣的  
  9.     for (i = 0 ; i < IRP_MJ_MAXIMUM_FUNCTION ; i++)  
  10.     {  
  11.         DriverObject->MajorFunction[i] = ccpDispatch;  
  12.     }  
  13.     //支持動態卸載  
  14.     DriverObject->DriverUnload = ccpUnload;  
  15.   
  16.     //綁定全部的串口  
  17.     ccpAttachAllComs(DriverObject);  
  18.   
  19.     return STATUS_SUCCESS;  
  20. }  


測試效果:

相關文章
相關標籤/搜索