IDT Hook是通过挂钩中断描述符表的一种Ring0挂钩形式。在保护模式下和实模式下的中断机制是完全不同的。实模式下有256个中断例程以供调用,可以通过对应的中断号来调用。在保护模式下中断机制变得相对复杂了,但更好用。保护模式下中断由对应的门描述符来描述,其中有三种格式,分别为:

  • 任务门描述符
  • 中断门描述符
  • 陷阱门描述符

任务门描述符一般用于不同特权级的非一致代码段间跳转。和IDT HOOK没有什么关系,因为IDT HOOK是通过挂钩中断门

比如希望低特权级代码段跳转至高特权级的代码段运行就一般需要使用任务门进行跳转。

首先来看一下这三种门描述符的格式:

可以看到三种门的格式都差不多,实际上访问这三种门描述符就相当于访问数据段内的数据一样。这里先粗略解释一下几个重要字段:

  • DPL是描述符特权级
  • Selecor字段是段选择子, 描述的是目标代码段位于哪一个段中, 即GDT表中的索引
  • Offset字段: 可以看到Offset字段被分割成了2部分分别位于0~15位和16~32位,组合起来是一个4字节的偏移地址

这里先有一个印象,具体之后慢慢说。

主流操作系统一般会分成两个模式:

  • 实模式
  • 保护模式

这两个有什么区别?

实模式

实模式是16位的,典型代表就是MSDOS

就这么理解吧,实模式的寄存器都是16位的,也就是说寄存器最多能够访问范围从0x0~0xFFFF。

也就是一次最多访问64KB的内存空间,但那时候已经有了20位的地址线,即可访1M大小的内存空间

但寄存器只有16位限定了只能访问64KB这怎么办?

于是出现了分段机制, 即 以段地址:偏移地址的形式来寻址1M大小的空间

将寄存器分成了段寄存器和通用寄存器,其大小都为16位。为了访问到1M大小的内存区域,采用了如下方法:

段寄存器 << 4 + 通用寄存器 (即段地址左移4位后加上偏移地址)

想一想, 0xFFFF左移4位相当于0xFFFF * 2^4变成了0xFFFF0对吧 , 然后再加上偏移0xFFFF最多可以访问0x10FFEF,这时已经可以访问超过1M的内存空间了,但实际上地址总线一共就20位,最多也就1M的内存

多出来的0xFFEF该怎么办? 但是的办法是实行回卷,即假设超出了1M的最大范围,又会从0x0开始, 可以想象成: 绝对地址 % 1MB

所以实模式下是将代码,数据进行分段,并以如上方式进行跨段访问。

保护模式

从i286开始就有保护模式了,但由于32位的CPU架构还没出来,保护模式名存实亡,到了i386开始保护模式正式实行,i386后架构变成了X86, 寄存器从原来的16位扩展成了32位的寄存器。

也就是说寄存器从本来只能访问64KB直接扩展到了4GB的访问访问。这样保护模式下一切都是平坦的,即不需要段寄存器的辅助,直接依靠一个通用寄存器就可以访问4GB内存。

所以保护模式下的段寄存器就换了作用,改了新的名称叫段选择子。

这也就是之前的Selector字段。

段选择子的结构如下:

  • TI位标识选择子是属于GDT还是LDT
  • RPL字段是请求特权级
  • Index就是在对应描述符表中的索引

来说一下GDT和LDT吧

GDT

GDT是全局描述符表,LDT是局部描述符表。两者结构相同,其结构如下:

在保护模式下,代码数据也是依照段来进行分配的。

假设你创建了一个代码段并且这个代码段的范围是全局的,那你就必须在GDT中进行注册,GDT实际上就是一个上述结构的数组,注册的意思就是在数组中添加一项。实际上GDT自身就位于一个数据段内并也被注册到GDT表中, 一般索引为0。

GDT表对每个段都进行了严格的描述。那我们如何才能找到GDT表的基址呢? GDT表又是从哪里来的。

当计算机被插电,操作系统刚开始运行时一般处在实模式下,GDT表的设置一般就是在这段时间内进行,实模式代码会把现有的段注册到GDT表中,然后把GDT表的首地址存在GDTR寄存器内。

