硬件断点的原理

 

Author:Lenus
From: www.popbase.net
E-mail:[email protected]
--------------------------------------------------
1.前言
     在我跨入ollydbg的门的时候,就对ollydbg里面的各种断点充满了疑问,以前我总是不明白普通断点,内存断点,硬件断点有什么区别,他们为什么有些时候不能混用,他们的原理是什么,在学习了前辈们的文章以后,终于明白了一些东西。希望这篇文章能让你对硬件断点的原理和使用有一些帮助
2.正文
--------------------------------------------------
   i.硬件断点的原理
   在寄存器中,有这么一些寄存器,它们用于调试。人们把他们称为调试寄存器,调试寄存器一共有8个名字分别从Dr0-Dr7。所以我们也把调试寄存器简单的称为Drx。
   对于Dr0-Dr3的四个调试寄存器,他们的作用是存放中断的地址,例如:401000
   对于Dr4,Dr5这两个寄存器我们一般不使用他们,保留
   对于Dr6,Dr7这两个寄存器的作用是用来记录你在Dr0-Dr3中下断的地址的属性,比如:对这个401000是硬件读还是写,或者是执行;是对字节还是对字,或者是双字。

   |---------------|----------------|
Dr0|                 用于一般断点的线性地址                   
   |---------------|----------------|
Dr1|                 用于一般断点的线性地址                   
   |---------------|----------------|
Dr2|                 用于一般断点的线性地址                   
   |---------------|----------------|
Dr3|                 用于一般断点的线性地址                   
   |---------------|----------------|
Dr4|                     保留                               
   |---------------|----------------|
Dr5|                     保留                               
   |---------------|----------------|
Dr6|                              |BBB                     BBB B |
   |                              |TSD                      3 2 1 0 |
   |---------------|----------------|
Dr7|RWE LEN   ...    RWE LEN    |  G               GLGLGLGLGL |
   | 3   3    ...        0    0     |  D               E E 3 3 2 21 100 |
   |---------------|----------------|
31                            15                                0

Dr0~3用于设置硬件断点,即在调试器中经常使用的bpm断点,由于只有4个断点寄存器,所以最多只能设置4个bpm断点。Dr7是一些控制位,用于控制断点的方式,Dr6用于显示是哪些引起断点的原因,如果是Dr0~3或单步(EFLAGS的TF)或由于GD置位时访问调试寄存器引起1号调试陷阱的话,则相应设置对应的位。下面对Dr6和Dr7的对应位做一些详细介绍:

调试控制寄存器Dr7:
==========
位0 L0和位1 G0:用于控制Dr0是全局断点还是局部断点,如果G0置位则是全局断点,L0置位则是局部断点。
G1L1~G3L3用于控制D1~Dr3,其功能同上。

LEN0:占两个位,开始于位15,用于控制Dr0的断点长度,可能取值:
00  1字节
01  2字节
10  保留
11  4字节
RWE0:从第17位开始,占两个位,控制Dr0的断点是读、写还是执行断点或是I/O端口断点:
00  只执行
01 写入数据断点
10 I/O端口断点(只用于pentium+,需设置CR4的DE位)
11 读或写数据断点
RWE1~3,LEN1~3分别用于控制Dr1~3的断点方式,含义如上。

还有一个GD位:用于保护DRx,如果GD位为1,则对Drx的任何访问都会导致进入1号调试陷阱。即IDT的对应入口,这样可以保证调试器在必要的时候完全控制Drx。

调试状态寄存器Dr6:
=========
该寄存器用于表示进入陷阱1的原因,各个位的含义如下:
B0~B3,如果其中任何一个位置位,则表示是相应的Dr0~3断点引发的调试陷阱。但还需注意的是,有时候不管GiLi如何设置,只要是遇到Drx指定的断点,总会设置Bi,如果看到多个Bi置位,则可以通过GiLi的情况判断究竟是哪个Dr寄存器引发的调试陷阱。
BD置位表示是GD位置位情况下访问调试寄存器引发的陷阱。
BT置位表示是因为TS置位即任务切换时TSS中TS位置1时切到第二个任务时第一条指令时引发的。
BS置位表示是单步中断引发的断点。。。。即EFLAGS的TF置位时引发的调试陷阱。

