当前位置: 代码迷 >> 综合 >> 《Windows内核安全与驱动编程》-第八章-键盘的过滤学习-day2
  详细解决方案

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

热度:85   发布时间:2023-12-04 05:49:09.0

文章目录

  • 键盘的过滤
    • 8.3 键盘过滤的请求处理
        • 8.3.1 通常的处理
        • 8.3.2 PNP的处理
        • 8.3.3 读的处理
        • 8.3.4 读完成的处理
    • 8.4 从请求中打印出按键信息
        • 8.4.2 从 KEYBOARD_INPUT_DATA 中得到的键
        • 8.4.3 从MakeCode 到实际字符
        • 明日计划

键盘的过滤

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

明日计划

继续驱动编程学习

  相关解决方案