第四章 键盘的过滤(1)

4.1.1预备知识
    并不是所有设备都可以通过绑定设备的方法进行过滤,其中硬盘就是一个例子,即使使用了IoAttachDevice,结果还是截获不到任何IRP.
    符号链接是对象的一个别名.
    PDO是物理设备的简称,是设备栈最下面的那个设备对象.
    nt!ObpCreateHandel.这是WinDbg中使用的表示方法,!号前的内容表示模块名,而之后的内容表示函数名或变量名.
    IoGetAttachedDeviceb函数用于获得一个设备所在的设备栈最顶端的那个设备
   
   
PDEVICE_OBJECT IoGetAttachedDevice(IN PDEVICE_OBJECT DeviceObject);
4.1.2Windwos从击键到内核
    应用程序不能通过设备名字来打开设备的,一般都通过符号链接名来打开.
    windows任务管理器中有一个进程叫csrss.exe,它有一个线程叫win32!RawInputThread,这个线程通过一个GUID(GUID_CLASS_KEYBOARD)获得键盘设备栈中的PDO符号链接名.
    win32!RawInputThread执行函数win32!OpenDevice传入PDO符号链接名,打开设备,并返回句柄供ReadFile,DeviceIoCrontrl等使用.
    win32!OpenDevice是调用ZwCreateFile完成打开设备工作的,ZwCreateFile通过系统服务,调用内核中的NtCreateFile.
      NtCreateFile执行到nt!IopParseDevice中调用nt!IoGetAttachedDevice,通过PDO获得键盘设备栈最顶端的设备对象.用这个设备对象作为参数调用函数IoAllocateIrp,创建IRP.调用!ntObCreateObject创建文件对象.调用nt!IopfCallDriver,将IRP发送给驱动,让驱动进行相应处理.
    之后一直返回到nt!ObOpenObjectByName中继续执行,调用nt!ObpCreateHandle在crss.exe进程的句柄表创建一个新的句柄,这个句柄对应的对象就是刚才创建的文件对象,文件对象中的DeviceObject指向键盘设备栈的PDO.
    win32!RawInputTheread在获得句柄后,会以这个句柄为参数,调用nt!ZwReadFile,创建一个IRP_MJ_READ的IRP发送给键盘驱动,要求读入数据.
    键盘驱动通常会使这个IRP Pending,即IRP_MJ_READ不会被满足,它会一直放在那里,等待来自键盘的数据,而发出这个请求的线程win32!RawInputThread也会等待这个读操作的完成.
    当键盘上有键被按下时,将触发键盘的那个中断,引起中断服务例程的执行,键盘中断服务例程由键盘驱动提供.键盘驱动从端口读取扫描码,经过一系列处理后,把数据交给IRP,最后结束这个IRP.
    这个IRP结束,将导致win32!RawInputTherad线程对这个读操作的等待结束,并对得到的数据做出处理,分发给合适的进程,一旦把输入数据处理完之后,win32!RawInputThread线程会立刻调用一个nt!ZwReadFile,向键盘要求读入数据,于是又开始一个等待,等待键盘上的键被按下.

    键盘设备栈的情况是这样的:
  • 最顶层的设备对象是驱动KBdclass生成的设备对象.
  • 中间层的设备对象是驱动i8042生成的设备对象.
  • 最底层的设备对象是驱动ACPI生成的设备对象.
4.1.3键盘硬件原理
    键盘和CPU的交互方式是中断和读取端口,这个操作是串行的.CPU只接收通知并读取端口的扫描码,这个通知只能知通知一个事件:某个键被按下或弹起.
    为此一个键实际需要两个扫描码:一个表示按下,一个表示弹起.如果按下的扫描码为X,则弹起的扫描码为X+0x80.
    键盘端口在xp下端口和中断号都是定死的,即中断号为0x93,商品号为0x60.每次0x93中断发生,CPU都去读取0x60中的扫描码.0x60中只保存一个字节,但扫描码可以有两个字节,此时会发生两次中断,CPU会先后读到扫描码的两个字节.

4.2.1找到所有的键盘设备
    哪果绑定了驱动KbdClass的所有设备对象,则代表键盘的设备一定在其中.驱动对象结构DRIVER_OBJECT下有一个域叫做DeviceObject,这是一个设备对象指针,每个DeviceObject中又有一个域叫NextDevice,指向同一个驱动中的下一个设备.所以这里实际是一个设备链.
    另一种获得驱动下所有设备对象的方法是调用函数IoEnumerateDeviceObjectList,穿上函数可以枚举出一个驱动下所有的设备.

    ObReferenceObjectByName,这个函数通过一个名字获得一个对象的指针.
    下面代码首先打开KbdClass,然后绑定它下面所有的设备:
   
   
