在科锐学习的第三阶段,要求设计DUBG。由于学习C++时间不长,代码写的很粗糙。自身水平太菜,设计中难免有疏漏之处,欢迎拍砖

Easydebug 设计文档
一.  功能
1.  创建被调试进程。
2.  可以添加删除显示INT3断点,内存断点,硬件断点。
3.  单步进入和单步步过。
4.  运行程序到指定地址处。
5.  显示指定地址的反汇编。

二.  编译环境、运行环境,第三方库。
编译环境:VC6,win32console,运行环境:windows2000,XP COMMAND命令行下。本程序使用的反汇编引擎是Diasm。

三.  命名约定:
BPH:硬件断点
BPM:内存断点
BPS:int3断点,本项目中为了区别于硬件断点,称为软断点。
处理命令类函数:OnCmdXXX
相应调试事件函数:OnDEXXX
相应异常函数:OnExcpXXX



四.  基本设计概念和处理流程
1.  原理:利用WIDNOWS的调试API。首先以DEBUG_ONLY_THIS_PROCESS 作为参数调用CreateProcess,创建一个被调试的进程。当被调试进程发生某些事件的时候,系统会产生一个调试事件,并将此调试事件发送给调试程序,被调试进程进入暂停状态,直到调试程序调用ContinueDebugEvent()。调试器需要单独建立一个线程,用来等待接收调试事件,用到的API为WaitForDebugEvent,当接收到一个调试事件的时候,需要对此事件进行处理,然后进入下一轮等待接收调试事件。
DEBUG_EVENT中的dwDebugEventCode成员记录了调试事件,其中比较重要的是EXCEPTION_DEBUG_EVENT,表示该调试事件为异常。
DEBUG_EVENT. u.Exception. ExceptionRecord.ExceptionCode中记录了异常的代码。
(参考资料:
   软件加密技术内幕   第3章 Win32 调试API  段钢 印豪……
   调试软件   张银奎
MSDN     MICROSOFT     )
   
2.  程序结构:把所有的命令处理,响应调试事件,响应异常处理。都封装在Cdebug类里。为了对脚本支持,采用CMD命令模式。为了以后方便扩展,移植,及代码简洁,本项目模仿MFC的消息映射的处理方式。
Cdebug包含了3张表,分别是
  CMD_MAP:记录命令和命令处理函数。
  DE_MAP:记录调试事件和对应的响应函数。
  EXCP_MAP:记录异常事件和对应的处理函数。
表的建立和声明参考MFC的消息隐射表的添加方式,用三个宏
DECLARE_TABLENAME_MAP
BEGIN_ TABLENAME_MAP
END_ TABLENAME_MAP
当需要添加一个命令及对应的处理函数,需要按照以下3个步骤:
(以CMD表为例,其他不再赘述)
(1)在Debug.h文件中,把函数声明添加在
// begin_define_CMDFn

// end_define_CMDFn 中间。
(2)在Debug.cpp文件中,把函数和命令的对应关系添加在
BEGIN_BASECMD_MAP(CDebug)

END_CMD_MAP()中间
(3)在Debug.cpp中添加函数的实现。

例如://Debug.h
// begin_define_CMDFn
  virtual void OnCmdBps(char *szCMD);
  virtual void OnCmdBph(char *szCMD);
  virtual void OnCmdBpm(char *szCMD);
// end_define_CMDFn

//Debug.cpp
BEGIN_BASECMD_MAP(CDebug)
  ON_CMD(BPS,OnCmdBps)
   ON_CMD(BPH,OnCmdBph)
   ON_CMD(BPM,OnCmdBpm)
END_CMD_MAP()
…………
void CDebug::OnCmdBph(char* szCmd)
{
  ……
}

void CDebug::OnCmdBpm(char* szCmd)
{
  ……
}

void CDebug::OnCmdBps(char* szCmd)
{
  ……
}

三张表中的函数定义分别如下:
typedef void (CDebug::*PCMD)(char *strCMDPar);
typedef void (CDebug::*PDEFN)(DEBUG_EVENT*);
typedef void (CDebug::*PEXCPFN)(DEBUG_EVENT*);

