驱动开发之 键盘过滤驱动--Hook键盘中断反过滤

下图是键盘输入流程的主要过程


而现在要说的过滤键盘的方法就是利用图中红色圈起来的部分,改变处理函数的首地址。

寒江独钓中是这样介绍的:

原理:
    计算机硬件往往通过中断来通知cpu某个事件的发生。
    int n可以触发软件中断(软件中断又叫异常),而IDT(中断描述符表)中,每一个中断号在这个表中都有一项,每一项的描述符中可以读到一个函数首地址。
    硬件中断一般被称为IRQ,比如IRQ1一定是PS/2键盘。一个IRQ一般都需要一个中断处理程序来处理,这就需要一个IRQ号到中断号的对应关系。
    IOAIPC的作用在于当一个IRQ发生时,这个硬件将负责决定将IRQ发送给哪个CPU核心,以及以何种形式发送等。IOAIPC是可编程的,因此可以将PS/2键盘的硬件中断请求(IRQ1)发送给某个CPU核心,让该核心的IDT中的某个中断对应的中断处理服务程序来处理。
    所以我们用两种方法来过滤键盘按键:修改IDT,或者修改IOAPIC重定位表。    

  
Hook键盘中断反过滤
      如果不想让键盘过滤驱动程序或回调函数首先获得按键,则必须比端口驱动更加底层一些。
       早期版本的QQ反盗号驱动的原理是这样的:用户要输入密码时(比如把输入焦点移动到了密码框里),就注册一个中断服务来接管键盘中断,比如0x93中断,之后按键就不关键驱动的事了。

1.中断:IRQ和INT


学过计算机体系结构的人都知道硬件往往是通过中断来通知CPU某个事件的发生。比如按键按下了。但是中断并不一定要有任何硬件的通知,一条指令就能使CPU“发生中断”。比如,在一个.c文件写上:
_asm int 3
这样的代码常常来人工设置一个断点,执行到这里程序会中断。int n(n为中断号)可以触发软件中断(软件中断又叫异常),触发的本质是:是CPU的执行暂停,并跳到中断处理函数,中断处理函数已经事先保存在内存中。同时,这些函数的首地址保存在一个叫做IDT(中断描述符表)的表中,每一个中断号在这个表中都有一项。


一旦一个int n被执行,则CPU会到IDT中去查找第n项。其中有一个中断描述符,在这个描述符里可以读到一个函数的首地址,然后CPU就跳到这个首地址去执行了。当然,适当的处理之后一般都会回来继续前面程序的执行。这就是中断的过程。


真正的中断一般被称为IRQ。某个IRQ来自什么硬件,这在很大程度上有规定的。比如IRQ1一定是PS/2键盘,只有少数几个IRQ留给用户自用。一个IRQ一般都需要一个中断函数来处理,但是IRQ没有中断号那么多,只有24个。IRQ的处理也是由中断处理函数来处理的,这就需要一个IRQ号到中断号的对应关系。这样一个IRQ发生时,CPU才知道跳转到哪里。


在IOAIPC出现之后,这个对应关系变得可以修改,在Windows上,PS/2键盘按键或者释放键发生一般都是int0x93,正是因为这个关系(IRQ1->int 0x93)被设置了的原因。
这样我们就有了一个简单的方案可以保护键盘中断:修改int 0x93在IDT中保存的地址。修改为我们自己写的一个函数,那么这个中断一定是我们先截获到,其他的过滤层都在我们之后了。

2.如何修改IDT


在一个应用程序中修改IDT由于权限问题是做不到的,但是在内核程序中做起来是完全可行的。IDT的内存地址是不定的,但是可以通过一条指令sidt获取。下面的代码可以获得中断描述符表的地址。
请注意,在多核CPU上,每一个核心都有自己的IDT。因此,应该注意对每个核心获取IDT。也就是说,必须确保下面的代码在每个核心上都得到执行。


// 由于这里我们必须明确一个域是多少位,所以我们预先定义几个明确知道多少位长度的变量,以避免不同环境下编译的麻烦.
typedef unsigned char P2C_U8;
typedef unsigned short P2C_U16;
typedef unsigned long P2C_U32;

