一、前言
对于IDT第一次的认知是int 2e ,在系统调用的时候原来R3进入R0的方式就是通过int 2e自陷进入内核,然后进入KiSystemService函数,在根据系统服务调用号调用系统服务函数。而2e就是IDT(系统中断描述符表)中的索引位2e的项,而KiSystemService就是该项的例程函数,后来为了提升效率,有了系统快速调用,intel的的cpu通过sysenter指令快速进入内核,直接通过kiFastCallEntry函数调用系统服务函数,各种杀软也做了这个地方的Hook来监控系统调用。因为每次中断都从IDT表中查找2e的那一项的例程函数,会降低效率。
最近在做调试器,对于int 3比较熟悉,也遇到各种问题,比如在R3下int 3断点的时候,用WaitForDebugEvent等待异常事件,第一次的时候FirstChance==TRUE,异常恢复的地方就在断点的后一个指令,在FirstChance==FALSE的时候,异常恢复的地址却是断点所在的地方,理论上来说,int 3属于陷阱类异常,恢复的地址是断点的后一个指令地址,但是在FirstChance==FALSE的时候EIP却是当前断点的地址。当时真的是非常不解,后来看了<软件调试>,上面说KiTrap03在内核做了一些事。
在windows系统中,操作系统的断点异常处理函数(KiTrap03)对于x86CPU的断点异常会有一个特殊的处理
.text:00436CF5 mov ebx, [ebp+68h] .text:00436CF8 dec ebx .text:00436CF9 mov ecx, 3 .text:00436CFE mov eax, 80000003h .text:00436D03 call CommonDispatchException ; 处理异常
出于这个原因,我们在调试器看到的程序指针仍然指向的是INT 3指令的位置。
而KiTrap03就是int 3的例程函数,3就是IDT表中的索引。
于是对于IDT中KiTrap03的Hook有了一些学习,在学习中也产生一些问题,不过特别注意不能对KiTrap03下断点,不然会死循环,系统直接卡死。
二、IDT hook
1、基本思路:IDT(Interrupt Descriptor Table)中断描述符表,是用来处理中断的。中断就是停下现在的活动,去完成新的任务。一个中断可以起源于软件或硬件。比如,软件中断int 3断点,调用IDT中的0x3,出现页错误,调用IDT中的0x0E。或用户进程请求系统服务(SSDT)时,调用IDT中的0x2E。我们现在就想办法,先在系统中找到IDT,然后确定0x3在IDT中的地址,最后用我们的函数地址去取代它,可以去监控是否是当前进程被调试。
2、需解决的问题:从上面分析可以看出,我们大概需要解决这几个问题:
1.IDT的获取,
①可以通过SIDT指令,它可以在内存中找到IDT,返回一个IDTR结构的地址。
②也可以通过kpcr结构获取,这个结构我们后面再说。
typedef struct { WORD IDTLimit; WORD LowIDTbase;//IDT的低半地址 WORD HiIDTbase;//IDT的高半地址 }IDTINFO; IDTINFO Idtr; __asm sidt Idtr
//方便获取地址存取的宏
#define MAKELONG(a,b)((LONG)(((WORD)(a))|((DWORD)((WORD)(b)))<<16))
#pragma pack(1) typedef struct { WORD LowOffset; //入口的低半地址 WORD selector; BYTE unused_lo; unsigned char unused_hi:5; // stored TYPE ? unsigned char DPL:2; unsigned char P:1; // vector is present WORD HiOffset; //入口地址的低半地址 } IDTENTRY; #pragma pack()
在windbg中可以通过!idt -a命令查看所有idt中例程的地址
在每项中我们看到有LowOffset和HiOffset这两个成员,这两个成员构成了处理例程的高4位和低4位。
知道了这个入口结构,就相当于知道了每间房(可以把IDT看作是一排有256间房组成的线性结构)的长度,我们先获取所有的入口idt_entrys,那么第0x3个房间的地址也就可以确定了,即idt_entrys[0x3]。
2.修改IDT表项中的LowOffset和HiOffset来修改IDT例程
DWORD KiRealSystemServiceISR_Ptr; // 真正的2E句柄,保存以便恢复hook #define NT_SYSTEM_SERVICE_INT 0x3 //我们的hook函数 int HookInterrupts() { IDTINFO idt_info; //SIDT将返回的结构 IDTENTRY* idt_entries; //IDT的所有入口 IDTENTRY* int2e_entry; //我们目标的入口 __asm{ sidt idt_info; //获取IDTINFO } //获取所有的入口 idt_entries = (IDTENTRY*)MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);
//保存真实的0x3地址 KiRealSystemServiceISR_Ptr = MAKELONG(idt_entries[NT_SYSTEM_SERVICE_INT].LowOffset, idt_entries[NT_SYSTEM_SERVICE_INT].HiOffset); //获取0x3的入口地址 int2e_entry = &(idt_entries[NT_SYSTEM_SERVICE_INT]);
__asm{ cli; // 屏蔽中断,防止被打扰 lea eax,MyKiSystemService; // 获得我们hook函数的地址,保存在eax mov ebx, int2e_entry; // 0x2E在IDT中的地址,ebx中分地高两个半地址 mov [ebx],ax; // 把我们hook函数的低半地址写入真是第半地址 shr eax,16 //eax右移16,得到高半地址 mov [ebx+6],ax; // 写入高半地址 sti; //开中断 } return 0;
3.修改完成
在替换成功之后,我们可以查看idt中已经使我们函数的地址了
4.过滤函数处理
①.对于NewKiTrap03的处理,我们按照KiTrap03中一样构造陷阱帧,获得当前寄存器的值
_declspec(naked) void NewKiTrap03() { __asm { push 0 mov word ptr [esp+2],0 push ebp push ebx push esi push edi push fs mov ebx,30h mov fs,bx mov ebx,dword ptr fs:[0] push ebx sub esp,4h push eax push ecx push edx push ds push es push gs mov ax,23h sub esp,30h//以上构造 push esp //陷阱帧首地址 call FilterExceptionInfo add esp,30h//恢复现场 pop gs pop es pop ds pop edx pop ecx pop eax add esp,4h pop ebx pop fs pop edi pop esi pop ebx pop ebp add esp,4h jmp g_OrigKiTrap03//跳回老函数 } } VOID __stdcall FilterExceptionInfo(PKTRAP_FRAME pTrapFrame) { //eip的值减一过int3,汇编代码分析中dec, DbgPrint("Eip:%x ",(pTrapFrame->Eip)-1); }
②.在NewKiTrap03函数中可以获得当前进程的信息,比较当前进程是否被下断点
#pragma pack(1) __declspec(naked) void NewKiTrap03() { __asm { pushfd // 保存标志寄存器 pushad // 保存所有的通用寄存器 push fs __asm { mov ebx, 30H // Set FS to PCR. mov fs, bx } call MyUserFilter //过滤函数 pop fs popad // 恢复通用寄存器 popfd // 恢复标志寄存器 jmp ulAddress // 跳到原来的中断服务程序 } } #pragma pack() VOID MyUserFilter() { KdPrint(("Crurrent IRQL: %d ",KeGetCurrentIrql())); if (Eprocess_DebugPort > 0) { //__asm int 3 PEPROCESS pEprocess = PsGetCurrentProcess(); ULONG eprocess = (ULONG)pEprocess; char strProcessPath[256] = {'