在科锐培训第三阶段,要求完成一个Win32汇编阶段调试器项目,再将代码放出来与大家分享心得,丑陋的代码丑陋的设计丑陋的调试器
辜负了钱老师的希望   不足之处多多包容

项目名称:调试器

开发语言:vc++

开发平台:vs2005

作者:creakerzgz(科锐学员)

开发中各个技术说明:

  1.被调试进程指令的反汇编
    指令的反汇编使用了现成的反汇编引擎Disasm。
    反汇编引擎(Disasm)中的函数Decode负责反汇编指令,这里为了使用方便,对其进行了封装。
    void
    __stdcall
    Decode2Asm(IN PBYTE pCodeEntry,
               OUT char* strAsmCode,
                OUT UINT* pnCodeSize,
                UINT nAddress);
    其中第一个参数PBYTE pCodeEntry是个输入参数,指向了需要反汇编的指令的地址,第二个参数char* strAsmCode是输出参数,
  用来保存反汇编后的字符串,第三个参数UINT* pnCodeSize也是一个输出参数,用来返回当前反汇编的这条指令的长度,而最后一个输入
  参数是nAddress,是被调试进程加载的映像基址。
    Decode2Asm的使用非常简单,下面是一个小例子:
    BYTE pCode[5] = {0x55, 0x8B, 0xEC, 0x6A, 0xFF};
    TCHAR szAsm[MAX_LENGTH] = {0};
    DWORD dwCodeSize = 0;
    DWORD dwImageBase = 0x400000;/*映像加载的基址,比如常见的0x400000,该地址可以通过分析PE文件的      IMAGE_NT_HEADERS.Image_Optional_Header.ImageBase成员而获得*/
    Decode2Asm(pCode, szAsm, &dwCodeSize, dwImageBase);
    执行之后,szAsm将是"push ebp",dwCodeSize是"push ebp"的指令长度1字节。再次之后可以移动pCode数组以反汇编下一条指令,
    如下所示:
    Decode2Asm(pCode + 1, szAsm, &dwCodeSize, dwImageBase);
    执行之后,szAsm将是"mov ebp, esp",dwCodeSize是"mov ebp, esp"的指令长度2字节。
    如此循环下去,即可将被调试进程的指令全部反汇编出来。


  2.被调试进程的初始化
    创建一个调试进程需要使用微软提供的API,
    BOOL CreateProcess(
        LPCTSTR lpApplicationName,                 // name of executable module
        LPTSTR lpCommandLine,                      // command line string
        LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
        LPSECURITY_ATTRIBUTES lpThreadAttributes,  // SD
        BOOL bInheritHandles,                      // handle inheritance option
        DWORD dwCreationFlags,                     // creation flags
       LPVOID lpEnvironment,                      // new environment block
        LPCTSTR lpCurrentDirectory,                // current directory name
        LPSTARTUPINFO lpStartupInfo,               // startup information
        LPPROCESS_INFORMATION lpProcessInformation // process information
    );
    使用时需要把其中dwCreationFlags设置成DEBUG_PROCESS。
    附加进程需要使用
    BOOL DebugActiveProcess(
        DWORD dwProcessId   // process to be debugged
      );
    其中dwProcessId为调试进程Id。可以通过进程快照或其他方法获得。
    创建或附加调试进程后就需要等待调试事件的到来,具体如下:
      DEBUG_EVENT DebugEv;                   // debugging event information 
      DWORD dwContinueStatus = DBG_CONTINUE; // exception continuation 
      for(;;) 
      { 
            WaitForDebugEvent(&DebugEv, INFINITE); 
            switch (DebugEv.dwDebugEventCode) 
            { 
                case EXCEPTION_DEBUG_EVENT: 
           switch (DebugEv.u.Exception.ExceptionRecord.ExceptionCode) 
                    { 
                        case EXCEPTION_ACCESS_VIOLATION:   
                        case EXCEPTION_BREAKPOINT:   
                        case EXCEPTION_DATATYPE_MISALIGNMENT:   
                        case EXCEPTION_SINGLE_STEP:   
                        case DBG_CONTROL_C:   . 
                    } 
              case CREATE_THREAD_DEBUG_EVENT:   
              case CREATE_PROCESS_DEBUG_EVENT:
              case EXIT_THREAD_DEBUG_EVENT: 
              case EXIT_PROCESS_DEBUG_EVENT: 
              case LOAD_DLL_DEBUG_EVENT: 
              case UNLOAD_DLL_DEBUG_EVENT: 
             case OUTPUT_DEBUG_STRING_EVENT: 
            } 
      ContinueDebugEvent(DebugEv.dwProcessId, 
            DebugEv.dwThreadId, dwContinueStatus); 
       } 
    该框架是微软在msdn上给出的,大家可以参考一下。

  3.普通断点
    普通断点的设置有两种方法,int3法和jmp法,jmp法不清楚,所以我使用了int3法。
    在设置断点的时候,需要获得设置断点的指令地址,并且为了方便恢复,在设置前还需要报存指
  令的第一个字节。然后使用WriteProcessMemory在该地址处写上0xCC(即int3的机器码),这样当被调试进
  程执行到我们设置0xCC的地址处时就会触发一个EXCEPTION_BREAKPOINT的调试事件,并由操作系统负责向
  我们发送,我们只需要在捕获这个EXCEPTION_BREAKPOINT事件并对其进行处理即可。
    设置断点之后,删除断点也非常容易,在删除断点时只需要把先前设置断点时保存的字节写回到指令
  处即可。但是这时eip已经指向了0xCC后的那一条指令,并且那一条指令很有可能是错误的,所以我们需要把
  eip恢复到执行0xCC之前,使用eip--即可完成;
    在普通断点的设置与删除时有一个问题就是同时设置多个断点的情况,此时应当建立一张普通断点的
  表,用以记录设置的每个普通断点,并且在捕获到EXCEPTION_BREAKPOINT事件时需要查看当前的断点是否在
  普通断点表中,如果在,则中断下来,不在则可以继续执行,而普通断点的删除和设置也要在普通断点表中有
  相应的操作,以便管理。
    这里给出读写内存的API的原型:
    BOOL WriteProcessMemory(
        HANDLE hProcess,               // handle to process
        LPVOID lpBaseAddress,          // base of memory area
        LPVOID lpBuffer,               // data buffer
        DWORD nSize,                   // number of bytes to write
        LPDWORD lpNumberOfBytesWritten // number of bytes written
      );
    BOOL ReadProcessMemory(
        HANDLE hProcess,             // handle to the process
        LPCVOID lpBaseAddress,       // base of memory area
        LPVOID lpBuffer,             // data buffer
        DWORD nSize,                 // number of bytes to read
        LPDWORD lpNumberOfBytesRead  // number of bytes read
      );

  4.内存断点
    内存断点的设置和普通断点大不相同,内存断点是根据操作系统对内存属性的管理来设置的。
    一般情况下,我们设置一个内存读断点,则需要把原来的内存属性去掉相应的读属性,那么
  被调试进程在读取这片内存时就会引发读保护,因为该内存区域已经不可读了。此时,操作系统会向
  我们发送一个访问权限(EXCEPTION_ACCESS_VIOLATION)的异常事件,我们捕获到这个时间之后就
  可以对其进行处理了。
    内存写断点和内存读断点一样,不同的是它去掉的是内存的写属性,使被调试进程在写内存时
  触发保护。
  下面是设置内存断点的过程中需要用到的两个函数:
    BOOL VirtualProtectEx(
        HANDLE hProcess,        // handle to process
        LPVOID lpAddress,       // region of committed pages
        SIZE_T dwSize,          // size of region
        DWORD flNewProtect,     // desired access protection
        PDWORD lpflOldProtect   // old protection
      );
    DWORD VirtualQueryEx(
        HANDLE hProcess,                     // handle to process
        LPCVOID lpAddress,                   // address of region
        PMEMORY_BASIC_INFORMATION lpBuffer,  // information buffer
        DWORD dwLength                       // size of buffer
      );
    具体的使用请查阅msdn。
    需要注意的是,修改内存属性时,是按照页来划分,一次至少修改一个页的内存属性,也就是
  说,对内存属性的操作是按页来进行的。

  5.硬件断点
    Intel公司自80386以来,在CPU内部引入了Dr0-Dr7八个调试寄存器专门用于程序的调试工作。
  调试寄存器的作用就不再多说,相信知道调试是怎么回事情的朋友都能明白调试寄存器对于调试过程的
  重要性。下面是使用的一些简单介绍:
    Dr0~Dr3用于存放欲设置断点的线性地址
    Dr4和Dr5保留
    Dr6保存了调试状态
    Dr7是调试控制寄存器

    调试寄存器的使用总体来说分为如下几步:

    1、  把欲监视的地址放入Dr0~Dr3中的一个寄存器

    2、  在Dr7种设置相应的控制位,使得Dr0~Dr3存放的监视地址生效

    3、  继续运行程序,接收EXCEPTION_SINGLE_STEP 调试消息,并在消息处理程序中作自己想做的事情

    DrX寄存器产生的断点是:EXCEPTION_SINGLE_STEP断点消息。
    附件里有一张从网上收集的调试寄存器的图片,里面对各个调试寄存器都有一些标注。

    对于Dr0~Dr7的读取和写入,我们可以使用以下两个API函数来完成:
      BOOL GetThreadContext(
         HANDLE hThread,       // handle to thread with context
          LPCONTEXT lpContext   // context structure
        );
      BOOL SetThreadContext(
          HANDLE hThread,            // handle to thread
          CONST CONTEXT *lpContext   // context structure
        );
    CONTEXT结构的定义可以在msdn中查询得到,其中就包含了Dr0~Dr7和其他一些寄存器标志位等,在调试器
  中就可以通过获得这个结构来读取当前寄存器的值,也可以通过SetThreadContext设置相关的寄存器为新值。

  6.单步进入
    在使用GetThreadContext获得的CONTEXT中,有一个Eflag的标志,在这个标志中保存了一些寄存器的值,
  诸如EAX,EBX,ECX,EDX,以及ESP,EBP等等,并且其中还包含了一个单步标志位TF。当该TF位置为1时,被调试
  进程将执行单步,并且执行过后,单步标志位又被重新置0。
    而当将TF置位,并且下一条指令正好是call或者其他类似余loop的指令时,将进入到call里面去,也就是
  od当中的单步进入。

    下面是将TF置位的代码示例:
    CONTEXT conText;
    GetThreadContext(hThread, &conText);
    conText.EFlags |= 0x100;
    SetThreadContext(hThread, &conText);
    
    此处的EFlags或上0x100正好使TF位置一。

    单步进入之后,TF位会重新被置0,此时会触发EXCEPTION_SINGLE_STEP_Handle异常。捕获这个异常之后可以进一步
  对其进行处理。如果需要再次单步,将TF位再次置一即可。

  7.单步步过
    单步步过要分两种情况。
    (1)。下一条指令不是call或者类似指令时,只需要将单步标志置位即可。
      也可以先计算出下一条指令的地址,然后在下一条指令上下普通断点(硬件断点也可),然后让被
    调试进程运行起来,那么被调试进程就会中断在下一条指令处。然后再清楚掉该断点即可(因为该断点是我们
    安装的临时断点,以后不会再需要了)。
    (2)。下一条指令是call或者类似指令。此时要想单步步过就不能简单的将单步标志TF置位了,因为单步标
  志TF会使被调试进程进入到call里。
    所以就只能在下一条指令处设置普通断点或者硬件断点,然后待中断之后将其删除即可。

    由此可以看出,普通断点和硬件断点都有临时断点和非临时断点两种,以便和用户设置的断点区分开。

  8.API函数名称解析
    API函数名称解析首先需要获取函数名称,还需要获取函数地址。所以需要在刚加载被调试进程时或者附加一个
  一个进程时对其导入表进行分析,以获取其函数名称和地址,并将其保存在一张hash表中。
    接下来要做的就是分析指令,分析call的几种指令形式,因为此时后面的指令可能还没执行,所以不能动态的
  获得call函数的入口。将call的指令分析之后就可以获得调用函数的地址,再在先前保存的函数名到地址的hash表中
  进行查找,然后将其显示出来。
    有一些函数是以序号的形式导入的,这时可以分析相应dll的导出表来获得函数名称。

  9.运行指令记录
    运行指令记录主要是用来记录指令的执行流程。
    在记录中为了减少记录的数据量,应该将调用API的call和执行过的指令放弃掉。所以就需要准确的获得各个
  API函数的准确地址,这样才能区分哪些是用户call,哪些是系统call。
    动态识别call地址的方法:
    在指令执行的时候需要判断下一条指令是不是call指令
  ------》是call,保存当前CONTEXT结构(保存当前的状态)
  ------》单步进入到call里面,在第一条指令处中断下来
  ------》获取当前EIP的值,该值就是call后的实际的函数地址
  ------》将保存的CONTEXT结构恢复回去,此时被调试进程又恢复到了进入call之前的状态。
  现在已经获得了call后的实际地址,然后根据该地址查找解析导入表时保存的函数名称地址表,进而判断出该call内的
  的指令是否需要记录。
    判断是否是循环过的指令的方法:
    首先在记录指令时建立一张已执行过指令地址表,然后在每一次记录前查找该表以判断是否需要记录。为了能够
  快速的进行查找,可以采用hash表来保存记录过的指令。



  下面是一些已解决的问题及解决方案:
    1.如何是被调试进程停在入口处?
    在被调试进程入口处下普通断点或者硬件断点。
    2.如何下多个断点?
    建立断点表,在里面保存每个断点及状态。
    3.如何获得触发内存异常的准确地址?
    DebugEv.u.Exception.ExceptionRecord.ExceptionInformation[1]。该数组的第二个元素即是触发内存异常的的地址信息。
    4.如何读写被调试进程的内存单元?
    WriteProcessMemory和ReadProcessMemory。