GDTR一般分为两部分:

  • 16位的表界限,即描述了GDT表所在段的总长
  • 32位线性基地址,即GDT表的基地址

有两个特权指令:

lgdt和sgdt

其中lgdt就是把对应的结构存入GDTR中而sgdt是获取GDTR中的内容。这样我们就可以获取GDT的基地址以及其段的总长了

实际上LDT, IDT等等都是这种方式,这里仅拿GDT举例

假设用户希望访问某个全局段,首先就要通过段选择子获取对应段位于GDT表中的索引,通过GDTR寄存器获取全局描述符表的基地址后借助选择子内的索引访问到如上图所示的结构后取出基地址(Base Address字段)。

然后在通过加上自身的偏移地址来获取线性地址。假设未开启分页机制那这个线性地址就是物理地址,如果开启分页机制了还必须通过分页寻找到物理地址,这里偏离主题就不再描述了。

下图是保护模式的寻址过程

LDT是局部描述符表,如果选择子中的TI位标识是1,则代表该选择子对应的GDT项中描述的是局部描述符表。这里可能不好理解我来仔细讲解一下。

LDT是局部描述符表,其实际上也是一个数组,里面每一项描述的可能是一个代码段,数据段,栈段等等都有可能。但这个数组也是要在内存中占据位置的。

正因为如此其也必须在GDT表中注册。LDT表的基址通过LDTR存储。所以总结下来,LDT表自身可能存在于一个数据段内,该数据段必须在GDT表中注册。

LDT表中可能注册了多个段,这些段都是局部任务,无需在GDT表中注册

说了那么多终于要到IDT表了,IDT表被称为中断描述符表,其中注册了一定数量的中断,中断描述符表也相当于一个数组,数组内有这种结构:

中断分为软中断和硬中断,软中断一般是软件产生的中断,比如说我代码发生了一个除零异常,那软中断就会中断当前代码跳转到对应软中断的中断处理程序中处理这个异常然后在转回去继续执行。

硬中断属于硬件中断,一般硬件中断都是和计算机硬件发生故障有关,其优先级大于软中断,即使软中断的中断例程正在执行,此时发生了硬中断,软中断例程会立马被阻塞转而去执行硬中断的中断处理

在WINDOWS内核态下,代码运行是分优先级的一般代码运行在PASSSIVE优先级上,在这个级别上的代码可以被其他中断打断,属于最低的优先级。再往上是APC_LEVEL和DPC_LEVEL。其中DPC级别属于软件运行最高的级别

再往上就是硬件级别的优先级了。即一般线程是无法阻塞处于DPC优先级级别的线程的。所以假设当前执行的代码段处于DPC级别,一般的软中断是无法阻塞的,只能等待当前代码执行完,优先级降到PASSIVE级别后在可以执行对应处理程序

讲了那么多背景后转回来,同样,IDT表也是需要在GDT中注册的,也就是说IDT表可能在一个数据段内,该数据段的基址以及其他一些重要信息会被注册到GDT表中。

上述结构是中断门的结构,Offset是段内偏移。Selector就是IDT所在数据段基址位于GDT表中具体项的索引。

可以理解成如下

  1. 首先操作系统从GDTR中获取GDT表的基地址,中断发生后通过Selector字段获取GDT表中的索引
  2. 通过索引和GDT的基地址获取对应IDT所在段位于GDT表中的具体项。
  3. 从该GDT项中获取IDT所在段的基地址
  4. 从中断门中获取中断处理程序在段内的偏移,即Offset字段
  5. 把段基址+段内偏移获取线性地址
  6. 根据是否开启分页决定线性地址是否进一步转换
  7. 获取具体的物理地址

因为键盘按键也是一种中断,即按下会中断一次,抬起来又中断一次,所以IDT HOOK所要做的就是把对应键盘中断的IDT项的Offset改成我们自己的函数从而达成HOOK的目的。这样每次键盘按下发生中断后就可以执行我们自己的代码了。

原理介绍完后来看下代码, 根据Intel手册中给出的IDT与IDTR结构定义了如上结构:

下面代码通过sidt从IDTR寄存器中获取IDT表的基地址

PS/2键盘的中断为0x93号,所以也就是IDT数组中的第0x93项。