//IoDriverObjectType实际上是一个全局变量,但是头文件中没有,只要声明就可以使用了
extern POBJECT_TYPE IoDriverObjectType;
//KbdClass驱动的名字
#define DBD_DRIVER_NAME L"\\Driver\\KbdClass"
//下面这个函数是事实存在的,只是文档中没有公开,声明下就可以使用了
NTSTATUS ObReferenceObjectByName
(
PUNICODE_STRING ObjectName,
ULONG Attributes,
PACCESS_STATE AccessState,
ACCESS_MASK DesiredAccess,
POBJECT_TYPE ObjectType,
KPROCESSOR_MODE AccessMode,
PVOID ParseContext,
PVOID *Object
);
//这个函数经过改造,能打开驱动对象KbdClass,然后绑定它下面的所有设备
NTSTATUS c2pAttachDevice(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;
PDRIVER_OBJECT KbdDriverObject = NULL;
//初始化一个字符串,就是KdbClass驱动的名字
RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
//打开驱动对象
status = ObReferenceObjectByName(&uniNtNameString,OBJ_CASE_INSENSITIVE,NULL,0,
IoDriverObjectType,KernelMode,NULL,(PVOID*)&KbdDriverObject);
//如果失败了就直接返回
if(!NT_SUCCESS(status))
{
DbgPrint("不能获得键盘驱动指针\n");
}
else
{
//调用ObReferenceObjectByName会导致驱动对象的引用计数增加,必须相应地调用ObDereferenceObject
ObDereferenceObject(DriverObject);
}
//这是设备链中的第一个设备
pTargetDeviceObject = KbdDriverObject->DeviceObject;
//现在开始遍历这个设备链
while(pTargetDeviceObject)
{
//生成一个过滤设备,注意第二个参数填入设备扩展结构的长度
status = IoCreateDevice(DriverObject,sizeof(C2P_DEV_EXT),NULL,
pTargetDeviceObject->DeviceType,pTargetDeviceObject->Characteristics,
FALSE,&pFilterDeviceObject);
//如果失败就直接退出
if(!NT_SUCCESS(status))
{
DbgPrint("不能生成虚拟键盘设备\n");
return status;
}
//绑定.pLowerDeviceObject是绑定之后得到的下一个设备也就是前面所说的真实设备
pLowerDeviceObject = IoAttachDeviceToDeviceStack(pFilterDeviceObject,pTargetDeviceObject);
//如果绑定失败,放弃之前的操作,退出
if(!pLowerDeviceObject)
{
DbgPrint("不能绑定设备\n");
IoDeleteDevice(pFilterDeviceObject);
pFilterDeviceObject = NULL;
return status;
}
//设备扩展
devExt = (PC2P_DEV_EXT)(pFilterDeviceObject->DeviceExtension);
c2pDevExtInit(devExt,pFilterDeviceObject,pTargetDeviceObject,pLowerDeviceObject);
pFilterDeviceObject->DeviceType = pLowerDeviceObject->DeviceType;
pFilterDeviceObject->Characteristics = 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;
}
4.2.2应用设备扩展
    在这个键盘过滤中,笔者专门定义了一个结构作为设备扩展:
   
   
typedef struct _C2P_DEV_EXt
{
//这个结构的大小
ULONG NodeSize;
//过滤设备对象
PDEVICE_OBJECT pFilterDeviceObject;
//同时调用时的保护锁
KSPIN_LOCK IoRequestsSpinLock;
//进程间同步处理
KEVENT IoInProgressEvent;
//绑定的设备对象
PDEVICE_OBJECT TargetDeviceObject;
//绑定前的底层设备对象
PDEVICE_OBJECT LowerDeviceObject;
}C2P_DEV_EXT,*PC2P_DEV_EXT;
    c2pDevExtInit,这个函数作用是如何填写设备扩展这个域
   
   
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;
KeInitializeSpinLock(&(devExt->IoRequestsSpinLock));
KeInitializeEvent(&(devExt->IoInProgressEvent),NotificationEvent,FALSE);
devExt->TargetDeviceObject = pTargetDeviceObject;
devExt->LowerDeviceObject = pLowerDeviceObject;
return STATUS_SUCCESS;
}
4.2.3键盘过滤模块的DriverEntry
   
   
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath)
{
ULONG i;
NTSTATUS status;
//这里填写所有分发函数的指针
for(i = 0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
{
DriverObject->MajorFunction[i] = c2pDispatchGeneral;
}
//单独地填写一个Read分发函数.因为重要的过滤就是读取按键信息
DriverObject->MajorFunction[IRP_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;
}
4.2.4键盘过滤模块的动态卸载
    键盘过滤模块的动态卸载和串口过滤稍有不同,这是因为键盘总是处于"有一个读请求没有完成"状态.
    下面是实际中动态卸载的处理
   
   
VOID c2pDetach(PDEVICE_OBJECT pDeviceObj)
{
PC2P_DEV_EXT devExt;
BOOLEAN NoRequstOutStand = FALSE;
devExt = (PC2P_DEV_EXT)(pDeviceObj->DeviceExtension);
IoDetachDevice(devExt->TargetDeviceObject);
devExt->TargetDeviceObject = NULL;
IoDeleteDevice(pDeviceObj);
devExt->pFilterDeviceObject = NULL;
return;
}
  
  
VOID c2pUnload(IN PDRIVER_OBJECT DriverObject)
{
PDEVICE_OBJECT DeviceObject;
PDEVICE_OBJECT OldDeviceObject;
PC2P_DEV_EXT devExt;
LARGE_INTEGER lDelay;
PRKTHREAD CurrentThread;
//delay some time
lDelay = RtlConvertLongToLargeInteger(100*DELAY_ONE_MILLISECOND);
CurrentThread = KeGetCurrentThread();
//把当前线程设置为低实时模式,以便让它的运行尽量少影响其它程序
KeSetPriorityThread(CurrentThread,LOW_REALTIME_PRIORITY);
UNREFERENCED_PARAMETER(DriverObject);
//遍历所有设备并一律解除绑定
DeviceObject = DriverObject->DeviceObject;
while(DeviceObject)
{
c2pDetach(DeviceObject);
DeviceObject = DeviceObject->NextDevice;
}
ASSERT(NULL == DriverObject->DeviceObject);
while(gC2pKeyCount)
{
KeDelayExecutionThread(KernelMode,FALSE,&lDelay);
}
return;
}
    gC2pKeyCount是一个全局变量,每次有一个读请求过来时,gC2pKeyCount被加1,每次完成时,则减1.只有所有请求都完成后,才结束等待,否则无休止等待下去.
    实际上只有一个键被按下时,这个卸载过程才结束.

4.3.1键盘过滤的普通处理
    普通请求的处理是直接发送到真实设备,跳过虚拟设备.
   
   
NTSTATUS c2pDispatchGeneral(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)
{
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(((PC2P_DEV_EXT)DeviceObject->DeviceExtension)->LowerDeviceObject,Irp);
}
    电源相关IRP与普通IRP的Skip处理区别:
  1. 在调用IoSkipCurrentIrpStackLocation之前,先调用PoStartNextPowerIrp.
  2. 用PoCallDriver代替IoCallDriver.
   
   
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);
}
4.3.2 PNP的处理
    当有一个设备被拔出时,则解除绑定,并删除过滤设备
    当PNP请求过来时,是没有必要担心有未完的成IRP的,因为系统要要求卸载设备时,windows自己已经处理掉了所有未决的IRP.
   
   
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:
//首先把请求发下去
IoSkipCurrentIrpStackLocation(Irp);
IoCallDriver(devExt->LowerDeviceObject,Irp);
//然后解除绑定
IoDetachDevice(devExt->LowerDeviceObject);
//删除生成的虚拟设备
IoDeleteDevice(DeviceObject);
status = STATUS_SUCCESS;
break;
default:
IoSkipCurrentIrpStackLocation(Irp);
status = IoCallDriver(devExt->LowerDeviceObject,Irp);
}
return status;
}
4.3.3读的处理
    一个读请求过来时,只是说windows要从键盘驱动读取一个键扫描码值,但是在完成之前显然这个值到底是多少并不清楚.
    要完成请求,可以采用如下步骤:
  1. 调用IoCopyCurrentIrpStackLocationToNext把当前IRP栈空间拷贝到下一个栈空间.
  2. 给这个IRP设置一个完成函数.
  3. 调用IoCallDriver把请求发送到下一个设备.
   
   
