《Windows內核安全與驅動編程》-第八章-鍵盤的過濾學習-day-4

鍵盤的過濾

Hook 分發函數

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

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

​ 在 I8042KeyboardInterruptService 中,從端口中讀出按鍵的掃描碼。放在一個 KEYBOARD_INPUT_DATA 中。將這個 KEYBOARD_INPUT_DATA 放入 i8042prt 的輸入數據隊列中,一箇中斷放入一個數據, DataIn 後移一格, InputCount 加一。(最後會調用內核 API 函數 KeInsertQueueDpc ,進行更多處理的延遲過程調用。)

​ 在這個調用中,會調用上層處理輸入的回調函數(即 KbdClass 處理輸入數據的函數) 取走 i8042prt 的輸入數據隊列裏的數據。回調函數的指針保存在設備擴展中。上層處理輸入的回調函數取走數據之後,i8042prt 的輸入數據隊列的 DataOut相應後移, InputCount 相應減少。

​ 這裏還有兩個特性:

  1. 當讀請求要讀的數據大於等於 i8042prt 的輸入數據隊列中的數據時,讀請求的處理函數會直接從 i8042prt 的輸入數據隊列裏讀出所有的輸入數據,不使用 KbdClass 的輸入數據隊列。在大多數情況下是如此。
  2. 當讀請求要求的數據大小小於 i8042prt 的輸入數據隊列中的數據時,讀請求的處理函數直接從 i8042prt 的輸入數據隊列中讀出它所要求的大小,然後這個讀請求被完成。 i8042prt 的輸入數據隊列中剩餘的數據,被放入 KbdClass 輸入數據隊列中。當應用層發來下一個讀請求時,那個讀請求將直接從 KbdClass 的輸入數據隊列中讀取數據,不需要等待。

8.5.5 找到關鍵的回調函數的條件

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

​ 現在的問題就是如何定位這個函數指針了。 i8042prt 驅動的設備擴展我們並不完全清楚;根據經驗指出:

  • 這個函數指針應該保存在 i8042prt 生成的設備的自定義設備擴展中。
  • 這個函數的開始地址應該在內核模塊 KbdClass 中。
  • 內核模塊 KbdClass 生成的一個設備對象的指針也保存在那個設備擴展中,而且在我們要找的函數指針之前。

​ 依據這三個規律就可以來尋找這個函數裏。首先第一個問題: 如何判斷一個地址是否在一個驅動中?

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

​ 那麼通過下面的代碼就可以簡單的判斷一個地址是否在一個 KbdClass 中了。

PVOID address
//回顧一下這兩個變量的數據類型
// PVOID DriverStart; 
// ULONG DriverSize;
size_t kbdDriverStart = KbdDriverObject->DriverStart;
size_t kbdDriverSize = KbdDirverObject->DriverSize;
...
if( (address > (PBYTE)kbdDriverStart) && (addrsss<(PBYTE)KbdSDriverStart+KbdDriverSize))
{
    //說明該地址在該驅動的內核空間地址內
}

8.5.6 定義常數和數據結構

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

// 鍵盤類驅動的名字
#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 *KEYBOARDCLASSSERVICECALLBACL)(
	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;
    KEYBOADRDCLASSSERVICECALLBACK serviceCallBack;
}KBD_CALLBACK,*PKBD_CALLBACK;
//初始化一個變量
KBD_CALLBACK gKbdCallBack={0};

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

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

