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

鍵盤的過濾

8.3 鍵盤過濾的請求處理

8.3.1 通常的處理

​ 最通常的處理就是直接發送到真實設備,跳過虛擬設備的處理。這和前面串口過濾用過的方法一樣。代碼如下:

NTSTATUS c2pDispatchGeneral(
	IN PDEVICE_OBJECT DeviceObject,
	IN PIRP Irp
)
{
    //一般的分發函數,直接skip,然後用 IoCallDriver 將 IRP 發送到真實設備的設備對象
    KdPrint(("Other Dispatch!"));
    IoSkipCurrentIrpStackLocation(Irp);
    return IoCallDriver(((PC2P_DEV_EXT)DeviceObject->DeviceExtension)->LowerDeviceObject,Irp);
}

​ 這裏與串口那裏有明顯的不同。我們不用再遍歷一個數組去尋找真實設備的設備對象指針了。而是直接使用了設備拓展中預先已經保留的指針。接下來再是對電源 IRP 的處理。

NTSTATUS c2pPower(
	IN PDEVICE_OBJECT DeviceObject,
	IN PIRP Irp
)
{
    PC2P_DEV_EXT devExt;
    devExt = (PC2P_DEV_EXT)DeviceObject->DeviceExtension;
    PoStartNextPowerIrp(Irp);
    IoSkipCurrentIrpStackLocation(Irp);
    return PoCallDriver(devExt->LowerDeviceObject,Irp);
}

8.3.2 PNP的處理

​ 唯一需要處理的是,當有一個設備被拔出的時候,解除綁定,並刪除過濾設備。代碼的實現大致如下:

NTSTATUS c2pPnP(
	IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp
)
{
	PC2P_DEV_EXT devExt;
	PIO_STACK_LOCATION irpStack;
	NTSTATUS status = STATUS_SUCCESS;
	KIRQL oldIrql;
	KEVENT event;
	
	//獲得真實設備
	devExt = (PC2P_DEV_EXT)DeviceObject->DeviceExtension;
	irpStack = IoGetCurrentIrpStackLocation(Irp);
	
	switch(irpStack->MinorFunction)
    {
    case IRP_MN_REMOVE_DEVICE:
    	KbPrint(("IRP_MN_REMOVE_DEVICE\n"));
    	
    	//首先把請求發下去
    	IoSkipCurrentIrpStackLocation(Irp);
    	IoCallDriver(devExt->LowerDeviceObject,Irp);
    	//解除綁定
    	IoDetachDevice(devExt->LowerDeviceObject);
    	//刪除我們生成虛擬設備
    	IoDeleteDevice(DeviceObject);
    	status = STATUS_SUCCESS;
    	break;
    defaule:
    	//對於其他類型的 IRP,全部都直接下發即可。
    	IoSkipCurrentIrpStackLocation(Irp);
    	status = IoCallDriver(devExt->LowerDeviceObject,Irp);
    }
    return status;
}

​ 當 PNP 請求過來時,不必擔心還有未完成的 IRP。這是因爲 Windwos 系統要求卸載設備時,Windows 自己應該已經處理了所有未決的 IRP。上述PNP 即拔出設備時,要求卸載該設備對象。

8.3.3 讀的處理

​ 當一個讀請求到來時候,只是說 Windwos 要從鍵盤驅動讀取一個鍵掃描碼值,但是在完成之前顯然這個值是多少我們不清楚。本章要過濾的目的,就是要獲得按下了什麼鍵,所以不得不換一種處理方法,即把這個請求下發之後,再去看這個值是多少。

​ 要完成請求,可以採用如下的步驟。

  1. 調用 IoCopyCurrentIrpStackLocationToNext 把當前棧空間拷貝到下一個棧空間(這與前面的調用 IoSkipCurrentIrpStackLocation 跳過當前棧空間形成對比)
  2. 給這個 IRP 設置一個完成函數,即回調函數。如果這個 IRP 完成了,系統就會回調這個函數。
  3. 調用 IoCallDriver 把請求發送到下一個設備。

​ 另外一個需要解決的問題就是我們前面所需要的一個鍵計數器。即一個請求來到則加一,完成就減1。這個處理比較簡單。完整的讀處理請求如下:

