解析Windows2000的IDT扩展机制

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 前言    今天我们谈谈Windows 2000下中断机制的扩展,首先申明本文提到的技术并非本人发现的,只不过是我在学习Windows内核过程中的一点心得罢了,目的在于为和我一样刚刚步入Windows底层学习的朋友提供一点实用的资料,同时也顺带记录下自己的学习过程。

前言

    今天我们谈谈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> 创建软件中断钩子的作用

    作为普通的Windows程序员,或许您需要的是熟悉对系统基本功能的操作,以及对通用程序开发的熟练掌握。但对于一个有想法的Windows内核级分析开发人员来说,对系统底层的深入了解是非常必要的,同时也是非常重要的。Hook为我们创造了一个绝好的机会,它使我们了解系统内部运行机制的想法成为了一种可能。同时,书写一个系统相关的监视程序可以自动的对系统内部操作进行记录与分析。当然我们不能局限于对系统的了解,我们更渴望实施对系统的修改与扩展,改变系统原有的操作特性,注入我们需要的功能组件,让系统做更适合我们自己,也是我们最希望看到的操作。前面我们曾经谈到了创建系统服务调用的钩子来截获系统服务调用,同样在Windows2000下,系统服务是通过系统服务中断(System Service Interrupt,int 0x2e)来实现的,通过截获软件中断同样可以达到监视并修改系统服务调用的功能。在此我们主要讨论的是为软件中断创建钩子,不过对于硬件中断和异常也同样不例外,我们同样可以将本文提到的方法应用于硬件中断和异常。比如我们也可以通过截获键盘驱动的中断调用来书写内核级的键盘记录器,它可以直接对每次击键和释放进行操作,效果是非常的明显,不过这还需要使用到一些微软为我们提供的与硬件中断钩子相关的函数。

5> 如何创建软件中断钩子?

    其实创建软件中断钩子的过程应该是比较明显了,下面我们将先简要介绍一下创建Hook的过程,然后以实际代码进行具体的讲解。首先我们通过汇编指令sidt(sidt: Store Interrupt Descriptor Table Register;lidt: Load Interrupt Descriptor Table Register)来获取IDT的基地址IDTBase,然后我们在中断描述符表中搜寻我们需要HOOK的中断号HOOKINTID,它应该是在0-255内的一个整数,虽然最新的Intel处理器声称支持8192个中断描述符单元,但由于某些限制原因,仍然只能处理前256个中断描述门。在找到我们需要Hook的中断描述门后,将它原本的中断执行代码偏移量(32位)保存到一个全局变量OldISR中,以备我们在执行中断处理或恢复IDT时使用。这样新的IDT中对应中断号的执行代码偏移量就指向了我们自己的处理代码了。在我们的处理代码NewISR中,注意先要保存一些线程环境,在处理完我们额外添加的执行程序(Monitor,监视注册表相关的16个系统服务调用)后,恢复现场并执行中断门以前指向的程序代码。这样,对外就看不出我们对中断门做了什么额外的处理,感觉和以前没什么两样!如果我们只是处理了我们添加的代码而没有继续执行中断门对应的以前的程序代码,那么系统必将混乱甚至崩溃!同样在我们卸载我们的软件中断钩子时,就是进行了一个逆向工作。先获取IDT的基地址,然后将保存在全局变量中的旧的执行代码地址偏移量赋给对应中断号的偏移量单元(OffsetLow/OffsetHigh)。大概过程讲得差不多了,相关程序为T-HookInt,我们再看看代码吧!

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> 添加软件中断的作用与原理

    通过添加软件中断,我们可以扩展系统的功能,改变系统的很多操作行为。在前面我们介绍过为系统添加新的系统服务调用来扩展系统,通过添加新的软件中断同样可以到达添加系统服务调用的目的,并且我们可以在新添的中断处理程序中执行Ring 0级别的任意代码,那是何等的让人欣慰!

    其实在IDT中,256个中断门单元并不是被完全利用的,还剩下一些流给将来扩展使用的中断门,我们可以自己给这些未使用的中断门添加一些机制为我所用。其实添加软件中断的过程和前面我们详细讲解的添加软件中断钩子有很多相似的地方,所以在此我就不做很详细的介绍了。同样是,首先获得IDT的基地址,然后在中断描述符表中查找我们将要添加的中断号对应的中断门描述符,之后给相关的参数赋值,使其成为名副其实的软件中断门。这时我们就可以在应用程序中使用中断指令int xx来调用我们自己中断门中的服务程序了。

