Windows內核安全編程__一個簡單的Windows串口過濾驅動程序的開發

 在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;
}


測試效果:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章