另外设计了3个函数,分别用于这三张表的查找和分发
(1)void CDebug::CmdDispatch(char *szCmd)
     当执行外部命令时,调用此函数,转入对应的命令处理
(2)  void DEDispatch(DEBUG_EVENT* pDebugEvent);
  当接收到调试事件时,调用此函数,转入对应的调试事件处理函数
(3)  void ExcpDispatch(DEBUG_EVENT *pDebugEvent);
当调试事件为异常时候,调用此函数,转入对应的异常处理。

       调试线程函数:
static DWORD WINAPI DebugThreadProc(void*)。
该函数首先建立被调试进程,然后循环接收调试事件并处理。

Main()函数主要是用来循环接收外界输入的命令,并调用
Debug.CmdDispatch(szInput).
        (参考资料:
MFC源代码     
         深入浅出MFC   侯捷)


3.断点的实现
断点有3种类型,分别是软件断点,硬件断点,内存断点。
对应3个类,分别为CBps,CBph,CBpm.他们的父类是CBpBase
(1)  CBpBase.类中定义了一些通用接口,以及三个子类中都要用到得函数,成员变量,定义如下
 class CBpBase  
{
public:
  HANDLE m_hProc;  //被调试进程句柄
  OPENTHREAD OpenThread;

  CBpBase();  
  virtual ~CBpBase();
//发生一个单步断点异常的处理
  virtual int OnExcpSingle(DEBUG_EVENT *pDebugEvent)=0;  
virtual BOOL AddBp(LPVOID pBpInfo) = 0;       //添加一个断点
  virtual BOOL DelBp(LPVOID pAddr) = 0;       //删除一个断点
//获取所有断点信息
  virtual BOOL GetBpList(CStringArray &pStrArry) = 0;

  BOOL SetContextInfo(CONTEXT *pContext,DWORD dwThreadId); 
  BOOL GetContextInfo(CONTEXT *pContext,DWORD dwThreadId);
  int  SetTF(DWORD dwThreadId);
  BOOL ReadMem(void *addr, void *buf, DWORD len);
  BOOL WriteMem(void *addr, void *buf, DWORD len);
  int Init(HANDLE hProc);
};
     OnExcpSingle、 AddBp、 DelBp、 GetBpList分别为纯虚函数。
OnExcpSingle(DEBUG_EVENT *pDebugEvent)函数的作用是:当被调试程序执行到断点时,该断点需要先删除当前断点,然后执行一个单步,在单步的时候把断点设置回去。
AddBp(),DelBp()分别为添加一个断点和删除一个断点。
GetBpList()函数为获取该断点类的所有断点。 
SetTF()函数的作用是:设置标志寄存器中的TF位为1。当此位被置位时候,CPU每执行一个指令,会产生一个调试异常,中断到调试寄存器。需要注意有某些指令比如POPF,会影响该标志位,所以在设置该位的时候,需要检查当前指令。
ReadMem()WriteMem分别为读写内存函数。
Init()函数设置m_ hProc成员变量,该变量记录当前被调试进程句柄。

(1)  INT3(软件)断点:CBps
原理介绍:当CPU执行到INT 3指令的时候,会产生一个EXCEPTION_BREAKPOINT异常,在该程序被调试的情况下,程序被暂停,该异常最终会被系统传递给调试器。直到调试器调用ContinueDebugEvent(),程序恢复执行。

类定义如下:
#include <list> 
#include "BpBase.h"

using namespace std; 
struct t_Bps
{
  BYTE bOld;            //保存原有字节
  LPVOID addr;             //断点地址
  DWORD BpCount;          //需要断下的次数。0为永远 。
  //重载==操作符,链表的REMOVE函数实现需要用到
  bool   operator==(const t_Bps&  Bps)   
  {   
    if   (addr==Bps.addr)   
      return   true;   
    else   
      return   false;        
  } 
};
class CBps : public CBpBase  
{
public:

  list<t_Bps> m_listBps; 
  t_Bps *m_pCurrentBps;  //用来标识当前断点。

