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

鍵盤的過濾

8.1 技術原理

8.1.1 預備知識

​ 何爲符號鏈接?符號鏈接其實就是一個“別名”,可以用一個不同的名字來代表一個設備對象。

ZwCreateFile 是很重要的函數。同名的函數實際上有兩個: 一個在內核中,一個在應用層。所以在應用程序中直接調用 CreateFile,就可以引發對這個函數的調用。它不但可以打開文件,而且可以打開設備對象(返回得到一個類似於文件句柄的句柄)。所以後面按常常會看到應用程序爲了與內核交互而調用這個函數,這個函數最終調用 NtCreateFile

​ 何爲 PDODODriver Object 的簡稱, PDOPhsiycal Device Object 的簡稱,字面上意義是物理設備對象。我們暫時可以理解爲設備棧下最下面的那個設備對象。

​ 此外 如果看到

nt!IoGetAttachedDevice
nt!ObpCreateHandle

​ 這是在調試工具 WinDbg 中常常出現的表示方法。!之前的內容標識模塊名,而之後的內容標識函數名或者變量名。

8.1.2 Windows 中從擊鍵到內核

​ 這一小節專門講述 Windows 是如何獲得按鍵,然後傳遞給各個應用程序的。

​ 打開任務管理器,可以看到一個名爲 Csrss.exe 的進程。該進程十分關鍵,它有一個線程叫做 win32!RawInputThread ,這個線程通過一個GUID 來獲得鍵盤設備棧的 PDO 的符號鏈接名。

​ 應用程序是不能直接根據設備名字來打開設備的,一般都通過符號鏈接名來打開。

win32k!RawInputThread 執行到函數 win32!OpenDevice ,它的一個參數可以找到鍵盤設備棧的 PDO 的符號鏈接名。 win32!OpenDevice 有一個 OBJECT_ATTRIBUES 結構的局部變量,它自己初始化這個局部變量,用傳入參數中的鍵盤設備棧的 PDO 的符號鏈接名賦值 OBJECT_ATTRIBUTES+0X8 處的 PUNICODE_STRING Object_name

​ …… 整個過程比較複雜,後續也不繼續跟進了。詳細可以再去網上查一下,我感覺書上寫的有一些不容易理解。有流程圖就更好了。不過暫時沒有必要了解這些細節。我們只需要知道我們要去綁定的那個設備就是驅動 KdbClass 的設備對象就可以了。

8.1.3 鍵盤硬件原理

​ 一個字符並不代表一個鍵,每個鍵只是有自己的掃描碼。鍵盤和 CPU 的交互方式是中斷和讀取端口,這個操作是串行的。鍵盤每給 CPU 一個通知,就會發生一次中斷。這個通知只能通知一個事件: 某個鍵被按下了,某個鍵彈起了。

​ 因此,一個鍵實際上需要兩個掃描碼: 一個表示按下;另一個表示鍵彈起。CPU 只接受通知並讀取端口的掃描碼,從不主動查看任何鍵。

8.2 鍵盤過濾的框架

8.2.1 找到所有的鍵盤設備

​ 要過濾一種設備,首先要綁定它。現在需要找到所有代表鍵盤的設備。從前面的原理來看,如果綁定了驅動 KdbClass 的所有設備對象,那麼代表鍵盤的設備一定在其中。那麼如何找到一個驅動下的所有對象呢? 聯想一下第二章中對驅動對象結構的介紹。一個 DRIVER_OBJECT 下有一個域叫作 DeviceObject ,這看似是一個設備對象的指針,但是由於每個 DeviceObject 中又有一個域叫做 NextDevice, 指向同一個驅動中的下一個設備,所以這裏實際上是一個設備鏈。

​ 除了使用上面描述的設備鏈以外,還有一種獲得驅動ia的所有設備對象的方法是調用函數 IoEnumerateDeviceObjectList ,這個函數也可以枚舉出一個驅動下的所有設備。

​ 現在來寫代碼。這些代碼來自一個開源的鍵盤過濾例子 ctrl2cap,在github上應該可以搜到。我們首先打開驅動對象 KbdClass ,然後綁定它下面的所有設備。這裏用到一個新的函數——ObReferenceObjectByName , 它用於通過一個名字來獲得一個對象的指針。該函數的解釋這裏有較詳細的