NTSTATUS c2pDispatchRead(
	IN PDEVICE_OBJECT DriverObject,
	IN PIRP Irp
)
{
    NTSTATUS status = STATUS_SUCCESS;
    PC2P_DEV_EXT devExt;
    PIO_STACK_LOCATION currentIrpStack;
    KEVENT waitEvent;
    KeInitializeEvent(&waitEvent,NotificationEvent,FALSE);
    
    if(Irp->CurrentLocation == 1)//判斷是否到達了irp棧的最低端,屬於錯誤處理
    {
        ULONG ReturnedInformation = 0;
        KdPrint(("Dispatch encountered bogus current location\n"));
        status = STATUS_INVALID_DEVICE_REQUEST;
        Irp->IoStatus.Status = status;
        Irp->Iostatus.Information = ReturnedInformation;
        IoCompleteRequest(Irp,IO_NO_INCREMENT);
        return status;
    }
    //全局變量鍵計數器加一
    gC2pKeyCount++;
    //得到設備拓展,目的爲了得到下一個設備的指針。
    devExt = (PC2P_DEV_EXT)DeviceObject->DeviceExtension;
    //設置回調函數並把IRP傳遞下去。之後讀的處理就結束了,等待請求完成。
    currentIrpStack = IoGetCurrentIrpStackLocation(Irp);
    IoCopyCurrentIrpStackLocationToNext(Irp);
    IoSetCompletionRoutine(Irp,c2pReadComplete,DeviceObject,TRUE,TRUE,TRUE);
    return IoCallDriver(devExt->LowerDeviceObject,Irp);
}

8.3.4 讀完成的處理

​ 讀請求完成之後,應該獲得輸出緩衝區,按鍵信息就在輸出緩衝區中,全局變量gC2pKeyCount應該減1.此外,就沒有其他的事情需要做的。所以相關代碼比較簡單。大致如下:

//IRP回調函數的原型
NTSTATUS c2pReadComplete(
	IN PDEVICE_OBJECT DeviceObject,
	IN PIRP Irp,
	IN PVOID Context
)
{
    PIO_STACK_LOCATION IrpSp;
    ULONG buf_len = 0;
    PUCHAR buf = NULL;
    size_t i;
    
    IrpSp = IoGetCurrentIrpStackLocation(Irp);
    
    //假設這個請求是成功的。
    if(NT_SUCCESS(Irp->IoStatus.Status))
    {
        buf = Irp->AssocitatedIrp.SystemBuffer;
        //獲取該緩衝區長度
        //一般來說,不管返回值多長都保存在Information中
       buf_len = Irp->Iostatus.Information;
       //...這裏讀者可以自定義處理,作者只是打印出了掃描碼
       for(i=0;i<buf_len;++i)
       {
           DbgPrint("ctrl2cap: %2x\r\n",buf[i]);
       }
    }
    gC2pKeyCount--;
    if(Irp->PendingReturned)
    {//所有的Irp完成函數裏都應包含這一句,作用爲告訴系統,我異步返回了,因爲上面可能在等待這個IRP的完成,請你在IRP完成的時候告訴我IRP完成了。
        IoMarkIrpPending(Irp);
    }
    return Irp->Iostatus.Status;
}

​ 這裏我們得到了輸出緩衝區,按鍵信息當然也在其中。但是這些信息是什麼格式保存的?又如何從這些信息裏打印出按鍵的情況呢?在下面的內容中進一步說明。

8.4 從請求中打印出按鍵信息

​ 在完成函數中能完成的任務有限,這是因爲受到中斷級別的限制。但是本章在完成函數中僅僅需要讀取一個鍵掃描碼,任務比較簡單,所以相關知識首先屏蔽。在後面磁盤過濾和文件過濾時我們會注意到不同之處。

​ 請求完成之後,讀到的信息在 irp->AssociatedIrl.SystemBuffer 中。這裏需要介紹一下這個緩衝區的數據格式。在這個緩衝區中可能含有 n 個 KEYBOARD_INPUT_DATA 結構。該結構定義如下:

typedef struce _KEYBOARD_INPUT_DATA{
    // 頭文件裏的解釋是這樣的;對於設備 \Devcie\KeyboardPort0,這個值是0;
    // 對於\Device\KeyboardPort1,這個值是1;以此類推
    USHORT UnitId;
    // 掃描碼
    USHORT MakeCode;
    // 一個標誌。標誌這是一個鍵按下還是彈起
    USHORT Flags;
    // 保留
    USHORT Reserved;
    // 擴展信息
    ULONG ExtraInformation;
}KEYBOARD_INPUT_DATA,*PKEYBOARD_INPUT_DATA;

​ 下面是 Flags 可能的值。老實的說,這些值的含義作者也不清楚,我們需要字節結合後面的代碼來理解。

#define KEY_MAKE 0
#define KEY_BREAK 1
#define KEY_E0 2
#define KEY_E1 4
#define KEY_TERMSRV_SET_LED 8
#define KEY_TERMSRV_SHADOW 0X10
#define KEY_TERMSRV_VKPACKET 0X20

