一. 科普一下
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高一点。