首页 > 技术文章 > 硬件断点还能这么玩?

northeast-coder 2022-01-10 22:53 原文

上一篇文章我们介绍了inlinehook(修改代码的hook方式),接下来准备介绍硬件断点+veh hook(无需修改代码的hook方式)。作为铺垫,本文先介绍硬件断点。

 

获取本文的实战代码、参考资料,请关注后,在聊天框回复:硬件断点。

 

硬件断点介绍

 

硬件断点与软件断点类似,都是一种代码调试的手段,可以让代码中断在需要的地方,方便调试。

 

软件断点是调试器在断点位置插入 int 3汇编指令实现的。硬件断点顾名思义是依赖硬件cpu,主要是靠dr0~dr7 ,8个调试寄存器实现的。

 

硬件断点比软件断点的功能更强,除了函数断点外,还可以数据断点,可以指定当数据被读或写时中断。

 

硬件断点的本质就是在指定内存下断点,内存可以位于代码段(函数断点)也可以是数据段(数据断点)。可以设置事件有执行、写入、读写时中断。

 

调试器使用硬件断点实现数据断点功能(例如gdb的watch命令)

Breakpoint 1, main (argc=0, argv=0x7fffffffe3e0) at xxx.cpp:27
27      {
(gdb) watch argc
Hardware watchpoint 2: argc

 

一些游戏外挂也会使用硬件断点来实现hook效果,介绍完硬件断点以后讲。

 

调试寄存器

硬件断点是cpu提供的功能,主要是与cpu上的dr0~dr7这8个调试寄存器打交道。下面参考intel手册详细介绍一下。

intel-325462-sdm-vol-1-2abcd-3abcd.pdf ,page 3415

 

dr0,dr1,dr2,dr3(Debug Address Registers)

 

dr0~dr3是调试地址寄存器,储存4个硬件断点的内存地址。硬件限制所以最多只有四个硬件断点。

 

dr4,dr5(保留,暂时没用)

 

 

目前没有用,不说了。

 

dr6(Debug Status Register)

 

 

调试状态寄存器,dr6按位使用,我们主要关注B0~B3位。触发硬件断点后,会将对应序号位设置为1。

 

dr7(Debug Control Register)

 

可以按位设置硬件断点的属性,包括:开关位、条件位、长度位。

 

开关位

 

dr7的开关位控制dr0~dr3号硬件断点是否启用。

 

条件位

 

dr7的条件位控制dr0~dr3如何被触发。00 执行时触发。01写入时触发,11读写时触发。

 

长度位

 

 

dr0~dr3指定的内存地址,dr7的长度位控制内存长度。

 

如果dr7的条件位设置为00执行,则对应的长度位必须是00。

 

代码实战

 

设置调试寄存器

 

windows提供了API,可以设置和获取寄存器。

BOOL SetThreadContext(
    _In_ HANDLE hThread,
    _In_ CONST CONTEXT* lpContext
    );
BOOL GetThreadContext(
    _In_ HANDLE hThread,
    _Inout_ LPCONTEXT lpContext
    );

CONTEXT是一个结构体,里面包含所有寄存器。因为每个cpu有一套寄存器,所以API里需要传线程句柄,设置哪个线程的寄存器。

 

dr7辅助类

 

dr7寄存器都是位操作,不方便设置也不方便看。写个辅助类。按位操作的都可以这样来弄,不用来回位移了。

 

// <<intel-325462-sdm>> page 3414
// Debug control register (DR7)
// Specifies the forms of memory or I / O access that cause breakpoints to be generated.
struct xx_dr7 {
  uint32_t L0 : 1;
  uint32_t G0 : 1;
  uint32_t L1 : 1;
  uint32_t G1 : 1;
  uint32_t L2 : 1;
  uint32_t G2 : 1;
  uint32_t L3 : 1;
  uint32_t G3 : 1;




  uint32_t LE : 1;
  uint32_t GE : 1;
  uint32_t no_use1 : 1;
  uint32_t RTM : 1;
  uint32_t no_use2 : 1;
  uint32_t GD : 1;
  uint32_t no_use3 : 2;




  uint32_t RW0 : 2;
  uint32_t LEN0 : 2;
  uint32_t RW1 : 2;
  uint32_t LEN1 : 2;
  uint32_t RW2 : 2;
  uint32_t LEN2 : 2;
  uint32_t RW3 : 2;
  uint32_t LEN3 : 2;
};

 

我们封装的函数

 

我们封装一个设置硬件断点的函数,利用前面的dr7辅助类。

参数有:线程句柄、硬件断点序号(0~3)、内存地址、事件类型(执行、写、读写)、内存长度。

#define RW_EXE    0b00
#define RW_WRITE  0b01
#define RW_RW    0b11


#define LEN_1B    0b00
#define LEN_2B    0b01
#define LEN_4B    0b11
static bool xx_set_hw_bp(HANDLE thread, int idx, void* addr, 
  int RW = RW_EXE, int LEN = LEN_4B)
{
  if (RW == RW_EXE) {
    //If the corresponding RWn field in register DR7 is 00 (instruction execution), then the LENn field should also be 00. 
    // The effect of using other lengths is undefined.See Section 17.2.5, “Breakpoint Field Recognition, ” below.
    LEN = 0b00;
  }


  // get context
  CONTEXT context = { 0 };
  context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
  BOOL get_ret = GetThreadContext(thread, &context);
  if (FALSE == get_ret) {
    return false;
  }


  // set dr7
  xx_dr7 dr7{ 0 };
  if (0 == idx) {
    memcpy(&context.Dr0, &addr, sizeof(addr));
    dr7.L0 = 1;
    dr7.G0 = 1;
    dr7.RW0 = RW;
    dr7.LEN0 = LEN;
  }
  else if (1 == idx) {
    memcpy(&context.Dr1, &addr, sizeof(addr));
    dr7.L1 = 1;
    dr7.G1 = 1;
    dr7.RW1 = RW;
    dr7.LEN1 = LEN;
  }
  else if (2 == idx) {
    memcpy(&context.Dr2, &addr, sizeof(addr));
    dr7.L2 = 1;
    dr7.G2 = 1;
    dr7.RW2 = RW;
    dr7.LEN2 = LEN;
  }
  else if (3 == idx) {
    memcpy(&context.Dr3, &addr, sizeof(addr));
    dr7.L3 = 1;
    dr7.G3 = 1;
    dr7.RW3 = RW;
    dr7.LEN3 = LEN;
  }
  // set context
  context.Dr7 = dr7.get();
  BOOL set_ret = SetThreadContext(thread, &context);
  return TRUE == set_ret;
}

 

代码逻辑不复杂,借助dr7辅助类,设置对应的控制位。

 

下面做一些测试

 

vs的数据断点

 

测试一下,vs中设置数据断点后,调试寄存器的值是怎样的?

测试代码:

void test_vs_data_bp() {
  int n = 0;// 对n下数据断点
  CONTEXT context = { 0 };
  context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
  auto ret = GetThreadContext(GetCurrentThread(), &context);
  xx_dr7 dr7{ 0 };
  dr7.set(context.Dr7);
}

在第3行代码下普通断点停住,再对 n下数据断点。

 

然后看看调试寄存器的值

 

借助dr7辅助类看看dr7情况。

 

控制位L0设置为0b1,条件位RW0设置为0b01(写事件),长度位为0b11(0x3,4字节)。

 

硬件断点执行事件

 

我们继续测试一下,执行事件的硬件断点。

void test_exe_hw_bp_func() {
  xx_set_hw_bp(GetCurrentThread(),1, &func, RW_EXE);
  func();
}

执行func时触发异常,我们没有设置veh处理,就被调试器捕捉到

 

硬件断点写事件

 

接下来试试写事件,测试代码如下:

void test_write_hw_bp() {
  int n = 0;
  xx_set_hw_bp(GetCurrentThread(), 0, &n, RW_WRITE);


  n = 1;//write
}

n=1时触发断点

 

硬件断点读写事件

继续试试读写事件,为啥不能设置只读事件?

void test_read_hw_bp() {
  int n = 0;
  xx_set_hw_bp(GetCurrentThread(), 0, &n, RW_RW);
  int b = n;//read


  b = b * b;


  n = 5;
}

读事件时中断

 

写事件时中断

 

硬件断点写事件1Byte

 

接下来试试,长度控制位。先设置写事件、1字节。再设置写事件、4字节。

void test_write_hw_bp_1byte() {
  char c[4];


  xx_set_hw_bp(GetCurrentThread(), 0, c, RW_WRITE,LEN_1B);
  c[0] = 0;
  c[1] = 0;


  xx_set_hw_bp(GetCurrentThread(), 0, c, RW_WRITE, LEN_4B);
  c[0] = 0;
  c[1] = 0;
}

不贴图了,在执行第5、9、10行时发生了中断。第6行没有中断,因为只设置了1字节。

 

硬件断点、调试寄存器介绍完了,下次就介绍硬件断点hook。

最后,求关注、点赞、转发~

东北码农,全网同名,求关~

推荐阅读