<寒江独钓>Windows内核安全编程__键盘过滤之内核级Hook(一)

 

Hook分发函数

前一篇文章讲述了进行键盘过滤,截取用户输入的方法。本篇文章开始更加深入地讨论键盘的过滤与反过滤对抗。无论是过滤还是饭过滤,原理都是过滤,取胜的关键在于谁第一个得到信息。

一种方发是Hook分发函数,即将键盘驱动的分发函数替换成自己的函数用来达到过滤的目的。

1.获得类驱动对象

首先要获得键盘类驱动对象,才能去替换下面的分发函数。这个操作较为简单,因为这个驱动的名字是“\\Device\\Kbdclass”,所以可以直接用函数ObReferenceObjectByName来获取。

代码如下:

//驱动的名字
#define  KBD_DRIVER_NAME L"\\Driver\\Kdbclass"
//当我们求得驱动对象指针时,将其放到这里
PDRIVER_OBJECT KdbDriverObject;
UNICODE_STRING uniNtNameString;

//初始化驱动的名字字符串
RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
//根据名字字符串来获得驱动对象
status = ObReferenceObjectByName(
					&uniNtNameString,
					OBJ_CASE_INSENSITIVE,
					NULL,
					0,
					IoDriverObjectType,
					KernelMode,
					&KdbDriverObject,
					);
if (!NT_SUCCESS(status))
{
	//如果失败
	DbgPrint("MyAttach:Couldn't get the kbd driver Object\n");
	return STATUS_SUCCESS;
}
else
{
	//凡是调用了Reference系列的函数都要通过调用ObDereferenceObject来解除引用
	ObDereferenceObject(KdbDriverObject);
}


这样就获得了驱动对象,然后只要替换其分发函数就行了。

 

2.修改类驱动的分发函数指针

虽然驱动对象不同,但是替换的方法还是一样的。值得注意的,必须保存原有的驱动对象的分发函数;否则,第一,替换之后将无法恢复;第二,完成我们自己的处理后无法继续调用原有的分发函数。

这里用到一个原子操作:InterlockedExchangePointer.这个操作的好处是,用户设置新的函数指针是原子的,不会被打断。插入其他可能要执行到调用这些分发函数的其他代码

//这个数组用来保存所有旧的指针
ULONG i;
PDRIVER_DISPATCH OldDispatchFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
.....
//把所有的分发函数指针替换成我们自己编写的同一个分发函数
for (i = 0 ; i <= IRP_MJ_MAXIMUM_FUNCTION ; ++i)
{
	//假设MyFilterDispatch是笔者已经写好的一个分发函数
	OldDispatchFunction[i] = KdbDriverObject->MajorFunction[i];
	//进行原子交换操作
	InterlockedExchangePointer(
		        &KbdDriverObject->MajorFunction[i],
				MyFilterDispatch
		);
}


3.类驱动之下的端口驱动

前面的过滤方式是替换分发函数指针。但是这是依然比较明显,因为分发函数的指针本来是已知的,如果安全监控软件有针对性地对这个指针进行检查和保护,就容易发现这个指针已经被替换掉的情况。

KbdClass被称为键盘类驱动,在Windows中,类驱动通常是指统管一类设备的驱动程序。不管是USB键盘,还是PS/2键盘均进过它,所以在这一层做拦截,能获得很好的通用性,类驱动之下和实际硬件交互的驱动被称为“端口驱动”。具体到键盘,i8042prt是PS/2键盘的端口驱动,USB键盘则是Kbdhid。

前面提到,键盘驱动的主要工作就是,当键盘上有按键按下引发中断时,键盘驱动从端口读出按键的扫描码,最终顺利地将它交给在键盘设备栈栈顶等待的那个主功能区号为IRP_MJ_READ的IRP。为了完成这个任务,键盘驱动使用了两个循环使用的缓冲区。

下面以比较古老的PS/2键盘为例进行介绍,因此下面介绍的端口驱动都是i8042prt。

i8042prt和KbdClass各自都有一个可以循环使用的缓冲区。缓冲区的每个单元都是一个KEYBOARD_INPUT_DATA结构,用来存放扫描码及其相关信息。在键盘驱动中,把这个循环使用的缓冲区叫做输入数据队列(input data queue),i8042prt的那个键盘缓冲区被叫做端口键盘输入队列,KbdClass的那个缓冲区被叫做类输入数据队列(class input data queue)。

 

