当前位置: 代码迷 >> 综合 >> 《Windows内核安全与驱动编程》-第八章-键盘的过滤学习-day5
  详细解决方案

《Windows内核安全与驱动编程》-第八章-键盘的过滤学习-day5

热度:21   发布时间:2023-12-04 05:46:04.0

文章目录

  • 键盘的过滤
    • 8.6 Hook键盘中断反过滤
        • 8.6.1 中断: IRQ和NT
        • 8.6.2 如何修改 IDT
        • 8.6.3 替换 IDT 中的跳转地址
    • 8.7 直接使用端口操作键盘
        • 8.7.1 读取键盘数据和命令端口
        • 明日计划

键盘的过滤

8.6 Hook键盘中断反过滤

? 如果不想让键盘驱过滤驱动程序或回调函数首先获得按键,则必须比端口驱动更加底层一些。举一个例子:

? 早期版本的 QQ 反盗号驱动原理是这样的: 在用户要输入密码时(比如将输入焦点移动到了密码框里), 就注册一个中断服务来接管键盘中断,比如 0x93 中断,之后按键就不关键盘驱动的事了。为此这个程序必须自己处理那些扫描码,并得出用户输入了什么密码,然后交给QQ。这个过程就很难被截获了。

8.6.1 中断: IRQ和NT

_asm int n

? 我们常常用这样的代码去人工的设置一个断点,比如常见的 int 3int n 可以触发软件中断(软件中断又叫异常),触发的本质是:使 CPU 的执行暂停,并跳转到中断处理函数中,中断处理函数已经实现保存在内存中。同时,这些函数的首地址保存在一个叫做 IDT(中断描述符表) 的表中,每一个中断号都在这个表中有一项。

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

? 真正的硬件中断一般被称为 IRQ 。某个 IRQ 来自什么硬件是有规定的。比如 IRP1 一定是 PS/2键盘 ,只有少数几个 IRQ 留给用户自用。一个 IRQ 一般都需要一个中断处理函数来处理,但是 IRQ 并没有中断号那么多。根据文档, 可编程的 IRQ 只有24 个。IRQ 的处理也是由中断处理函数来处理的,这就需要一个 IRQ 号到中断号的对应关系。这样当一个 IRQ 发生时,CPU 才知道要跳转去哪里执行。

? 在 IOAIPC 出现之后,这个对应关系变得可以修改了。 在 Windows上, PS/2键盘 按键或者释放键发生一般都是 int 0x93, 正因为这个关系( IRQ1->int 0x93 )被设置了。

? 这样就有了一个简单的方案可以保护键盘: 修改 int 0x93IDT 中保存的函数地址。修改为我们自己写的一个函数。那么这个中断一定是我们先截获到,其他的过滤层都在我们之后了。

8.6.2 如何修改 IDT

? 由于权限问题,在一个应用程序中修改 IDT 是做不到的,但是在内核程序中缺完全是可以做到的。 IDT 的内存地址是固定不变的,可以通过一条指令 sidt 获取。

? 注意,在多核 CPU 上,每个核心都有自己的 IDT,因此,应该注意对每个核心获取 IDT。即要保证下面的代码在每个核心上都得到执行。

//由于这里必须明确一个域是多少位,所以我们预先定义几个
//明确知道多少位长度的变量,以避免不同环境下的编译的麻烦
typedef unsigned char P2C_U8;
typedef unsigned short P2C_U16;
typedef unsigned long P2C_U32;//通过 sidt 指令获得一个如下结构。从这里可以得到 IDT 的开始地址
//注意数据结构使用1字节对齐,避免对齐问题导致数据结构内容错位
//这是给编译器用的参数设置,有关结构体字节对齐方式的设置大概是指把原来对答齐方式设置压栈,并设新的设置为1
#pragma pack(push,1)
typedef struct P2C_IDTR_
{
    P2C_U16 limit;		//范围P2C_U32 base		//开始地址
}P2C_IDTR,*PPP2C_IDTR
#pragma pack(pop)//下面这个函数用 sidt 指令读出一个 P2C_IDTR 结构,并返回 IDT 的地址
void *p2cGetIdt()
{
    P2C_IDTR idtr;//一句汇编指令读取到 IDT 的位置_asm sidt idtrreturn (void *)idtr.base
}

? 获得了 IDT 的地址后,这个内存空间是一个数组,每个元素都有如下结构:

#pragma pack(push,1)
typedef struct P2C_IDT_ENTRY_ {P2C_U16 offset_low;PC2_U16 selector;P2C_U8 reserved;P2C_U8 type:4;P2C_U8 alaways0:1;P2C_U8 dpl:2;P2C_U8 present:1;P2C_U16 offset_high;
}P2C_IDTENTRY,*PP2C_IDRENTRY;
#pramga pack(pop)