​ 至於有多少個這樣的結構,則取決於輸入緩衝區到底多長,實際上,這種結構的個數應該爲:

size = buf_len/sizeof(KEYBOARD_INPUT_DATA);

8.4.2 從 KEYBOARD_INPUT_DATA 中得到的鍵

KEYBOARD_INPUT_DATA 下的 MakeCode 就是掃描碼。對於 Flags ,這裏的代碼只是考慮了 KEY_MAKE(0)KEY_BREAK(非0) 兩種可能: 一種表示按下;另一種則表示彈起。相關的代碼如下:

KeyData = Irp->AssociatedIrp.SystemBuffer;
numKeys = Irp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);

for(i=0;i<numKey;i++)
{
    //下面打印按鍵的信息
    DbgPrint("numKeys: %d,numKey");
    DbgPrint("ScabCideL %x,KeyData->MakeCode");
    DbgPrint("%s\n",KeyData->Flags ? "UP":"Down");
    MyPrintKeyStroke((UCHAR)KeyData->MakeCode);
    
    //這是一個小測試,如果發現有 Caps Lock 鍵,我們就改寫Ctrl鍵。證明鍵盤按鍵是可以被攔截修改的,其效果是 Caps Lock 可以起到和 Ctrl 一樣的作用。
    if(KeyData->MakeCode == CAPS_LOCK)
    {
        KeyData->MakeCode = LCONTROL;
    }
}

​ 應該注意到,有幾個鍵會英雄從掃描碼到實際字符的轉換。

8.4.3 從MakeCode 到實際字符

​ 本節盡力把按鍵現實成可以顯示的字符。這涉及掃描碼和實際的字符是如何對應的。

​ 所謂的實際字符是 ASCII 碼。大家都知道大小寫的 ASCII 碼並不相同,但是鍵是同一個。即掃描碼是相同的,具體是取決於幾個鍵盤的狀態(包括 shitf 、 Caps Lock)。因此,這個模塊在過濾按鍵的同時,也必須把這幾個控制鍵的狀態保存下來。尤其注意 Shift 鍵是按下生效,而 Caps Lock 是每按一次切換一次狀態。因此過濾方法不同。

//鍵按下的狀態
#define S_SHITF 1
#define S_CAPS  2
#define S_NUM	4

//一個標誌,用來保存鍵盤當前的狀態。其中有三個位分別表示
//Caps Lock  Num Lock 和 Shitf 是否按下了
static int kb_status = S_NUM;
void __stdcall print_keystroke(UCHAR sch)
{
    UCHAR 	ch = 0;
    int 	off = 0;
    
    if((sch & 0x80 ) == 0)	//如果是按下
    {
        //如果按下了字母或者數字等可見字符
        if((sch<0x47) || ((sch>=0x47 && sch<0x54) && (kb_status & S_NUM)))
        {
            //最終得到的字符必須由 定義的三個鍵狀態決定。所以卸載一張表中
            ch = asciiTbl[off+sch];
        }
        switch(sch)
        {
            //Caps Lock 與 Num Lock 都是按下兩次等於沒按過,所以用異或來設置標誌
        case 0x3A:
        	kb_status ^= S_CAPS;
        	break;
        	//shift 則是左右各一個 使用不同的碼。但是作用相同。按下時起作用,彈起則消失作用。所以使用或來設置標誌
        case 0x2A:
        case 0x36:
        	kb_status |= S_SHIFT;
        	break;
        	//Num Lock 鍵
        case 0x45:
        	kb_status ^= S_NUM;
        }
    }
    else	//彈起
    {
        if (sch == 0xAA || sch == 0xB6)
        	//即如果按下了 shitf 就 恢復狀態
        	kb_status &= ~S_SHIFT;
    }
    if(ch >= 20 && ch < 0x7F)
    {
        DbgPrint(%C \n,ch);
    }
}

​ 這裏使用了很多位運算,不熟悉二進制的可能看着比較難懂。定義的 4 2 1 用二進制表示分別爲 100 010 001 所以是不同的標誌位。而初始化的 status = 4(100) 則默認表示開啓了數字鍵盤。後續用異或取反等操作,從這三位二進制數字的角度去看就很容易理解了。三位二進制,每一位取1則代表對應的鍵被按下。比如100(4) 是 Num Lock 按下。010(2) 是 Caps Lock 按下。001(1) 則是 Shift 按下。對應的也可以組合使用。

明日計劃

繼續驅動編程學習

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