注意I/O端口断点是586+以上CPU才有的功能,受CR4的DE位的控制,DE为1才有效。(DE是CR4的第3位)。

   好了,从这里你可能明白一些东西。
1.   为什么在OD里面只能下4个硬件断点?  
2.   为什么下硬件断点有byte,word,dword只分?
3.   为什么下硬件断点有读,写,执行只分?
   ii.关于F4,F8,F7,F2的区别
   在ollydbug的help里面只是提到如何使用F7和F8的使用,并没说明他们的实现原理
   现在我们来做一个实验
实验一(F4的原理)
1.随便找一个程序,载入OD,构造一个死循环
就象这样:
00400154  >  90               nop                                   //EP停在这里
00400155     90               nop
00400156     90               nop
00400157     90               nop
00400158   ^ EB FA            jmp short 天2国际.<ModuleEntryPoint>  //构造一个死循环
0040015A     61               popad
0040015B     94               xchg eax,esp
2.对0040015A这一行按下F4,由于死循环,程序一直运行
3.调试器的窗口里,右键--查看调试寄存器
结果在Drx里面显示:
DR0 0040015A                      //地址
DR1 00000000
DR2 00000000
DR3 00000000
DR6 FFFF0FF0                     //断点属性
DR7 00000401
实验二(F8原理)
1.随便找一个程序,载入OD,构造一个子程序的死循环
就像这样
00400154 t&gt;  E8 0100D03F      call 4010015A         //EP,停在这里
00400159     90               nop
0040015A     90               nop               
0040015B     90               nop
0040015C     90               nop                //对这里下F2断点
0040015D     C3               retn              // 返回
2.按下F8,由于INT3断点,程序中断在0040015C
3.调试器的窗口里,右键--查看调试寄存器
结果在Drx里面显示:
DR0 00400159                             //call的返回地址
DR1 00000000
DR2 00000000
DR3 00000000
DR6 FFFF4FF1                          //断点属性
DR7 00000401
实验三(F7原理)
1.随便找一个程序,载入OD
2.双击调试器的窗口里的T标志,将TF从原来的0变成1
3.F9运行
结果程序断在了下面的一行
实验四(F2的原理)
1.用98的notepad吧,载入OD,构造一个死循环
004010CC N&gt;  90               nop                                 //EP,挺在这里
004010CD     90               nop
004010CE   ^ EB FC            jmp short NOTEPAD.<ModuleEntryPoint> //死循环
004010D0     90               nop                                //在这里按下F2,普通断点
004010D1     90               nop
2.按下F9,由于死循环,程序一直运行着
3.使用LordPE(不要用ollydump)将这个程序dump下来
4.重新载入OD
来看看成什么样子了
004010CC d&gt; $  90             nop
004010CD    .  90             nop
004010CE    .^ EB FC          jmp short dumped.<ModuleEntryPoint>
004010D0       CC             int3                                  //这里变成了CC了
004010D1       90             nop
--------------------------------------------------
3.总结
     从实验一和实验二我们能清楚的看到,F4是直接将该行的地址放入drx里面,F8是将下一行的地址放入到drx里面,他们都使用了调试寄存器。从实验三中我们知道对于F7来说很可能使用的是将TF置一的办法,也就是说当我们按下F7的时候OD把TF置一。对于F2来说他是将,第一个字节悄悄的修改成了CC,虽然并没有显示给我看到这个是一个CC,当我们按下F2的时候,OD还没有运行,只是把这个表示记录下来,当运行的时候他就把所有标记的字节修改了,尽管还是显示原来的代码,当然当他一暂停下来就又修改回来了。
     上面的是实验中,F7的原理只是猜测,还没有很好的办法能证明他就是使用TF,下面我继续猜测一下内存断点的原理