// IoDriverObjectTyp 實際上是一個全局變量,但是頭文件中沒有,只要聲名之後就可以使用了
extern POBJECT_TPYE IoDriverObjectType;
//KbdClass 驅動的名字
#define KBD_DRIVER_NAME L"\\Driver\\Kbdclass"
//這個函數是事實存在的,只是文檔中沒有公開
//聲名一下就可以使用了
NTSTATUS ObReferenceObjectByName(
	PUNICODE_STRING ObjectName,
    ULONG Attributes,
    PACCESS_STATE AccessState,
    ACCESS_MASK DesiredAccess,
    POBJET_TYPE ObjectType,
    KPROCESSOR_MODE AccessMode,
    PVOID ParseContext,
    PVOID *Object
);

//打開驅動對象KdbClass,然後綁定它下面的所有設備
NTSTATUS c2pAttachDevices(
		IN PDRIVER_OBJECT DriverObject,
    	IN PUNICODE_STRING RegistryPath
)
{
	NTSTATUS status = 0;
	UNICODE_STRING uniNtNameString;
	PC2P_DEV_EXT devExt;
	PDEVICE_OBJECT pFilterDeviceObject = NULL;
	PDEVICE_OBJECT pTargetDeviceObject = NULL;
	PDEVICE_OBJECT pLowerDeviceObject = NULL;
	
	PDEVICE_OBJECT KbdDriverObject = NULL;
	
	KdPrint(("My Attach\n"));
	
	//初始化一個字符串,即 KbdClass 驅動的名字
RtlInitUnicodeString(&uniNtNameString,KBD_DRIBER_NAME);
	//參照前面打開設備對象的例子,這是這裏打開的是驅動對象
	status = ObReferenceObjectByName(
		&uniNtNameString,
		OBJ_CASE_INSENSITIVE,
		NULL,
		0,
		IoDriverObjectType,
		KernelMode,
		NULL,
		&KdbDriverObject
	);
	//如果失敗了就返回
	if(!NT_SUCCESS(status))
    {
    	KdPrint(("couldn't get the KdbDriverObject!!\n"));
    	return status;
    }
    else
    {
    	//調用 ObReferenceObjectByName 會導致對驅動對象的引用計數增加
    	//必須相應的調用 ObDereferenceObject 進心解引用
    	ObDereferenceObject(&DriverObject);
    }
    //這是設備鏈中的第一個設備
    pTargetDeviceObject = KbdDirverObject->DeviceObject;
    
    //開始遍歷這個設備鏈並逐個綁定
    while(pTargetDeviceObject)
    {
    	//生成一個過濾設備
    	status = IoCreateDevice(
    		IN DriverObject,
    		IN sizeof(C2P_DEV_EXT),//指定要爲設備對象的設備擴展分配的驅動程序確定的字節數。
    		0,
    		IN pTargetDeviceObject->DeviceType,
    		IN pTargetDeviceObject->Characteristics,
    		IN FALSE,
    		OUT &pFilterDeviceObject
    	);
    	
    	//如果失敗了就退出
    	if(!NT_SUCCESS(status))
        {
        	KdPrint(("Create filterDeviceObject fail!\b"));
        	return status;
        }
        //綁定,其中 pLowerDeviceObject 就是綁定之後得到的下一個設備,也就是前面說的所謂的真實設備
    pLowerDeviceObject = IoAttachDeviceToDeviceStack(&pFilterDeviceObject,&pTargetDeviceObject);
    //如果綁定失敗了,就放棄之前的操作,退出。
    if(!pLowerDeviceObject)
    {
    	KdPrint(("Attach fail!!"));
    	IoDeleteDevice(pFilterDeviceObject);
    	pFilterDeviceObject = NULL;
    	return status;
    }
    
    //設備拓展。下面要詳細講述設備拓展的應用。並且在上述創建過濾設備的時候預留了設備拓展的空間。
    devExt = (PC2P_DEV_EXT)(pFilterDeviceObject->DeviceExtension);
    c2pDevExtInit(
    	devExt,
    	pFilterDeviceObject,
    	pTargetDeviceObject,
    	pLowerDeviceObject
    );
    //下面的操作是爲了保持一致,在串口過濾時候講過
    pFilterDeviceObject->DeviceType = pLowerDeviceObject->DeviceTpye;
    pFilterDeviceObject->Characteristic = pLowerDeviceObject->Characteristics;
    pFilterDeviceObject->StackSize = pLowerDeviceObject->StackSize+1;
    pFilterDeviceObject->Flags |= pLowerDeviceObject->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO | DO_POWER_PAGABLE);
    
    //繼續遍歷下一個目標設備
    pTargetDeviceObject = pTargetDeviceObject->NextDevice;
    
    }
    return status;
    
}

