第四章 Hook键盘中断和IOAPIC重定位

    如果不想让键盘过滤驱动程序或回调函数首先获得按键,则必须比端口驱动更加底层一些.端口驱动再往下,USB和PS/2就不同了.
    早期版本QQ的反盗号驱动的原理是这样的:在用户要输入密码时(比如把输入焦点移动到密码框里),就注册一个中断服务来接管键盘中断,比如0x93中断,之后按键就不关键盘驱动的事了.为此这个程序必须处理那些扫描码,并得出用户输入了什么密码,然后交给QQ.

    黑客当然可能在QQ之前就接管了键盘中断,但是这没有用处,因为中断的接管总是最后一个起作用.如果QQ刚好在输入密码之前接管中断,那么由于只有QQ知道现在要输入密码了,所以在绝大部分情况下QQ的盗号驱动会是输入密码前最后一个接管中断的程序.

    如果不对QQ的盗号驱动打针对性的内存补丁,就很难截获按键,除非做得比0x93中断更底层.当然这是可能的,但是这样做往往又和不同型号的硬件相关,很难做到通用.因此Hook中断这样简单而古老的技术起到了很好的效果.当然我们还是有办法在QQ驱动截获数据之前来获得数据,请看后面的例子.

4.6.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没有中断号那么多.可编程的IRQ只有24个.IRQ的处理也是中断处理函数来处理的,这就需要一个IRQ号到中断号的对应关系.这样一个IRQ发生时,CPU才知道跳转到哪个去处理.

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

4.6.2如何修改IDT
    在一个应用程序中修改IDT由于 权限问题是做不到的,但是内核程序中做起来却是完全可行的.IDT的内存地址是不定的,但是可以通过一条指令sidt获取.下面的代码可以获得中断描述表的地址.
    请注意,在多核CPU上,每一个核心都有自己的IDT.因此,应该注意对每个核心获取IDT.也就是说,必须确保下面的代码在每个核心上都得到执行.
     
     
     
     
//由于这里必须明确一个域是多少位,所以我们预先定义几个明确知道多少位长度的变量,以避免不同环境下编译的麻烦
typedef UCHAR P2C_U8;
typedef USHORT P2C_U16;
typedef ULONG P2C_U32;
//从sidt指令获得如下一个结构.从这里可以得到IDT的开始地址,请注意数据结构使用1字节对齐.
#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; //为0则表示这个中断号为空闲的
P2C_U8 always0:1;
P2C_U8 dpl:2;
P2C_U8 present:1;
P2C_U16 offset_high;
}P2C_IDTENTRY,*PP2C_IDTENTRY;
#pragma pack(pop)
    带有冒号的域称为位域:这个成员的宽度甚至少于1个字节,只有1~7位。冒号后的数字表示位数,比如type有4位,dpl有2位,always有1位,present有1位,刚才共有8位,他们实际占用1个字节空间。
    中断服务的跳转地址实际上是一个32位的虚拟地址。但是这个地址被奇特地分开保存了,高16位保存在offset_high,低16位保存在offset_low.
    这里没有中断号,那里因为中断号就是这个表中的索引。因此,第0x93项这个结构,就是读者所需要关心的。

4.6.3替换IDT中的跳转地址
    中断的发生并不是用call跳转过去的,所以也不能通过ret回来。一般的说,中断应该用iret指令返回。但是为了避免更多的问题,我们还是处理后跳转到原有的中断处理函数入口,让它来代替我们返回较好。这时我们需要一段不含C编译器生成的函数框架的汇编代码。读者可以直接用asm汇编来写,但是笔者这里使用了C语言嵌入汇编。
     请注意:用 __declspec(naked)修饰可以生成一个"裸"函数.MS的C编译器不会再生成函数框架指令,不对函数进行优化,函数的所有实现包括堆栈平衡,参数的压栈,ebp的赋值,还原都要我们来做,甚至是ret/retn都要我们来做, 裸函数中什么都没有,所以也不能使用局部变量,只能全部用内嵌汇编实现.
     
     
     
     
