分类:
2008-10-14 14:54:11
解析Windows2000的IDT扩展机制
作者:Brief
原文出处:
前言
今天我们谈谈Windows 2000下中断机制的扩展,首先申明本文提到的技术并非本人发现的,只不过是我在学习Windows内核过程中的一点心得罢了,目的在于为和我一样刚刚步入Windows底层学习的朋友提供一点实用的资料,同时也顺带记录下自己的学习过程。如果您是Windows Kernel高手,还望有时间能多多指点一下我们这些晚辈;如果您也是初学者,同样欢迎到我们FZ5FZ网站来交流探讨!那好吧,我们就直接进入正题,如果您对中断还不怎么了解,那眼前将是一次激动人心的旅程。
1、Windows陷阱机制简介
陷阱(Trap)是Windows系统中一种不可缺少的系统机制。当系统中发生中断(硬件中断或软件中断),异常时,处理器会捕捉这个动作,并将系统的控制转移到一个固定的处理程序处,进行相应的操作处理。在处理器开始处理发生的中断或异常前,必须保存一些处理器环境参数到堆栈中以备系统还原时使用。系统是通过一种称为陷阱帧(Trap Frame)的方式来实现的,它将系统中全部线程的环境数据保存到内核堆栈(Kernel Stack)中,在执行完后通过堆栈的出栈机制来恢复系统控制流程中的执行点。内核中的陷阱机制分为中断和异常。中断是系统中随即发生的异步事件,与当前系统的处理器状态无关。同时系统中的中断可分为可屏蔽中断和不可屏蔽中断。而异常则是一种同步事件,在特定情况下异常可以重现,而中断不可以。中断又可以分为硬件中断和软件中断。很明显硬件中断是与硬件相关的,比如I/O设备执行的某些操作,处理器时钟或硬件端口上的处理等。软件中断则是通过中断指令int xx引入的,它往往是应用程序在用户模式执行后进入操作系统的代码,这时系统为用户提供了各种各样的系统服务。比如我们上次提到的系统服务调用(System Service Call),在Windows NT/2000下就是通过软件中断int 0x2e(System Service Interrupt)来实现的,虽然在Windows XP/2003下微软使用了一种称为“快速系统调用接口”来为用户提供系统服务,不过大量的中断服务仍然存在与系统之中的。
2、中断处理及其相关流程
此处我们讨论的是与特定处理器相关的数据结构,所以会有一些移植方面的问题,本文仅针对Intel的x86 Family处理器,并且本文附带的程序也只支持在Intel x86处理器上正常执行。何为IDT?IDT(Interrupt Descriptor Table)称为中断描述符表。它是可容纳8192个单元的数组,数组中的每个成员是称之为“门”的长度为8字节的段描述符。在IDT中门可分为三种:中断门(Interrrupt Gate),陷阱门(Trap Gate)和任务门(Task Gate),但主要的是中断门和陷阱门。而它们两者之间也只有少许差别,我们在此只关心IDT中的中断门,如果您对这方面比较感兴趣,请查阅Intel处理器的相关文档《Intel Architecture Software Developer''s Manual,Volume 3》。同时,在系统中存在一个中断描述符表寄存器(IDTR),它包含了系统中断描述符表的基地址和IDT的限制信息,它于一条汇编指令sidt息息相关。在下文中我们将看到它是我们实现各种中断描述符表扩展的基础和关键!还有一点是需要注意的,在Windows系统中引入了分页,分段和虚拟存储机制后,就存在这一种调度机制,将需要执行的代码和数据调入内存,将不需要的数据调到外存(辅助存储器,如硬盘等)。如果我们在执行某些代码时发现了我们需要的数据不在内存中时,就会发出一个“缺页中断”,这时系统就会在IDT中搜寻这个中断的ISR(Interrupt Service Routine,中断服务例程),执行相应的调入工作。大家可以想象如果我们的中断描述符表被调出到外存后会是什么样的结果?那时系统将无法定位“缺页中断”的服务例程,至此系统将会崩溃掉!
在中断描述符表中,我们刚才提到了一个感兴趣的寄存器IDTR,当然我们更关心对我们来说更直接的数据:IDT中的代码段选择器(Code Segment Selector),中断执行代码的偏移量(Offset)和中断描述符的权限等级(Descriptor Privilege Level)参数。下面我们看看中断指令的执行流程,我们应该知道应用程序执行在用户模式(Ring 3)下,而中断描述符表则是存在于内核模式(Ring 0)才可以访问的系统地址空间内的。在软件中断发生后,也就是应用程序调用了某条软件中断指令后,处理器首先在IDT中检索传入的中断号参数,找到响应的入口单元后就检查中断门的权限等级参数,看是否允许Ring 3下的应用程序调用,这样操作系统就为我们保留了对软件中断调用控制的权力,然而硬件中断和异常是不会关注权限方面的信息。如果当前权限等级(Current Privilge Level,CPL)数值大于中断门描述符需要的权限(Descriptor Privilege Level),也就是权限不够时会引发一个通用保护故障(General Protection Fault),反之则进行处理器的切换从用户堆栈到内核堆栈。现在是保存线程环境的时候了,处理器将在用户模式下的堆栈指针(SS:ESP)和标准的中断帧(EFLAGS和CS:EIP)压入堆栈。之后处理器进入我们的中断服务例程,执行相关的代码处理后通过汇编指令iretd返回到调用的应用程序。在指令iretd执行时,系统将存储在堆栈中的线程环境数据出栈还原,待系统恢复中断指令执行前的环境后就接着执行应用程序的后续代码。
3、中断相关数据结构
首先我们介绍一下前面我们提到的一条关键汇编指令sidt的相关数据结构。在执行指令sidt后,系统将会把中断描述符表的基地址和限制(总共长六字节)保存在指令中指向的变量指针中,这就是我们进行IDT操作的入门口。
typedef struct _idtr { //定义中断描述符表的限制,长度两字节; short IDTLimit; //定义中断描述服表的基址,长度四字节; unsigned int IDTBase; }IDTR,*PIDTR;当我们获得了IDT的入口后,就会在中断描述符表中检索我们需要处理的中断号对应的IDT单元,单元中包含了很多我们需要注意的数据结构,其中我们最为关心的是代码段选择器,中断代码执行的偏移量和特权等级等,那好我们先给出它的定义,在下文中我们将详细讨论它们的具体应用。
typedef struct _idtentry { //中断执行代码偏移量的底16位; unsigned short OffsetLow; //选择器,也就是寄存器; unsigned short Selector; //保留位,始终为零; unsigned char Reserved; //IDT中的门的类型:包括中断门,陷阱门和任务门; unsigned char Type:4; //段标识位; unsigned char SegmentFlag:1; //中断门的权限等级,0表示内核级,3表示用户级; unsigned char DPL:2; //呈现标志位; unsigned char Present:1; //中断执行代码偏移量的高16位; unsigned short OffsetHigh; }IDTENTRY,*PIDTENTRY;4、创建软件中断钩子的作用
VOID HookInt(VOID) { //保存IDT入口的基地址和限制信息的数据结构; IDTR idtr; //记录IDT数组的指针,通过它可以查找到我们需要Hook中断号对应的中断门; PIDTENTRY IdtEntry; //汇编指令sidt,获取IDT入口信息; __asm sidt idtr; //赋予IDT基地址值; IdtEntry = (PIDTENTRY)idtr.IDTBase; //保存中断号HOOKINTID对应中断门所指向的执行代码偏移量,以备执行中断处理或恢复时使用; OldISR = ((unsigned int)IdtEntry[HOOKINTID].OffsetHigh << 16) │ (IdtEntry[HOOKINTID].OffsetLow); //关中断 __asm cli //更新执行代码偏移量的底16位; IdtEntry[HOOKINTID].OffsetLow = (unsigned short)NewISR; //更新执行代码偏移量的高16位; IdtEntry[HOOKINTID].OffsetHigh = (unsigned short)((unsigned int)NewISR >> 16); //开中断 __asm sti; } VOID UnhookInt(VOID) { IDTR idtr; PIDTENTRY IdtEntry; __asm sidt idtr; IdtEntry = (PIDTENTRY)idtr.IDTBase; __asm cli //恢复中断号HOOKINTID对应中断门执行代码偏移量的底16位; IdtEntry[HOOKINTID].OffsetLow = (unsigned short)OldISR; //恢复中断号HOOKINTID对应中断门执行代码偏移量的高16位; IdtEntry[HOOKINTID].OffsetHigh = (unsigned short)((unsigned int)OldISR >> 16); __asm sti; } VOID __fastcall Monitor() { …… //由于我们处理的中断号为0x2e, //对应于系统服务中断(System Service Interrupt), //通过获取eax寄存器中的数值来区分系统服务调用; __asm mov dwServiceId,eax; //执行内核函数获取当前进程的ID号; dwProcessId = (unsigned int)PsGetCurrentProcessId(); //提升当前IRQL,防止被中断; KeRaiseIrql(HIGH_LEVEL,&OldIrql); switch(dwServiceId) { //如果eax对应的数值为0x23, //则对应于Windows2000的ZwCreateKey系统服务调用; case 0x23: DbgPrint("ProcessId: %d ZwCreateKey\n",dwProcessId); break; …… default: break; } //恢复原始IRQL; KeLowerIrql(OldIrql); }6、添加软件中断的作用与原理
NTSTATUS InstallIG() { …… //判断我们想要添加的中断是否已被占用; if(IdtEntry[ADDINTID].OffsetLow != 0 ││ IdtEntry[ADDINTID].OffsetHigh != 0) { return STATUS_UNSUCCESSFUL; } //复制原始的中断门描述信息; RtlCopyMemory(&OldIdtEntry,&IdtEntry[ADDINTID],sizeof(OldIdtEntry)); //关中断 __asm cli //更新执行代码偏移量的底16位; IdtEntry[ADDINTID].OffsetLow = (unsigned short)InterruptServiceRoutine; //目的代码段的段选择器,CS为8; IdtEntry[ADDINTID].Selector = 8; //保留位,始终为零; IdtEntry[ADDINTID].Reserved = 0; //门类型,0xe代表中断门; IdtEntry[ADDINTID].Type = 0xe; //SegmentFlag设置0代码为段; IdtEntry[ADDINTID].SegmentFlag = 0; //描述符权限等级为3,允许用户模式程序调用本中断; IdtEntry[ADDINTID].DPL = 3; //呈现标志位,设置为一; IdtEntry[ADDINTID].Present = 1; //更新执行代码偏移量的高16位; IdtEntry[ADDINTID].OffsetHigh = (unsigned short)((unsigned int)InterruptServiceRoutine >> 16); //开中断 __asm sti return STATUS_SUCCESS; } VOID RemoveIG() { …… __asm cli //恢复我们修改过的中断门描述符; RtlCopyMemory(&IdtEntry[ADDINTID],&OldIdtEntry,sizeof(OldIdtEntry)); __asm sti } extern void _cdecl InterruptServiceRoutine(VOID) { unsigned int Command; //获取eax寄存器中的数值,接受从用户模式传入的命令参数; __asm mov Command,eax; //执行内核代码,获取操作系统版本号; DbgPrint("NtBuildNumber == %d\n",(unsigned short)NtBuildNumber); //中断返回; __asm iretd; }后记