标 题: 调试器设计(1) 
作 者: Tweek
时 间: 2010-2-25

工具:WINHEX  OD   debuggee(简单的弹出hello窗口的win32程序。)
平台:win7

         由于我还没有实现一个完整调试器。对没有测试和实现的部分,都以我自己的设计和理论分析介绍

       最开始了解调试器是看了《应用程序调试技术》这本书。当时觉得,实现调试器很简单,函数也不是很多。但是,后来做起来才发现一个很致命的问题。虽然,调试API不是很多。但是,如果对进程和PE的结构不够熟悉的话,完全无法解决出现的异常和运行错误。

       所以,在第一篇 介绍调试器实现的帖子里面。

      将大致介绍PE文件加载到内存中的形式。以及Ollydbg在分析和处理某些断点的过程中的问题来反映调试器实现的一些细节。


PE加载到内存:

1.  当你双击一个exe文件,系统将创建一个新的进程,对于xp系统,一般是2G用户态内存,2G内核态内存。当然,你自己可以通过修改编译器设置,修改这个大小。首先,PE_LOADER检测imagebase的地址是否可用。如果可用,按照exe给的Imagebase,为exe保留内存空间。如果不可用,则重新保留一片“没有用”的内存。事实上,windows并没有像我们想象的那样,在进程中为exe开辟一片空白的内存空间。Windows只是保留了一片空间。PE_LOADER利用MapViewOfFile将exe文件映射到内存中。但是和我们平时用MapViewOfFile不一样。PE_LOADER是以IMAGE的方式映射到内存。我相信(这个是推测)MS在内核态为保留地址和exe映射后的地址 维护了一张表,以便在需要的时候进行相互的转换。这样,只需要维护一张表,却节约了大批的内存。例如:我们运行debuggee,然后用winhex查看内存



PE_LOADER为debuggee保留了一片内存,从13c0000开始(基地址被重定位)。但是,这个13C0000的地址指向的并不是内存,而是映射而来的文件。当真正运行到相应代码的时候,才被读入内存。但是,在我们ring3的编程看来,他们都在内存里面了,这片内存已经被使用。

然后,PE_LOADER开始通过DOS头到Option头 再到数据目录表。然后找到exe将要加载的dll:



同样,dll的加载也只是保留地址,然后将dll文件映射到相应的地址,系统没有真的分配内存。PE_LOADER完全映射整个dll,并不是只映射exe会用到的函数。首先加载了Safemon,所有没有重定位,之后的所有dll都被重定位。

看似用户态内存被占用了很大。其实,就只是内核态在为映射和保留地址花费了小量的内存。

如果,我们再次运行一个debugee,PE_LOADER将不会重新创建映射,它会用刚才创建的映射文件。然后在新的进程里面,为exe的执行保留地址,建立对应的映射关系。两个进程用到的exe在物理上是同一个东西。
所以,在没有关闭前面的debuggee时,再次运行debuggee。虽然debuggee会重定位。但是



他们重定位的地址是一样的。

如果关闭debuggee后,在运行,重定位的地址就只有很小的可能性是相同地址(也存在可能)
下图是我关闭debuggee后再次运行,重定位的地址



但是,虽然他们物理上是同一个,逻辑上却是两个。Windows利用copy-on-write,避免他们之间的读写冲突。(这个是操作系统的范围,不是我们讨论的范围了。细节请参考操作系统方面的书籍)
但是,理论上,个人觉得还是有可能出现冲突的,比如:OD加载某个程序,你再次直接打开那个程序,然后做一些操作,关闭。OD有可能会有一些异常返回。


关于一些调试器的细节:

2.  在我们看来,PE_LOADER已经把exe和静态连接的dll都加载到内存里面了。然后PE_LOADER到了运行期库,开始初始化一些内存。然后,PE_LOADER利用重定位信息和可选头的入口地址到了我们自己写的入口(例如C 的main函数)。执行权就这样到了我们手里。但是,我们还不知道,我们的程序是以调试态在运行。到程序执行中出现异常。当然,权限又一次到了PE_LOADER手上,它就会按照一开始设计的方式,向调试程序发送相应的调试异常。

如果我们对debuggee 的MessageBoxW下断点。可以有两种方式,在程序代码里面调用MessageBoxW的位置设置断点。也可以在user32.dll的MessageBoxW函数上设置断点(因为通过上面的过程,我们已经知道user32.dll是被加载到我们的进程用户态空间。所以,对他的操作就和对自己的程序的操作需要的权限是一样的。)

