Windows內核安全編程__鍵盤過濾之內核級Hook(一)

 

Hook分發函數

前一篇文章講述了進行鍵盤過濾,截取用戶輸入的方法。本篇文章開始更加深入地討論鍵盤的過濾與反過濾對抗。無論是過濾還是飯過濾,原理都是過濾,取勝的關鍵在於誰第一個得到信息。

一種方發是Hook分發函數,即將鍵盤驅動的分發函數替換成自己的函數用來達到過濾的目的。

1.獲得類驅動對象

首先要獲得鍵盤類驅動對象,才能去替換下面的分發函數。這個操作較爲簡單,因爲這個驅動的名字是“\\Device\\Kbdclass”,所以可以直接用函數ObReferenceObjectByName來獲取。

代碼如下:

//驅動的名字
#define  KBD_DRIVER_NAME L"\\Driver\\Kdbclass"
//當我們求得驅動對象指針時,將其放到這裏
PDRIVER_OBJECT KdbDriverObject;
UNICODE_STRING uniNtNameString;

//初始化驅動的名字字符串
RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
//根據名字字符串來獲得驅動對象
status = ObReferenceObjectByName(
					&uniNtNameString,
					OBJ_CASE_INSENSITIVE,
					NULL,
					0,
					IoDriverObjectType,
					KernelMode,
					&KdbDriverObject,
					);
if (!NT_SUCCESS(status))
{
	//如果失敗
	DbgPrint("MyAttach:Couldn't get the kbd driver Object\n");
	return STATUS_SUCCESS;
}
else
{
	//凡是調用了Reference系列的函數都要通過調用ObDereferenceObject來解除引用
	ObDereferenceObject(KdbDriverObject);
}


這樣就獲得了驅動對象,然後只要替換其分發函數就行了。

 

2.修改類驅動的分發函數指針

雖然驅動對象不同,但是替換的方法還是一樣的。值得注意的,必須保存原有的驅動對象的分發函數;否則,第一,替換之後將無法恢復;第二,完成我們自己的處理後無法繼續調用原有的分發函數。

這裏用到一個原子操作:InterlockedExchangePointer.這個操作的好處是,用戶設置新的函數指針是原子的,不會被打斷。插入其他可能要執行到調用這些分發函數的其他代碼

//這個數組用來保存所有舊的指針
ULONG i;
PDRIVER_DISPATCH OldDispatchFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
.....
//把所有的分發函數指針替換成我們自己編寫的同一個分發函數
for (i = 0 ; i <= IRP_MJ_MAXIMUM_FUNCTION ; ++i)
{
	//假設MyFilterDispatch是筆者已經寫好的一個分發函數
	OldDispatchFunction[i] = KdbDriverObject->MajorFunction[i];
	//進行原子交換操作
	InterlockedExchangePointer(
		        &KbdDriverObject->MajorFunction[i],
				MyFilterDispatch
		);
}


3.類驅動之下的端口驅動

前面的過濾方式是替換分發函數指針。但是這是依然比較明顯,因爲分發函數的指針本來是已知的,如果安全監控軟件有針對性地對這個指針進行檢查和保護,就容易發現這個指針已經被替換掉的情況。

KbdClass被稱爲鍵盤類驅動,在Windows中,類驅動通常是指統管一類設備的驅動程序。不管是USB鍵盤,還是PS/2鍵盤均進過它,所以在這一層做攔截,能獲得很好的通用性,類驅動之下和實際硬件交互的驅動被稱爲“端口驅動”。具體到鍵盤,i8042prt是PS/2鍵盤的端口驅動,USB鍵盤則是Kbdhid。

前面提到,鍵盤驅動的主要工作就是,當鍵盤上有按鍵按下引發中斷時,鍵盤驅動從端口讀出按鍵的掃描碼,最終順利地將它交給在鍵盤設備棧棧頂等待的那個主功能區號爲IRP_MJ_READ的IRP。爲了完成這個任務,鍵盤驅動使用了兩個循環使用的緩衝區。

下面以比較古老的PS/2鍵盤爲例進行介紹,因此下面介紹的端口驅動都是i8042prt。