8.2.2 應用設備拓展

​ 前面用到了設備拓展,聯繫前面串口過濾的例子,實際上我們用了兩個數組: 一個用於保存所有的過濾設備;另一個用於保存所有的真實設備。兩個數組起到一一映射的表的作用;拿到過濾設備的指針馬上就可以找到真實設備的指針。

​ 但是實際上是沒有必要這樣做的。在生成一個過濾設備時,我們可以給這個設備指定一個任意長度的”設備擴展“,這個擴展中的內容可以任意填寫,作爲一個自定義的數據結構。

​ 這樣就可以把真實設備的指針保存在過濾設備對象裏了。就沒有必要用兩個數組分別保存它們了。

​ 在這個鍵盤過濾中,作者定義的一個專門的結構作爲設備拓展如下:

typedef struct _C2P_DEV_EXT
{
	//這個設備的大小
    ULONG NodeSize;
    //過濾設備對象
    PDEVICE_OBJECT pFilterDeviceObject;
    //同時調用時的保護所
    KSPIN_LOCK IoRequestesSpinLock;
    //進程間同步處理
    KEVENT IoInProgressEvent;
    //綁定的設備對象
    PDEVICE_OBJECT TargetDeviceObject;
    //綁定前底層設備對象
    PDEVICE_OBJECT LowerDeviceObject;
}C2P_DEV_EXT,*PC2P_DEV_EXT;

​ 這裏保存的除了 LowerDeviceObject 還有其他一些信息,暫時不用瞭解。

​ 注意在調用 IoCreateDevice 時,第二個參數的填寫

status = IoCreateDevice(
    		IN DriverObject,
    		IN sizeof(C2P_DEV_EXT),//指定要爲設備對象的設備擴展分配的驅動程序確定的字節數。
    		0,
    		IN pTargetDeviceObject->DeviceType,
    		IN pTargetDeviceObject->Characteristics,
    		IN FALSE,
    		OUT &pFilterDeviceObject
    	);

​ 接下來就可以在預留的設備拓展的空間裏填寫我需要的信息。相關代碼如下:

//設備拓展。下面要詳細講述設備拓展的應用。並且在上述創建過濾設備的時候預留了設備拓展的空間。
    devExt = (PC2P_DEV_EXT)(pFilterDeviceObject->DeviceExtension);
    c2pDevExtInit(
    	devExt,
    	pFilterDeviceObject,
    	pTargetDeviceObject,
    	pLowerDeviceObject
    );
NTSTATUS c2pDevExtInit(
		IN PC2P_DEV_EXT devExt;
		IN PDEVICE_OBJECT pFilterDeviceObject,
		IN PDEVICE_OBJECT pTargetDeviceObject,
		IN PDEVICE_OBJECT pLowerDeviceObject
){
    memset(devExt,0,sizeof(C2P_DEV_EXT));
    devExt->NodeSize = sizeof(C2P_DEV_EXT)
    devExt->pFilterDeviceObject = pFilterDeviceObject;
    devExt->pTargetDeviceObject = pTargetDeviceObject;
    devExt->pLowerDeviceObject = pLowerDeviceObject;
    KeInitializeSpinLock(&(devExt->IoRequestsSpinLock));
    KeInitializeEvent(&(devExt->IoInProgressEvent),NotifiacationEvent,FALSE);//不自動復位的等待事件
    return STATUS_SUCCESS;
}

8.2.3 鍵盤過濾模塊的 DriverEntry

​ 下面是 DriverEntry 函數的代碼。這個函數就相當於應用編程裏的 main 函數。下面的函數一進入就直接去找 KdbClass 下所有的設備進行綁定。調用了之前所完成的 c2pAttachDevices

