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<a href="file://driver//i8042prt">\\Driver\\i8042prt</a>&nb
补充:综合编程 , 安全编程 ,