i8042prt和KbdClass各自都有一個可以循環使用的緩衝區。緩衝區的每個單元都是一個KEYBOARD_INPUT_DATA結構,用來存放掃描碼及其相關信息。在鍵盤驅動中,把這個循環使用的緩衝區叫做輸入數據隊列(input data queue),i8042prt的那個鍵盤緩衝區被叫做端口鍵盤輸入隊列,KbdClass的那個緩衝區被叫做類輸入數據隊列(class input data queue)。

 

4.端口驅動和類驅動之間的協作機制

當鍵盤上一個鍵被按下時,產生一個Make Code,引起鍵盤中斷;當鍵盤上一個鍵被鬆開時,產生一個Break Code,引發鍵盤中斷。鍵盤中斷導致鍵盤服務例程執行,導致最終i8042prt的I8042KeyboardInterruptService被執行。

在I8042KeyboardInterruptService中,從端口讀出按鍵的掃描碼,放在一個KEYBOARD_INPUT_DATA中。將這個KEYBORAD_INPUT_DATA放入i8042prt的輸入隊列中。

在這個調用中,會調用上層處理輸入的回調函數(也就是KbdClass處理輸入數據函數),取走i8042prt的輸入數據隊列的數據。因爲設備擴展中保存着上層處理輸入數據的回調函數的入口地址,所以他知道該調用誰。上層處理輸入的回調函數(也就是KbdClass處理輸入數據的函數)取走數據。KbdClass處理輸入數據的函數中,滿足那個應用層發來的讀請求。

 

5.找到關鍵的回調函數的條件

從上面的原理來看,I8042KeyboardInterruptService中調用的類驅動的那個回調函數非常關鍵。如果找到這個回調函數,通過Hook,替換或者類似的手段,就可以輕易地獲取鍵盤的輸入。而且這個函數非常深入,也沒有公開,安全軟件很難去顧及。

現在的問題就是如何去定位這個函數指針了。i8042prt驅動的設備擴展我們並不完全清楚;此外WDK也不可能公開這個函數地址,但是“有識之士”根據經驗指出:

(1)這個函數指針應該保存在i8042prt生成的設備的自定義設備擴展中。

(2)這個函數的開始地址應該保存在KbdClass中

(3)內核模塊KbdClass生成的一個設備對象指針也保存在那個設備擴展中,而且在我們要找到函數之前

有了這3個規律就可以來尋找這個函數了,當然,這裏有個問題,即如何判斷一個地址是否在某一個驅動中?

這裏所說的不是驅動對象,而是這個內核模塊在內存空間中的地址。這是一個常用的技巧:在驅動對象中DriverStart域和DriverSize域分別記載着這個驅動對象所代表的內核模塊在內核空間中的開始地址和大小。

在前面的代碼中,我們已經打開了驅動對象KbdDriverObject,那麼KbdDriverObject->DriverStart就是驅動KbdDriverObject的開始地址;KbdDriverObject->DriverSize就是這個驅動的字節大小。

這樣,可以通過下面的簡單代碼判斷一個地址是否在kbdClass這個驅動中。

PVOID address;
size_t kbdDriverStart = kbdDriverObject->DriverStart;
size_t kbdDriverSize = kbdDriverObject->DriverSize;
...
if ((address > kbdDriverStart)&&(address < (PBYTE)kbdDriverStart+kbdDriverSize))
{
	//說明在這個驅動中
}


6.定義常數和數據結構

下面的方法實現了搜索這個關鍵的回調函數的指針。這些代碼考慮的更加寬泛,把USB鍵盤的情況也考慮進去了。涉及到如下3個驅動,這裏都定義成字符串。

//鍵盤類驅動的名字
#define KBD_DRIVER_NAME L"\\Driver\\kbdclass"
//USB鍵盤端口驅動名
#define USBKBD_DRIVER_NAME L"\\Driver\\Kbdhid"
//PS/2鍵盤驅動
#define PS2KBD_DRIVER_NAME L\\Driver\\i8042prt


然後,對於我們要搜索的回調函數的類型定義如下:

typedef VOID
(_stdcall *KEYBOARDCLASSSERVICECALLBACK)( 
	                  IN PDEVICE_OBJECT DeviceObject,
					  IN PKEYBOARD_INPUT_DATA InputDataStart,
					  IN PKEYBOARD_INPUT_DATA InputDataEnd,
					  IN OUT PULONG InputDataConsumed
	);