__declspec(naked)P2cInterruptProc()
{
__asm
{
pushad //保存所有的通用寄存器
pushfd //保存所有标志寄存器
call p2cUserFilter //调用我们自己的一个函数.
popfd //恢复标志寄存器
popad //恢复通用寄存器
jmp g_p2c_old
}
}
    下面的代码直接替换了IDT中的0x93号中断服务,包括获得IDT地址和替换等.但是要注意的是,这些代码只能运行在单核,32位的操作系统上;如果有多核的话,sidt只能获得当前CPU核的IDT.
     
     
     
     
#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))
//这个函数修改IDT表中第0x93项,修改为p2cInterruptProc
//在修改之前要保存到g_p2c_old中
void * g_p2c_old = NULL;
void p2cHookInt93(BOOLEAN hook_or_unhook)
{
PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
idt_addr +=0x93;
DbgPrint("当前地址 = %x\n",(void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high));
if(hook_or_unhook)
{
//进行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
{
//取消Hook
idt_addr->offset_low = P2C_LOW16_OF_32(g_p2c_old);
idt_addr->offset_high = P2C_HIGH16_OF_32(g_p2c_old);
}
DbgPrint("当前地址为%x\n",(void*)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high));
}
4.6.4 QQ的PS/2反过滤措施
    前面几个小节已经展示了设置IDT中的中断服务程序的地址.如果直接去设置0x93这个中断号,对键盘当然会起作用.但是不够完美的,因为Windows是可以自由重设中断号.因此,QQ中的过滤部分实际上使用了内核函数HalGetInterruptVector来获得这个中断号,然后在输入密码时替换了它.
    使用HalGetInterrupVector已经很底层了.此时很难在此之前对0x93中断进行过滤,因为之后一定会被QQ的驱动程序覆盖掉.这是一种很好的保护方式,但是依然不然底层.前面说过在IOAPIC机制下从IRQ到int的映射关系是可以修改的,而且读者将会发现,即使使用IOAPIC修改了IRQ到int的映射关系,HalGetInterrupVector也不会发觉这一点.这个函数将会返回和以前一样的值.这说明传闻中韩国技术还是不够完美.更好的方法是从IOAPIC入手接管中断.

4.7.1什么是IOAPIC
    IOAPIC是可以用于多个核心CPU的新型中断控制器,所以或许应该理解为一种新的可编程硬件.如今使用着的CPU都已经有了IOAPIC机制.
    IOAPIC的作用在于当一个IRQ发生时,这个硬件将负责决定将IRQ发送给哪个CPU核心,以及何种形式发送等.IOAPIC.IOAPIC是可以编程的,可以将PS/2键盘的硬件中断请求发送给某个CPU核心,让该核心的IDT中的某个中断号对应的中断服务程序来处理.
    配置IOAPIC的方法是修改IOAPIC中的寄存器,这些寄存器是通过内存映射的方法来访问的.也就是说,直接读/写内存中的某些地址就可以修改这些寄存器值.

4.7.2如何访问IOAPIC
    Windows把访问IOAPIC系统寄存器所需要的两个寄存器已经映射到物理地址0xfec00000和0xfec00010位置,在Windows下这是固定的.
    要操作IOAPIC的寄存器,必须通过如下两个寄存器:
  1. IOREGSEL----I/O寄存器选择寄存器.这个寄存器可以通过内存物理地址0xfec00000(限于Windows下)直接访问,可读可写,宽度为32位,但是只有低8位起作用,其他的位被保存.它的作用是选择一人要操作的IOAPIC寄存器.只要通过要操作的IOAPIC寄存器的偏移(一个字节)放入这个寄存器,就可以通过另一个寄存器(I/O窗口寄存器)来操作它了.
  2. IOWIN----I/O窗口寄存器.这个寄存器可以通过内存物理地址0xfec00010(仅限于Windows下)直接访问,可读可写,宽度为32位.它的作用是用来读/写由I/O寄存器选择寄存器指定的IOAPIC寄存器.
    比如一个IOAPIC寄存器叫做IOAPIC版本寄存器,从这个寄存器中可以读出IOAPiC的版本号.查阅手册,可知版本寄存器的偏移为0x01,那么要读取它应该如下步骤操作:
  1. 把0x01写入内存物理地址为0xfec00000的低8位,选择该版本寄存器.
  2. 读取内存物理地址为0xfec00010,读出版本号.
