小言_互联网的博客

《Windows内核安全与驱动编程》-第八章-键盘的过滤学习-day1

488人阅读  评论(0)

键盘的过滤

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 的生成。

明日计划

毕业设计的内容补充

继续学习驱动编程第八章


转载:https://blog.csdn.net/qq_38025365/article/details/105739341
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场