键盘过滤

文章目录

  • 键盘过滤
    • 键盘过滤的框架
    • HOOK分发函数
    • HOOK端口驱动的回调函数
    • HOOK键盘中断的反过滤

键盘过滤

PDO:Physical Device Object,字面意义是物理设备,暂时可以这么理解:PDO为设备栈中最下面的那个设备对象

键盘工作原理:P121页;简述:当键盘有按键按下/松开时,将会触发中断处理函数,中断服务例程由键盘驱动提供,键盘的驱动会从端口读取扫描码,经过处理后,将获取到的数据提交给IRP,将会有对应线程对该IRP进行读取获取键盘信息。

一般来说一个PS/2的键盘的设备栈(没有安装其它键盘过滤驱动的话)为:顶层设备对象为KbdClass驱动生成的设备对象,中间层的设备对象是驱动8042prt生成的设备对象,底层设备对象是驱动ACPI生成的设备对象,

无须关心上面的细节,只需要知道一个正规的键盘过滤驱动一般是去绑定KbdClass驱动生成的设备对象。

键盘过滤的框架

  • 找到并绑定所有的键盘设备。

    • KbdClass驱动名为\Driver\Kbdclass,通过ObReferenceObjectByName,可以打开该驱动。
    • 通过设备链遍历KbdClass驱动的设备对象,利用IoCreateDevice创建过滤设备对象,使用IoAttachDeviceToDeviceStack绑定每一个设备即可绑定到所有的键盘设备,将该API返回的参数以设备扩展的方式保存,方便直接下发IRP给下层设备对象。
  • 设置相应的IRP回调,需要关心的有IRP_MJ_READ(因为要过滤的就是读取按键信息)、IRP_MJ_POWER(因为这类请求需要使用PoCallDriver和一个PoStartNextPowerIrp来向下层设备发送IRP)、IRP_MJ_PNP(当设备被插拔时的分发函数)、以及相应的卸载函数。

    • IRP_MJ_READ

      当截到read的irp请求时,应该有一个全局变量计数器对其进行+1操作,目的是为了在卸载驱动的时候,等待该全局变量计数器归0再去卸载,不然会造成irp完成例程还没结束时就卸载的情况,此时就会蓝屏。

      因为读取的IRP还没下发,所以并不知道读取了什么内容,只能通过设置完成例程来获取IRP读取的内容。通过如下代码拷贝栈空间,设置IRP完成例程来读取键盘的内容。

      IoCopyCurrentIrpStackLocationToNext(Irp);
      IoSetCompletionRoutine( Irp, c2pReadComplete, 
              DeviceObject, TRUE, TRUE, TRUE ); 
      

      在完成例程中可以通过如下代码来获取所有读取的内容,也可以修改读取的内容来讲LOCK键改成CTRL键,当完成例程结束时还需要将全局变量-1.

      if( NT_SUCCESS( Irp->IoStatus.Status ) ) 
           {
              // 获得读请求完成后输出的缓冲区
              buf = Irp->AssociatedIrp.SystemBuffer;
      		KeyData = (PKEYBOARD_INPUT_DATA)buf;
              // 获得这个缓冲区的长度。一般的说返回值有多长都保存在
              // Information中。
              buf_len = Irp->IoStatus.Information;
              numKeys = buf_len / sizeof(KEYBOARD_INPUT_DATA);
              //… 这里可以做进一步的处理。我这里很简单的打印出所有的扫
              // 描码。
              //for(i=0;i
      		for(i=0;i<numKeys;++i)
              {
                  //DbgPrint("ctrl2cap: %2x\r\n", buf[i]);
      			DbgPrint("\n");
      			DbgPrint("numKeys : %d",numKeys);
      			DbgPrint("ScanCode: %x ", KeyData->MakeCode ); 
      			DbgPrint("%s\n", KeyData->Flags ?"Up" : "Down" );
      			print_keystroke((UCHAR)KeyData->MakeCode);
      
      			if( KeyData->MakeCode == CAPS_LOCK) 
      			{ 
      				KeyData->MakeCode = LCONTROL; 
      			} 
              }
          }
          //别忘记全局变量还要-1
          gC2pKeyCount--;
      
    • IRP_MJ_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: 
              KdPrint(("IRP_MN_REMOVE_DEVICE\n")); 
      
              // 首先把请求发下去
              IoSkipCurrentIrpStackLocation(Irp); 
              IoCallDriver(devExt->LowerDeviceObject, Irp); 
              // 然后解除绑定。
              IoDetachDevice(devExt->LowerDeviceObject); 
              // 删除我们自己生成的虚拟设备。
              IoDeleteDevice(DeviceObject); 
              status = STATUS_SUCCESS; 
              break; 
      
          default: 
              // 对于其他类型的IRP,全部都直接下发即可。 
              IoSkipCurrentIrpStackLocation(Irp); 
              status = IoCallDriver(devExt->LowerDeviceObject, Irp); 
          } 
          return status; 
      }
      
    • unload

      等待几秒钟,将所有的设备解除绑定,删除所有的过滤设备,等待全局变量归零(所有irp完成例程结束),再返回。

      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); 
          KdPrint(("DriverEntry 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; 
      } 
      

HOOK分发函数

  • 获取类驱动对象,修改类驱动对象的分发函数

    需要HOOK的是键盘类驱动对象的分发函数,这个类驱动对象的名字是KbdClass,所以还是用ObReferenceObjectByName来获取该驱动对象,使用InterlockedExchangePointer这个原子操作的宏来替换原来的分发函数。

    但是这么做呢有几个缺点,一是你要确保在hook的这一刻没有相关irp要处理,不然的话就会关联不上;二个是HOOK这个驱动的分发函数这么做特别容易被杀毒软件发现。

HOOK端口驱动的回调函数

  • 类驱动之下的端口驱动

    • KbdClass称为键盘类驱动,类驱动通常指的是统管一类设备的驱动程序。
    • 在类驱动之下的和实际硬件交互的驱动被称为端口驱动,具体到键盘,i8042prt是PS/2键盘的端口驱动,USB键盘则是Kdbhid。
  • 端口驱动和类驱动之间的协作机制

    • 当键盘上一个按键按下时,会导致中断服务例程被执行最终会导致端口驱动的函数执行,在端口驱动中会从端口读出按键的扫描码放入输入队列中,然后会调用回调函数取走扫描后的数据,该函数由类驱动提供。
  • 找到回调函数的条件

    • 从上面来看,该回调函数地址是在类驱动之中的,由端口驱动调用,根据前辈的经验指出:

      • 这个回调函数的指针应该保存在端口驱动生成的设备的自定义设备扩展中。
      • 这个回调函数的地址在内核模块KbdClass之中。
      • 类驱动KbdClass模块生成的一个设备对象的指针也保存在那个设备扩展中,而且在要找的回调函数之前。

      根据以上规律即可找到这个回调函数了。

  • 该回调函数的原型

    typedef VOID(_stdcall *KEYBOARDCLASSSERVICECALLBACK)
    (IN PDEVICE_OBJECT DeviceObject,
    	IN PKEYBOARD_INPUT_DATA InputDataStart,
    	IN PKEYBOARD_INPUT_DATA	InputDataEnd,
    	IN OUT PULONG InputDataConsumed
    	);
    
  • 打开键盘端口驱动寻找设备。

    • 打开USB键盘端口驱动对象,找到该驱动对象的设备扩展。
    • 打开KbdClass类驱动,获取该驱动模块的基址和大小。
  • 遍历类驱动的设备对象,同时以一个指针大小遍历端口驱动的设备对象的设备扩展。判断是否有指针与类驱动的设备对象的指针相同,同时判断是否有指针指向的地址空间在类驱动范围之类(该指针即是我们hook需要的函数指针)。

  • 修改这个得到的函数指针进行hook。

HOOK键盘中断的反过滤

  • 获取IDT的基址

    // 从sidt指令获得一个如下的结构。从这里可以得到IDT的开始地址
    #pragma pack(push,1)
    typedef struct P2C_IDTR_ {
    	P2C_U16 limit;		// 范围
    	P2C_U32 base;		// 基地址(就是开始地址)
    } P2C_IDTR, *PP2C_IDTR;
    #pragma pack(pop)
    
    // 下面这个函数用sidt指令读出一个P2C_IDTR结构,并返回IDT的地址。
    void *p2cGetIdt()
    {
    	P2C_IDTR idtr;
        // 一句汇编读取到IDT的位置。
    	_asm sidt idtr
    	return (void *)idtr.base;
    }
    

    IDT数组的内存空间表示

    #pragma pack(push,1)
    typedef struct P2C_IDT_ENTRY_ {
    		P2C_U16 offset_low;
    		P2C_U16 selector;
    		P2C_U8 reserved;
    		P2C_U8 type:4;
    		P2C_U8 always0:1;
    		P2C_U8 dpl:2;
    		P2C_U8 present:1;
    		P2C_U16 offset_high;
    } P2C_IDTENTRY, *PP2C_IDTENTRY;
    #pragma pack(pop)
    
  • 找到第IDT的第93项,修改为我们HOOK的函数,同时保存原来的中断函数地址。

    // 这个函数修改IDT表中的第0x93项,修改为p2cInterruptProc。
    // 在修改之前要保存到g_p2c_old中。
    void p2cHookInt93(BOOLEAN hook_or_unhook)
    {
        PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
        idt_addr += 0x93;
        KdPrint(("p2c: the current address = %x.\r\n",
            (void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high)));
        if(hook_or_unhook)
        {
            KdPrint(("p2c: try to hook interrupt.\r\n"));
            // 如果g_p2c_old是NULL,那么进行hook
            g_p2c_old = (void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high);
            idt_addr->offset_low = P2C_LOW16_OF_32(p2cInterruptProc);
            idt_addr->offset_high = P2C_HIGH16_OF_32(p2cInterruptProc);
        }
        else
        {
            KdPrint(("p2c: try to recovery interrupt.\r\n"));
            // 如果g_p2c_old不是NULL,那么取消hook.
            idt_addr->offset_low = P2C_LOW16_OF_32(g_p2c_old);
            idt_addr->offset_high = P2C_HIGH16_OF_32(g_p2c_old);
        }
        KdPrint(("p2c: the current address = %x.\r\n",
            (void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high)));
    }
    

    修改为自己的函数为裸函数,这个裸函数为中间跳板,跳转到真正希望执行的函数之中去。

    __declspec(naked) p2cInterruptProc()
    {
    	__asm
    	{
    		pushad					// 保存所有的通用寄存器
    		pushfd					// 保存标志寄存器
    		call p2cUserFilter	// 调一个我们自己的函数。 这个函数将实现
    								    // 一些我们自己的功能
    		popfd					// 恢复标志寄存器
    		popad					// 恢复通用寄存器
    		jmp	g_p2c_old		// 跳到原来的中断服务程序
    	}
    }
    

    自己实现的函数

    // 首先读端口获得按键扫描码打印出来。然后将这个扫
    // 描码写回端口,以便别的应用程序能正确接收到按键。
    // 如果不想让别的程序截获按键,可以写回一个任意的
    // 数据。
    void p2cUserFilter()
    {
        static P2C_U8 sch_pre = 0;
    	P2C_U8	sch;
    	p2cWaitForKbRead();
        _asm in al,0x60
        _asm mov sch,al
        KdPrint(("p2c: scan code = %2x\r\n",sch));
       //  把数据写回端口,以便让别的程序可以正确读取。
    	if(sch_pre != sch)
    	{
    		sch_pre = sch;
            _asm mov al,0xd2
            _asm out 0x64,al
    		p2cWaitForKbWrite();
            _asm mov al,sch
            _asm out 0x60,al
    	}
    

    键盘不是随时都可以读写数据的,以下的函数可以等到键盘可以读写的时机。

    ULONG p2cWaitForKbRead()
    {
    	int i = 100;
    	P2C_U8 mychar;	
    	do
    	{
            _asm in al,0x64
            _asm mov mychar,al
    	    KeStallExecutionProcessor(50);
    	    if(!(mychar & OBUFFER_FULL)) break;
    	} while (i--);
    	if(i) return TRUE;
    	return FALSE;
    }
    
    ULONG p2cWaitForKbWrite()
    {
    	int i = 100;
    	P2C_U8 mychar;
    	do
    	{
            _asm in al,0x64
            _asm mov mychar,al
    		KeStallExecutionProcessor(50);
    		if(!(mychar & IBUFFER_FULL)) break;
    	} while (i--);
    	if(i) return TRUE;
    	return FALSE;
    }
    

    还有一种HOOK的版本是设置IOAPIC的。这个后面再写。。。

    当然这样的IDT hook有一种缺陷,因为多核CPU有多个IDT表。。。

你可能感兴趣的:(内核与驱动)