1.将设置的内存断点的地址记录下来
2.对这个地址的内存页面修改其属性
如果是内存写断点,就修改为RE(可读,可执行)
如果是内存访问断点,就修改为NO ACCESS(不可访问)
3.只要访问到这个页面就会产生相应的异常,然后由OD来判断是否与记录的断点一致,从而是否中断下来

 ============================

简析利用调试器寄存器实现内核函数的HOOK 

               简介:这是简析利用调试寄存器实现内核函数的hook的详细页面,介绍了和c/c++,寄存器,断点,reloadcr0andsti,有关的知识,加入收藏请按键盘ctrl+D,谢谢大家的观看!要查看更多有关信息,请点击此处

某些rk,木马会经常hook一些关键函数从而达到隐藏等目的,而相应的ark检测软件也会通常会先恢复这些关键函数的hook(譬如利用硬盘文件恢复),然后再调用来检测rk,这样就可以检测出某些隐藏.下面就介绍利用调试器实现某些内核函数的hook. 
intel386以后的系列cpu增加了8个32位的调试寄存器,从dr0到dr7,方便调试使用.如果设置了相应的调试信息,在条件满足的情况下将会发生 1 号(db例外)中断,cpu就会陷入中断例程,执行中断代码,我们的hook目的就可以通过这个实现. 
首先看下面百度出来的对寄存器组的使用方法的解释: 
这八个寄存器中由四个用于断点,两个用于控制,另两个保留未用。对这八个寄存器的访问,只能在0级特权级进行。在其它任何特权级对这八个寄存器中的任意一个寄存器进行读或写访问,都将产生无效操作码异常。此外,这八个寄存器还可用dr6及dr7中的bd位和gd位进行进一步的保护,使其即使是在0级也不能进行读出或写入。  
对这些寄存器的访问使用通常的mov指令:  
mov reg dri 该指令将调试寄存器i中的内容读至通用寄存器reg中; 

和 "简析利用调试寄存器实现内核函数的hook" 有关的c#、asp.net、c++编程小帖士:

strong>变量.Length 数字型 

取字串长度:  
如: string str="中国";  
int Len = str.Length ; //Len是自定义变量, str是求测的字串的变量名  
mov dri reg 该指令将通用寄存器reg中的内容写至调试寄存器i中。此处i的取值可以为0至7中的任意值。  
这些寄存器的功能如下:  
dr0—dr3 寄存器dr0—dr3包含有与四个断点条件的每一个相联系的线性地址(断点条件则在dr7中)。因为这里使用的是线性地址,所以,断点设施的操作,无论分页机制是否启用,都是相同的。  
dr4—dr5 保留。  
dr6是调试状态寄存器。当一个调试异常产生时,处理器设置dr6的相应位,用于指示调试异常发生的原因,帮助调试异常处理程序分析、判断,以及作出相应处理。  
dr7是调试控制寄存器。分别对应四个断点寄存器的控制位,对断点的启用及断点类型的选择进行控制。所有断点寄存器的保护也在此寄存器中规定。(下面这图怎么也改不好,大家还是去百度吧) 
    |---------------|----------------| 
dr6 | |bbb   bbb b | 
    | tsd   3 2 1 0 | 
    | --------------|----------------| 
dr7 |rwe len   ...     rwe len   | g   glglglglgl | 
    | 3   3   ...     0   0   | d   ee33221100 | 
    |---------------|----------------| 
  31 15     0 