#define P2C_MAKELONG(low, high) \
 ((P2C_U32)(((P2C_U16)((P2C_U32)(low) & 0xffff)) | ((P2C_U32)((P2C_U16)((P2C_U32)(high) & 0xffff))) << 16))

#define P2C_LOW16_OF_32(data) \
 ((P2C_U16)(((P2C_U32)data) & 0xffff))

#define P2C_HIGH16_OF_32(data) \
 ((P2C_U16)(((P2C_U32)data) >> 16))

// 从sidt指令获得一个如下的结构。从这里可以得到IDT的开始地址
#pragma pack(push,1)
typedef struct P2C_IDTR_ {
 P2C_U16 limit;  // 范围
 P2C_U32 base;  // 基地址(就是开始地址)
} P2C_IDTR, *PP2C_IDTR;
#pragma pack(pop)

// 下面这个函数用sidt指令读出一个P2C_IDTR结构,并返回IDT的地址。
void *p2cGetIdt()
{
 P2C_IDTR idtr;
 // 一句汇编读取到IDT的位置。
 _asm sidt idtr
  return (void *)idtr.base;
}
获得IDT的地址之后,这个内存空间是一个数组。每一个元素都有如下结构:
#pragma pack(push,1)
typedef struct P2C_IDT_ENTRY_ {
 P2C_U16 offset_low;
 P2C_U16 selector;
 P2C_U8 reserved;
 P2C_U8 type:4;
 P2C_U8 always0:1;
 P2C_U8 dpl:2;
 P2C_U8 present:1;
 P2C_U16 offset_high;
} P2C_IDTENTRY, *PP2C_IDTENTRY;
#pragma pack(pop)


有些人可能对这种成员变量之后带单个冒号的结构体写法不太习惯。带有冒号的域称为位域。这是这样一种域:这个成员的宽度甚至小于一个字节,只有1~7位。冒号之后的数字表示位数,比如type有4位,always有1位等。
中断服务的跳转地址实际上是一个32位的虚拟地址。但是这个地址被很奇怪地分开保存了,高16位保存在offset_High中,低16位保存在offset_low中。
这里没有中断号,那是应为中断号就是这个表中的索引。因此,第0x93项这个结构,就是读者所需要关心的。

3.替换IDT中的跳转地址


写一个函数来代替那个中断服务地址是可以的,但是请注意这个函数的写法。中断的发生并不是用call跳转过去的,所以也不能通过ret回来。一般的说,中断应该用iret指令返回。但是为了避免更多问题,我们还是处理后跳转原有的中断处理函数入口,让它来替换我们返回比较好。这时我们需要一段不含C编译器生成的函数框架的纯汇编代码。读者可以直接用asm汇编来写,但是笔者在这里使用了C语言嵌入汇编。请注意用__declspec(naked)修饰可以生成一个裸函数。下面这个函数是一个例子:
void *g_p2c_old = NULL;

__declspec(naked) p2cInterruptProc()
{
 __asm
 {
   pushad     // 保存所有的通用寄存器
   pushfd     // 保存标志寄存器
   call p2cUserFilter // 调一个我们自己的函数。这个函数将实现
   // 一些我们自己的功能
   popfd     // 恢复标志寄存器
   popad     // 恢复通用寄存器
   jmp g_p2c_old  // 跳到原来的中断服务程序
 }
}
裸函数中什么都没有,所以也不能使用局部变量,只能全部使用内嵌汇编实现。但是读者大多数还是习惯用C语言的,所以我们可以简单的用汇编来实现一个C函数的调用。C函数可能会改变寄存器的内容,这可能是后面真正的中断处理函数所不期望的。所以在调用的前后,分别保存和恢复这些寄存器。


下面代码直接替换了IDT中的0x93号中断服务,包括获得IDT地址和替换等。但是要注意的是,这些代码只能运行在单核的,32,位操作系统上;如果有多核的话,sidt只能获得当前CPU核IDT。请注意:这个函数不但能替换,也可以完成恢复。