OD支持这两种方式。
所以,我们用OD加载debuggee。



看到OD,成功获得程序的实际加载地址。

先用bp MessageBoxW



在user32.dll上面 设置了int3断点。

查看内存,75E4EABF已经被改成CC



在查找模块名称  找到MessageBoxW然后下断



OD利用自己强大分析能力,找到了debuggee里面调用MessageBoxW的位置
断点下在debuggee上



查看内存



也已经被CC替代。

看起来很好了,没有什么问题,其实不然。
直接bp MessageBoxW运行的很好。成功中断,去掉断点,运行正常。

但是,在debuggee上的断点呢?

我们调试开始。程序在点击ok后




成功中断。但是,好像有点问题了,如果我们去掉这个断点。继续运行,并没有弹出MessageBox框。程序异常终止。

//谢谢 bbsl。你对了。以下OD的分析都是错的。为了不影响前面看过的坛友,错误的我就不删除了。直接注释下就Ok

/*
我们再次来到OD断下的位置,滚动鼠标,你会发现。断点处的指令变了



再看刚才的地方  BF EAE475EA 如果按照内存数据的翻译过来 75E4EABF...



再看看MessageBoxW在dll上的地址 75E4EABF。

相信你已经知道是为什么了吧!

013A2440指向的并不是一条指令的开始。而是上一条指令结束。它们正好和下一条指令拼凑出了MessageBoxW的地址。

OD在分析debuggee调用MessageBoxW的时候。直接找到MessageBoxW在dll里面的地址75E4EABF,然后比对内存里面相同的位置。然后在此下断点。如果ctrl+G跳到对于地址,OD会直接用这个地址做为指令的开始。进行反汇编。刚好就那么巧,我写的debuggee就出现了个偶合。所以,断点下在了上一条指令的结束处。实现过断点之类的朋友都知道,程序断下后,EIP已经到了断点后面的地址。在恢复断点的时候,需要将EIP-1。如果断点刚好在指令的开始 ,那么,-1后正好。如果想刚才那样,在指令的结束,-1后,EIP并不会指向指令的开始。于是,程序反复异常,无法继续。
*/

新的分析:
OD的断点是下在.idata上的。那部分都是些只读的数据。并不是指令。所以,在没有加壳的情况下:在debuggee这个执行程序的模块里面,除了.text段的反汇编是合理的外,其他段,OD反汇编代码。都是没有实际意义的。

也就是说OD的断点是下在一段数据上。如果将某个数据改成CC.会怎么样呢?

我们将debuggee的某一条指令后面的MOV EAX,01097C9C  改成 MOV EAX,01097CCC
相当于在数据上下断点。然后再在这条指令上下断点。运行程序,中断在这条指令上,F8程序正常执行完MOV.并没有因为操作数的变化,而出现异常。
同样,如果我们在.idata里面修改成CC。不会在执行到那儿时异常。但是,会因为找不到改成CC后,执行的新地址的函数而产生异常。这个时候,我们删除断点,那么程序将按照正常的断点删除进行操作。修改CC为原来的数据。然后,EIP-1。而这个时候,程序已经执行到很多条指令后去了。EIP-1并不能让程序重新执行前面所有的指令。

为了再次证实这个这个推测是否正确,我们做一下测试。

在MessageBoxW上下载断点。那么,按照我们的分析。查看内存,原来放这MessageBoxW在dll里面地址的75ADEABF 被改成了 75ADEACC。

我们再跳到75ADEACC,下断点。运行程序。程序中断在,75ADEACC



但是,你肯定会想到,75ADEACC可能也是数据。或者是因为执行前面的地方,调用了这个地址的函数之类什么的。所以,我们将原来设置在MessageBoxW上的断点去掉。只保留75ADEACC的断点。运行程序。程序正常。

95%可以确定我们的推断是正确的了吧!

那5%还是留给读者自己分析了。

现在我就假设我的是对的了。

再看OD,其他OD也意识到这个问题,看这个。我们ctrl+G跳到一条数据的开始。

得到的提示是


但是,却在设计函数断点的时候。没有再次检查是否有这个问题的存在。

对于我的失误,我要对看过这篇贴的坛友说声:对不起。
再次感谢bbsl指出我的错误。

上面,大概介绍了一些关于PE加载到内存和调试器的一些实现细节。
希望可以让大家简单了解调试器和实现需要先知道的东西。

                                                             待续……