7> 添加软件中断的实现过程

    相关程序为T-ADDIG(Add Interrupt Gate),我们来看看代码哈~

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;
}

后记

    写到这儿,我们只是介绍了扩展IDT的一些基本方法,当然还有很多更深入的,更值得我们研究的课题需要大家努力去探索。比如我们可以将T-HookInt扩展,不仅仅是监视系统注册表操作相关的系统服务调用,不过在Windows XP/2003上由于其内在机制的一些变更,所以通过Hook int 0x2e来截获系统服务调用就不这么现实了。当然还有基于IDT的内核级后门,可以通过添加新的软件中断为任意用户提供SYSTEM权限级别的Command等。总之,探究Windows内核奥秘的旅行还未结束,或许这只能算是一次起航罢了。

目录
相关文章
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
3月前
|
存储 缓存 算法
分布式锁服务深度解析:以Apache Flink的Checkpointing机制为例
【10月更文挑战第7天】在分布式系统中,多个进程或节点可能需要同时访问和操作共享资源。为了确保数据的一致性和系统的稳定性,我们需要一种机制来协调这些进程或节点的访问,避免并发冲突和竞态条件。分布式锁服务正是为此而生的一种解决方案。它通过在网络环境中实现锁机制,确保同一时间只有一个进程或节点能够访问和操作共享资源。
110 3
|
11天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
11天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
11天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
1月前
|
PHP 开发者 UED
PHP中的异常处理机制解析####
本文深入探讨了PHP中的异常处理机制,通过实例解析try-catch语句的用法,并对比传统错误处理方式,揭示其在提升代码健壮性与可维护性方面的优势。文章还简要介绍了自定义异常类的创建及其应用场景,为开发者提供实用的技术参考。 ####
|
2月前
|
存储 缓存 监控
后端开发中的缓存机制:深度解析与最佳实践####
本文深入探讨了后端开发中不可或缺的一环——缓存机制,旨在为读者提供一份详尽的指南,涵盖缓存的基本原理、常见类型(如内存缓存、磁盘缓存、分布式缓存等)、主流技术选型(Redis、Memcached、Ehcache等),以及在实际项目中如何根据业务需求设计并实施高效的缓存策略。不同于常规摘要的概述性质,本摘要直接点明文章将围绕“深度解析”与“最佳实践”两大核心展开,既适合初学者构建基础认知框架,也为有经验的开发者提供优化建议与实战技巧。 ####
|
2月前
|
缓存 NoSQL Java
千万级电商线上无阻塞双buffer缓冲优化ID生成机制深度解析
【11月更文挑战第30天】在千万级电商系统中,ID生成机制是核心基础设施之一。一个高效、可靠的ID生成系统对于保障系统的稳定性和性能至关重要。本文将深入探讨一种在千万级电商线上广泛应用的ID生成机制——无阻塞双buffer缓冲优化方案。本文从概述、功能点、背景、业务点、底层原理等多个维度进行解析,并通过Java语言实现多个示例,指出各自实践的优缺点。希望给需要的同学提供一些参考。
51 7
|
1月前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
2月前
|
Java 开发者 Spring
深入解析:Spring AOP的底层实现机制
在现代软件开发中,Spring框架的AOP(面向切面编程)功能因其能够有效分离横切关注点(如日志记录、事务管理等)而备受青睐。本文将深入探讨Spring AOP的底层原理,揭示其如何通过动态代理技术实现方法的增强。
77 8

热门文章

最新文章

推荐镜像

更多