4.7.3编程修改IOAPIC重定位表
    IOAPIC重定位表标识每个IRQ被重定位到哪个中断处理函数.除了中断处理函数之外,还有一些其他信息.这个表一共有24项,每一项用两个IOAPIC寄存器来存取(64位).
    这个表的寄存器偏移从0x10开始,一直到0x3f为止,一共48个.IRQ1对应的表项所在寄存器偏移为0x12的0x13,而中断号保存在0x12寄存器的低8位(1字节),修改这个字节就可以替换成其他的中断号了.在Windows下如果没有被有意修改过的话,读出来应该是0x93.下面的函数可以获取或者设置这个字节.
    这个函数有一个参数new_ch,如果为0,则不做设置,只返回旧的中断号;如果不为0,则会设置为这人新的值,同进返回旧值.
     
     
     
     
//搜索IOAPIC获得键盘中断,或者设置这个值
P2C_U8 p2cSeachOrSetIrq1(P2C_U8 new_ch)
{
//选择寄存器.选择寄存器虽然是32位的寄存器,但是只能使用低8位,其它位都被保留
P2C_U8 *io_reg_sel;
//窗口寄存器,用来读/写被选择寄存器选择的值,是32位的
P2C_U32 *io_win;
P2C_U32 ch,ch1;
//定义一个物理地址,这个地址为0xfec0000,正是IOAPIC正是IOAPIC寄存器组在Windows上的开始地址
PHYSICAL_ADDRESS phys;
PVOID paddr;
RtlZeroMemory(&phys,sizeof(PHYSICAL_ADDRESS));
phys.u.LowPart = 0xfec00000;
//物理地址是不能直接读写的,MmMapIoSpace把物理地址映射为系统空间虚拟地址.0x14是这片空间的长度
paddr = MmMapIoSpace(phys,0x14,MmNonCached);
//如果映射失败了就返回0
if(!MmIsAddressValid(paddr))
return 0;
//选择寄存器的偏移为0
io_reg_sel = (P2C_U8*)paddr;
//窗口寄存器的偏移为0x10
io_win = (P2C_U32*)((P2C_U8*)(paddr)+0x10);
//选择第0x12项,刚好是IRQ1的项
*io_reg_sel = 0x12;
ch = *io_win; //ch=原来的中断号
//如果new_ch不为0,就设置新值,并返回旧值
if(new_ch !=0)
{
ch1 = *io_win;
ch1 &=0xffffff00; //将ch1的低8位置零, 窗口寄存器里读出来的值是32位的,但是我们只需要1字节就可以了.这个字节就是中断向量的值
ch1 |=(P2C_U32)new_ch; //为ch1赋值
*io_win = ch1;
DbgPrint("p2cSeachOrSetIrq1:set %2x to irq1.\n",(P2C_U8)new_ch);//只打印一字节
}
ch &= 0xff;
MmUnmapIoSpace(paddr,0x14);
DbgPrint("the old vec of irql is %2x.\n",(P2C_U8)ch);
return (P2C_U8)ch;
}
4.7.4插入新的中断处理
    既然修改了IRQ1的中断处理重定位,那么在IDT中就应该增加一个新的中断处理.这件事不难:我们已经知道第0x93个IDT项目原有的IRQ1处理,那么显然在IDT中寻找一个空闲的项目,然后把第0x93项复制一份即可.当然,其中的中断处理函数的入口地址要修改成新的函数地址.
    下面的函数寻找一个空闲的项.只要IDT项中的type为0,就说明这个项是空闲的.
    
    
    
    