dr6各位的功能 
b0—b3(对应0-3位) 当断点线性地址寄存器规定的条件被检测到时,将对应的b0—b3位置1。置位b0—b3与断点条件是否被启用无关。即b0—b3的某位被置1,并不表示要进行对应的断点异常处理。  
bd(13位) 如下一条指令要对八个调试寄存器之一进行读或写时,则在指令的边界bd位置1。在一条指令内,每当即将读写调试寄存器时,也bd位置1。bd位置1与dr7中gd位启用与否无关。  
bs(14位) 如果单步异常发生时,bs位被置1。单步条件由eflags寄存器中的tf位启用。如果程序由于单步条件进入调试处理程序,则bs位被置1。与dr6中的其它位不同的是,bs位只在单步陷阱实际发生时才置位,而不是检测到单步条件就置位。  
bt(15位) bt位对任务切换导致tss中的调试陷阱位被启用而造成的调试异常,指示其原因。对这一条件,在dr7中没有启用位。  
dr6中的各个标志位,在处理机的各种清除操作中不受影响,因此,调试异常处理程序在运行以前,应清除dr6,以避免下一次检测到异常条件时,受到原来的dr6中状态位的影响。  
dr7各位的功能  
len len为一个两位的字段,用以指示断点的长度。每一断点寄存器对应一个这样的字段,所以共有四个这样的字段分别对应四个断点寄存器。len的四种译码状态对应的断点长度如下  
len 说明  
0 0 断点为一字节  
0 1 断点为两字节  
1 0 保留  
1 1 断点为四字节  
这里,如果断点是多字节长度,则必须按对应多字节边界进行对齐。如果对应断点是一个指令地址,则len必须为00  
rwe rwe也是两位的字段,用以指示引起断点异常的访问类型。共有四个rwe字段分别对应四个断点寄存器,rwe的四种译码状态对应的访问类型如下  
rwe 说明  
0 0 指令  
0 1 数据写  
1 0 保留  
1 1 数据读和写  
ge/le ge/le为分别指示准确的全局/局部数据断点。如果ge或le被置位,则处理器将放慢执行速度,使得数据断点准确地把产生断点的指令报告出来。如果这些位没有置位,则处理器在执行数据写的指令接近执行结束稍前一点报告断点条件。建议读者每当启用数据断点时,启用le或ge。降低处理机执行速度除稍微降低一点性能以外,不会引起别的问题。但是,对速度要求严格的代码区域除外。这时,必须禁用ge及le,并且必须容许某些不太精确的调试异常报告。  
l0—l3/g0—g3 l0—l3及g0—g3位  
分别为四个断点寄存器的局部及全局启用信号。如果有任一个局部或全局启用位被置位,则由对应断点寄存器dri规定的断点被启用。  
gd gd位启用调试寄存器保护条件。注意,处理程序在每次转入调试异常处理程序入口处清除gd位,从而使处理程序可以不受限制地访问调试寄存器。  
前述的各个l位(即le,l0—l3)是有关任务的局部位,使调试条件只在特定的任务启用。而各个g位(即gd,g0—g3)是全局的,调试条件对系统中的所有任务皆有效。在每次任务切换时,处理器都要清除l位。  
  如果你耐心把上面的信息看完了,基本上也就应该明白了.其实我们可以利用调试寄存器做的不只是函数的hook,也可以进行i/o的hook,下面要说的是在指定的内核函数上下指令执行断点,然后挂接1号中断实现hook(有很多种方法可以实现,看你自己喜欢哪种了). 
  首先把dr0寄存器设置为要挂接的内核函数的地址(譬如zwcreatefile),然后修改 dr7的l0和g0(第0和第1位)都为1, dr7的 r/w0(16.17位) 为00, len0(18.19位)位为00,这样当cpu执行到zwcreatefile地址的时候,就会进入1号中断例程. 
  然后我们应该去修改idt表,将1号中断指向我们的处理程序. 
  接着需要考虑在中断例程我们要做的事情,关于中断时cpu具体做了什么,大家可以去搜索.下面只介绍我们所关心的在内核空间发生db例外时的情况:中断发生时,依次将 eflags,cs,eip压入堆栈,然后进入中断程序(由于中断地址本来就在内核空间,所以不需要切换堆栈).在中断处理程序中作出相应的处理,然后使用 iretd 指令退出中断.( iretd 指令: 依次将堆栈弹出到 eip,cs,eflags),我们可以通过修改堆栈中eip的值,在中断返回时跳转实现hook. 
  上面就是主要的内容,但是还有点问题.windows在kifastcall和线程切换时会修改drx的值,为了防止我们的断点被清除,可以利用dr7:gd位保护寄存器,这样任何对调试寄存器的操作(读和写)都会产生db例外然后进入1号中断例程.这样,我们在中断例程中又需要利用dr6的标识位处理那些因为操作调试寄存器产生的例外(关于这个我只是简单的跳过了那些对drx操作的代码,并没有详细分析). 