  virtual BOOL AddBp(LPVOID pBpInfo);
  virtual BOOL DelBp(LPVOID pAddr);
  virtual BOOL OnEcxpBp(DEBUG_EVENT *pDebugEvent);
  virtual BOOL OnExcpSingle(DEBUG_EVENT *pDebugEvent);
  virtual BOOL GetBpList(CStringArray &pStrArry) ;
  BOOL AddBpInProc(LPVOID dwAddr,BYTE *byResCode);
  BOOL GetOldCode(LPVOID dwAddr,BYTE *pByOldCode);
  CBps();
  virtual ~CBps();
};

AddBp(LPVOID pBpInfo),添加一个断点。参数pBpInfo为一个指向t_Bps结构的指针。该函数首先pBpInfo中的addr处读取一个字节保存到bOld,然后向该地址写入一个CC(int 3),然后将该断点保存到m_listBps中。

DelBp(LPVOID pAddr),删除一个断点。参数pBpInfo为一个指向t_Bps结构的指针。该函数首先在m_listBps中检查有无此断点。如有,则先把bOld处记录的原指令写入被调试进程,然后在m_listBps中删除该记录。

OnEcxpBp(DEBUG_EVENT *pDebugEvent)
响应EXCEPTION_BREAKPOINT异常处理。参数为发生异常时候的DEBUG_EVENT,pDebugEvent->u.Exception.ExceptionRecord为发生异常时候的异常地址。该函数先根据异常地址遍历m_listBps,判断是否自己设置的断点。如是,首先还原原来的指令,然后将EIP--,设置一个单步断点。最后,将该断点信息记录在m_pCurrentBps中。注意,在t_Bps有记录的断点次数,如果该数为1,则不需要设置单步断点。

OnExcpSingle(DEBUG_EVENT  *pDebugEvent)
响应EXCEPTION_SINGLE_STEP异常处理。参数为发生异常时候的DEBUG_EVENT。该函数首先判断m_pCurrentBps是否为0,如非0,则表明该单步断点是OnEcxpBp()函数中设置的,然后重新设置m_pCurrentBps中记录的断点。

(2)  内存断点CBps
原理介绍:用VirtualProtectEx修改需要中断的内存属性,这样,当程序访问到该内存的时候会抛出一个内存访问异常,该异常将被DEBUG捕获到,在DEBUG中修改回原内存属性,并设置一个单步执行。当单步执行完,DEBUG再次捕获到单步的异常,然后再次修改内存属性。需要注意的是:1.每次修改内存属性的时候,是以0X1000为单位的。如果发生内存访问异常,DEBUG要对该地址进行判断,是否在自己设置的内存属性内。2.设置内存属性的时候,要先记录原内存属性,而内存是以0X1000为单位的,考虑如下一个内存断点:ADDR 0X401fff,Len 4.该范围正好跨2个内存页,这个时候要设置2个内存页的属性。同样,还原的时候也是如此。
CBpm定义如下:
struct t_MemPageInfo 
{
  DWORD dwPage;   //内存页面地址
  DWORD dwOldProtect;   //内存页面属性
};

struct t_Bpm
{
  LPVOID pAddr;    //断点地址
  DWORD  dwLen;    //断点长度
  DWORD  dwProtect;   //断点属性
  list<t_MemPageInfo> listPageInfo;  //记录原页面的内存属性,由于内存段可能有跨页面,所以用LIST
  bool   operator==(const t_Bpm&  Bpm)   
    {   
      if   (pAddr==Bpm.pAddr && dwProtect==Bpm.dwProtect)   
        return   true;   
      else   
        return   false;        
  } 
};

