一. 科普一下
IDT,即中断向量表,就是一个以KIDTENTRY构成的结构数组,一共有256项。我们可以用windbg了解其结构:
kd> dt nt!_KIDTENTRY
+0x000 Offset : Uint2B
+0x002 Selector : Uint2B
+0x004 Access : Uint2B
+0x006 ExtendedOffset : Uint2B
由Offset表示的高位和ExtendedOffset表示的低位组成的4字节就构成了一个中断向量的处理函数地址。当一个中断或异常被触发时,
CPU就会保存当前线程的上下文信息,并将控制权交给中断处理程序。IDT的头32个向量用于异常处理,他们是Intel预先定义的。
IDT中包含了3个门描述符,分别是中断门,任务门和陷阱门。在这里我们需要关注的是中断门和陷阱门。中断门和陷阱门的不同点在
于他们被触发时的EFLAGES寄存器中的IF标志位的状态。当中断或异常是由中断门引起时,CPU会自动清除IF标志位,如果是由陷阱门
引起的,则不会影响IF标志位。
异常可以归为3类:陷阱(Trap),错误(Fault)和终止(Aborts)。终止通常表示严重错误,出错的任务不允许再获得重新运行的机会,
例如Machine Check异常(int 0x12)。陷阱和错误允许出错的任务在异常得到处理后继续运行。陷阱和错误的区别在于保存在系统栈
中的中断返回地址,对于错误类异常,其返回地址指向导致异常的那一条指令所在的地址,在异常处理完毕后,CPU将任务的上下文
状态恢复到异常指令执行前的状态并重新执行此指令,例如缺页异常(int 0xE)。而对于陷阱类异常,中断返回地址指向触发异常的
指令的下一个地址,这样就不再执行触发异常的指令了。
在此我们需要特别关注的是调试异常(int 1),它可以由trap或fault触发。如下所示:
1、指令断点
2、内存访问断点
3、IO端口访问断点
4、General detect condition (当DR7的GD位设为1)
5、当EFLAGES寄存器中的TF标志位被设置的时候,每执行一条语句都会产生调试中断,即所谓的单步中断
6、由Int 1指令触发
在指令断点和GD触发类型是fault,其他的触发类型是traps,表示异常处理后的返回地址是触发异常的下一条指令。
我们要控制调试处理程序运行,那就必须了解一下与之密切相关的调试寄存器。
调试寄存器
IA-32有8个调试寄存器,分别为DR0~DR7。DR1~DR3是调试地址寄存器,用来指定断点的地址,即通常所说的硬件中断存放的地方。
DR6(debug status register)指示调试程序时异常发生的原因。当调试异常发生时,DR6的有关位自动置1。为避免在识别各种调试
异常时的混乱,调试服务程序返回前应复位DR6。
B3~B0(breakpoint condition detected flags):断点异常发生指示位。
当Bi(i=0~3)为1时,表示对应断点的异常已经发生。
BD(debug register access detected flag):调试寄存器处理检测位。当BD=1时,表明下一条指令将读/写调试寄存器。
BS(single step flag):单步异常标志位。当BS=1时,表示异常是由标志寄存器中TF=1时单步自陷引起的。
BT(task switch flag):任务转换标志位。当BT=1时,表示因为转换而发生异常。
这些调试寄存器给80486微处理器带来了先进的调试功能,如设置数据断点、代码断点(包括ROM断点)和对任务转换进行调试。
DR7 (debug control register):调试控制寄存器,用于指示中断发生的条件及断点的类型。
L3~L0(local breakpoint enable flags):局部断点使能标志位。
Li(i=0~3)设置为1时,表示i号断点局部允许使用,断点仅在某一任务内发生,Li 位在任务转换时清0。若要使某个断点
在某个任务中有效,则该任务在TSS中的T位应置为1。此后,在任务转换取得CPU控制权时发生异常,则可在其处理程序中将Li位置
为1,即能保证该断点在此任务内有效。
G3~G0(global breakpoint enable flags):全局断点使能标志位。
Gi(i=0~3)设置为1时,表示i号断点全局允许使用,无论是操作系统还是某一任务,只要满足条件便会产生中断。
LE和GE(local and global exact breakpoint enable flags):局部断点、全局断点类型标志位。
当LE和GE为1,表示全局断点或局部断点为精明断点。精明断点为立即报告的断点。非精明断点为可以隔若干条指令后再报告或不报告的断点。
GD(general detect enable flag):调试寄存器保护标志位。
当GD为1时,调试寄存器处于保护状态,并产生中断。
R/W3~R/W0(read/write fields):发生中断时系统读/写标志位。
R/W3~R/W0分别指示当L3~L0局部断点和G3~G0全局断点发生时,系统在进行何种操作。
LEN3~LEN0(length fields):断点地址开始存放的数据长度。
LEN3~LEN0分别指示断点地址寄存器DR3~DR0在存储器中存放的情况。
二. 利用中断向量还原IDT hook
现在一些键盘嗅探器和驱动保护等都喜欢在IDT上动手脚,在他们启动时都接管中断向量或者对中断处理函数进行hook。以下介绍一下如何
用IDT hook进行anti-rootkit处理。此方法的好处是什么呢,我想最大的好处就是某些猥琐程序对IDT动手脚后,在它下一条指令运行前,
我们已经把IDT还原了。
我们首先要处理的是调试异常int 1。
从上面的介绍可以知道,如果我们在调试地址寄存器中放入我们要监视的内核地址时,所有对这个内核地址的访问操作都会触发int 1异常,
这时就需要实现我们自己的int 1异常处理程序,但触发情况是我们感兴趣的条件时,就进入我们的处理程序,但不是我们感兴趣的情况时,
则交由原中断函数进行处理。例如我们要保护int 60中断函数的处理地址,可以将中断处理函数的地址放到中断地址寄存器中,并修改DR7
的相关标志位使得任何对其的读写操作都触发一个异常,然后在我们的int 1处理程序中进行处理。为了简化代码,下面只对一个CPU的情况
进行处理,如果是多CPU的话,可以读取内核变量KeNumberProcessors,然后对所有的CPU都处理一遍。
在处理之前我们先要获得原来的中断向量处理地址
void GetOldIdtHandler()
{
ULONG idtbase = 0;
UCHAR idtr[8];
__asm
{
sidt idtr
lea ebx,idtr
mov ecx,dword ptr[ebx+2]
/////save INT1
add ecx,8
mov ebx,0
mov bx,word ptr[ecx+6]
shl ebx,16
mov bx,word ptr[ecx]
mov g_trap1_addr,ebx
}
}
然后编写我们自定义的int 1处理函数,在此过程中,必须注意的是保证堆栈平衡,和代码运行在非分页内存中,否则就会出现令人恼火的BOSD。
在中断发生时,CPU会见EFLAGE、CS和EIP分别压入堆栈,进入中断处理程序时栈顶结构如下所示:
|----|
| EIP | esp
|----|
| CS | esp+4
|----|
| EFLAGS | esp+8
|----|
由此可以建立一个结构用于表示产生异常发生时的堆栈结构
typedef struct _INT_STACK
{
ULONG SaveEip;
ULONG SaveCS;
ULONG SaveEFLAGS;
} INT_STACK, *PINT_STACK;
接着写新的Trap 01处理函数,判断一下产生异常的情况,如果是我们感兴趣的异常,就有自定义的int 1处理函数进行处理,如果是其他情况
引发的异常,就交给原中断处理函数处理。
#pragma LOCKEDCODE
void __declspec(naked) NewTrap01()
{
_asm
{
pushfd
pushad
mov ebx, esp
add ebx, 36
push ebx
call Rootkit_Trap1
cmp eax, 0
je OrgHandle
popad
popfd
iretd
OrgHandle:
push 0
mov word ptr [esp+2], 0
jmp g_trap1_addr+9
}
}
下面就是自定义的处理函数。在我们将要保护的地址放入中断寄存器的时候,就可以监视所有对其进行操作的一举一动了。但是,如果别人的
程序对中断寄存器清0怎么办。这时可以借助DR7的GD位来解决,此标志位全称为:General Detect Enable,当设置此位时,如果CPU检测到有
修改Drx寄存器的指令时,就会执行这条指令前先产生一个调试异常,我们就可以对这些情况进行处理。一般对调试寄存器的操作都是3字节指
令,为了保护Drx,可以简单的将堆栈中的中断返回地址+3,跳过这些指令。当然最好的方法就是用智能的反汇编引擎分析指令的操作数再做相
应处理。因为在进入中断处理程序的时候,CPU会自动把GD位清除,使得中断处理程序可以操作DRx,所以在离开中断处理程序的时候,还必需
手动重设GD位,继续保护DRx寄存器。
ULONG Rootkit_Trap1(PINT_STACK pStack)
{
unsigned long eip;
DR6 dr6;
dr6 = GetDR6();
// CPU发现要修改DRx的指令,这里简单的跳过此指令,并清除DB位
if (dr6.BD == 1)
{
// 跳过对DRx操作的指令
pStack->SaveEip = eip + 3;
// 清除DB位
_asm
{
mov eax, dr6
and eax, 0xFFFFDFFF
mov dr6, eax
// 恢复GD位
mov eax, dr7
or eax, 0x2000
mov dr7, eax
}
return 1;
}
// DR0处的断点被触发
if (dr6.B0 == 1)
{
_asm
{
cli
mov eax, cr0
mov g_cr0, eax
and eax, 0fffeffffh
mov cr0, eax
// 先屏蔽DR0中断
mov eax, dr7
and eax, 0xFFFFFFFC
mov dr7, eax
// 在这里我们就可以恢复IDT hook
lea esi, g_Org_Trap0Byte
mov edi, g_trap0_addr
mov ecx, 5
cld
rep movsb
// 开启内存写保护
mov eax, g_cr0
mov cr0, eax
// 恢复GD位
mov eax, dr7
or eax, 0x2000
mov dr7, eax
sti
}
return 1;
}
// 恢复GD位
_asm
{
mov eax, dr7
or eax, 0x2000
mov dr7, eax
}
return 0;
}
三. 利用IDT hook实现代码stealth inlinehook
有时候我们的程序正是邪恶程序那如何处理…………
通过上面的代码我们发现,既然中断向量的返回地址已经压入堆栈中,我们不就可以通过改变这个返回地址实现指令流程的改变了吗。
inline hook是我们通常用来改变程序流程的一种方法,其实现需要改变原指令,将其JMP XXXX到自己的处理流程中,但这种方法非常容易被
检测。而通过中断向量,我们可以用一种不改变原指令的方式实现更加隐蔽的inline hook。
首先,我们要把打算inline hook的地址放到中断寄存器中(Dr0~Dr3,~~~这就是通常意义上的猥琐断点),并改变Dr7标志位使得这些这个地
址的指令时触发int 1调试异常。这样我们就可以在自定义的int 1异常中改变堆栈中的中断返回地址,实现控制程序流程的转变。
这个时候我们需要改变一下Rootkit_Trap1这个处理函数
ULONG Rootkit_Trap1(PINT_STACK pStack)
{
unsigned long eip;
DR6 dr6;
dr6 = GetDR6();
// 获得中断时的EIP
eip = pStack->SaveEip;
// CPU发现要修改DRx的指令,这里简单的跳过此指令,并清除DB位
if (dr6.BD == 1)
{
// 跳过对DRx操作的指令
pStack->SaveEip = eip + 3;
// 清除DB位
_asm
{
mov eax, dr6
and eax, 0xFFFFDFFF
mov dr6, eax
// 恢复GD位
mov eax, dr7
or eax, 0x2000
mov dr7, eax
}
return 1;
}
// DR0处的断点被触发
if (dr6.B0 == 1)
{
/////////// 恢复inline hook idt ///////////////
_asm
{
cli
mov eax, cr0
mov g_cr0, eax
and eax, 0fffeffffh
mov cr0, eax
// 先屏蔽DR0中断
mov eax, dr7
and eax, 0xFFFFFFFC
mov dr7, eax
}
// 劫持中断返回指令地址,指向我们的中继函数
pStack->SaveEip = (unsigned long)Hijack;
// 开启内存写保护
_asm
{
mov eax, g_cr0
mov cr0, eax
// 恢复GD位
mov eax, dr7
or eax, 0x2000
mov dr7, eax
sti
}
return 1;
}
// 恢复GD位
_asm
{
mov eax, dr7
or eax, 0x2000
mov dr7, eax
}
return 0;
}
// 中继函数
void __declspec(naked) Hijack()
{
_asm
{
pushf
pushad
call FakeFunc
popad
popf
jmp g_HookEip // 调回原函数继续执行
}
}
// 自定义函数,用于实现任意不为人知的猥琐功能
void FakeFunc()
{
…………
}
这个方法的缺陷就是中断寄存器太少了,只有4个,这样只能控制4个地址。但其隐蔽性比一般的SSDT hook和inline hook高一点。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)