//在IDT表中找到一个空闲的项identry的位置,然后返回这个id
//这里为了能填入新的键盘中断处理入口中,如果找不到就返回0,这种情况无法安装新的中断处理
P2C_U8 p2cGetIdleIdtVec()
{
P2C_U8 i;
PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
//从下标20搜索到2a即可
for(i = 0x20;i<0x2a;i++)
{
//如果类型为0就说明空闲位置,返回即可
if(idt_addr[i].type == 0)
return i;
}
return 0;
}
    下面的函数完成复制IDT项和修改新的中断处理函数入口.
     
     
     
     
P2C_U8 p2cCopyANewIdt93(P2C_U8 id,void *interrupt_proc)
{
//我们写入一个新的中断门.这个门完全拷贝原来的0x93上的identry,只是处理函数地址不同
PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
idt_addr[id]= idt_addr[0x93];
idt_addr[id].offset_low = P2C_LOW16_OF_32(interrupt_proc);
idt_addr[id].offset_high = P2C_HIGH16_OF_32(interrupt_proc);
return id;
}
    下面的函数利用了上面的功能,一步到位地实现了IOAPIC重定位修改,将IRQ1重定位到我们自己写的一个IDT项.这个函数也可以实现IOAPIC重定位的恢复.
     
     
     
     
void p2cResetIoApic(BOOLEAN set_or_recovery)
{
static P2C_U8 idle_id = 0;
PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
P2C_U8 old_id = 0;
if(set_or_recovery)
{
//如果是设置新的IOAPIC定位,那么首先在g_p2c_old中保存原函数的入口
idt_addr +=0x93;
g_p2c_old = (void*)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high);
//然后获得一个空闲位,将IRQ1处理中断门复制一个进去,里面的跳转函数填写为新的处理函数
idle_id = p2cGetIdleIdtVec();
if(idle_id !=0)
{
p2cCopyANewIdt93(idle_id,p2cInterruptProc);
//再重新定位到这个中断
old_id = p2cSeachOrSetIrq1(idle_id);
//在32们Windows XP下这个中断默认应该是定位到0x93的,如果不是则终止程序
ASSERT(old_id == 0x93);
}
}
else
{
//如果要恢复...
old_id = p2cSeachOrSetIrq1(0x93);
ASSERT(old_id == idle_id);
//现在那个中断门没用了,设置type = 0使之空闲
idt_addr[old_id].type = 0;
}
}
4.7.5驱动入口和卸载的实现
    读者可以编写一个内核模块,这个内核模块可以首先捕获键盘的输入;可以用VC编写一个ActiveX控件,并把这个内核模块打包到ActiveX控件的安装包中.在网站上,提供访问者下载,当访问者要输入用户名和密码时,就让这个内核模块起作用:首先捕获键盘输入,并且不再往下传递,这样黑客程序就不可能在中途截获密码了.然后让ActiveX控件用DeviceIoControl与内核模块通信,获得密码.
    本节讲述的是IDT中断Hook方法和IOAPIC重定位方法,IOAPIC重定位方法更底层一些.下面编写DriverEntry,将两种方法都列出,用编译选项来分开编译.
     
     
     
     
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath)
{
ULONG i;
DriverObject->DriverUnload = p2cUnload;
#ifdef BUILd_FOR_IDT_HOOK
p2cHookInt93(TRUE);
#else
p2cResetIoApic(TRUE);
#endif
return STATUS_SUCCESS;
}
    驱动卸载函数如下(注意延时的作用)
     
     
     
     