class CBpm : public CBpBase  
{
public:
  int OnExcpAV(DEBUG_EVENT *pDebugEvent);
  list<t_Bpm> m_listBpm; 
  t_MemPageInfo *m_pCurrentPage;  //用来标识当前断点。在单步后需要还原
  virtual BOOL AddBp(LPVOID pBpmInfo);
  virtual BOOL DelBp(LPVOID pAddr);
  virtual BOOL OnExcpSingle(DEBUG_EVENT *pDebugEvent);
  virtual BOOL GetBpList(CStringArray &pStrArry) ;
  DWORD SetBpmInProc(LPVOID dwAddr,DWORD dwOldProtect);
  CBpm();
  virtual ~CBpm();
};
AddBp()是用来添加一个内存断点。该函数对需要下断的内存页面依次进行属性设置,然后记录到m_listBpm。
DelBp()用来删除一个内存断点。该函数对指定地址处的内存页面依次还原原来的内存属性,然后从m_listBpm中删除掉该元素。
OnExcpAV()用来响应内存异常的处理。该函数首先检测内存异常的地址是否在自己设置断点的页面内,如果是,先设置单步并还原该页面内存属性,使用m_pCurrentPage记录该断点。然后进一步判断是否在自己设置的内存范围内,如果是,暂停被调试程序,否则继续执行程序。
OnExcpSingle()用来响应单步异常。该函数需要还原m_pCurrentPage中记录的断点信息,然后删除m_pCurrentPage。

(3)  硬件断点:CBph
 原理说明:硬件断点是依靠调试寄存器,其中CR0-DR3记录断点地址。DR7为控制位,用于控制断点的方式,Dr6用于显示是哪些引起断点的原因,如果是Dr0~3或单步(EFLAGS的TF)的话,则相应设置对应的位。发生硬件 断时,会产生一个EXCEPTION_SINGLE_STEP异常,注意,当设置单步断点时,也会引发一个EXCEPTION_SINGLE_STEP异常。DEBUG中捕获此异常,并进行相关处理。另外需要注意的一点:设置调试寄存器的时候,被调试程序必须处于暂停状态。(本处资料引自于FORGOT: 硬件断点与调试寄存器)。

定义如下:

typedef  HANDLE (WINAPI *DEBUGBREAKPROCESS)(HANDLE);
struct t_Bph
{
  DWORD dwAddr;  //断点地址
  DWORD dwLen;   //断点长度,只能为1 2 4
  DWORD dwStyle;   //断点类型,读,写,执行。
  BOOL  bAvaildFlag;   //说明该断点是否有效。
};
//该结构用来添加或者删除硬件断点时用。设置DRX的时候,需要ThreadContext.
struct t_AddBph
{
  DWORD dwAddr;  //断点地址
  DWORD dwLen;   //断点长度,只能为1 2 4
  DWORD dwStyle;   //断点类型,读,写,执行。
  BOOL  bAvaildFlag;   //说明该断点是否有效。
  DEBUG_EVENT *pDebugEvent;
};
class CBph : public CBpBase  
{
public:
  t_Bph m_Bph[4];
  t_Bph *m_pCurrentBph;        //当发生硬件断时,记录断点信息,设置单步用
  t_Bph *m_pAddBphOnBp;            //当添加或者删除断点信息时,用来记录断点信息。需要先中断被调试进程
  t_Bph *m_pDelBphOnBp;
  DEBUGBREAKPROCESS DebugBreakProcess;//DebugBreakProcess()函数指针
  virtual BOOL AddBp(LPVOID pAddBphInfo);
  virtual BOOL DelBp(LPVOID pAddr);
  virtual BOOL OnExcpSingle(DEBUG_EVENT *pDebugEvent);
  virtual BOOL GetBpList(CStringArray &pStrArry);
  BOOL OnEcxpBp(DEBUG_EVENT *pDebugEvent);
  BOOL SetDrx(DEBUG_EVENT *pDebugEvent);
  BOOL UnsetDrx(DEBUG_EVENT *pDebugEvent);
  CBph();
  virtual ~CBph();
};
AddBp()用来添加一个硬件断点。硬件寄存器只有4个,用m_Bph记录对应断点信息,该函数首先检测被调试程序是否处于暂停状态,如是,则设置断点寄存器,否则调用DebugBreakProcess(该函数实质是在被调试进程中插入一个远程线程,代码是INT 3),暂停被调试程序,并将要添加的断点记录在m_pAddBphOnBp中。
DelBp()用来删除一个硬件断点。函数流程同AddBp();
OnEcxpBp()该函数用来响应DebugBreakProcess()设置的INT3断点,进行添加或者删除一个硬件断点。
OnExcpSingle()该函数一是用来响应硬件断点。当发生硬件断的时候,首先检测断点信息是否为自己设置的断点。如果是,则先设置单步断点,并记录断点信息,删除该硬件断点,返回。如非,则先判断是否自身单步引起,
如是单步引起,则还原断点信息。