// 这个函数修改IDT表中的第x93项,修改为p2cInterruptProc。
// 在修改之前要保存到g_p2c_old中。
void p2cHookInt93(BOOLEAN hook_or_unhook)
{
 PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
 idt_addr += 0x93;
 KdPrint(("p2c: the current address = %x.\r\n",(void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high)));
 if(hook_or_unhook)
 {
  KdPrint(("p2c: try to hook interrupt.\r\n"));
  // 如果g_p2c_old是NULL,那么进行hook
  g_p2c_old = (void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high);
  idt_addr->offset_low = P2C_LOW16_OF_32(p2cInterruptProc);
  idt_addr->offset_high = P2C_HIGH16_OF_32(p2cInterruptProc);
 }
 else
 {
  KdPrint(("p2c: try to recovery interrupt.\r\n"));
  // 如果g_p2c_old不是NULL,那么取消hook.
  idt_addr->offset_low = P2C_LOW16_OF_32(g_p2c_old);
  idt_addr->offset_high = P2C_HIGH16_OF_32(g_p2c_old);
 }
 KdPrint(("p2c: the current address = %x.\r\n",
  (void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high)));
}

示例代码请见下篇


编写Windows内核程序,就意味着这个程序可以执行任意指令,可以访问计算机所有的软件、硬件资源。因此,稍有不慎就有可能将系统变得不稳定。Windows的设计者设计了各种驱动模型或者框架,如NT式内核驱动模型、WDM框架和新推出的WDF框架。在这些模型框架下编程,就使内核编程变得简单,同样也降低了内核程序崩溃的机会。其实,Windows驱动程序员和黑客都在写内核程序,唯一不同的是驱动程序员按照微软设计的模型写程序,而黑客可以不按照这些框架写。Windows设计的这些框架,可以将操作系统的原理隐藏起来,只暴露一些接口,驱动程序员只要把这些接口写好就可以了。从这个角度看,驱动开发并不难,尤其是读完本书后,更会觉得不难了。但是想完成一些特殊的功能,如内核级隐藏进程等,Windows的这些框架就没什么用处了,程序员就需要对Windows内核有全面的了解,通过直接修改Windows内核来实现这些目的。往往黑客对这种技术乐此不疲,通过修改Windows内核,你会发现你的程序几乎无所不能。   编写内核程序是一件很痛苦的事情,回想起这些年学习内核程序开发的经历,真是感慨万千。就如同谭文所说:编写内核程序的人从某种程度讲是孤独的。当一个经验并不丰富的小程序员面对庞大复杂的并且不开源的Windows框架时,那是一种怎样的无助感啊!谭文是我比较钦佩的程序员之一,他对技术非常执着,并且精力充沛。内核程序的知识涉及面非常广,不同类别的内核程序差别也特别大,他几乎都有所涉猎。相信读者在读完这本书后,能对Windows内核开发有比较详细的了解,同时也能结合书中的实例写出很优秀的内核程序了 本书从Windows内核编程出发,全面系统地介绍了串口、键盘、磁盘、文件系统、网络等相关的Windows内核模块的编程技术,以及基于这些技术实现的输入密码保护、防毒引擎、文件加密、网络嗅探、网络防火墙等信息安全软件的核心组件的具体编程。主要知识重点包括:Windows串口与键盘过滤驱动、Windows虚拟存储设备与存储设备过滤驱动、Windows文件系统过滤驱动、文件系统透明加密/解密驱动、Windows各类网络驱动(包括TDI过滤驱动及3类NDIS驱动),以及最新的WDF驱动开发模型。有助于读者熟悉Windows内核驱动的体系结构,并精通信息安全类的内核编程技术。本书的大部分代码具有广泛的兼容性,适合从Windows 2000一直到目前最新的Windows 7 Beta版。  本书则基本上介绍的是正统的内核编程技术,是微软在内核编程中给信息安全软件开发者提供的相关接口的大集合,是名门正派的技术,不沾邪气。一个好的内核程序员,“正邪兼修”是有必要的。   本书既适合于有志于成为软件程序员的学生使用,也适合于希望加强自己的技术实力的Windows程序员阅读,同时更适合于从事信息安全行业的Windows软件的开发者作为手头参考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值