《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 按下。对应的也可以组合使用。

明日计划

继续驱动编程学习

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
void NotifyKBEvent(wchar_t ch) { SHORT vks = VkKeyScanW(ch); BYTE vk = LOBYTE(vks); BYTE Shift = HIBYTE(vks); if (vk == (BYTE)-1/* || Shift == (BYTE)-1*/) {//UNICODE 字符 INPUT input[2]; input[0].type = INPUT_KEYBOARD; input[0].ki.wVk = 0; input[0].ki.wScan = ch; input[0].ki.dwFlags = 0x4;//KEYEVENTF_UNICODE; input[1].type = INPUT_KEYBOARD; input[1].ki.wVk = 0; input[1].ki.wScan = ch; input[1].ki.dwFlags = KEYEVENTF_KEYUP | 0x4;//KEYEVENTF_UNICODE; SendInput(2, input, sizeof(INPUT)); } else {// if (Shift) { INPUT input[4] = {0}; input[0].type = INPUT_KEYBOARD; input[0].ki.wVk = Shift;//VK_SHIFT; input[1].type = INPUT_KEYBOARD; input[1].ki.wVk = ch; input[2].type = INPUT_KEYBOARD; input[2].ki.wVk = ch; input[2].ki.dwFlags = KEYEVENTF_KEYUP; input[3].type = INPUT_KEYBOARD; input[3].ki.wVk = Shift;//VK_SHIFT; input[3].ki.dwFlags = KEYEVENTF_KEYUP; SendInput(4, input, sizeof(INPUT)); } else { INPUT input[2] = {0}; input[0].type = INPUT_KEYBOARD; input[0].ki.wVk = vks; input[1].type = INPUT_KEYBOARD; input[1].ki.wVk = vks; input[1].ki.dwFlags = KEYEVENTF_KEYUP; SendInput(2, input, sizeof(INPUT)); } } } void NotifyKBEvent(wchar_t* chs) { if (chs == NULL) return ; while(*chs) NotifyKBEvent(*chs++); } void SendKBEvent(WORD wVk, DWORD dwFlags = 0, DWORD dwExtraInfo = 0) { INPUT input[1] = {0}; input[0].type = INPUT_KEYBOARD; input[0].ki.wVk = wVk; input[0].ki.wScan = MapVirtualKey(wVk, 0); input[0].ki.dwFlags = dwFlags; input[0].ki.dwExtraInfo = dwExtraInfo; input[0].ki.time = GetTickCount(); SendInput(1, input, sizeof(INPUT)); } //去掉任务栏图标 和 始终不处于活动状态 ModifyStyleEx(WS_EX_APPWINDOW,WS_EX_TOOLWINDOW | 0x08000000); //初始不活动 SetWindowPos(&CWnd;::wndTopMost, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值