4.G命令的实现:
  当被调试程序发生异常被暂停的时候,调用函数ContinueDebugEvent(),最后一个参数应该为DBG_CONTINUE,表示已经处理了异常,程序可以继续运行。G ADDR命令,相当于OD中的F4,表示运行到ADDR表示的地址。该函数是首先在ADDR处设置一个INT3断点,断点次数为1次,然后,调用函数ContinueDebugEvent。

5.T\P命令 (单步步过,单步步入)的实现。
  当把E***中的TF为置位时,被调试程序会首先执行一条指令,然后引发一个EXCEPTION_SINGLE_STEP异常,从而实现单步步入。单步步过,是先解析当前指令是否为CALL,如是则在下一个指令上设置一个一次性INT3断点。否则就直接设置调用T命令的函数。
6.  其他需要处理的调试事件:
调试事件共有如下几种
  CREATE_PROCESS_DEBUG_EVENT  
EXCEPTION_DEBUG_EVENT  
CREATE_THREAD_DEBUG_EVENT
   EXIT_THREAD_DEBUG_EVENT
  EXIT_PROCESS_DEBUG_EVENT
  LOAD_DLL_DEBUG_EVENT
  UNLOAD_DLL_DEBUG_EVENT
  OUTPUT_DEBUG_STRING_EVENT
  RIP_EVENT
前边描述的异常对应EXCEPTION_DEBUG_EVENT。本项目中还处理了CREATE_PROCESS_DEBUG_EVENT,主要记录进程的相关信息,并且在OEP处设置一个一次性INT3断点,当程序执行到OEP时,被中断。LOAD_DLL_DEBUG_EVENT,该异常表示一个DLL被加载,处理该事件主要记录DLL的信息,包括DLL名字,占用的内存空间,该DLL的导入导出函数。
因项目时间紧张,暂时只完成这些,余下内容待以后完成。
五.  反汇编引擎的说明:
      本项目使用的反汇编引擎是DIASM。该库中导出函数为:
    Void __stdcall Decode2Asm(IN PBYTE pCodeEntry,
                                 OUT char* strAsmCode,
                                 OUT UINT* pnCodeSize,
                                 UINT nAddress)
       pCodeEntry:要转换成ASM的机器码的缓冲区。
strAsmCode:函数成功完成后,会将ASM放入此缓冲区。
pnCodeSize:函数成功完成后,会将此参数设置为本条指令的长度。
nAddress  :本条指令的虚拟地址。此参数是为了正确反汇编含有相对偏移的指令。

六.  命令使用说明:
1.启动参数:exeFullPath argument,例:
   Easydebug.exe c:\notepad.exe readme.txt
2.CREATEPROC  exeFullPath argument.开始调试一个进程,例
Createproc c:\notepad.exe readme.txt
3.BP addr。  在制定地址下INT3断点,例:
BP 10073A4
INT3断点可以设置多个
4.BPM startaddr len rw。在指定内存处设置内存断点startaddr表示内存起点,endaddr表示内存终点。a表示访问内存,w表示写内存。例
Bpm 10010cc 4 a
内存断点可以设置多个
5.BPH addr len awe . 在指定位置处设置硬件断点,LEN表示断点的长度,A表示读,W表示写,E表示执行,例
BPH 10010CC 1 A
6.G addr ,执行到指定地址处,相当于OD中的F4,如G后无指定地址,则相当于OD中的F9,例:
G
   G 10073CC
7.  T ,单步进入,相当于OD中的F7
8.  P,单步步过,相当于OD中的F8
9.   u addr,显示指定地址处的汇编指令。后边如无指定地址,则显示当前被调试进程EIP处指令例
    U 10073CC.

上传的附件 EasyDebug1.rar