NTSTATUS c2pDispatchRead(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)
{
NTSTATUS status = STATUS_SUCCESS;
PC2P_DEV_EXT devExt;
KEVENT waitEvent;
KeInitializeEvent(&waitEvent,NotificationEvent,FALSE);
if(Irp->CurrentLocation == 1)
{
//如果当前设备栈为最底层的物理设备
ULONG ReturnedInformation = 0;
DbgPrint("");
status = STATUS_INVALID_DEVICE_REQUEST;
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = ReturnedInformation;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return status;
}
//全局变量键计数器加1
gC2pKeyCount++;
devExt=(PC2P_DEV_EXT)DeviceObject->DeviceExtension;
//设置回调函数并把IRP传递下去.之后读的处理也就结束了,剩下的任务要等读请求完成
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(Irp,c2pReadComplete,DeviceObject,TRUE,TRUE,TRUE);
return IoCallDriver(devExt->LowerDeviceObject,Irp);
}

4.3.4读完成的处理
   
   
//这是一个IRP完成函数的原型
NTSTATUS c2pReadComplete(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp,IN PVOID Context)
{
ULONG buf_len = 0;
PIO_STACK_LOCATION IrpSp;
PUCHAR buf= NULL;
size_t i;
IrpSp = IoGetCurrentIrpStackLocation(Irp);
//假设这个请求是成功的.
if(NT_SUCCESS(Irp->IoStatus.Status))
{
//获得请求完成之后的输出缓冲区
buf =Irp->AssociatedIrp.SystemBuffer;
//获得缓冲区的长度,不管返回值有多长都保存在Information中
buf_len = Irp->IoStatus.Information;
//简单的打印出所有的扫描码
for(i=0;i<buf_len;++i)
{
DbgPrint("扫描码:%x\r\n",buf[i]);
}
gC2pKeyCount--;
if(Irp->PendingReturned)
{
IoMarkIrpPending(Irp);
}
return Irp->IoStatus.Status;
}
}
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值