前一篇文章讲述了进行键盘过滤,截取用户输入的方法。本篇文章开始更加深入地讨论键盘的过滤与反过滤对抗。无论是过滤还是饭过滤,原理都是过滤,取胜的关键在于谁第一个得到信息。
一种方发是Hook分发函数,即将键盘驱动的分发函数替换成自己的函数用来达到过滤的目的。
一、hook“\\Device\\Kbdclass”驱动的分发函数
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、修改类驱动的分发函数指针
虽然驱动对象不同,但是替换的方法还是一样的。值得注意的,必须保存原有的驱动对象的分发函数;否则,第一,替换之后将无法恢复;第二,完成我们自己的处理后无法继续调用原有的分发函数。除非所有的功能我们我们都编程代替原有驱动的分发函数做了,否则windows的整个键盘输入系统会直接崩溃。
这里用到一个原子操作: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.可能存在的问题
在替换过程中,一部分分发函数已经被替换,另一部分分发函数还没有被替换,此时又刚好有几个连续的irp处理,中间具有相关性。这种情况下可能破坏他们之间的关
联。但这种几率较小,因此要确保函数替换完成之后再有效。
之后就可以编写过滤分发函数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的输入数据队列的