文章目錄
鍵盤的過濾
8.1 技術原理
8.1.1 預備知識
何爲符號鏈接?符號鏈接其實就是一個“別名”,可以用一個不同的名字來代表一個設備對象。
ZwCreateFile 是很重要的函數。同名的函數實際上有兩個: 一個在內核中,一個在應用層。所以在應用程序中直接調用 CreateFile,就可以引發對這個函數的調用。它不但可以打開文件,而且可以打開設備對象(返回得到一個類似於文件句柄的句柄)。所以後面按常常會看到應用程序爲了與內核交互而調用這個函數,這個函數最終調用 NtCreateFile。
何爲 PDO? DO 是 Driver Object 的簡稱, PDO 是 Phsiycal 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 的生成。
明日計劃
畢業設計的內容補充
繼續學習驅動編程第八章