4.端口驱动和类驱动之间的协作机制

当键盘上一个键被按下时,产生一个Make Code,引起键盘中断;当键盘上一个键被松开时,产生一个Break Code,引发键盘中断。键盘中断导致键盘服务例程执行,导致最终i8042prt的I8042KeyboardInterruptService被执行。

在I8042KeyboardInterruptService中,从端口读出按键的扫描码,放在一个KEYBOARD_INPUT_DATA中。将这个KEYBORAD_INPUT_DATA放入i8042prt的输入队列中。

在这个调用中,会调用上层处理输入的回调函数(也就是KbdClass处理输入数据函数),取走i8042prt的输入数据队列的数据。因为设备扩展中保存着上层处理输入数据的回调函数的入口地址,所以他知道该调用谁。上层处理输入的回调函数(也就是KbdClass处理输入数据的函数)取走数据。KbdClass处理输入数据的函数中,满足那个应用层发来的读请求。

 

5.找到关键的回调函数的条件

从上面的原理来看,I8042KeyboardInterruptService中调用的类驱动的那个回调函数非常关键。如果找到这个回调函数,通过Hook,替换或者类似的手段,就可以轻易地获取键盘的输入。而且这个函数非常深入,也没有公开,安全软件很难去顾及。

现在的问题就是如何去定位这个函数指针了。i8042prt驱动的设备扩展我们并不完全清楚;此外WDK也不可能公开这个函数地址,但是“有识之士”根据经验指出:

(1)这个函数指针应该保存在i8042prt生成的设备的自定义设备扩展中。

(2)这个函数的开始地址应该保存在KbdClass中

(3)内核模块KbdClass生成的一个设备对象指针也保存在那个设备扩展中,而且在我们要找到函数之前

有了这3个规律就可以来寻找这个函数了,当然,这里有个问题,即如何判断一个地址是否在某一个驱动中?

这里所说的不是驱动对象,而是这个内核模块在内存空间中的地址。这是一个常用的技巧:在驱动对象中DriverStart域和DriverSize域分别记载着这个驱动对象所代表的内核模块在内核空间中的开始地址和大小。

在前面的代码中,我们已经打开了驱动对象KbdDriverObject,那么KbdDriverObject->DriverStart就是驱动KbdDriverObject的开始地址;KbdDriverObject->DriverSize就是这个驱动的字节大小。

这样,可以通过下面的简单代码判断一个地址是否在kbdClass这个驱动中。

PVOID address;
size_t kbdDriverStart = kbdDriverObject->DriverStart;
size_t kbdDriverSize = kbdDriverObject->DriverSize;
...
if ((address > kbdDriverStart)&&(address < (PBYTE)kbdDriverStart+kbdDriverSize))
{
	//说明在这个驱动中
}


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 InputDataStart,
					  IN PKEYBOARD_INPUT_DATA InputDataEnd,
					  IN OUT PULONG InputDataConsumed
	);


接下来,定义一个局部变量,来接收搜索到的回调函数,实际上我们不但搜索一个回调函数,还搜索类驱动生成的一个设备对象。这个设备对象的指针保存在端口驱动设备对象扩展中,而且必须先找到它,后面才能搜索回调函数。这个设备对象保存在全局变量gKbdClassBack.classDeviceObject中,而gKbdClassBack.serviceCallBack则将保存搜索的回调函数,下面是全局变量gKbdCallBack的定义。

typedef struct _KBD_CALLBACK
{
	PDEVICE_OBJECT classDeviceObject;
	KEYBOARDCLASSSERVICECALLBACK serviceCallBack;
}KBD_CALLBACK,*PKBD_CALLBACK;
KBD_CALLBACK gKbdCallBack = {0};


7.打开两种键盘端口驱动寻找设备

下面开始写一个函数进行搜索,搜索结果被填写到上面定义的全局变量gKbdCallBack中。原理是这样的:预先不可能知道机器上装的是USB键盘还是PS/2键盘,所以一开始是尝试打开这两个驱动。在很多情况下只有一个可以打开,比较极端的情况是两个都可以打开(用户同时安装有两个种类的键盘),或者两个都打不开。