NTSTATUS SearchServiceCallBack(
	IN PDRIVER_OBJECT DriverObject
)
{
    // 定義用到的一組局部變量。這些變量大多是顧名思義的。
    NTSTATUS statsu = STATUS_UNSUCCESSFUL;
    int i = 0;
    UNICODE_STRING uniNtNameString;
    PDRIVER_OBJECT pTargetDeivceObject = NULL;
    PDRIVER_OBJECT KbdDriverObject = NULL;
    PDRIVER_OBJECT KbdhidDriverObject = NULL;
    PDRIVER_OBJECT kbd8042DriverObject = NULL;
    PDRIVER_OBJECT UsingDriverObject = NULL;
    PDRIVER_OBJECT UsingDeviceObject = NULL;
    PVOID KbdDriverStart = NULL;
    ULONG KbdDriverSize = 0;
    PVOID UsingDeviceExt = NULL;
    
    // 這裏的代碼用來打開 USB鍵盤 端口驅動的驅動對象
    RtlInitUnicodeString(&uniNtNameString,USBKBD_DRIVER_NAME);
    status = ObReferenceObjectByName(
    	&uniNtNameString,
    	OBJ_CASE_INSENSITIVE,
    	NULL,
    	0,
    	IoDriverObjectType,
    	KernelMode,
    	NULL,
    	&KbdhidDriverObject
    );
    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");
    }
    
    // 這段代碼只考慮只有一個鍵盤起作用的情況。如果兩種鍵盤同時存在,則返回失敗
    if(kbd8042DriverObject && KbdhidDriverObject)
    {
    	Dbgprint("more than two kbd!\n");
    	return STATUS_UNSUCCESSFUL;
    }
    
    // 如果兩個都沒有找到...系統用了其他種類的鍵盤,則直接返回失敗
    if(!kbd8042DriverObject && !KbdhidDriverObject)
    {
    	Dbgprint("no kbd!\n");
    	return STATUS_UNSUCCESSFUL;
    }
    
    //找到合適的一個。
    UsingDriverObject = kbd8042DriverObject? kbd8042DriverObject:kbdhidDriverObject;
    //找到這個驅動對象下的第一個設備對象
    UsingDeviceObject = UsingDriverObject->DeviceObject;
    //找到這個設備對象的設備擴展
    UsingDeviceExt = UsingDeviceObject->DeviceExtension;
    ...
}
現在,已經把設備拓展的地址放到 **UsingDeviceExt** 裏面了。根據前面的預測,這裏面應該有一個函數指針,其地址是在驅動 **KbdClass** 中的,找到它就完成了。

8.5.8 搜索在 KbdClass 類驅動中的地址

​ 這裏面接着寫前面那個函數中沒有完成的代碼。目前的目的已經非常明確。

//首先必須打開驅動 KbdClass,以使從驅動對象中得到其開始地址和大小。
RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
status = ObReferenceObjectByName(
    	&uniNtNameString,
    	OBJ_CASE_INSENSITIVE,
    	NULL,
    	0,
    	IoDriverObjectType,
    	KernelMode,
    	NULL,
    	&KbdriverObject
    );
    if(NT_SUCCESS(status))
    {
    	DbgPrint("Couldn't get the Kbd driver Object\n");
    }
    else
    {
    	ObDereferenceObject(Kbd8042DriverObject);
    	//成功找到則獲取 KbdClass 的開始地址和大小
    	KbdDriverStart = KbdDriverObject->DriverStart;
    	KbdDriverSize = KbdDriverObject->DriverSize;
    }

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

​ 所以我們定義了一個臨時的指針 DeviceExt, 從前面得到的 UsingDeviceExt 的地址開始遍歷,每次增加一個指針的寬度。

//遍歷 KbdDriverObject 下的設備對象
pTargetDeviceObject = KbdDriverObject->DeviceObject;
while(pTargetDeviceObject)
{
    DeviceExt = (PBYTE)UsingDeviceExt;
    //遍歷我們先前找到的端口驅動的設備擴展下的每一個指針
    for(;i<4086;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(""ClassDeviceObjext %8x\n,tmp);
         continue;
   	 }
   	 
   	 //如果在設備拓展中找到一個地址位於KbdClass 這個設備驅動中
   	 //可以認爲,這就是我們要找的回調函數地址
   	 if(
   	 	(tmp > KbdDriverStart)
   	 	&& (tmp < (PBYTE)KbdDriverStart + KbdDriverSize)
   	 	&& MmIsAddressValid(tmp)
   	 )
   	 {
         //將這個回調函數記錄下來
         gKbdCallBack.serviceCallBack = (KEYBOARDCLASSSERVICECALLBACK)tmp;
         AddrServiceCallBack = (PVOID*)DeviceExt;//存放函數地址的地址
         DbgPrint("serviceCallBack: %8x\n AddrServiceCallBack: %8x\n",tmp,AddrServiceCallBack")        
   	 }
    }
    //如果沒找到,則繼續測試下一個設備
   	 pTargetDeviceObject = pTargetDeviceObject->NextDevice;
    
}
//如果成功的找到了,就把這個函數替換成我們自己的回調函數
//之後的過濾就可以自己任意操作了
if(AddrServiceCallBack && gKbdCallBack.serviceCallBack)
{
    DbgPrint("Hook KeyboardClassServiceCallback\n");
    *AddrServiceCallBack = MyKeyboardClassServiceCallback;//我們自己的回調函數
}
//至此 該hook函數結束
return status;


不得不說鍵盤驅動底下東西有點多,明天爭取啃下來。。一次搞太多也不好消化。剛剛回去看了一下之前的從 Make Code 到實際按鍵,其中的 off 偏差作者默認給的0,但是具體是多少還是要我們自己去查一下的。

明日計劃

Hook IDT 相關內容,端口操作鍵盤

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