#define DELAY_ONE_MICROSECOND (-10)
#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
#define DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)
void p2cUnload(PDRIVER_OBJECT drv)
{
LARGE_INTEGER interval;
#ifdef BUILD_FOR_IDT_HOOK
p2cHookInt93(FALSE);
#else
p2cResetIoApic(FALSE);
#endif
interval.QuadPart = (5*1000*DELAY_ONE_MILLISECOND);
KeDelayExecutionThread(KernelMode,FALSE,&interval);
}

下面是完整的代码:
#include "ntddk.h"

#define DELAY_ONE_MICROSECOND (-10)
#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
#define DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)


//由于这里必须明确一个域是多少位,所以我们预先定义几个明确知道多少位长度的变量,以避免不同环境下编译的麻烦
typedef UCHAR P2C_U8;
typedef USHORT P2C_U16;
typedef ULONG P2C_U32;
//从sidt指令获得如下一个结构.从这里可以得到IDT的开始地址,请注意数据结构使用1字节对齐.
#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;
}

#pragma pack(push,1)
typedef struct P2C_IDT_ENTRY_{
	P2C_U16 offset_low;
	P2C_U16 selector;
	P2C_U8 reserved;
	P2C_U8 type:4; //为0则表示这个中断号为空闲的
	P2C_U8 always0:1;
	P2C_U8 dpl:2;
	P2C_U8 present:1;
	P2C_U16 offset_high;
}P2C_IDTENTRY,*PP2C_IDTENTRY;
#pragma pack(pop)

//3个宏,便于取数据的高低字节部分,或从高低字节部分组合数据
#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))


//在修改之前要保存到g_p2c_old中
void * g_p2c_old = NULL;



void p2cUserFilter()
{
	DbgPrint("中断\n");
}

__declspec(naked)void p2cInterruptProc()
{
	__asm
	{
		pushad //保存所有的通用寄存器
			pushfd //保存所有标志寄存器
			call p2cUserFilter //调用我们自己的一个函数.
			popfd //恢复标志寄存器
			popad //恢复通用寄存器
			jmp g_p2c_old
	}
}
//这个函数修改IDT表中第0x93项,修改为p2cInterruptProc
void p2cHookInt93(BOOLEAN hook_or_unhook)
{
	PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
	idt_addr +=0x93;
	DbgPrint("当前地址 = %x\n",(void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high));
	if(hook_or_unhook)
	{
		//进行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
	{
		//取消Hook
		idt_addr->offset_low = P2C_LOW16_OF_32(g_p2c_old);
		idt_addr->offset_high = P2C_HIGH16_OF_32(g_p2c_old);
	}
	DbgPrint("当前地址为%x\n",(void*)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high));
}



//在IDT表中找到一个空闲的项identry的位置,然后返回这个id
//这里为了能填入新的键盘中断处理入口中,如果找不到就返回0,这种情况无法安装新的中断处理
P2C_U8 p2cGetIdleIdtVec()
{
	P2C_U8 i;
	PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
	//从下标20搜索到2a即可
	for(i = 0x20;i<0x2a;i++)
	{
		//如果类型为0就说明空闲位置,返回即可
		if(idt_addr[i].type == 0)
			return i;
	}
	return 0;
}

P2C_U8 p2cCopyANewIdt93(P2C_U8 id,void *interrupt_proc)
{
	//我们写入一个新的中断门.这个门完全拷贝原来的0x93上的identry,只是处理函数地址不同
	PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
	idt_addr[id]= idt_addr[0x93];
	idt_addr[id].offset_low = P2C_LOW16_OF_32(interrupt_proc);
	idt_addr[id].offset_high = P2C_HIGH16_OF_32(interrupt_proc);
	return id;
}