下面可以看详细的实现代码: 
简单实现hook下zwcreatefile,xp下,其他系统慎用 
/* 
drxhook.h 
written by [email protected] 
*/ 
#ifndef _drx_hook 
#define _drx_hook 
#include <ntddk.h> 
typedef unsigned long dword; 
typedef unsigned char bool; 
#pragma pack(push,1) 
typedef struct _idtr 

  //定义中断描述符表的限制,长度两字节; 
  short     idtlimit; 
  //定义中断描述服表的基址,长度四字节; 
  unsigned int   idtbase; 
}idtr,*pidtr; 
typedef struct _idtentry 

  unsigned short lowoffset; 
  unsigned short selector; 
  unsigned char unused_lo; 
  unsigned char segment_type:4;   //0x0e is an interrupt gate 
  unsigned char system_segment_flag:1; 
  unsigned char dpl:2;   // descriptor privilege level  
  unsigned char p:1; /* present */ 
  unsigned short hioffset; 
} idtentry,*pidtentry; 
#pragma pack(pop) 
dword getdbentry(); 
void hookdbint(); 
void unhookdbint(); 
#endif 
/* 
drxhook.cpp 
written by [email protected] 
*/ 
#include "drxhook.h" 
dword g_olddbentry; 
idtr g_idtr; 
dword g_oldcreatefile; 
dword g_hooknumber = 0; 
dword g_cr0; 
bool g_bexit; 
void reloadcr0andsti() 

  __asm 
  { 
    push   eax 
  mov     eax, g_cr0 
  mov     cr0, eax 
  pop     eax 
  sti 
  } 

void clianddisablewpbit() 

  __asm 
  { 
    cli 
  push   eax 
  mov     eax, cr0 
  mov     g_cr0, eax 
  and     eax, 0xfffeffff 
  mov     cr0, eax 
  pop     eax 
  } 

void printhook() 

  dbgprint(" now get in zwcreatefile hook: %d...pid: %d...\n", g_hooknumber++, (dword)psgetcurrentprocessid());

__declspec(naked) void newzwcreatefile() 

  __asm 
  {    
    pushfd;     // 仅仅适合于 xp 操作系统 
    call printhook; 
    popfd; 
    mov eax,0x25;    
    jmp g_oldcreatefile; 
  } 

void sethb()   // set hardware breakpoint 设置硬件断点 

  __asm 
  { 
    mov eax, zwcreatefile;     // 想要挂接的函数或者地址 
    mov dr0, eax; 
    mov eax, dr7; 
    or eax, 0x2703;   // 也要修改 dr7:gd 位,以免drx被操作系统或其他程序修改 
    and eax, 0xfff0ffff; 
    mov dr7, eax; 
  } 

__declspec(naked) void newdbentry() 

  __asm 
  { 
    pushfd; 
    push eax; 
    mov eax, dr6; 
    test eax, 0x2000; 
    jz not_edit_drx; 
    // 以下是如果有对drx的操作的简单处理,如有需要可以修改 
    // 我只是简单的跳过这些指令 
    and eax, 0xffffdfff; 
    mov dr6, eax;   // 清除dr6的标志 
    cmp g_bexit, 0; 
    jnz my_drv_exit;     // 驱动 unload 
    mov eax, [esp+8];     // 获取堆栈中的 eip 
    add eax, 3;     // 由于所有对 drx 的操作全都是3个字节的 
    mov [esp+8], eax;     // 修改 eip ,跳过当前指令,返回时执行下条指令 
    jmp my_int_end; 
not_edit_drx: 
    mov eax, dr6; 
    test eax, 0x1; 
    jz sys_int;   // 如果不是dr0 产生的中断,则跳回原系统中断 
    mov eax, [esp+8]; 
    cmp eax, zwcreatefile;   // 判断一下是不是 zwcreatefile 的线性地址 
    jnz sys_int; 
    mov eax, newzwcreatefile; 
    mov [esp+8],eax;     // 修改堆栈中的 eip ,实现返回时跳转 
my_int_end:    
    mov eax, dr7; 
    or eax, 0x2000;     // 恢复 gd 位 
    mov dr7, eax; 
my_drv_exit: // 整个驱动 unload 时,不恢复 dr7 
    pop eax; 
    popfd; 
    iretd; 
sys_int: 
    pop eax; 
    popfd; 
    jmp g_olddbentry; 
     
  } 