通过下面三个宏来将2个字拼成一个双字,以及将一个双字分成2个字

完整代码如下:

#include <ntddk.h>
#include <windef.h>#define MAKE_IDTADDR(low, high) (ULONG)((ULONG)((USHORT)(low) & 0xFFFF) | ((ULONG)((USHORT)(high) & 0xFFFF) << 16))
#define LOW_PART_OF_ULONG(ulong) (USHORT)((ULONG)(ulong) & 0xFFFF)
#define HIGH_PART_OF_ULONG(ulong) (USHORT)((ULONG)(ulong) >> 16)PVOID g_pAddrOfIntrFunc = NULL;// 中段描述符结构
#pragma pack(1)
typedef struct _IDTEntry
{USHORT usLowOffset; USHORT usSelector;UCHAR Reserved0 : 4;UCHAR Reserved1 : 3;UCHAR Reserved2 : 1;UCHAR Type : 4;UCHAR ucZero0 : 1;UCHAR DPL : 2;UCHAR Present : 1;USHORT usHighOffset;
} IDTEntry, *PIDTEntry;
#pragma pack()// IDTR结构
#pragma pack(1)
typedef struct _IDTREntry
{USHORT usLen;ULONG  ulBaseAddr;
} IDTREntry, *PIDTREntry;
#pragma pack()VOID
PrintUlong(ULONG ulNum)
{for (int i = 0; i < sizeof(ULONG) * 8; ++i){if (ulNum & 0x80000000){KdPrint(("1"));}else{KdPrint(("0"));}ulNum = ulNum << 1;}KdPrint(("\n"));
}VOID
PrintUshort(USHORT usNum)
{for (int i = 0; i < sizeof(USHORT) * 8; ++i){if (usNum & 0x8000){KdPrint(("1"));}else{KdPrint(("0"));}usNum = usNum << 1;}KdPrint(("\n"));
}PVOID
GetIDT()
{IDTREntry idtr;_asm sidt idtr; // 将IDTR中内容取出,保存到idtr变量内return((PVOID)(idtr.ulBaseAddr));
}VOID HookFunc()
{KdPrint(("键盘Hook!\n"));
}VOID __declspec(naked) MyInterrupt()
{__asm{pushadpushfdcall HookFuncpopfdpopadjmp g_pAddrOfIntrFunc }
}VOID
HookIDT(BOOL fHook)
{PIDTEntry pstIDT = (PIDTEntry)GetIDT();NTSTATUS status = STATUS_SUCCESS;// 偏移到第0x39号中断pstIDT += 0x93;if (fHook){if (NULL != g_pAddrOfIntrFunc){// 如果已经挂钩,先UnhookHookIDT(FALSE);}// 保存0x93中断地址g_pAddrOfIntrFunc = MAKE_IDTADDR(pstIDT->usLowOffset, pstIDT->usHighOffset);// 挂钩pstIDT->usLowOffset = LOW_PART_OF_ULONG((ULONG)MyInterrupt);pstIDT->usHighOffset = HIGH_PART_OF_ULONG((ULONG)MyInterrupt);KdPrint(("成功Hook IDT!\n"));}else{if (NULL != g_pAddrOfIntrFunc){// UnhookpstIDT->usLowOffset = LOW_PART_OF_ULONG(g_pAddrOfIntrFunc);pstIDT->usHighOffset = HIGH_PART_OF_ULONG(g_pAddrOfIntrFunc);g_pAddrOfIntrFunc = NULL;KdPrint(("成功Unhook IDT!\n"));}else{KdPrint(("原本就没有Hook!\n"));}}KdPrint(("第0x93号中断的地址: 0x%08X\n", g_pAddrOfIntrFunc));
}VOID
Unload(IN PDRIVER_OBJECT pDriverObject)
{KdPrint(("卸载驱动!\n"));HookIDT(FALSE);
}NTSTATUS
DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath)
{KdPrint(("加载驱动!\n"));pDriverObject->DriverUnload = Unload;HookIDT(TRUE);return(STATUS_SUCCESS);
}

(完)

