[内核安全4]内核态Rootkit之IDT Hook

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
	{
		pushad
		pushfd
		call HookFunc
		popfd
		popad
		jmp g_pAddrOfIntrFunc 
	}
}

VOID 
HookIDT(
	BOOL fHook
	)
{
	PIDTEntry pstIDT = (PIDTEntry)GetIDT();
	NTSTATUS status = STATUS_SUCCESS;
	// 偏移到第0x39号中断
	pstIDT += 0x93;

	if (fHook)
	{
		if (NULL != g_pAddrOfIntrFunc)
		{
			// 如果已经挂钩,先Unhook
			HookIDT(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)
		{
			// Unhook
			pstIDT->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);
}

(完)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值