最近一直在实现一个有OD所有功能的调试器,下面给出对于实现部分的方法和遇到的问题,希望对也在实现调试器的朋友有所帮助。

设置内存断点和对输入表函数里面的函数等等,如果大家觉得需要,我在发帖写出来。

下面是硬件断点的设置问题。

主要实现代码是:
  
           CONTEXT cx;

  cx.ContextFlags = CONTEXT_DEBUG_REGISTERS;

  if(FALSE==GetThreadContext(pi->hThread, &cx))
                
                {

                err=GetLastError();  

                }      

  cx.Dr0 = dwBreakPointAddress;//硬件断点地址
  
                cx.Dr7 |= 0x401;
  
                if(FALSE==SetThreadContext(pi->hThread, &cx))
               
               {

                err=GetLastError();  

                };

代码虽然很短,但是有很多需要注意的地方,比如,pi的权限,获取线程信息的时间。
我就只挑我出现问题的地方做出解释:

cx.ContextFlags = CONTEXT_DEBUG_REGISTERS;这句不能用CONTEXT_CONTROL。
看定义

#define CONTEXT_CONTROL         (CONTEXT_i386 | 0x00000001L) // SS:SP, CS:IP, FLAGS, BP  

知道为什么了吧。


这句

 cx.Dr7 |= 0x401;

设置dr7不是很复杂的问题,大家看看intel的手册就明白了 ,如果只想用dr0设置一个断点,那么这里用0x1或者0x101都可以。如果要有其他复杂的控制,就仔细看看Intel  的手册吧。

下硬件断点的时间需要重点注意下:

在进入调试循环后,第一次EXCEPT_BREAKPOINT就设置硬件断点,肯定不会行的(我在网上查资料的时候,看见很多人犯了这个错误),因为他不是main真正的现场CONTEXT(It's because at the time of the DEBUG_BREAK hasn't the main thread it's real context. NT sets the real context later via the NtContinue API. So a possible method to set a BPM directly at the EntryPoint...)。所以需要在入口地址后下断点 ,然后设置硬件断点。


其他问题和一些异常处理太多我就没有给出代码和分析了。

导入函数断点:

实现原理和OD好像是一样的():

struct Im_F{

    DWORD DataAddress;   //address in .text 
    DWORD IdataAddress;  //address in .idata
    std::string NameOfDll;     //name of dll
    std::string NameOfFunc;    //name of fucntion
}ImportInfo;

在对pe结构分析的时候,获取导入函数的地址以及名字。保存在上面的结构体数组里面。
在反汇编的时候,获取FF15开头的指令对应的地址(代码段里面的地址)和被调地址(这个不一定是导入表的函数),然后通过pe分析得到的导入表的地址范围判断被调地址是否是导入函数,然后将地址保存在一个结构体数组里面。

具体的问题很多,大概说下比较烦的问题。

1,在获取导入函数名的时候,由于我是镜像到内存,所以用了ImageRvaToVa得到镜像里面的函数名位置,但是,获取导入函数的地址的时候,就不是ImageRvaToVa转换过后的地址,而是原来pe文件里面给出的RVA。

2,导入函数的地址(在pe加载器加载到内存时候对应的地址)并不是IMAGE_IMPORT_BY_NAME结构的地址,而是FirstThunk指向的IMAGE_THUNK_DATA的地址加上对应偏移量(当然,它也是在.idata段里面)

其他小问题就多了,就不具体说了,在网上都有很多介绍。

调试器的单步调试很多地方都有用到,比如,下一个int3断点后,在程序被抛出异常被调试器捕捉后,为了让程序继续运行下去,就需要设置EIP,并且改回断点原来的数据。然后,单步调试程序,在EIP到下一个指令的时候,又一次将断点位置的数据改成int3.

下单步的方法很简单:

获取线程(GetThreadContext)然后lpcontext.EFlags|=0x100;,在SetTreadContext就可以了,但是每次都会被cpu清零,如果要继续单步,需要再次设置。

还有需要小心的是,硬件断点在没有其他特殊处理的话,是作为一个单步异常抛出的(EXCEPTION_SINGLE_STEP),如果调试器有硬件断点功能,但是,有没有专门的处理代码,那么,在EXCEPTION_SINGLE_STEP里面,至少需要对调试寄存器的值清零。否则,程序将反复在硬件断点地址来回运行。

其他小问题还是很多,就不多说了。

实现调试器,本来可以直接用fopen或者文件流读取文件信息,检测是否是正常的PE文件,再获取入口地址等部分信息就可以了。但是,为了让大家在用调试器的时候不要开一个PE_Infomation之类的软件,所以,我在调试器里面完全获取所有pe信息。

实现方法比较简单,我就大概说下:

由于完全面向对象,所以,为了体现面向对象的优势,我做了如下处理:

先定义一个抽象类(做用户访问的接口)


class PE_interface{

public:

  virtual PIMAGE_DOS_HEADER GetDosHeader()=0;
  virtual PIMAGE_NT_HEADERS GetNtHeaders()=0;
  virtual PIMAGE_SECTION_HEADER GetSHeader()=0;
  virtual void GetDataDir_Ex_Im(PIMAGE_IMPORT_DESCRIPTOR& pIm,PIMAGE_EXPORT_DIRECTORY& pEx)=0;
};


然后定义一个类,实现PE信息获取的功能。

class PE_Implement{

public:


  PE_Implement(const char *filename);
  struct stMapFile{
    stMapFile(){hFile=NULL;hMapping=NULL;ImageBase=NULL;}
    HANDLE hFile;
    HANDLE hMapping;
    LPVOID ImageBase;
  };
  struct IM_TABLE{
  DWORD address;
  std::string function;
  };

  bool LoadFile();
  void UnLoadFile();
  bool IsPEFile();
  LPVOID GetImageBase();
  PIMAGE_DOS_HEADER GetDosHeader_I();
  PIMAGE_NT_HEADERS GetNtHeaders_I();
  PIMAGE_OPTIONAL_HEADER GetOpHeader_I();
  PIMAGE_FILE_HEADER GetFHeader_I();
  PIMAGE_SECTION_HEADER GetSHeader_I();
  LPVOID GetDataDirEx_I();
  LPVOID GetDataDirIm_I();
  IM_TABLE *GetImportTable();


private:
    stMapFile MapFile;

    char szFilePath[MAX_PATH];

    IM_TABLE ImportTable[200];
};

而后定义一个user类,多重继承于上面两个类。


class USER:public PE_Implement,public PE_interface{
  
public:
  USER(const char *FN=NULL);
  ~USER();
  PIMAGE_DOS_HEADER GetDosHeader();
  PIMAGE_NT_HEADERS GetNtHeaders();
  PIMAGE_SECTION_HEADER GetSHeader();
  void GetDataDir_Ex_Im(PIMAGE_IMPORT_DESCRIPTOR& pIm,PIMAGE_EXPORT_DIRECTORY& pEx);
private:
      char FilePath[MAX_PATH];
};
这样就可以直接用PE_interface *p=new USER。来对调用功能函数,但是,被调用的函数只能是接口抽象类PE_interface里面声明了的函数。实现与接口分开,减少模块之间的依赖性。为了避免命名空间的泛滥,我都用的std::string形式限定命名空间。

USER类是功能的实现,界面显示是另外的一个类实现的。由于所有的显示都是控制台,所有,也就没有想把代码发出来,等图形化后,一定分享代码。


写的过程中出现的问题:(希望对其他写PE分析软件的朋友有所帮助)

1,获取PE信息,可以直接镜像到内存或者打开文件直接操作,但是,所以,需要对RVA,VA,OFFSET这些不同状态下的偏移地址清楚划分。一般情况下,.text,,.rdata,.data段之间SizeOfRawData文件对齐大小就是内存里面占用的空间大小,所以他们之间的内存偏移之差就是SizeOfRawData,但是,一定不要被这个误导了,这个只是巧合(很多时候有这个巧合),不是真正的意义所在,在计算的时候,不要利用这些巧合。

2,如果文件里面,指令占用了0x300个字节,但是对齐大小后是0x400字节,加载到内存后是0x1000个字节,在反汇编的时候,如何辨别。这个IDA做的相当的好,我也仅仅实现后两个之间的辨别,所以,也就不好意思多说了,大家仔细看看pe结构的分析吧。

3,获取输入表信息确实是比较复杂的问题,如果你全部自己写代码的话。个人觉得麻烦的主要是关于OriginalFirstThunk和FirstThunk以及他们指向的结构IMAGE_IMPORT_BY_NAME,
由于编译器不同,本来改指向一个IMAGE_THUNK_DATA结构体数组的OriginalFirstThunk可能被设为0,这是,只有通过查找输入函数的信息。但是如果FirstThunk指向的IMAGE_THUNK_DATA的最高进位是1,那么函数是序号引入的,就应该从低位提取序号。如果不为1,就是应该读取指向的IMAGE_IMPORT_BY_NAME结构体。为了兼容不同系统,需要对各种情况进行解析,我只做了解析,没有做解析后的处理(精力有限哈)


其他还有许多小问题,上网搜索就okay了。

前几天和一位前辈聊天,觉得不应该在调试器的实现上过多模仿OD的设计。

但是,在看过OD实现消息断点的方式后,觉得确实有借鉴之处,所以,暂时用了OD设置消息断点的过程,但是,我并不打算在我的调试器可以成型后继续用这个方法作为消息断点。

OD的消息断点,实际上就是一个自动产生的条件断点。借用它的帮助文档里面的话:利用GetWindowLong获取所有窗口过程的地址。然后在对应地址生成这样[ESP+4]==00001234 && [ESP+8] IN (0F0..0F7,135)之类的条件,并且下一个int3断点,断下对应消息。

由于我是模仿OD的实现,所以,就不多做介绍了,OD的帮助文档里面有。
就只是对我模仿时候的一些改动和问题告诉大家,希望对同是设计调试器的朋友有一些帮助。

GetWindowLong获取对应窗口过程的地址,以及ID等等信息,肯定要在被调试程序的进程空间进行。所以,需要注入到被调试空间。在OD 实现的时候,由于作者觉得注入代码不是很安全,所以,只有在被调试程序挂起时候才进行注入。而需要得到信息的窗体我就不太清楚OD是怎么搞出来的。我用的是EnumChildWindows这个函数,得到被调试程序的窗体句柄。在注入的时候,我开始也用的直接writeprocessmemory然后创建远程线程完成。但是,问题很多,后来就改用了CreateRemoteThread和Loadlibrary的方式了,而且在我测试的时候,即使不挂起程序,也可以完成注入(仅仅是我测试的那几个程序可以,其他的我无法保证)。获取窗体的信息。



设计过程中,遇到的问题不是很多(都是windows编程的问题,网上优秀的文章很多,我就不多说了)

1,注意注入时候权限的设置。

2,调试器和被调试器进程间的数据传递是个需要小心考虑的问题了。(我用的DLL共享传递的)

浪费我时间最多的问题……  得感谢figo和慵懒的午后。具体是什么,就不好意思提了(个人的大意),相信不会有人也这么倒霉,遇到这样的问题。