文章目录
键盘的过滤
Hook 分发函数
8.5.4 端口驱动和类驱动之间的协作机制
当键盘上一个键被按下时,产生一个 Make Code 引发键盘中断; 当键盘上一个键被松开时,产生一个 Break Code 引发键盘中断。键盘中断导致键盘中断服务例程被执行,最终导致 i8042prt 的 I8042KeyboardInterruptService 被执行。
在 I8042KeyboardInterruptService 中,从端口中读出按键的扫描码。放在一个 KEYBOARD_INPUT_DATA 中。将这个 KEYBOARD_INPUT_DATA 放入 i8042prt 的输入数据队列中,一个中断放入一个数据, DataIn 后移一格, InputCount 加一。(最后会调用内核 API 函数 KeInsertQueueDpc ,进行更多处理的延迟过程调用。)
在这个调用中,会调用上层处理输入的回调函数(即 KbdClass 处理输入数据的函数) 取走 i8042prt 的输入数据队列里的数据。回调函数的指针保存在设备扩展中。上层处理输入的回调函数取走数据之后,i8042prt 的输入数据队列的 DataOut相应后移, InputCount 相应减少。
这里还有两个特性:
- 当读请求要读的数据大于等于 i8042prt 的输入数据队列中的数据时,读请求的处理函数会直接从 i8042prt 的输入数据队列里读出所有的输入数据,不使用 KbdClass 的输入数据队列。在大多数情况下是如此。
- 当读请求要求的数据大小小于 i8042prt 的输入数据队列中的数据时,读请求的处理函数直接从 i8042prt 的输入数据队列中读出它所要求的大小,然后这个读请求被完成。 i8042prt 的输入数据队列中剩余的数据,被放入 KbdClass 输入数据队列中。当应用层发来下一个读请求时,那个读请求将直接从 KbdClass 的输入数据队列中读取数据,不需要等待。
8.5.5 找到关键的回调函数的条件
从上述描述的原理来看,I8042KeyboardInterruptService 中调用的类驱动的那个回调函数非常关键。如果找到了该函数,通过 Hook 、替换或者类似的手段,就可以轻易的获取键盘的输入了。而且这个函数现在非常深入,也并没有被公开。安全软件很难顾及到。
现在的问题就是如何定位这个函数指针了。 i8042prt 驱动的设备扩展我们并不完全清楚;根据经验指出:
- 这个函数指针应该保存在 i8042prt 生成的设备的自定义设备扩展中。
- 这个函数的开始地址应该在内核模块 KbdClass 中。
- 内核模块 KbdClass 生成的一个设备对象的指针也保存在那个设备扩展中,而且在我们要找的函数指针之前。
依据这三个规律就可以来寻找这个函数里。首先第一个问题: 如何判断一个地址是否在一个驱动中?
这里所说的不是驱动对象,而是这个内核模块在内存空间的地址。这是一个常用的技巧: 在驱动对象中 DriverStart 域和 DirverSize 域分别记载着这个驱动对象所代表的内核模块在内核空间中的开始地址和大小。
那么通过下面的代码就可以简单的判断一个地址是否在一个 KbdClass 中了。
PVOID address
//回顾一下这两个变量的数据类型
// PVOID DriverStart;
// ULONG DriverSize;
size_t kbdDriverStart = KbdDriverObject->DriverStart;
size_t kbdDriverSize = KbdDirverObject->DriverSize;
...
if( (address > (PBYTE)kbdDriverStart) && (addrsss<(PBYTE)KbdSDriverStart+KbdDriverSize))
{
//说明该地址在该驱动的内核空间地址内
}
8.5.6 定义常数和数据结构
下面的方法实现了搜索这个关键的回调函数的指针。这些代码考虑的更加宽泛,将 USB 键盘 的情况也考虑进去了。涉及如下三个驱动,这里都定义成字符串。
// 键盘类驱动的名字
#define KBD_DRIVER_NAME L"\\Driver\\KbdClass";
// USB 键盘端口驱动名
#define USBKBD_DRIVER_NAME L"Driver\\kbdhid";
// PS/2 键盘驱动名
#define PS2KBD_DRIVER_NAME L"\\Driver\\i8042prt";
然后,我们要搜索的回调函数的类型定义如下:
typedef VOID (_stdcall *KEYBOARDCLASSSERVICECALLBACL)(
IN PDEVICE_OBJECT DeviceObject,
IN PKEYBOARD_INPUT_DATA InputDataStart,
IN PKEYBOARD_INPUT_DATA InputDataEnd,
IN OUT PULONG InputDataConsumed
);
接下来,定义一个全局变量来接受搜索到的回调函数。实际上,我们不但搜索一个回调函数,还搜索类驱动生成的一个设备对象。这个设备对象的指针保存在端口驱动的设备对象的扩展中。而且必须先找到它,后面才能搜索回调函数(根据前面提到的三个规律)。这个设备对象保存在全局变量 gKbdClassBack.classDeviceObject 中,而 gKbdClassBack.serviceCallBack 则保存要搜索的回调函数。下面是对全局变量 gKbdCallBack 的定义。
typedef struct _KBD_CALLBACK
{
PDEVICE_OBJECT classDeviceObject;
KEYBOADRDCLASSSERVICECALLBACK serviceCallBack;
}KBD_CALLBACK,*PKBD_CALLBACK;
//初始化一个变量
KBD_CALLBACK gKbdCallBack={0};
8.5.7 打开两种键盘端口驱动寻找设备
下面写一个函数来进行搜索,搜索结果将被填写到上面定义的全局变量 gKbdCallBack 中。原理是这样的: 预先不可能知道机器上装的是 USB 键盘 还是 PS/2 键盘 ,所以一开始是尝试打开这两个驱动。在很多情况下之后一个可以打开,比较极端的情况是两个都可以打开 (用户同时安装有两种键盘), 这并不是不可能的,或者是两个都打不开。对于这两种极端的情况,都简单地返回失败即可。
NTSTATUS SearchServiceCallBack(
IN PDRIVER_OBJECT DriverObject
)
{
// 定义用到的一组局部变量。这些变量大多是顾名思义的。
NTSTATUS statsu = STATUS_UNSUCCESSFUL;
int i = 0;
UNICODE_STRING uniNtNameString;
PDRIVER_OBJECT pTargetDeivceObject = NULL;
PDRIVER_OBJECT KbdDriverObject = NULL;
PDRIVER_OBJECT KbdhidDriverObject = NULL;
PDRIVER_OBJECT kbd8042DriverObject = NULL;
PDRIVER_OBJECT UsingDriverObject = NULL;
PDRIVER_OBJECT UsingDeviceObject = NULL;
PVOID KbdDriverStart = NULL;
ULONG KbdDriverSize = 0;
PVOID UsingDeviceExt = NULL;
// 这里的代码用来打开 USB键盘 端口驱动的驱动对象
RtlInitUnicodeString(&uniNtNameString,USBKBD_DRIVER_NAME);
status = ObReferenceObjectByName(
&uniNtNameString,
OBJ_CASE_INSENSITIVE,
NULL,
0,
IoDriverObjectType,
KernelMode,
NULL,
&KbdhidDriverObject
);
if(NT_SUCCESS(status))
{
DbgPrint("Couldn't get the USB driver Object\n");
}
else
{
ObDereferenceObject(KbdhidDriverObject);
DbgPrint("get the USB driver Object\n");
}
// 打开PS/2键盘的驱动对象
RtlInitUnicodeString(&uniNtNameString,PS2KBD_DRIVER_NAME);
status = ObReferenceObjectByName(
&uniNtNameString,
OBJ_CASE_INSENSITIVE,
NULL,
0,
IoDriverObjectType,
KernelMode,
NULL,
&Kbd8042DriverObject
);
if(NT_SUCCESS(status))
{
DbgPrint("Couldn't get the PS/2 driver Object\n");
}
else
{
ObDereferenceObject(Kbd8042DriverObject);
DbgPrint("get the PS/2 driver Object\n");
}
// 这段代码只考虑只有一个键盘起作用的情况。如果两种键盘同时存在,则返回失败
if(kbd8042DriverObject && KbdhidDriverObject)
{
Dbgprint("more than two kbd!\n");
return STATUS_UNSUCCESSFUL;
}
// 如果两个都没有找到...系统用了其他种类的键盘,则直接返回失败
if(!kbd8042DriverObject && !KbdhidDriverObject)
{
Dbgprint("no kbd!\n");
return STATUS_UNSUCCESSFUL;
}
//找到合适的一个。
UsingDriverObject = kbd8042DriverObject? kbd8042DriverObject:kbdhidDriverObject;
//找到这个驱动对象下的第一个设备对象
UsingDeviceObject = UsingDriverObject->DeviceObject;
//找到这个设备对象的设备扩展
UsingDeviceExt = UsingDeviceObject->DeviceExtension;
...
}
现在,已经把设备拓展的地址放到 **UsingDeviceExt** 里面了。根据前面的预测,这里面应该有一个函数指针,其地址是在驱动 **KbdClass** 中的,找到它就完成了。
8.5.8 搜索在 KbdClass 类驱动中的地址
这里面接着写前面那个函数中没有完成的代码。目前的目的已经非常明确。
//首先必须打开驱动 KbdClass,以使从驱动对象中得到其开始地址和大小。
RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
status = ObReferenceObjectByName(
&uniNtNameString,
OBJ_CASE_INSENSITIVE,
NULL,
0,
IoDriverObjectType,
KernelMode,
NULL,
&KbdriverObject
);
if(NT_SUCCESS(status))
{
DbgPrint("Couldn't get the Kbd driver Object\n");
}
else
{
ObDereferenceObject(Kbd8042DriverObject);
//成功找到则获取 KbdClass 的开始地址和大小
KbdDriverStart = KbdDriverObject->DriverStart;
KbdDriverSize = KbdDriverObject->DriverSize;
}
下面就是搜索过程。首先遍历 KbdClass 下的所有设备,找到驱动对象下的第一个设备对象,然后根据设备对象的 Next 指针连续遍历即可。在这些设备中,有一个会保存在端口驱动的设备扩展中。这就是我们要寻找的。
所以我们定义了一个临时的指针 DeviceExt, 从前面得到的 UsingDeviceExt 的地址开始遍历,每次增加一个指针的宽度。
//遍历 KbdDriverObject 下的设备对象
pTargetDeviceObject = KbdDriverObject->DeviceObject;
while(pTargetDeviceObject)
{
DeviceExt = (PBYTE)UsingDeviceExt;
//遍历我们先前找到的端口驱动的设备扩展下的每一个指针
for(;i<4086;i++,DeviceExt+sizeof(PBYTE))
{
PVOID tmp;
if(!MmIsAddressValid(DeviceExt)){
break;
}
//找到后会填写到这个全局变量中。这里检查是否已经填好了
//如果已经填好了就不用继续找,直接跳出
if(gKbdCallBack.classDeviceObject && gKbdCallBack.serviceCallBack)
{
status = STATUS_SUCCESS;
break;
}
//在端口驱动的设备拓展中,找到了类驱动的设备对象,
//填好类驱动设备对象后继续
tmp = *(PVOID*)DeviceExt;
if(tmp == pTargetDeviceObject)
{
gKbdCallBack.classDeviceObject = (PDEVICE_OBJECT)tmp;
DbgPrint(""ClassDeviceObjext %8x\n,tmp);
continue;
}
//如果在设备拓展中找到一个地址位于KbdClass 这个设备驱动中
//可以认为,这就是我们要找的回调函数地址
if(
(tmp > KbdDriverStart)
&& (tmp < (PBYTE)KbdDriverStart + KbdDriverSize)
&& MmIsAddressValid(tmp)
)
{
//将这个回调函数记录下来
gKbdCallBack.serviceCallBack = (KEYBOARDCLASSSERVICECALLBACK)tmp;
AddrServiceCallBack = (PVOID*)DeviceExt;//存放函数地址的地址
DbgPrint("serviceCallBack: %8x\n AddrServiceCallBack: %8x\n",tmp,AddrServiceCallBack")
}
}
//如果没找到,则继续测试下一个设备
pTargetDeviceObject = pTargetDeviceObject->NextDevice;
}
//如果成功的找到了,就把这个函数替换成我们自己的回调函数
//之后的过滤就可以自己任意操作了
if(AddrServiceCallBack && gKbdCallBack.serviceCallBack)
{
DbgPrint("Hook KeyboardClassServiceCallback\n");
*AddrServiceCallBack = MyKeyboardClassServiceCallback;//我们自己的回调函数
}
//至此 该hook函数结束
return status;
不得不说键盘驱动底下东西有点多,明天争取啃下来。。一次搞太多也不好消化。刚刚回去看了一下之前的从 Make Code 到实际按键,其中的 off 偏差作者默认给的0,但是具体是多少还是要我们自己去查一下的。
明日计划
Hook IDT 相关内容,端口操作键盘