dword getdbentry() 

  pidtentry idtentry; 
  dword entry; 
  __asm sidt g_idtr; 
  idtentry = (pidtentry)(g_idtr.idtbase + 8); 
  entry = idtentry->hioffset << 16; 
   
  entry |= idtentry->lowoffset; 
  return entry;  

void hookdbint() 

  dword newentry; 
  pidtentry idtentry; 
  newentry = (dword)newdbentry; 
  g_oldcreatefile = (dword)zwcreatefile + 5;     // 新的要跳转过去的地址 
  g_olddbentry = getdbentry(); 
  idtentry = (pidtentry)(g_idtr.idtbase + 8); 
  clianddisablewpbit(); 
  idtentry->lowoffset = (ushort)newentry; 
  idtentry->hioffset = (ushort)( newentry >> 16 ); 
  reloadcr0andsti(); 
  sethb(); 
  g_bexit = false; 
  return; 

void unhookdbint() 

  pidtentry idtentry; 
  dword entry; 
   
  __asm sidt g_idtr; 
   
  idtentry = (pidtentry)(g_idtr.idtbase + 8); 
  clianddisablewpbit(); 
  g_bexit = true; 
  __asm mov eax, dr7;     // 产生一次例外并且清除dr7:gd 
  if ( g_olddbentry != 0 ) 
  { 
    idtentry->lowoffset = (ushort)g_olddbentry; 
     
    idtentry->hioffset = (ushort)( g_olddbentry >> 16 );      
  } 
  reloadcr0andsti(); 
   
  dbgprint(" unload drx hook..\n"); 
  return; 

ntstatus driverunload(in pdriver_object driverobject) 
{    
  unhookdbint(); 
   
  return status_success; 

ntstatus driverentry( 
  in pdriver_object driverobject, 
  in punicode_string registrypath 
  ) 

  hookdbint(); 
   
  driverobject->driverunload = driverunload; 
  dbgprint("load drxhook driver ok...\n"); 
  return status_success; 

/***********************/ 
以上代码实现了简单的zwcreatefile 函数的hook,可以拿dbgview查看效果. 
由于本人水平有限,代码中难免有错误出现,希望指正. 
同时也希望各位牛人来指点,[email protected] 
/**************下面是罗嗦几句********************/ 
1.这个方法呢首先不怎么实用,因为你用了调试寄存器后某些壳也想用,因此就冲突了,可能会使某些东西失效,不如传统的hook好用(据说利用缺页中断hook也比较好用,没试过)。还有人认为呢这个hook虽然是hook成功了,但是还得hook中断向量,没有必要。其实呢,只是多了种思路罢了,多给大家提供一些想法而已。 
2.其次呢,这个方法是我在调试某ark时想到的,这个ark的作者说他们会恢复函数的inline hook然后才去调用(大面积的恢复,甚至是整个文件的恢复),于是我就用调试器在该函数上下断点,结果自然是没有断下了,因为下的 (0xcc)断点被恢复了。于是就索性下了个硬件断点,这下就断住了,然后呢就想到了拿这个东西来hook。然后就去网上搜资料,发现不少人还是稍微提到过这个方法的,包括 vxk,xikug,都说过。

 

 

你可能感兴趣的:(职场,原理,断点,硬件,休闲)