接下來,定義一個局部變量,來接收搜索到的回調函數,實際上我們不但搜索一個回調函數,還搜索類驅動生成的一個設備對象。這個設備對象的指針保存在端口驅動設備對象擴展中,而且必須先找到它,後面才能搜索回調函數。這個設備對象保存在全局變量gKbdClassBack.classDeviceObject中,而gKbdClassBack.serviceCallBack則將保存搜索的回調函數,下面是全局變量gKbdCallBack的定義。

typedef struct _KBD_CALLBACK
{
	PDEVICE_OBJECT classDeviceObject;
	KEYBOARDCLASSSERVICECALLBACK serviceCallBack;
}KBD_CALLBACK,*PKBD_CALLBACK;
KBD_CALLBACK gKbdCallBack = {0};


7.打開兩種鍵盤端口驅動尋找設備

下面開始寫一個函數進行搜索,搜索結果被填寫到上面定義的全局變量gKbdCallBack中。原理是這樣的:預先不可能知道機器上裝的是USB鍵盤還是PS/2鍵盤,所以一開始是嘗試打開這兩個驅動。在很多情況下只有一個可以打開,比較極端的情況是兩個都可以打開(用戶同時安裝有兩個種類的鍵盤),或者兩個都打不開。

NTSTATUS
SearchServiceCallBack(IN PDRIVER_OBJECT DriverObject)
{
	//定義用到的一組全局變量,這些變量大多數是顧名思義的
	NTSTATUS status = STATUS_SUCCESS;
	int i = 0;
	UNICODE_STRING uniNtNameString;
    PDEVICE_OBJECT pTargetDeviceObject = NULL;
	PDRIVER_OBJECT KbdDriverObject = NULL;
	PDRIVER_OBJECT KbdhidDriverObject = NULL;
	PDRIVER_OBJECT Kbd8042DriverObject = NULL;
	PDRIVER_OBJECT UsingDriverObject = NULL;
	PDRIVER_OBJECT UsingDeviceObject = NULL;
	ULONG KbdDriverSize = 0;
	PVOID UsingDeviceExt = NULL;

	//這裏的代碼用來打開USB鍵盤端口驅動的驅動對象
	RtlInitUnicodeString(&uniNtNameString,USBKBD_DRIVER_NAME);
	status = ObReferenceObjectByName(
		                   &uniNtNameString,
						   OBJ_CASE_INSENSITIVE,
						   NULL,
						   0,
						   IoDriverObjectType,
						   KernelMode,
						   NULL,
						   &KbdDriverObject);
	if (!NT_SUCCESS(status))
	{
		DbgPrint("Couldn't get the USB driver Object\n");
	}
	else
	{
		ObDereferenceObject(KbdhidDriverObject);
		DbgPrint("get the USB driver Object\n");
	}
	//打開PS/2鍵盤的驅動對象
	RtlInitUnicodeString(&uniNtNameString,PS2KBD_DRIVER_NAME);
	status = ObReferenceObjectByName(
		                   &uniNtNameString,
						   OBJ_CASE_INSENSITIVE,
						   NULL,
						   0,
						   IoDriverObjectType,
						   KernelMode,
						   NULL,
						   &Kbd8042DriverObject);

	if (!NT_SUCCESS(status))
	{
		DbgPrint("Couldn't get the PS/2 driver Object\n");
	}
	else
	{
		ObDereferenceObject(Kbd8042DriverObject);
		DbgPrint("get the PS/2 driver Object\n");
	}

	//這段代碼考慮有一個鍵盤起作用的情況。如果USB鍵盤和PS/2鍵盤同時存在,直接返回失敗即可
	if (Kbd8042DriverObject && KbdhidDriverObject)
	{
		DbgPrint("more than two kbd!\n");
		return STATUS_UNSUCCESSFUL;
	}
	//如果兩個設備都沒有找到
	if (!Kbd8042DriverObject && !KbdhidDriverObject)
	{
		DbgPrint("no kbd!\n");
		return STATUS_SUCCESS;
	}
	//找到合適的驅動對象,不管是USB還是PS/2,反正一定要找到一個
	UsingDriverObject = Kbd8042DriverObject?
            Kbd8042DriverObject:KbdhidDriverObject;
	//找到這個這個驅動對象的下一個設備對象
	UsingDeviceObject = UsingDriverObject->DeviceObject;
	//找到這個設備對象的設備擴展
	UsingDeviceExt = UsingDeviceExt->DeviceExtension;
	......

}


 