? 这种成员变量后带单个冒号的结构体域称位位域。这是这样一种域: 这个成员的宽度至少1字节,只有1~7位。冒号之后的数字表示位数。比如 type alaways dpl present 分别有4 1 2 1位,它们加起来共有八位,所以他们实际占的空间为1字节。 显然,这是一种C语言的强大,其他语言很少能这样表示。

? 中断服务的跳转地址实际上是一个 32 位的虚拟地址,但是这个地址被很奇特的愤慨保存。高16位保存在 offset_high 中,相应的还有低16位。

? 这里没有中断号,那是因为中断号就是这个表的索引。因此,第 0x93 项这个结构,就是我们要找的。

8.6.3 替换 IDT 中的跳转地址

? 写一个函数来代替那个中断服务地址是可以的,但是需要注意这个函数的写法。中断的发生并不是直接用 call 跳转过去的。所以也不能通过 ret 回来。一般来说,中断应该用 iret 指令返回。但是为了避免更多的问题,我们还是处理后跳转到原有的中断处理函数入口,让它来代替我们返回比较好。这时我们需要一段不含C编译器生成的函数框架的纯汇编代码。我们可以使用 ASM 汇编来写,也可以使用 C 语言内嵌汇编。

? 使用 __declspec (naked) 修饰可以生成一个裸函数。 MS 的 C 编译器不会再生成函数框架指令。下面给出例子:

__declsppec(naked) p2cInterruptProc()
{__asm{pushad					//保存所有的通用寄存器pushfd					//保存标志寄存器call p2cUserFilter		//调用我们自己的函数popfd					//返回标志寄存器popad					//返回通用寄存器jmp g_p2c_old			//跳到原来的中断服务程序}
}

? 裸函数中什么都没有,所以也不能使用局部变量,只能用内嵌汇编来实现。但是大多数读者还是习惯使用C 语言的,所以这里我们简单的用汇编来实现对一个 C 函数的调用。

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

//三个宏,便于取数据的高低字节部分,或者从高低字节部分组合数据
#define P2C_MAKELONG(low,high) \
((P2C_U32)(((P2C_U16)((P2C_U32)(low) & 0xffff)) | \
((P2C_U32)((P2C_U16)((P23_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))//这个函数修改 IDT 表中的第 0x93 项,修改为 p2cInterruptProc
//在修改之前要保存到 g_p2c_old 中
void p2cHookInt93(BOLLEAN hook_or_unhook)
{PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();idt_addr += 0x93; //因为获得的是一个数组指针,所以直接加0x93KdPrint(("p2c: the current address = %x.\r\n",(void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high)));if(hook_or_unhook){KdPrint(("try to hook interrupt"));//进行hookg_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_LOW16_OF_32(p2cInterruptProc);}else{KdPrint("p2c: try to recovery interrupt.\r\n");//取消hookidt_addr->offset_low = P2C_LOW16_OF_32(g_p2c_old);idt_addr->offset_high = P2C_HIGH16_OF_32(g_p2c_old);}
}

8.7 直接使用端口操作键盘

8.7.1 读取键盘数据和命令端口

? PS/2 键盘 的数据端口是0x60,直接读取这个端口就能读到数据。但是前提是,键盘必须处于可读状态。

? 在驱动中没有对端口的读取进行限制,直接使用汇编指令就可以读取。但是注意每次读取只能读一个字节。

// 定义一个字节
P2C_U8 sch;
_asm in al,0x60
_asm mov sch,al

? 这段代码将从 0x60读取一个字节到sch中。如何确定键盘是否可读呢?答案是读取键盘的命令端口,如果读出的值没有 OBUFFER_FULL 标志的话,则说明可以读取。

? 下面的代码可以等待到键盘有数据可读。

#define OBUFFER_FULL 0X02
#define IBUFFER_FULL 0X01
ULONG p2cWaitForKbRead()
{int i = 100;P2C_U8 mychar;do{_asm in al,0x60_asm mov sch,alKeStallExecutionProcessor(50);if(!(mychar & OBUFFER_FULL)) break;}while(i--);if(i) return TRUE;return FALSE;
}

? 这段代码就是设置一个100次的询问,每次间隔50微秒。然后查看当前是否存在 OBUFFER_FULL 标志。如果不存在,则返回 TRUE;

? 同样,键盘也不是随时可以写入数据的。下面的代码可以等待到键盘可写

ULONG p2cWaitForKbRead()
{int i = 100;P2C_U8 mychar;do{_asm in al,0x60_asm mov sch,alKeStallExecutionProcessor(50);if(!(mychar & IBUFFER_FULL)) break;}while(i--);if(i) return TRUE;return FALSE;
}

明日计划

继续学习驱动编程

翻译工作

  相关解决方案