zoukankan      html  css  js  c++  java
  • 键盘过滤之内核级Hook(二)

    键盘过滤之内核级Hook(二)

    如果不想让键盘过滤驱动程序或回调函数首先获得按键,则必须比端口驱动更加底层一些。
    早期版本的QQ反盗号驱动的原理是这样的:用户要输入密码时(比如把输入焦点移动到了密码框里),就注册一个中断服务来接管键盘中断,比如0x93中断,之后按键就不关键驱动的事了。
    首先就来介绍一下Hook键盘中断。
    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)));
    }
    利用IOAPIC重定位中断处理函数
    1.什么是IOAPIC
    IOAPIC是可以用于多个核心CPU的新型中断控制器,所以或与应该理解为一种新的可编程硬件。
    IOAPIC的作用在于当一个IRQ发生时,这个硬件将负责决定将IRQ发送给哪个CPU核心,以及以何种形式发送等。IOAPIC是可以编程的,因此可以通过编程,也可以将PS/2键盘的硬件中断请求(IRQ1)发送给某个CPU核心,让核心的IDT中的某个中断号对应中断处理服务来处理。

  • 相关阅读:
    HDU 1813 Escape from Tetris
    BZOJ 2276 Temperature
    BZOJ 4499 线性函数
    BZOJ 3131 淘金
    HDU 5738 Eureka
    POJ 2409 Let it Bead
    POJ 1286 Necklace of Beads
    POJ 1696 Space Ant
    Fox And Jumping
    Recover the String
  • 原文地址:https://www.cnblogs.com/guanlaiy/p/2915325.html
Copyright © 2011-2022 走看看