NTSTATUS DriverEntry(
	IN PDRIVER_OBJECT DriverObject,
	IN PUNICODE_STRING RegistryPath
)
{
    ULONG i;
    NTSTATUS statusl
    KdPrint(("c2p.SYS: entering DriverEntry"));
    
    // 填寫所有分發函數的指針
    for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
    {
        DriverObject->MajorFunction[i] = c2pDispatchGeneral;
    }
    
    // 單獨地填寫一個 Read 分發函數。因爲重要的過濾就是讀取來的按鍵信息。而其他的暫時都不考慮
    DriverObject->MajorFunction[IPR_MJ_READ] = c2pDispatchRead;
    
    //單獨填寫一個 IRP_MJ_POWER 函數。這是因爲這類請求中間要調用一個
    // PoCallDriver 和一個 PoStartNextPowerIrp,比較特殊
    DriverObject->MajorFunction[IRP_MJ_POWER] = c2pPower;
    
    //我們想知道什麼時候綁定過的一個設備被卸載了(比如從機器上拔掉了),所以專門寫一個PNP(即插即用)分發函數
    DriverObject->MajorFunction[IRP_MJ_PNP] = c2pPnP;
    
    //卸載函數
    DriverObject->DriverUnload = c2pUnload;
    gDriverObject = DriverObject;
    //綁定所有的鍵盤設備
    status = c2pAttachDevices(DriverObject,RegistryPath);
    return status;
}

8.2.4 鍵盤過濾模塊的動態卸載

​ 鍵盤過濾模塊的動態卸載和前面的串口過濾稍有不同。這是因爲鍵盤總是處於”有一個讀請求沒有完成“的狀態。

​ ”“當鍵盤上有鍵被按下時,將出發鍵盤的那個中斷,引起中斷服務例程的執行,鍵盤中斷的中斷服務例程由鍵盤驅動提供。鍵盤驅動從端口讀取掃描碼,經過一系列的處理之後,把鍵盤得到的數據交給 IRP,然後結束這個 IRP。這個 IRP 的結束,將導致 win32k!RawInputThread 線程對這個讀操作的等待結束。win32k!RawInputThread 將會對得到的數據做出處理,分發給合適的進程。一旦把輸入數據處理完之後,win32k!RawInputThread 線程會立刻再調用一個 nt!ZwReadFile ,向鍵盤驅動要求讀入數據,於是又一個等待開始,等待鍵盤上的鍵被按下。”

​ 換句話說,就算類似串口驅動一樣等待5秒,這個請求未必會完成。這樣如果卸載了所有的過濾驅動,那麼下一次一按鍵,這個請求就被處理,很可能馬上藍屏崩潰。

​ 下面是對實際中動態卸載的處理。

VOID c2pUnload(In PDRIVER_OBJECT DriverObject)
{
    PDEVICE_OBJECT DevictObject;
    PDEVICE_OBJECT OldDeviceObject;
    PC2P_DEV_EXT devExt;
    
    LARGE_INTEGER lDelay;
    PRKTHREAD CurretThread;
    //延遲一些時間
    lDelay = RtlConvertLongToLargeInteger(100 * DELAY_ONE_MILLISECOND);
    CurrentThread = KeGetCurrentThread();
    //把當前線程設置爲低實時模式,以便讓它的運行儘量少影響其他程序
    KeSetPriorityThread(CurrentThread,LOW_REALTIME_PRIORITY);
    
    UNREFERENCE_PARAMETER(DriverObject);
    KdPrint(("DriverObject unloading...\n"));
    
    //遍歷所有設備並一律解除綁定
    DeviceObject = DriverObject->DeviceObject;
    while(DeviceObject)
    {
       	//解除綁定並刪除所有的設備
        c2pDetach(DeviceObject);
        DeviceObject = DeviceObject->nextDevice;
    }
    ASSERT(NULL == DriverObject->DeviceObject);
    while(gC2pKeyCount)
    {
        KeDelayExecutionThread(KernelMode,FALSE,&lDelay);
    }
    KdPrint("DriverEntry unload OK!\n");
    return;
}

​ 這裏防止請求沒有完成的方法就是使用 gC2pKeyCount。在上述代碼裏 gC2pKeyCount是一個全局變量,每次有一個讀請求到來時,gC2pKeyCount被加一;每次完成時候則減1。於是只有所有請求被完成的時候,才能結束等待。否則無休止的等待下去。

​ 實際上,只有一個鍵被按下時,這卸載過程才結束。在下一節介紹 gC2pKeyCount 的生成。

明日計劃

畢業設計的內容補充

繼續學習驅動編程第八章

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