NTSTATUS
SearchServiceCallBack(IN PDRIVER_OBJECT DriverObject)
{
	//定义用到的一组全局变量,这些变量大多数是顾名思义的
	NTSTATUS status = STATUS_SUCCESS;
	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;
	PDRIVER_OBJECT UsingDeviceObject = NULL;
	ULONG KbdDriverSize = 0;
	PVOID UsingDeviceExt = NULL;

	//这里的代码用来打开USB键盘端口驱动的驱动对象
	RtlInitUnicodeString(&uniNtNameString,USBKBD_DRIVER_NAME);
	status = ObReferenceObjectByName(
		                   &uniNtNameString,
						   OBJ_CASE_INSENSITIVE,
						   NULL,
						   0,
						   IoDriverObjectType,
						   KernelMode,
						   NULL,
						   &KbdDriverObject);
	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");
	}

	//这段代码考虑有一个键盘起作用的情况。如果USB键盘和PS/2键盘同时存在,直接返回失败即可
	if (Kbd8042DriverObject && KbdhidDriverObject)
	{
		DbgPrint("more than two kbd!\n");
		return STATUS_UNSUCCESSFUL;
	}
	//如果两个设备都没有找到
	if (!Kbd8042DriverObject && !KbdhidDriverObject)
	{
		DbgPrint("no kbd!\n");
		return STATUS_SUCCESS;
	}
	//找到合适的驱动对象,不管是USB还是PS/2,反正一定要找到一个
	UsingDriverObject = Kbd8042DriverObject?
            Kbd8042DriverObject:KbdhidDriverObject;
	//找到这个这个驱动对象的下一个设备对象
	UsingDeviceObject = UsingDriverObject->DeviceObject;
	//找到这个设备对象的设备扩展
	UsingDeviceExt = UsingDeviceExt->DeviceExtension;
	......

}


 

8.搜索KbdClass类驱动中的地址

这里接着写前面那个函数中没有完成的代码。目的已经明确,就是为了寻找UsingDeviceExt中保存的一个驱动KbdClass中的地址。

   RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
   status = ObReferenceObjectByName(
	                      &uniNtNameString,
						  OBJ_CASE_INSENSITIVE,
						  NULL,
						  0,
						  IoDriverObjectType,
						  KernelMode,
						  NULL,
						  &KbdDriverObject);
   if (!NT_SUCCESS(status))
   {
	   //如果没有成功,直接返回即可
	   DbgPrint("MyAttach: Coundn't get the kbd driver Object\n");
	   return STATUS_UNSUCCESSFUL;
   }
   else
   {
	   ObDereferenceObject(KbdDriverObject);
	   //如果成功了,就找到KbdClass的开始地址和大小
	   KbdDriverStart = KbdDriverObject->DriverStart;
	   KbdDriverSize->kbdDriverObject->DriverSize;
   }


下面就是搜索过程,首先遍历KbdClass下面的所有设备,找到驱动对象下的第一个设备,然后找设备对象下的Next指针连续遍历即可。在这些设备中,有一个会保存在端口驱动的设备扩展中,这是我们要寻找的。当然,更重要的是寻找那个回调函数。

//遍历KbdDriverObject下的设备对象
   pTargetDeviceObject = KbdDriverObject->DeviceObject;
   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 = (KEYBOADCLASSSERVICECALLBACK)tmp;
			   AddServerCallBack = (PVOID*)DeviceExt;
			   DbgPrint("serviceCallBack: %8x
                          AddrServiceCallBack: %8x\n",
							tmp,AddrServiceCallBack);

		   }
	   }
	   //换成下一个设备,继续遍历
	   pTargetDeviceObject = pTargetDeviceObject->NextDevice;
   }

   //如果成功找到了,就把这个函数替换成我们自己的回调函数
   if (AddrServiceCallBack && gKbdCallBack.serviceCallBack)
   {
	   DbgPrint("Hook keyboradClassServiceCallback\n");
	   *AddrServiceCallBack = MyKeyboardClassServiceCallBack;
   }
   return status;


 

你可能感兴趣的:(编程,windows,object,input,扩展,hook)