[内核安全4]内核态Rootkit之IDT Hook相关推荐

  1. 模拟linux内核异常,Linux内核态缺页会发生什么 - 玩转Exception fixup表

    近日,我在写内核模块的时候犯了一个低级错误: 直接access用户态的内存而没有使用copy_to_user/copy_from_user! 在内核看来,用户态提供的虚拟地址是不可信的,所以在一旦在内 ...

  2. 【梅哥的Ring0湿润插入教程】重磅第三课:Ring0下的PE Loader及重加载内核秒杀一切内核级钩子(上篇)...

    [梅哥的Ring0湿润插入教程] Email:mlkui@163.com 转载请注明出处,谢绝喷子记者等,如引起各类不适请自觉滚J8蛋! 第三课:Ring0下PE Loader及重加载内核绕过一切内核 ...

  3. 零代价修复海量服务器的内核缺陷——UCloud内核热补丁技术揭秘

    下述为UCloud资深工程师邱模炯在InfoQ架构师峰会上的演讲--<UCloud云平台的内核实践>中非常受关注的内核热补丁技术的一部分.给大家揭开了UCloud云平台内核技术的神秘面纱. ...

  4. 【内核】linux内核启动流程详细分析【转】

    转自:http://www.cnblogs.com/lcw/p/3337937.html Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件 ...

  5. 【内核】linux内核启动流程详细分析

    Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件,包括内核入口ENTRY(stext)到start_kernel间的初始化代码, 主要作用 ...

  6. Linux内核:VFIO 内核文档 (实例,API,bus驱动API)

    <ARM SMMU原理与IOMMU技术("VT-d" DMA.I/O虚拟化.内存虚拟化)> <提升KVM异构虚拟机启动效率:透传(pass-through).DM ...

  7. Linux 内核 vs Windows 内核

    Windows 和 Linux 可以说是我们比较常见的两款操作系统的. Windows 基本占领了电脑时代的市场,商业上取得了很大成功,但是它并不开源,所以要想接触源码得加入 Windows 的开发团 ...

  8. 深度:一文看懂Linux内核,Linux内核架构和工作原理详解

    简介 作用是将应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址.目前支持模块的动态装卸(裁剪).Linux内核就是基于这个策略实现的.Linux进程1.采用层次结构,每个 ...

  9. 微内核、宏内核、混合内核的对比分析

    - 什么是内核 宏内核 微内核 混合内核 - 相关产品分析 Linux RT-Thread 一.什么是内核     内核是操作系统的核心部分,管理着系统的各种资源,是连接应用程序和硬件的一座桥梁,也是 ...

最新文章

  1. php yar swoole 比较,syar:Swoole 实现的 Yar 服务
  2. gdb调试报错:Missing separate debuginfos, use: debuginfo-install glibc-XXX
  3. 学计算机与学英语作文,初二英语作文(关于计算机与学习)
  4. python 入门程序_非Python程序员的Python速成课程-如何快速入门
  5. Could not create the view: An unexpected exception was thrown.
  6. 向量积 和 它的计算_7
  7. pythonfor循环加2_python – 在For循环中添加List(我最初将List设置为什么?)
  8. Build Settings发布设置
  9. ARM处理器系统初始化过程
  10. 【Lolttery】项目开发日志 (七)socket io 结合 react js实现简单聊天室
  11. JavaScript小练习2
  12. 禁止暴风影音stormtray.exe进程
  13. 交易记录表设计注意点
  14. java工作流程引擎比较,技术架构选型。你喜欢用那种?
  15. cmd操作 以及几个常用快捷键
  16. java数据库加密(druid)
  17. ECCV2022论文列表(中英对照)
  18. circos 可视化手册- text 篇
  19. 华为机试 HJ21简单密码【java实现】
  20. 计算机的随想作文600字,随想作文600字

热门文章

  1. UIP协议栈笔记·一
  2. Android N Android O 默认MTP模式 实时文件扫描
  3. MMKV的简单实用一
  4. Mysql 民族数据库
  5. Windows bat 脚本命令基础
  6. GetTickCount 得到时间进行比较计算遇到的异常
  7. 【移动通信】5GC:5G的QoS (Quality of Service) 控制 服务质量管理
  8. css中审核图标,一个简单实用的css loading图标
  9. Windows下配置PHP环境
  10. Angular 基础