在Windows系統上與安全軟件相關的驅動開發過程中,“過濾(filter)”是極其重要的一個概念。過濾是在不影響上層和下層接口的情況下,在Windows系統內核中加入新的層,從而不需要修改上層的軟件和下層的真實驅動,就加入了新的功能。
過濾的概念和基礎
1.設備綁定的內核API之一
進行過濾的最主要方法是對一個設備對象(Device Object)進行綁定。通過編程生成一個虛擬設備,並“綁定”(Attach)在一個真實的設備上。一旦綁定,則本來操作系統發送給真實設備的請求,就會首先發送的這個虛擬設備。
在WDK中,有多個內核API能實現綁定功能。下面是其中一個函數的原型:
NTSTATUS
IoAttachDevice(
IN PDEVICE_OBJECT SourceDevice,
IN PUNICODE_STRING TargetDevice,
OUT PDEVICE_OBJECT *AttachedDevice
);
IoAttachDevice的參數如下:
SouceDevice是調用者生成的用來過濾的虛擬設備;而TargetDevice是要被綁定的目標設備。請注意這裏的TargetDevice並不是一個PDEVICE_OBJECT,而是設備的名字。
如果一個設備被其他設備綁定了,他們在一起的一組設備,被稱爲設備棧。實際上,IoAttachDevice總會綁定設備棧最上層的那個設備。
AttachedDevice是一個用來返回的指針的指針。綁定成功後,被綁定的設備的指針返回到這個地址。
下面這個例子綁定串口1。之所以這裏綁定很方便,是因爲在Windows中,串口設備有固定的名字。第一個串口名字爲“\Device\Serial0”,第二個爲“\Device\Serial1”,以此類推。請注意實際編程時C語言中的“\”要寫成“\\”。
UNICODE_STRING com_name = RLT_CONSTANT_STRING(L"\\Device\\Serial0");
NTSTATUS status = IoAttachDevice(
com_filter_device, //生成的過濾設備
&com_device_name, //串口的設備名
&attached_device //被綁定的設備指針返回到這裏
);
2.綁定設備的內核API之二
並不是所有設備都有設備名字,所以依靠IoAttachDevice無法綁定沒有名字的設備。另外還有兩個API:一個是IoAttachDeviceToDeviceStack,另一個是IoAttachDeivceToDeviceStackSafe。這兩個函數功能一樣,都是根據設備對象的指針(而不是名字)進行綁定;區別是後者更加安全,而且只有在Windows2000SP4和Windows XP以上的系統中才有。
NTSTATUS
IoAttachDeviceToDeviceStackSafe(
IN PDEVICE_OBJECT SourceDevice, //過濾設備
IN PDEVICE_OBJECT TargetDevice, //要被綁定的設備
IN OUT PDEVICE_OBJECT *AttachedToDeviceObject //返回最終綁定的設備
);
和第一個API類似,只是TargetDevice換成了一個指針。另外,AttachedToDeviceObject同樣也是返回最終被綁定的設備,實際上也就是之前設備棧上最頂端的那個設備。
3.生成過濾設備並綁定
在綁定一個設備之前,先要知道如何生成一個用於過濾的過濾設備。函數IoCreateDevice被用於生成設備:
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
DriverObject:輸入參數,每個驅動程序中會有唯一的驅動對象與之對應,但每個驅動對象會有若干個設備對象。DriverObject指向的就是驅動對象指針。
DeviceExtensionSize:輸入參數,指定設備擴展的大小,I/O管理器會根據這個大小,在內存中創建設備擴展,並與驅動對象關聯。
DeviceName:輸入參數,設置設備對象的名字。一個規則是,過濾設備一般不需要設備名,傳入NULL即可
DeviceType:輸入參數,設備類型,保持和被綁定的設備類型一致即可。
DeviceCharacterristics:輸入參數,設備對象的特徵。
Exclusive:輸入參數,設置設備對象是否爲內核模式下使用,一般設置爲TRUE。
DeviceObject:輸出參數,I/O管理器負責創建這個設備對象,並返回設備對象的地址。
但值得注意的是,在綁定一個設備之前,應該把這個設備對象的多個子域設置成和要綁定的目標對象一致,包括標誌和特徵。下面是一個示例函數,這個函數可以生成一個設備,並綁定到另一個設備上。
NTSTATUS
ccpAttachDevice(
PDRIVER_OBJECT driver,
PDEVICE_OBJECT oldobj,
PDEVICE_OBJECT *fltobj,
PDEVICE_OBJECT *next)
{
NTSTATUS status;
PDEVICE_OBJECT topdev = NULL;
//生成設備然後綁定
status = IoCreateDevice(driver,
0,
NULL,
oldobj->DeviceType,
0,
FALSE,
fltobj);
if (status != STATUS_SUCCESS)
{
return status;
}
//拷貝重要標誌位
if (oldobj->Flags & DO_BUFFERED_IO)
{
(*fltobj)->Flags |= DO_BUFFERED_IO;
}
if(oldobj->Flags & DO_DIRECT_IO)
{
(*fltobj)->Flags |= DO_DIRECT_IO;
}
if (oldobj->Characteristics & FILE_DEVICE_SECURE_OPEN)
{
(*fltobj)->Characteristics |= FILE_DEVICE_SECURE_OPEN;
}
(*fltobj)->Flags |= DO_POWER_PAGABLE;
//將一個設備綁定到另一個設備
topdev = IoAttachDeviceToDeviceStack(*fltobj,oldobj);
if (topdev == NULL)
{
//如果綁定失敗了,銷燬設備,返回錯誤
IoDeleteDevice(*fltobj);
*float = NULL;
status = STATUS_UNSUCCESSFUL;
return status;
}
*next = topdev;
//設置這個設備已經啓動
(*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}
4.從名字獲得設備對象
在知道一個設備名字的情況下,使用IoGetDeviceObjectPointer可以獲得這個設備對象的指針。這個函數的原型如下:
NTSTATUS
IoGetDeviceObjectPointer(
IN PUNICODE_STRING ObjectName,
IN ACCESS_MASK DesiredAccess,
OUT PFILE_OBJECT *FileObject,
OUT PDEVICE_OBJECT *DeviceObject
);
其中ObjectName就是設備名字。
DesireAccess是期望訪問的權限。實際使用時不要顧慮那麼多,直接填寫FILE_ACCESS_ALL即可。
FileObject是一個返回參數,即獲得設備對象的同時會得到一個文件對象(File Object)。就打開串口這件事而言,這個文件對象沒有什麼用處。但必須注意:在使用這個函數之後必須把這個文件對象“解除引用”,否則會引起內存泄露。
要得到的設備對象就返回在參數DeviceObject中了。
//打開一個端口設備
PDEVICE_OBJECT ccpOpenCom(ULONG id,NTSTATUS *status)
{
//外面輸入的是串口的id,這裏會改寫成字符串的形式
UNICODE_STRING name_str;
static WCHAR name[32] = {0};
PFILE_OBJECT fileObj = NULL;
PDEVICE_OBJECT devObj = NULL;
//根據id轉換成串口的名字
memset(name,0,sizeof(WCHAR)*32);
RtlStringCchPrintfW(
name,32,
L"\\Device\\Serial%d",id);
RtlInitUnicodeString(&name_str,name);
//打開設備
*status = IoGetDeviceObjectPointer(
&name_str,
FILE_ALL_ACCESS,
&fileObj,&devObj);
//如果打開成功了,記得一定要把文件對象解除引用
if (*status == NT_SUCCESS)
{
ObDereferenceObject(fileObj);
}
//返回設備對象
return devObj;
}
5.綁定所有端口
下面是一個簡單的函數,實現了綁定本機上所有串口的功能。這個函數用到了前面提供的ccpOpenCom和ccpAttachDevice這兩個函數
//計算機上最多隻有32個串口,這裏是筆者的假定
#define CCP_MAX_COM_IO 32
//保存所有過濾設備指針
static PDEVICE_OBJECT s_fltObj[CCP_MAX_COM_IO] = {0};
//保存所有真實設備指針
static PDEVICE_OBJECT s_nextObj[CCP_MAX_COM_IO] = {0};
//這個函數綁定所有的串口
void ccpAttachAllComs(PDRIVER_OBJECT driver)
{
ULONG i;
PDEVICE_OBJECT com_ob;
NTSTATUS status;
for(i = 0 ; i < CCP_MAX_COM_IO ; i++)
{
//獲得object引用
com_ob = ccpOpenCom(i,&status);
if (com_ob == NULL)
{
continue;
}
//在這裏綁定,並不管綁定是否成功
ccpAttachDevice(driver,com_ob,&s_fltObj[i],&s_nextObj[i]);
}
}
沒必要關心這個綁定是否成功,就算失敗,看下一個s_fltObj即可。這個數組中不爲NULL的成員表示已經綁定,爲NULL的成員則是沒有綁定成功或者綁定失敗的。這個函數需要一個DRIVER_OBJECT的指針。
獲得實際數據
1.請求的區分
Windows的內核開發者們確定了很多數據結構,例如:驅動對象(DriverObject),設備對象(DeviceObject),文件對象(FileObject)等,需要了解的是:
(1)每個驅動程序只有一個驅動對象
(2)每個驅動程序可以生成若干個設備對象,這些設備對象從屬於一個驅動對象
(3)若干個設備(他們可以屬於不同的驅動)依次綁定形成一個設備棧,總是最頂端的設備先接收到請求。
(4)IRP是上層設備之間傳遞請求的常見數據結構,但不是唯一的數據結構
串口設備接收到的都是IRP,因此只要對所有IRP進行過濾,就可以得到串口流過的數據。請求可以通過IRP的主功能號區分。例如下面代碼:
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
if (irpsp->MajorFunction == IRP_MJ_WRITE)
{
//如果是寫....
}
else if (irpsp->MajorFunction == IRP_MJ_READ)
{
//如果是讀....
}
2.請求的結局
對請求的過濾,最終的結局有3種:
(1)請求被通過了,過濾不做任何事情,或者簡單的獲取請求的一些信息。但是請求本身不受干擾。
(2)請求直接被否決了,下層驅動根本收不到這個請求。
(3)過濾完成了這個請求。
串口過濾要捕獲兩種數據:一種是發送出的數據(也就是寫請求的數據),另一種是接收的數據(也就是讀請求的數據)。爲了簡單起見,我們只捕獲發送出去的請求。這樣,只需要採取第一種處理方法即可。
這種處理最爲簡單。首先調用IoSkipCurrentIrpStackLocation跳到當前棧空間;然後調用IoCallDriver把這個請求發送給真實的設備。請注意:因爲真實的設備已經被過濾設備綁定,所以首先接收到IRP的是過濾設備對象。代碼如下:
//跳到當前棧空間
IoSkipCurrentIrpStackLocation(irp);
status = IoCallDriver(s_nextObj[i],irp);
3.寫請求的數據
那麼,一個寫請求(也就是串口一次發送的數據)保存在哪呢?IRP的結構中有三個地方可以描述緩衝區:一個是irp->MDLAddress,一個是irp->UserBuffer,一個是irp->AssociatedIrp.SystemBuffer.三種結構的具體區別參見(Windows驅動技術開發詳解__派遣函數)。
回到串口的問題,那麼串口的寫請求到底是用哪種方式呢?我們不知道,但是可以用下面方法獲得:
PBYTE buffer = NULL;
if (IRP->MdlAddress != NULL)
buffer = (PBYTE)MmGetSystemAddressForMdlSafe(IRP->MdlAddress);
else
buffer = (PBYTE)IRP->UserBuffer;
if (buffer == NULL)
buffer = (PBYTE)IRP->AssociatedIrp.SystemBuffer;
完整的代碼
1.完整的分發函數(派遣函數)
NTSTATUS ccpDispatch(PDEVICE_OBJECT device,PIRP irp)
{
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
NTSTATUS status;
ULONG i,j;
//首先得知道發送給哪個設備,設備一共最多CCP_MAX_COM_ID個
//是前面的代碼保存好的你都在s_fltObj中
for (i = 0 ; i < CCP_MAX_COM_ID ; i++)
{
if (s_fltObj[i] == device)
{
//所有電源操作全部直接放過
if (irpsp->MajorFunction == IRP_MJ_POWER)
{
//直接發送,然後返回說已被處理
PoStartNextPowerIrp(irp);
IoSkipCurrentIrpStackLocation(irp);
return PoCallDriver(s_nextObj[i],irp);
}
}
//此外我們只過濾寫請求
if (irpsp->MajorFunction == IRP_MJ_WRITE)
{
//如果是寫,先獲得長度
ULONG len = irpsp->Parameters.Write.Length;
//然後獲得緩衝區
PUCHAR buf = NULL;
if(irp->MdlAddress != NULL)
buf = (PUCHAR)
MmGetSystemAddressForMdlSafe(irp->MdlAddress,NormalPagePriority);
else
buf = (PUCHAR)irp->UserBuffer;
if(buf == NULL)
buf = (PUCHAR)irp->AssociatedIrp.SystemBuffer;
//打印內容
for (j = 0 ; j < len ; j++)
{
DbgPrint("comcap:Send Data:%2x\r\n",buf[j]);
}
}
//這些請求直接下發即可
IoSkipCurrentIrpStackLocation(irp);
return IoCallDriver(s_nextObj[i],irp);
}
//如果根本就不在被綁定的設備中,那是有問題的,直接返回參數錯誤
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
IoCompleteRequest(irp,IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
2.動態卸載
前面說了如何綁定,但是沒說如何解除綁定。如果要把這個模塊做成可以動態卸載的模塊,則必須提供一個卸載函數。我們應該在卸載函數中完成解除綁定的功能,否則,一旦卸載一定會藍屏。
#define DELAY_ONE_MICROSECOND (-10)
#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
#define DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)
VOID ccpUnload(PDRIVER_OBJECT drv)
{
ULONG i;
LARGE_INTEGER interval;
//首先解除綁定
for (i = 0 ; i < CCP_MAX_COM_ID ; i++)
{
if(s_nextObj[i] != NULL)
IoDeleteDevice(s_nextObj[i]);
}
//睡眠5秒,等待所有IRP處理結束
interval.QuadPart = (5*1000 *DELAY_ONE_MICROSECOND);
KeDelayExecutionThread(KernelMode,FALSE,&interval);
//刪除這些設備
for (i = 0 ; i < CCP_MAX_COM_ID ; i++)
{
if(s_fltObj[i] != NULL)
IoDeleteDevice(s_fltObj[i]);
}
}
DriverEntry函數代碼:
NTSTATUS DriverEntry(
IN OUT PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
DbgPrint("Enter Driver\r\n");
size_t i;
//所有分發函數都設置成一樣的
for (i = 0 ; i < IRP_MJ_MAXIMUM_FUNCTION ; i++)
{
DriverObject->MajorFunction[i] = ccpDispatch;
}
//支持動態卸載
DriverObject->DriverUnload = ccpUnload;
//綁定所有的串口
ccpAttachAllComs(DriverObject);
return STATUS_SUCCESS;
}
測試效果: