两种HOOK IDT方法
IDT :就是系统中断表,是存放Windows的中断和异常的处理函数的表
IDT的数据结构:
-
typedef struct _IDTENTRY
-
{
-
unsigned short LowOffset; //处理函数的低2字节
-
unsigned short selector; //处理函数所在的段选择子
-
unsigned char retention : 5;
-
unsigned char zero1 : 3;
-
unsigned char gate_type : 1;
-
unsigned char zero2 : 1;
-
unsigned char interrupt_gate_size : 1;
-
unsigned char zero3 : 1;
-
unsigned char zero4 : 1;
-
unsigned char DPL : 2; //descriptor privilege level
-
unsigned char P : 1;
-
unsigned short HiOffset; //处理函数的高2字节
-
} IDTENTRY, *PIDTENTRY;
何为段选择子参照上一篇博客
处理函数的地址可以通过一个MAKELONG(LoOffset,HiOffset)宏来获取
寻找IDT表:IDT表的长度与地址是由CPU的IDTR寄存器来描述的
IDTR的高32位为IDT表的地址,IDTR的低16位为IDT表的长度(以字节为单位)
求IDT表元素个数则需要用IDTR.IDT_limit / 8获取
-
typedef struct _IDTR {
-
USHORT IDT_limit;
-
USHORT IDT_LOWbase;
-
USHORT IDT_HIGbase;
-
}IDTR, *PIDTR;
获取IDTR可以用SIDT指令获取
-
IDTR idtr;
-
__asm SIDT idtr;
获取IDT的地址可以通过IDTR的高32位地址获取,也可以通过遍历KiProcessorBlock数组;
为何遍历KiProcessorBlock:因为每个CPU都有自己的IDT表,所以对于多核CPU时候要获取每一个CPU对应的IDT结构。
怎么通过KiProcessorBlock来获取IDT:
KiProcessorBlock数组的每一个非0项都是指向一个_kprcb结构,而_kprcb结构正是再_kpcr结构的+0x120处,_kpcr结构的+0x38就是_IDT结构地址。
每一个IDT的成员项为8字节的_KIDTENTRY:
typedef struct _IDTENTRY
{
unsigned short LowOffset; //段基址低2字节
unsigned short selector; //段选择子
unsigned char retention : 5;
unsigned char zero1 : 3;
unsigned char gate_type : 1;
unsigned char zero2 : 1;
unsigned char interrupt_gate_size : 1;
unsigned char zero3 : 1;
unsigned char zero4 : 1;
unsigned char DPL : 2;
unsigned char P : 1;
unsigned short HiOffset; //段基址高两字节
} IDTENTRY, *PIDTENTRY;
可以通过windbg查看这_kpcr结构:
-
lkd> dt _kpcr
-
nt!_KPCR
-
+0x000 NtTib : _NT_TIB
-
+0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
-
+0x004 Used_StackBase : Ptr32 Void
-
+0x008 Spare2 : Ptr32 Void
-
+0x00c TssCopy : Ptr32 Void
-
+0x010 ContextSwitches : Uint4B
-
+0x014 SetMemberCopy : Uint4B
-
+0x018 Used_Self : Ptr32 Void
-
+0x01c SelfPcr : Ptr32 _KPCR
-
+0x020 Prcb : Ptr32 _KPRCB
-
+0x024 Irql : UChar
-
+0x028 IRR : Uint4B
-
+0x02c IrrActive : Uint4B
-
+0x030 IDR : Uint4B
-
+0x034 KdVersionBlock : Ptr32 Void
-
+0x038 IDT : Ptr32 _KIDTENTRY
-
+0x03c GDT : Ptr32 _KGDTENTRY
-
+0x040 TSS : Ptr32 _KTSS
-
+0x044 MajorVersion : Uint2B
-
+0x046 MinorVersion : Uint2B
-
+0x048 SetMember : Uint4B
-
+0x04c StallScaleFactor : Uint4B
-
+0x050 SpareUnused : UChar
-
+0x051 Number : UChar
-
+0x052 Spare0 : UChar
-
+0x053 SecondLevelCacheAssociativity : UChar
-
+0x054 VdmAlert : Uint4B
-
+0x058 KernelReserved : [14] Uint4B
-
+0x090 SecondLevelCacheSize : Uint4B
-
+0x094 HalReserved : [16] Uint4B
-
+0x0d4 InterruptMode : Uint4B
-
+0x0d8 Spare1 : UChar
-
+0x0dc KernelReserved2 : [17] Uint4B
-
+0x120 PrcbData : _KPRCB
那么获取IDT的方法知道了,如何获取KiProcessorBlock:
用IDA分析内核模块ntkrnlpa模块,加载符号链接后(将windbg的symsrv.yes拷贝到IDA目录下),然后搜索KiProcessorBlock,一般ntkrnlpa导出的内核函数会有使用这个全局变量的,这里选取KeSetTimeIncrementAPI,这里直接通过硬编码定位到这个KiProcessorBlock:
-
ULONG GetKiProcessorBlock()
-
{
-
UNICODE_STRING usFuncName;
-
ULONG uFuncAddrKeSetTimeIncrement;
-
RtlInitUnicodeString(&usFuncName,L"KeSetTimeIncrement");
-
uFuncAddrKeSetTimeIncrement = (ULONG)MmGetSystemRoutineAddress(&usFuncName);
-
if (!MmIsAddressValid((PVOID)uFuncAddrKeSetTimeIncrement))
-
return 0;
-
return *(ULONG *)(uFuncAddrKeSetTimeIncrement + 44); //通过IDA搜索KiProcessorBlock来获取到偏移
-
}
获取到IDT表的地址后就能够进行HOOK操作:
比如要HOOK IDT的3 号中断函数:
一法:直接修改IDT中成员的地址为Hook函数(要初始化fs寄存器,因为这是内核环境下,并且要还原fs寄存器,供原IDT函数使用):
-
_declspec(naked)
-
void Fake_InterruptFun()
-
{
-
_asm {
-
pushad
-
pushfd
-
push fs
-
push 0x30
-
pop fs //初始化fs寄存器
-
call FilterInterruptFun;
-
pop fs
-
popfd
-
popad
-
jmp g_Interrupt3
-
}
-
};
FilterInterruptFun这个就是自己写的过滤函数,具体想怎么实现过滤或者其他操作自己写。
后面再跳转回原来的IDT函数地址。
关键代码:
二法:
通过修改段选择子实现
段选择子结构
段选择子就是一个数字,一共有16位,结构如下:
-
| 1 | 0 | 字节
-
|7654321076543 2 10| 比特
-
|-------------|-|--| 占位
-
| INDEX |T|R | 含义
-
| |I|P |
-
| | |L |
- INDEX:在GDT数组或LDT数组的索引号
- TI:Table Indicator,这个值为0表示查找GDT,1则查找LDT
- RPL:请求特权级。以什么样的权限去访问段。
段描述符结构
-
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 字节
-
|76543210|7 6 5 4 3210 |7 65 4 3210|76543210|76543210|76543210|76543210|76543210| 比特
-
|--------|-|-|-|-|---- |-|--|-|----|--------|--------|--------|--------|--------| 占位
-
| BASE |G|D|0|A|LIMIT|P|D |S|TYPE|<------- BASE 23-0 ------>|<-- LIMIT 15-0 ->| 含义
-
| 31-24 | |/| |V|19-16| |P |
-
|B| |L| | |L |
- BASE: 段基址,由上图中的两部分(BASE 31-24 和 BASE 23-0)组成
- G:LIMIT的单位,该位 0 表示单位是字节,1表示单位是 4KB
- D/B: 该位为 0 表示这是一个 16 位的段,1 表示这是一个 32 位段
- AVL: 该位是用户位,可以被用户自由使用
- LIMIT: 段的界限,单位由 G 位决定。数值上(经过单位换算后的值)等于段的长度(字节)- 1。
- P: 段存在位,该位为 0 表示该段不存在,为 1 表示存在。
- DPL:段权限
- S: 该位为 1 表示这是一个数据段或者代码段。为 0 表示这是一个系统段(比如调用门,中断门等)
- TYPE: 根据 S 位的结果,再次对段类型进行细分。
在保护模式下不像在实模式下通过cs:xx来访问,而是通过段选择子和段描述符表来得到段基址 再加上IDTENTRY中的虚拟偏移得到真正的地址
所以可以修改IDT表指定中断IDTENTRY的段选择子(SELECTOR)为我们再GDT表中自己创建的GDTENTRY,再修改我们修改的GDTENTRY的offset来实现跳转到我们的Fake_InterruptFunction,实现Hook。
注意:
1.只修改想要修改的GDTENTRY的OFFSET其他的属性都不去改变
2.只修改IDTENTRY的段选择子部分
3.在Fake_InterruptFunction中要跳转回原来的段内再实现过滤或者其他的操作,Fake_InterruptFunction只是实现跳转回远啦的段内的功能。(保护模式下段间跳转使用jmp fword ptr [address],address为6位大小,前四位为跳转的函数虚拟偏移地址,后两位为所在的段选择子)
4.在实现过滤操作时候一定要保存下fs的,因为在返回用户层时候,会恢复原来的fs寄存器。