IDT hook

IDT hook是工作在RING 0的hook技术。

IDT是一个拥有256个元素的线性表,每个IDT的元素是个8字节的描述符,所以整个IDT表的大小为256*8=2048 bytes。IDT(Interrupt Descriptor Table )的全称是中断描述表,其中每个元素关联了一个中断处理例程。

操作系统使用IDTR寄存器来记录IDT位置和大小,IDTR寄存器是48位寄存器,低16位代表IDT的大小,大小为7FFH,高32位代表IDT的基地址。我们可以利用sidt指令读出IDTR寄存器中的信息。

在Windbg中使用kd > r idtr查看寄存器内容:

kd> r idtr

idtr=80b95400

IDT有三种不同的描述符,分别是:

1.任务门描述符

2.中断门描述符

3.陷阱门描述符

使用命令 kd> !idt –a 可以进行印证:

Dumping IDT: 80b95400

00:   83c8d690 nt!KiTrap00

02:   Task Selector = 0×0058

2d:   83c8db68 nt!KiDebugService

……

也就是说,在保护模式下,80386只有通过中断门、陷阱门或任务门才能转移到对应的中断或异常处理例程。

这三种描述符分别如下图所示:

其中中断门与陷阱门比较相似,只有1bit的区别,其处理方式上也相同,由Segment Selector作为索引在GDT中找到基地址,offset 15:0和offset 31:16组合成偏移地址,两者相加就是中断处理例程的线性地址。

关于保护模式的更多内容可以看我前面写的一篇总结“操作系统内存保护”。

接下来,我们看看任务门描述符的情况。首先,根据IDT中任务门描述符的Segment  Selector 直接在GDT中找相应TSS描述符,这个描述符中记录了任务状态段的位置和大小,我们根据任务状态段描述符中的15-31位Base Address找到TSS的内存位置后,就可以使用它进行任务切换。

下面是关于IDT hook的两个例子(来自网络):

a)查看256个中断向量对应的中断服务例程(ISR),其中任务门没有ISR,由于整个IDT中只有3个任务门,我们忽略这部分。

#include "ntddk.h"
#include <stdio.h>
#define MAKELONG(a, b) ((unsigned long) (((unsigned short) (a)) | ((unsigned long) ((unsigned short) (b))) << 16))
 
#define MAX_IDT_ENTRIES 0xFF
 
#pragma pack(1)
/* IDT entries */
typedef struct
{
    unsigned short LowOffset;
    unsigned short selector;
    unsigned char unused_lo;
    unsigned char segment_type : 4;
    unsigned char system_segment_flag : 1;
    unsigned char DPL : 2;
    unsigned char P : 1;
    unsigned short HiOffset;
} IDTENTRY;
 
/* SIDT */
typedef struct
{
    unsigned short IDTLimit;
    unsigned short LowIDTbase;
    unsigned short HiIDTbase;
} IDTINFO;
#pragma pack()
 
VOID OnUnload(PDRIVER_OBJECT DriverObject)
{
    DbgPrint("OnUnload called.");
}
 
NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath)
{
    IDTINFO    idt_info;
    IDTENTRY*  idt_entries;
    unsigned long  count;
 
    theDriverObject->DriverUnload = OnUnload;
 
    __asm  sidt  idt_info
 
    idt_entries = (IDTENTRY*)MAKELONG(idt_info.LowIDTbase, idt_info.HiIDTbase);
 
    for (count = 0; count < MAX_IDT_ENTRIES; count++)
    {
        char _t[256];
        IDTENTRY *i = &idt_entries[count];
        unsigned long addr = 0;
        addr = MAKELONG(i->LowOffset, i->HiOffset);
 
        _snprintf(_t, 255, "Interrupt %d: ISR 0x%08X", count, addr);
        DbgPrint(_t);
    }
 
    return STATUS_SUCCESS;
}

b)替换IDT中某个中断向量的ISR

#include "ntddk.h"
#include <stdio.h>
 
#define MAKELONG(a, b) ((unsigned long) (((unsigned short) (a)) | ((unsigned long) ((unsigned short) (b))) << 16))
 
#define MAX_IDT_ENTRIES 0xFF
#define NT_INT_TIMER 0×30
 
unsigned long g_i_count = 0;
 
#pragma pack(1)
typedef struct
{
    unsigned short LowOffset;
    unsigned short selector;
    unsigned char unused_lo;
    unsigned char segment_type : 4;
    unsigned char system_segment_flag : 1;
    unsigned char DPL : 2;
    unsigned char P : 1;
    unsigned short HiOffset;
} IDTENTRY;
 
typedef struct
{
    unsigned short IDTLimit;
    unsigned short LowIDTbase;
    unsigned short HiIDTbase;
} IDTINFO;
#pragma pack()
 
unsigned long old_ISR_pointer;
 
VOID OnUnload(IN PDRIVER_OBJECT DriverObject)
{
    IDTINFO    idt_info;
    IDTENTRY*  idt_entries;
 
    __asm  sidt  idt_info
    idt_entries = (IDTENTRY*)MAKELONG(idt_info.LowIDTbase, idt_info.HiIDTbase);
 
    DbgPrint("UnHooking Interrupt…");
 
    /* 关中断,恢复ISR正确地址 */
    __asm cli
    idt_entries[NT_INT_TIMER].LowOffset = (unsigned short)old_ISR_pointer;
    idt_entries[NT_INT_TIMER].HiOffset = (unsigned short)((unsigned long)old_ISR_pointer >> 16);
    __asm sti
 
    DbgPrint("UnHooking Interrupt complete.");
}
 
/* __stdcall由调用者清理栈 */
void __stdcall count_syscall(unsigned long system_call_number)
{
    g_i_count++;
}
 
/* 执行完hook功能,跳转到旧ISR */
__declspec(naked) my_interrupt_hook()
{
    __asm
    {
        push eax
        call count_syscall
        jmp old_ISR_pointer
    }
}
 
NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath)
{
    IDTINFO    idt_info;
    IDTENTRY*  idt_entries;
 
    theDriverObject->DriverUnload = OnUnload;
 
    __asm  sidt  idt_info
    idt_entries = (IDTENTRY*)MAKELONG(idt_info.LowIDTbase, idt_info.HiIDTbase);
 
    DbgPrint("Hooking Interrupt…");
 
    old_ISR_pointer = MAKELONG(idt_entries[NT_INT_TIMER].LowOffset, idt_entries[NT_INT_TIMER].HiOffset);
 
    __asm cli
    idt_entries[NT_INT_TIMER].LowOffset = (unsigned short)my_interrupt_hook;
    idt_entries[NT_INT_TIMER].HiOffset = (unsigned short)((unsigned long)my_interrupt_hook >> 16);
    __asm sti
 
    DbgPrint("Hooking Interrupt complete");
 
    return STATUS_SUCCESS;
}

IDT hook实现比较简单,但它的缺点也很明显:很容易被检测到。一般情况下,IDT的描述符引用存在于ntoskrnl.exe内存映像中的函数。如果某个描述符偏移地址是一个位于ntoskrnl.exe模块范围之外的值,那么很明显存在问题。

在多核处理器流行的今天,上面的代码是不能很好运行的,它并不能hook每个CPU的IDT,这里有两个办法,这算是IDT hook最精彩的地方了。

  1. 使用粗暴的手法,在一个while(1)的循环中不停的开线程hook,检测到地址已经被替换就退出线程,没有则进行替换

  2. 使用undocumented的函数KeSetAffinityThread进行CPUs遍历并hook


你可能感兴趣的:(IDT hook)