[内核编程] 4.5 HOOK分发函数
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 + ]; ... ... //把所有的分发函数指针都替换成我们自己编写的同一个分发函数
for( i=; 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 = {};
4.5.7 打开两种键盘端口驱动寻找设备
&
4.5.8 搜索在KdbClass类驱动中的地址
NTSTATUS
SearchServiceCallBack(
IN PDRIVER_OBJECT DriverObject
)
{
//定义用到的一组局部变量。这些变量大多是顾名思义的
NTSTATUS status = STATUS_UNSUCCESSFUL;
int i = ;
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,
,
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,
,
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,
,
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<; 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
);
[内核编程] 4.5 HOOK分发函数的更多相关文章
- 4.5 HOOK分发函数
4.5 HOOK分发函数 本节开始深入的探讨键盘的过滤与反过滤.有趣的是,无论是过滤还是反过 滤,其原理都是进行过滤.取胜的关键在于:谁将第一个得到信息. 黑客可能会通过修改一个已经存在的驱动对象(比 ...
- 《寒江独钓_Windows内核安全编程》中修改类驱动分发函数
最近在阅读<寒江独钓_Windows内核安全编程>一书的过程中,发现修改类驱动分发函数这一技术点,书中只给出了具体思路和部分代码,没有完整的例子. 按照作者的思路和代码,将例子补充完整,发 ...
- hook键盘驱动中的分发函数实现键盘输入数据的拦截
我自己在看<寒江独钓>这本书的时候,书中除了给出了利用过滤的方式来拦截键盘数据之外,也提到了另外一种方法,就是hook键盘分发函数,将它替换成我们自己的,然后再自己的分发函数中获取这个数据 ...
- 《天书夜读:从汇编语言到windows内核编程》六 驱动、设备、与请求
1)跳入到基础篇的内核编程第7章,驱动入口函数DriverEnter的返回值决定驱动程序是否加载成功,当打算反汇编阅读驱动内核程序时,可寻找该位置. 2)DRIVER_OBJECT下的派遣函数(分发函 ...
- windows内核编程之常用数据结构
1.返回状态 绝大部分的内核api返回值都是一个返回状态,也就是一个错误代码.这个类型为NTSTATUS.我们自己写的函数也大部分这样做. NTSTATUS MyFunction() { NTSTAT ...
- 《天书夜读:从汇编语言到windows内核编程》十一 用C++编写内核程序
---恢复内容开始--- 1) C++的"高级"特性,是它的优点也是它的缺点,微软对于使用C++写内核程序即不推崇也不排斥,使用C++写驱动需注意: a)New等操作符不能直接使用 ...
- 《天书夜读:从汇编语言到windows内核编程》五 WDM驱动开发环境搭建
(原书)所有内核空间共享,DriverEntery是内核程序入口,在内核程序被加载时,这个函数被调用,加载入的进程为system进程,xp下它的pid是4.内核程序的编写有一定的规则: 不能调用win ...
- 64位内核开发第二讲.内核编程注意事项,以及UNICODE_STRING
目录 一丶驱动是如何运行的 1.服务注册驱动 二丶Ring3跟Ring0通讯的几种方式 1.IOCTRL_CODE 控制代码的几种IO 2.非控制 缓冲区的三种方式. 三丶Ring3跟Ring0开发区 ...
- windows内核编程基础知识
/* 1.基本的驱动数据结构 //驱动对象结构体 typedef struct _DRIVER_OBJECT { CSHORT Type; //结构类型 CSHORT Size; //结构大小 PDE ...
随机推荐
- 杭电(hdu)2053 Switch Game 水题
Switch Game Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Tota ...
- Qt自定义类型使用QHash等算法(Qt已经自定义了34种类型,包括int, QString, QDate等基本数据类型)
自定义类型 #include <QCoreApplication> #include <QSet> #include <QDebug> class testCust ...
- android图片特效处理之怀旧效果
图片特效处理系列将介绍图片的像素点的特效处理,这些物资注重的是原理.也就是说只要你知道这些算法不管是C++,VB,C#,Java都可以做出相同的特效.下面将介绍图片怀旧效果的算法.算法如下: 上面公式 ...
- elasticsearch java 客户端之action简介
上一篇介绍了elasticsearch的client结构,client只是一个门面,在每个方法后面都有一个action来承接相应的功能.但是action也并非是真正的功能实现者,它只是一个代理,它的真 ...
- while 循环的理解
if 与 while 的主要区别:if 只判断和执行一次,而 while 却代表着一个循环,执行多少次,要视情况而定: 两种情况(A.B)都会让循环体执行: while A or B: 两种情况(A. ...
- js--递归详解
1 函数的调用 eg1:阶乘算法 var f = function (x) { if (x === 1) { return 1; } else { return x * f(x - 1); } }; ...
- 仙人掌的同构(hash)
关于仙人掌的同构,主要是我太蒟蒻了QAQ,问了好几位大佬才弄好. 手撕仙人掌,你得先有手套 ,你得先了解以下基本知识 a.点双连通分量,没什么好说得,仙人掌上有环,判环用点双 b.树的hash点这里 ...
- numpy_basic3
矩陣 矩阵是numpy.matrix类类型的对象,该类继承自numpy.ndarray,任何针对多维数组的操作,对矩阵同样有效,但是作为子类矩阵又结合其自身的特点,做了必要的扩充,比如:乘法计算.求逆 ...
- Gmail 收信的一些规则
Gmail 收信的一些规则 用 apache+php+MDaemon 调试 mail2www 时,发往gmail的邮件失败, 提示: Our system detected an illegal at ...
- mysql中配置ssl_key、ssl-cert、ssl-ca的路径及建立ssl连接(适用于5.7以下版本,5.7及以上请看本文末尾的备注)
1.创建 CA 私钥和 CA 证书 (1)下载并安装openssl,将bin目录配置到环境变量: (2)设置openssl.cfg路径(若不设置会报错,找不到openssl配置文件) \bin\ope ...