8.搜索KbdClass類驅動中的地址

這裏接着寫前面那個函數中沒有完成的代碼。目的已經明確,就是爲了尋找UsingDeviceExt中保存的一個驅動KbdClass中的地址。

   RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
   status = ObReferenceObjectByName(
	                      &uniNtNameString,
						  OBJ_CASE_INSENSITIVE,
						  NULL,
						  0,
						  IoDriverObjectType,
						  KernelMode,
						  NULL,
						  &KbdDriverObject);
   if (!NT_SUCCESS(status))
   {
	   //如果沒有成功,直接返回即可
	   DbgPrint("MyAttach: Coundn't get the kbd driver Object\n");
	   return STATUS_UNSUCCESSFUL;
   }
   else
   {
	   ObDereferenceObject(KbdDriverObject);
	   //如果成功了,就找到KbdClass的開始地址和大小
	   KbdDriverStart = KbdDriverObject->DriverStart;
	   KbdDriverSize->kbdDriverObject->DriverSize;
   }


下面就是搜索過程,首先遍歷KbdClass下面的所有設備,找到驅動對象下的第一個設備,然後找設備對象下的Next指針連續遍歷即可。在這些設備中,有一個會保存在端口驅動的設備擴展中,這是我們要尋找的。當然,更重要的是尋找那個回調函數。

//遍歷KbdDriverObject下的設備對象
   pTargetDeviceObject = KbdDriverObject->DeviceObject;
   while(pTargetDeviceObject)
   {
	   DeviceExt = (PBYTE)UsingDeviceExt;
	   //遍歷我們先找到的端口驅動的設備擴展的每一個指針
	   for (;i<4096;i++,DeviceExt+=sizeof(PBYTE))
	   {
		   PVOID tmp;
		   if (!MmIsAddressValid(DeviceExt))
		   {
			   break;
		   }
		   //找到後會填寫到這個全局變量中,這裏檢查是否已經填好了
		   //如果已經填好了就不用繼續找了,可以直接退出
		   if (gKbdCallBack.classDeviceObject && gKbdCallBack.serviceCallBack)
		   {
			   status = STATUS_SUCCESS;
			   break;
		   }
		   //在端口驅動的設備擴展裏,找到了類驅動設備對象,填好類驅動設備對象後繼續
		   tmp = *(PVOID*)DeviceExt;
		   if (tmp == pTargetDeviceObject)
		   {
			   gKbdCallBack.classDeviceObject = (PDEVICE_OBJECT)tmp;
			   DbgPrint("classDeviceObject %8x\n",tmp);
			   continue;
		   }

		   //如果在設備擴展中找到一個地址位於KbdClass這個驅動中,就可以認爲,這就是我們要找的回調函數
		   if ((tmp > KbdDriverStart) &&
			   (tmp < (PBYTE)KbdDriverStart+KbdDriverSize) &&
			   (MmIsAddressValid(tmp)))
		   {
			   //將這個回調函數記錄下來
			   gKbdCallBack.serviceCallBack = (KEYBOADCLASSSERVICECALLBACK)tmp;
			   AddServerCallBack = (PVOID*)DeviceExt;
			   DbgPrint("serviceCallBack: %8x
                          AddrServiceCallBack: %8x\n",
							tmp,AddrServiceCallBack);

		   }
	   }
	   //換成下一個設備,繼續遍歷
	   pTargetDeviceObject = pTargetDeviceObject->NextDevice;
   }

   //如果成功找到了,就把這個函數替換成我們自己的回調函數
   if (AddrServiceCallBack && gKbdCallBack.serviceCallBack)
   {
	   DbgPrint("Hook keyboradClassServiceCallback\n");
	   *AddrServiceCallBack = MyKeyboardClassServiceCallBack;
   }
   return status;


 

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