项目中的一些命令说明:
    由于项目时间只有10天不到,所以还有很多的功能没有完成,并且还很不稳定
    d 401d03  查看401d03处的内存
    e 401d03  编辑401d03处的内存
    在反汇编处双击可下普通断点

项目中的一些重要的类及其成员简述:
    CPE_Header类:主要用来分析PE文件格式
        构造函数:  CPE_Header(char *szFileName);    //读取文件进行分析
            CPE_Header(HANDLE hProcess);    //读取进程进行分析
        virtual BOOL IsPEFile();        //是否是PE格式的文件
        DWORD GetOEP(void);          // 返回OEP
    CInitProcess类:主要管理被调试进程
        构造函数    CInitProcess(CString &csFileName, BOOL bIsOpen, CWnd* pWnd);  //打开一可执行文件进行调试
              CInitProcess(DWORD dwProcessID, BOOL bIsOpen, CWnd* pWnd);  //附加一进程进行调试
        该类的一些不要成员
              //0xCC断点表
              CMap<DWORD, DWORD&, INT3BP*, INT3BP*> m_cMap0xCC;
              //内存断点表
              CMap<DWORD, DWORD&, MEMBREAKSTRUCT*, MEMBREAKSTRUCT*> m_cMapMem;
              //硬件断点表
              CArray<DWORD, REGISTERBP*> m_cArrayReg;
              //指令表
              CArray<MAPSTRUCT*, MAPSTRUCT*> m_cAsmArray;
              //导入函数表
              CPtrList *m_pIATInfoList;
              //函数调用栈,使用链表结构
              CList<DWORD, DWORD&> *m_pFuncStack;
              DWORD m_dwFuncNum;    //m_pFuncStack中函数元素的个数
              CFile m_cStackFile;    //记录函数调用栈中的函数调用关系
              //用于查询call的函数名
              CMap<DWORD, DWORD&, IAT*, IAT*> m_cMapIAT;
        其中的结构体 INT3BP,MEMBREAKSTRUCT,REGISTERBP,MAPSTRUCT,IAT 在项目中都有详细的说明.

    CDebugAppDlg类:主界面对话框类。
        在其成员函数
        BOOL CreateWorkThread(CInitProcess* pInitProcess);
        中创建一调试线程,而DebugWorkThread是线程函数,线程函数参数是CInitProcess的一个指针。
        而CInitProcess类中有回指向CDebugAppDlg的成员m_pWnd,于是通过pInitProcess->pWnd就可调用CDebugAppDlg的成员
        函数了。
        // 反汇编代码
        BOOL CDebugAppDlg::DeCode2ASM(DWORD dwEipAddress);
        // 安装int3断点
        BOOL CDebugAppDlg::Install_Int3Break(HANDLE hProcess, DWORD dwAddress, INT3BPTYPE BpType);
        // 卸载int3断点
        BOOL CDebugAppDlg::UnInstall_Int3Break(HANDLE hProcess, DWORD dwAddress);
        // 安装内存断点
        BOOL CDebugAppDlg::Install_MemBreak(HANDLE hProcess, DWORD dwAddress, DWORD dwSize, DWORD dwBreak);
        // 卸载内存断点
        BOOL CDebugAppDlg::UnInstall_MemBreak(HANDLE hProcess, DWORD dwAddress);
        // 安装硬件断点
        BOOL CDebugAppDlg::Install_Register(DWORD dwAddress, int nSize, DWORD dwState);
        // 卸载硬件断点
        BOOL CDebugAppDlg::UnInstall_RegisterBp(DWORD dwAddress);
        // 显示内存中的数据
        BOOL CDebugAppDlg::ReadData(DWORD dwAddress, DWORD dwSize);

        // EXCEPTION_ACCESS_VIOLATION访问异常处理
        BOOL CDebugAppDlg::EXCEPTION_ACCESS_VIOLATION_Handle(DEBUG_EVENT& DebugEv, HANDLE hProcess);
        // 断点异常处理
        BOOL CDebugAppDlg::EXCEPTION_BREAKPOINT_Handle(DEBUG_EVENT& DebugEv, HANDLE hProcess);
        // 单步异常处理
        BOOL CDebugAppDlg::EXCEPTION_SINGLE_STEP_Handle(DEBUG_EVENT& DebugEv, HANDLE hProcess);
        // 创建进程事件
        BOOL CDebugAppDlg::CREATE_PROCESS_DEBUG_EVENT_Handle(DEBUG_EVENT& DebugEv, HANDLE hProcess);
    CHelp类:封装了两个静态的读写内存的函数
          // 写内存
          static DWORD WriteProcessMem(HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize);
          // 读内存
          static DWORD ReadProcessMem(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize);
          //字符串转16进制
          static void Str2Hex(char *pBuf, int nLen, byte *pByte);
    
    CLookBpDlg类:查看断点对话框
    CModuleDlg类:查看模块列表类
    CSetBpDlg类: 设置断点对话框
    CWriteDataDlg类:编辑内存数据对话框

    全局函数:DWORD WINAPI DebugWorkThread(LPVOID LParam);是一个线程函数,在该函数中初始化调试实例,并进入等待调试事件的状态。

    一些重要的结构体:
      //下一次单步时应当执行的动作结构
          typedef struct tagNextStep 
          {
          public:
            typedef  enum enumACTION
            {
              INSTALLINT3,    //安装int3断点
              UNINSTALLINT3,    //卸载int3断点
              INSTALLMEM,      //安装内存断点
              UNINSTALLMEM,    //卸载内存断点
              INSTALLREGISTER,  //安装硬件断点
              UNINSTALLREGISTER  //卸载硬件断点
            }ACTION;
            //地址
            unsigned long ulAddress;
            //大小
            unsigned long ulSize;
            //状态
            unsigned long ulState;
            //动作
            ACTION      Action;
            //是否人工断点,表示是否是用户下的断点
            INT3BPTYPE    BpType;
          }NEXTSTEP;

          typedef enum tagBpType         //断点类型
          {
            //INT3
            BPTYPE_INT3,
            //MEM
            BPTYPE_MEM,
            //REGISTER
            BPTYPE_REGISTER
          }BPTYPE;

          typedef enum tagInt3BpType  //普通断点类型
          {
            BPMANUAL,      //人工断点
            BPAUTO        //自动断点
          }INT3BPTYPE;

                    2009.6.4

上传的附件 DebugApp.rar [由于附件过大,请到论坛下载:http://bbs.pediy.com/showthread.php?t=90756 ]