//搜索IOAPIC获得键盘中断,或者设置这个值
P2C_U8 p2cSeachOrSetIrq1(P2C_U8 new_ch)
{
	//选择寄存器.选择寄存器虽然是32位的寄存器,但是只能使用低8位,其它位都被保留
	P2C_U8 *io_reg_sel;
		//窗口寄存器,用来读/写被选择寄存器选择的值,是32位的
	P2C_U32 *io_win;
	P2C_U32 ch,ch1;
	//定义一个物理地址,这个地址为0xfec0000,正是IOAPIC正是IOAPIC寄存器组在Windows上的开始地址
	PHYSICAL_ADDRESS phys;
	PVOID paddr;
	RtlZeroMemory(&phys,sizeof(PHYSICAL_ADDRESS));
	phys.u.LowPart = 0xfec00000;
	//物理地址是不能直接读写的,MmMapIoSpace把物理地址映射为系统空间虚拟地址.0x14是这片空间的长度
	paddr = MmMapIoSpace(phys,0x14,MmNonCached);
	//如果映射失败了就返回0
	if(!MmIsAddressValid(paddr))
		return 0;
	//选择寄存器的偏移为0
	io_reg_sel = (P2C_U8*)paddr;
	//窗口寄存器的偏移为0x10
	io_win = (P2C_U32*)((P2C_U8*)(paddr)+0x10);
	//选择第0x12项,刚好是IRQ1的项
	*io_reg_sel = 0x12;
	ch = *io_win; //ch=原来的中断号
	//如果new_ch不为0,就设置新值,并返回旧值
	if(new_ch !=0)
	{
		ch1 = *io_win;
		ch1 &=0xffffff00; //将ch1的低8位置零,窗口寄存器里读出来的值是32位的,但是我们只需要1字节就可以了.这个字节就是中断向量的值
		ch1 |=(P2C_U32)new_ch; //为ch1赋值
		*io_win = ch1;
		DbgPrint("p2cSeachOrSetIrq1:set %2x to irq1.\n",(P2C_U8)new_ch);//只打印一字节
	}

	ch &= 0xff;
	MmUnmapIoSpace(paddr,0x14);
	DbgPrint("the old vec of irql is %2x.\n",(P2C_U8)ch);
	return (P2C_U8)ch;
}

void p2cResetIoApic(BOOLEAN set_or_recovery)
{
	static P2C_U8 idle_id = 0;
	PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
	P2C_U8 old_id = 0;
	if(set_or_recovery)
	{
		//如果是设置新的IOAPIC定位,那么首先在g_p2c_old中保存原函数的入口
		idt_addr +=0x93;
		g_p2c_old = (void*)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high);
		//然后获得一个空闲位,将IRQ1处理中断门复制一个进去,里面的跳转函数填写为新的处理函数
		idle_id = p2cGetIdleIdtVec();
		if(idle_id !=0)
		{
			p2cCopyANewIdt93(idle_id,p2cInterruptProc);
			//再重新定位到这个中断
			old_id = p2cSeachOrSetIrq1(idle_id);
			//在32们Windows XP下这个中断默认应该是定位到0x93的,如果不是则终止程序
			ASSERT(old_id == 0x93);
		}
	}
	else
	{
		//如果要恢复...
		old_id = p2cSeachOrSetIrq1(0x93);
		ASSERT(old_id == idle_id);
		//现在那个中断门没用了,设置type = 0使之空闲
		idt_addr[old_id].type = 0;
	}
}

void p2cUnload(PDRIVER_OBJECT drv)
{
	LARGE_INTEGER interval;
	//hook IDT
	//p2cHookInt93(FALSE);

	//修改IOAPIC
	p2cResetIoApic(FALSE);
	interval.QuadPart = (2*1000*DELAY_ONE_MILLISECOND);
	KeDelayExecutionThread(KernelMode,FALSE,&interval);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath)
{
	ULONG i;
	DriverObject->DriverUnload = p2cUnload;
	//hook IDT
	//p2cHookInt93(TRUE);

	//修改IOAPIC
	p2cResetIoApic(TRUE);
	return STATUS_SUCCESS;
}






















































你可能感兴趣的:(驱动开发,驱动开发,键盘过滤,Hook,IDT,修改IOAPIC)