前面两节讲的是通过在设备栈上绑定一个新的设备实现键盘过滤.这是非常正统的方法,是合法软件行为.一般黑客软件不会采用这么正道的方法.
黑客可以通过修改一个已经存在的驱动对象(比如前面提到的KbdClass)分发函数的指针来实现过滤所有请求的目的.
4.5.1获得类驱动对象
首先要获得键盘类驱动对象,才能替换下面的分发函数.这个驱动的名字是"\\Driver\\Kbdclass",可以直接用函数ObReferenceObjectByName来获得.
//驱动名字
#define KBD_DRIVER_NAME L"\\Driver\\Kbdclass"
//存放驱动对象的指针
PDRIVER_OBJECT KbdDriverObject;
UNICODE_STRING uniNtNameString;
//初始化驱动的名字字符串
RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
//根据名字字符串来获得驱动对象
status = ObReferenceObjectByName(&uniNtNameString,OBJ_CASE_INSENSITIVE,
NULL,0,IoDriverObjectType,KernelMode,NULL,&KbdDriverObject);
if(!NT_SUCCESS(status))
{
//如果失败了
DbgPrint("不能获取驱动对象指针\n");
return STATUS_UNSUCCESSFUL;
}
else
{
//凡是调用了Reference系列的函数都要通过调用ObDereferenceObject来解除引用
ObDereferenceObject(KbdDriverObject)
}
4.5.2修改类驱动的分发函数指针
这里用到一个原子操作:InterlockedExchangePointer,设置新的函数指针的操作是原子的,不会被打断.
//这个数组用来保存所有旧的指针
ULONG i
PDRIVER_DISPATCH 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],MyFilterDispatch);
}
4.5.3类驱动之下的端口驱动
前面的过滤方法是填的分发函数指针.介是这依然比较明显,因为分发函数指针本来是已知的,如果安全监控软件有针对性对这个指针进行检查和保护,就容易发现这个指针已经被替换掉的情况.但是从分发函数出发,下面的各个调用层出不穷,任何一个地方都可能被替换,安全程序又怎么可能一一去保护呢?
下面是比邪道的方法:
直接寻找一个用于端口驱动中读取输入缓冲区的函数(但是这个函数实际上 类驱动提供的).Hook这个函数实现过滤.
KbdClass被称为键盘类驱动,在windows中类驱动通常是指统管一类设备的驱动程序.不管是USB键盘,还是PS/2键盘都经过它,所以在这层做拦截,能获得很好以通用性.类驱动之下和实际硬件交互的驱动被称为"端口驱动".具体到键盘,i8042prt是PS/2键盘的端口驱动,USB键盘则是Kbdhid.
键盘驱动的主要工作就是,当键盘上有键按下引发中断时,键盘驱动从端口读出按键的扫描码,最终顺利的将它交给键盘设备栈栈顶等待的那个主功能号为IRP_MJ_READ的IRP.为了完成这个任务,键盘驱动使用了两个循环使用的缓冲区.
i8042prt和KbdClass各有一个自己的可以循环使用的缓冲区.缓冲区的每个单元是一个KEYBOARD_INPUT_DATA结构,用来存放一个扫描码及其相关信息.
在键盘驱动中,这个循环使用的缓冲区统一叫输入数据队列(input data queue),i8042prt的那个缓冲区叫做端口键盘输入数据队列,KbdClass的那个缓冲区叫类输入数据队列.
实际上i8042prt的自定义设备扩展中,保存着一些指针和计数值,用来使用他的输入数据队列.包括:
- PKEYBOARD_INPUT_DATA类型的InputData,DataIn,DataOut,DataEnd.
- ULONG类型的InputCout.
- InputData指针,指向输入数据队列的开头.
- DataEnd指针,指向输入数据队列的结尾.
- DataIn指针,指向要进入队列的新数据,被放在队列中的位置.
- DataOut指针,指向要出队列的数据,在队列中开始的位置.
- InputCount值,为输入数据队列中数据的个数.
同时,在KbdClass的自定义设备扩展中,也保存着一些指针和计数值,用来使用它的输入数据队列.名字和类型与上面的数据完全一样.
4.5.4端口驱动和类驱动之间的协作机制
当键盘上一个键被按下时,产生一个Make Code,引发键盘中断,当键盘上一个键被松开时,产生一个Break Code,引发键盘中断.键盘中断导致键盘中断服务例程被执行,导致最终i8042prt的I8042KeyboardInterruptService被执行.
在i804KeyboardInterruptService中,从端口读出按键的扫描码,存放在一个KEYBOARD_INPUT_DATA中.将这个KEYBOARD_INPUT_DATA放入i804prt的输入数据队列中,一个中断放入一个数据,DataIn后移一格,InputCount加1.最后调用内核API函数KeInsertQueueDpc,一个进行更多处理的延迟过程调用.
这个调用中,会调用上层处理输入的回调函数(也就是KbdClass处理输入数据的函数),取走i8042prt输入数据队列里的数据.之后i8042prt的输入数据列的DataOut相就后移,InputCount相应减少.KbdClass处理输入数据的函数后,满足那个应用层发来的等着读请求.
当读请求要求读的数据大小大于等于i8042prt的输入数据队列中的数据时,读请求的处理函数直接从i804prt的输入数据队列中读出所有输入数据,不使用KbdClass的输入数据队列.
当读请求要求读的数据的大小小于i8042prt的输入数据队列中的数据时,读请求的处理函数直接从i8042prt的输入数据队列中读出它所要求的大小,然后这个读请求被完成.i8042prt的输入数据队列中剩余的数据,被放入KbdClass的输入数据队列中.当应用层发来下一个读请求时,那个读请求将直接从KbdClass的输入数据队列中读取数据,不需要等待.
4.5.5找到关键的回调函数的条件
从上面的原来来看,I8042KeyboardInterruptService中调用的类驱动的那个回调函数非常关键.如果找到了这个函数,通过Hook就可以轻易的获得键盘的输入了.
现在问题就是如何定位这个函数指针了.i8042prt驱动的设备扩展我们并不完全清楚;此外,WDK也不可能公开这样一个函数的地址.
但是"有识之士"根据经验指出:
- 这个函数指针应该保存在i8042prt生成的设备自定义设备扩展中.
- 这个函数的开始地址应该在内核模块KbClass中.
- 内核模块KbClass生成的一个设备对象的指针也保存在那个设备扩展中,而且在我们要找的函数指针之前.
通过下面的代码就可以判断一个地址是否是在KbdClass这个驱动中了
PVOID address;
size_t kdbDriverStart = KdbDriverObject->DriverStart;
size_t kdbDriverSize = KdbDriverObject->DriverSize;
...
if((address>KbdDriverStart)&&(address<(PBYTE)KbdDriverStart+KbdDriverSize))
{
//说明在这个驱动中
}
4.5.6定义常数和数据结构
下面的方法实现了搜索这个关键的回调函数的指针.涉及到如下3个驱动,这里都定义成字符串.
//键盘类驱动的名字
#define KBD_DRIVER_NAME L"\\Driver\\Kbdclass"
#define USBKBD_DRIVER_NAME L"\\Driver\\Kbdhid"
//PS/键盘驱动名
#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};
4.5.7打开两种键盘端口驱动寻找设备
原理是这样的:预先不可能知道机器上装的是USB键盘还是PS/2键盘,所以一开始是尝试打开这两个驱动.在很多情况下只有一个可以打开,比较极端的是两个都可以打开(用户同时装有两个种类的键盘),这并不是不可能的,或者是两个都打不开(用户安装一种我们没见过的各类的键盘).对于这两个极端的情况,都简单地返回失败即可.
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 = 0;
PVOID UsingDeviceExt = NULL;
//这里的代码用来打开USB端口驱动的驱动对象
RtlInitUnicodeString(&uniNtNameString,USBKBD_DRIVER_NAME);
status = ObReferenceObjectByName(&uniNtNameString,OBJ_CASE_INSENSITIVE,NULL,0,IoDriverObjectType,
KernelMode,NULL,(PVOID*)&KbdhidDriverObject);
if(!NT_SUCCESS(status))
{
DbgPrint("获取USB驱动指针失败\n");
}
else
{
ObDereferenceObject(KbdhidDriverObject);
}
//打开PS/2键盘的驱动对象
RtlInitUnicodeString(&uniNtNameString,PS2KBD_DRIVER_NAME);
status = ObReferenceObjectByName(&uniNtNameString,OBJ_CASE_INSENSITIVE,NULL,0,IoDriverObjectType,KernelMode,NULL,
(PVOID*)&Kbd8042DriverObject);if(!NT_SUCCESS(status))
{
DbgPrint("获取PS/2驱动指针失败\n");
}
else
{
ObDereferenceObject(Kbd8042DriverObject);
}
//这段代码只考虑一个键盘起作用的情况.
if(Kbd8042DriverObject && KbdhidDriverObject)
{
DbgPrint("一个以上键盘设备");
return STATUS_UNSUCCESSFUL;
}
//系统用了其它种类的键盘,则返回失败
if(!Kbd8042DriverObject&&!KbdhidDriverObject)
{
DbgPrint("没发现键盘设备");
return STATUS_UNSUCCESSFUL;
}
//找到合适的驱动对象.不管是USB还是PS/2,反正是一定要找到一个的
UsingDriverObject = Kbd8042DriverObject?Kbd8042DriverObject:KbdhidDriverObject;
//找到这个驱动对象下的第一个设备对象
UsingDeviceObject = UsingDriverObject->DeviceObject;
//找到这个设备对象的设备扩展
UsingDeviceExt = UsingDeviceObject->DeviceExtension;
...
}
4.5.8搜索在KbdClass类驱动的地址
接着写前面那个函数中没有完成的代码.目的就是为了寻找UsingDeviceExt中保存的一个在驱动KbdClass中的地址.
//首先必须打开驱动KdbClass,以便从驱动对象中得到其开始地址和大小
RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
status = ObReferenceObjectByName(&uniNtNameString,OBJ_CASE_INSENSITIVE,NULL,0,IoDriverObjectType,KernelMode,NULL,(PVOID*)&KbdDriverObject);
if(!NT_SUCCESS(status))
{
DbgPrint("获取键盘类指针失败");
return STATUS_UNSUCCESSFUL;
}
else
{
ObDereferenceObject(KbdDriverObject);
//如果成功了,就找到了KbdClass的开始地址和大小
KbdDriverStart = KbdDriverObject->DriverStart;
KbdDriverSize = KbdDriverObject->DriverSize;
}
下面就是搜索过程.首先遍历KdbClass下面的所有设备,找到驱动对象下的第一个设备对象,然后找设备对象下的Next指针连续遍历即可.在这些设备中,有一个会保存在端口驱动的设备扩展中(也就是DeviceExt指针开始的地址),这就是我们要寻找的,当然,更重要的是要寻找到那个回调函数.
所以我们定义了一个临时的指针DeviceExt,从前面得到的UsingDeviceExt的地址开始遍历,每次增加一个指针的宽度.
//遍历kbdDriverObject下的设备对象
pTargetDeviceObject = KbdDriverObject->DeviceObject;
PVOID AddrServiceCallBack;//发现原代码有一个Bug,没有考虑UsingDriverObject->Next, 正常应该是再嵌套一个循环//while(UsingDeviceObject)//{// UsingDeviceExt = UsingDeviceObject->DeviceExtension;// while(pTargetDeviceObject)// {// ...// }// UsingDriverObject->Next;//}while(pTargetDeviceObject)
{
PUCHAR DeviceExt = (PUCHAR)UsingDeviceExt;
for(;i<4096;i++,DeviceExt +=sizeof(PUCHAR))
{
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;
continue;
}
//如果在设备扩展上找到一个地址位于KbdClass这个驱动中,就可以认为,这就是我们要找的回调函数地址
if((tmp>KbdDriverStart)&&(tmp<(PBYTE)KbdDriverStart+KbdDriverSize)&&MmIsAddressValid(tmp))
{
//将这个回调函数记录下来
gKbdCallBack.serviceCallBack=(KEYBOARDCLASSSERVICECALLBACK)tmp;
AddrServiceCallBack =(PVOID*)DeviceExt;
}
}
//换成下一个设备,继续遍历
pTargetDeviceObject =pTargetDeviceObject->NextDevice;
}
//如果成功的找好了,就把这个函数替换成我们自己的回调函数
//之后的过滤就可以自己想做什么就做什么了
if(AddrServiceCallBack && gKbdCallBack.serviceCallBack)
{
*AddrServiceCallBack = MyKeyboardClassServiceCallback;
}
return status;
我对这一节的想法:就算成功Hook掉这个回调函数,系统也不会去调用我们的函数,当系统在加载驱动的时候,i8042prt就记住了该回调函数真实地址,在回调的时候,不需要再去查设备扩展里的这个回调函数 地址了.
不过获得这个回调函数还是很有意义的,比如我们可以自己写程序,直接调用这个回调函数发送键盘驱动到上层应用程序,实现模拟键盘操作,跳过一些网络游戏的模拟键盘保护.
以上只是我的推测,如果不对,欢迎批评指正.
下面是我把这一小节修改过后的代码:
#include "ntddk.h"
#include <ntddkbd.h>
//键盘类驱动的名字
#define KBD_DRIVER_NAME L"\\Driver\\Kbdclass"
#define USBKBD_DRIVER_NAME L"\\Driver\\Kbdhid"
//PS/键盘驱动名
#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);
KEYBOARDCLASSSERVICECALLBACK gServiceCallBack;
PULONG gAddrServiceCallBack = NULL;
VOID MyKeyboardClassServiceCallback(
IN PDEVICE_OBJECT DeviceObject,
IN PKEYBOARD_INPUT_DATA InputDataStart,
IN PKEYBOARD_INPUT_DATA InputDataEnd,
IN OUT PULONG InputDataConsumed)
{
DbgPrint("MyKeyboardClassServiceCallback 被调用\n");
gServiceCallBack(DeviceObject,InputDataStart,InputDataEnd,InputDataConsumed);
}
//IoDriverObjectType实际上是一个全局变量,但是头文件中没有,只要声明就可以使用了
extern POBJECT_TYPE IoDriverObjectType;
//下面这个函数是事实存在的,只是文档中没有公开,声明下就可以使用了
NTSTATUS ObReferenceObjectByName
(
PUNICODE_STRING ObjectName,
ULONG Attributes,
PACCESS_STATE AccessState,
ACCESS_MASK DesiredAccess,
POBJECT_TYPE ObjectType,
KPROCESSOR_MODE AccessMode,
PVOID ParseContext,
PVOID *Object
);
NTSTATUS SearchServiceCallBack()
{
//定义用到的一组局部变量.这些变量大多是顾名思义的
int i = 0;
NTSTATUS status = STATUS_UNSUCCESSFUL;
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 = 0;
PULONG UsingDeviceExt = NULL;
//这里的代码用来打开USB端口驱动的驱动对象
RtlInitUnicodeString(&uniNtNameString,USBKBD_DRIVER_NAME);
status = ObReferenceObjectByName(&uniNtNameString,OBJ_CASE_INSENSITIVE,NULL,0,IoDriverObjectType,
KernelMode,NULL,(PVOID*)&KbdhidDriverObject);
if(!NT_SUCCESS(status))
{
DbgPrint("获取USB驱动指针失败\n");
}
else
{
ObDereferenceObject(KbdhidDriverObject);
}
//打开PS/2键盘的驱动对象
RtlInitUnicodeString(&uniNtNameString,PS2KBD_DRIVER_NAME);
status = ObReferenceObjectByName(&uniNtNameString,OBJ_CASE_INSENSITIVE,NULL,0,IoDriverObjectType,KernelMode,NULL,(PVOID*)&Kbd8042DriverObject);
if(!NT_SUCCESS(status))
{
DbgPrint("获取PS/2驱动指针失败\n");
}
else
{
ObDereferenceObject(Kbd8042DriverObject);
}
//这段代码只考虑一个键盘起作用的情况.
if(Kbd8042DriverObject && KbdhidDriverObject)
{
DbgPrint("一个以上键盘设备");
return STATUS_UNSUCCESSFUL;
}
//系统用了其它种类的键盘,则返回失败
if(!Kbd8042DriverObject&&!KbdhidDriverObject)
{
DbgPrint("没发现键盘设备");
return STATUS_UNSUCCESSFUL;
}
//找到合适的驱动对象.不管是USB还是PS/2,反正是一定要找到一个的
UsingDriverObject = Kbd8042DriverObject?Kbd8042DriverObject:KbdhidDriverObject;
//找到这个驱动对象下的第一个设备对象
UsingDeviceObject = UsingDriverObject->DeviceObject;
//找到这个设备对象的设备扩展
UsingDeviceExt = (PULONG)UsingDeviceObject->DeviceExtension;
//首先必须打开驱动KdbClass,以便从驱动对象中得到其开始地址和大小
RtlInitUnicodeString(&uniNtNameString,KBD_DRIVER_NAME);
status = ObReferenceObjectByName(&uniNtNameString,OBJ_CASE_INSENSITIVE,NULL,0,IoDriverObjectType,KernelMode,NULL,(PVOID*)&KbdDriverObject);
if(!NT_SUCCESS(status))
{
DbgPrint("获取键盘类指针失败");
return STATUS_UNSUCCESSFUL;
}
else
{
ObDereferenceObject(KbdDriverObject);
//如果成功了,就找到了KbdClass的开始地址和大小
KbdDriverStart = KbdDriverObject->DriverStart;
KbdDriverSize = KbdDriverObject->DriverSize;
}
- //下面是主要修改的地方,因为用windebug查看后知道,回调函数就存放在对应设备对象的下一个内存地址
while (UsingDeviceObject)
{
pTargetDeviceObject = KbdDriverObject->DeviceObject;
while (pTargetDeviceObject)
{
UsingDeviceExt = (PULONG)UsingDeviceObject->DeviceExtension;
for (i = 0; i < 200; i++)
{
if (UsingDeviceExt[i] == (ULONG)pTargetDeviceObject &&
UsingDeviceExt[i + 1] > (ULONG)KbdDriverStart &&
UsingDeviceExt[i + 1] < (ULONG)KbdDriverStart + KbdDriverSize)
{
gServiceCallBack=(KEYBOARDCLASSSERVICECALLBACK)UsingDeviceExt[i + 1];
gAddrServiceCallBack = &(UsingDeviceExt[i + 1]);
DbgPrint("回调函数存放地址 = %x\n",gAddrServiceCallBack);
DbgPrint("回调函数地址 == 0x%x\n", gServiceCallBack);
*gAddrServiceCallBack=(ULONG)MyKeyboardClassServiceCallback;
DbgPrint("hook后回调函数地址== 0x%x\n", UsingDeviceExt[i + 1]);
return STATUS_SUCCESS;
}
}
pTargetDeviceObject = pTargetDeviceObject->NextDevice;
}
UsingDeviceObject = UsingDeviceObject->NextDevice;
}
//主要修改的地方
return STATUS_UNSUCCESSFUL;
}
void Unload(PDRIVER_OBJECT drv)
{
if (gAddrServiceCallBack)
{
*gAddrServiceCallBack = gServiceCallBack;
}
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg_path)
{
driver->DriverUnload = Unload;
return SearchServiceCallBack();
}