本文介绍的原理以大家所熟知的OllyDbg为例进行讲解。
Ollydbg的断点功能是基于异常处理来实现的,通过捕获程序执行过程中的异常信息来中断程序的执行流程。Ollydbg常用的断点类型有三种:INT3断点,内存断点,硬件断点。每种断点都是一种制造异常的方法,首先使程序在运行过程中产生错误,然后由Ollydbg的异常处理来接管,从而实现断点的功能。
1、加载调试程序
调试程序的第一步就是使用OllyDbg来加载程序,加载的过程是通过创建新进程来完成的。OllyDbg通过CreateProcess以调试的方式开启新进程。在创建调成程序前,OllyDbg需要进行一些必要的检查工作。
首先是针对快捷方式的检查。OllyDbg根据可执行程序的后缀名来判断分析程序是否为一个快捷方式,如果是快捷方式,则会找到这个快捷方式所对应的可执行程序的全路径。通过检查DOS头与NT头来判定分析文件是否为合法的PE文件。当调试文件为DLL动态库时,Olly/Dbg会使用自带的LoadDll.exe将Dll文件进行加载。当调试文件为exe可执行程序时,会跳过Dll文件的处理部分,直接获取相关的配置文件信息并进行加载和调试。
2、异常处理机制
异常就是程序运行过程中产生的错误。OllyDbg利用异常机制捕获调试程序在运行过程中产生的异常,对异常进行排查,从而实现断点功能,使程序暂停运行。OllyDbg将异常处理过程放置在一个大消息循环中,捕获异常的流程如下:
- 进入消息循环
- 利用WaitForDebugEvent函数捕获异常信息,如果捕获失败,则回到循环起始处
- 捕获到异常,率先由OllyDbg插件进行异常处理
- 检查是否为调试异常,如果不是,则继续执行程序,回到循环起始处
- 如果是调试异常,则进行相关检查,进入断点异常处理函数中
当进入最后一步时,程序已经被成功断下,调试程序出于挂起状态,等待调试者的处理。异常处理首先检查调试事件类型,如果调试信息为异常,则进入异常处理部分,判断异常类型。先判断异常是否为INT3断点所产生的,如果是,则通过跳转指令执行对应的代码。下面介绍INT3断点的捕获过程:OllyDbg将调试程序停留在正确的INT3断点处,在显示反汇编代码的过程中,没有直接显示断点处机器码0xCC或0xCD,而是通过查找断点信息表中所对应的原机器码的信息来进行显示,以防止因修改指令造成的指令混乱。
在调试人员发出在此运行的指令后,OllyDbg将会先修复INT3断点处的内存数据,然后再次运行修复后的指令代码。INT3断点处的指令被执行后,此处将会被再次设置为INT3断点。
如果检测INT3断点失败,则会开始内存断点的异常检查。内存断点的设置过程是通过修改内存属性来达到触发异常的目的的。因此,内存断点的触发便是内存访问类错误。其流程如下:
- 得到线程信息
- 跳转到相应的异常处理分支
- 若得到线程信息,则根据线程信息的eip进行赋值,否则根据异常地址进行赋值
- 得到异常所处的模块的信息,并解析反汇编信息,以进行相关检查
- 若模块为自解压(SFX)模式,则进行相应的检查以及错误处理
- 检查内存断点是否在dll中,弹出提示窗口,并将断点去除
- 最后调整优先级并退出
硬件断点的捕获过程是由调试寄存器来完成的,因此OllyDbg没有捕获处理过程。
3、INT3断点
INT3断点是最常用的断点,其工作流程时通过修改机器码为0xCC来制造异常。当程序执行0xCC代码时会触发INT3异常,OllyDbg将捕获此异常并等待用户的处理。跳过INT3断点则是将0xCC处的代码恢复,在此运行,以保证程序的正常运行。
OllyDbg实现INT3断点的主要流程如下:
- 检查INT3断点是否在记录的断点信息表中
- 将INT3断点信息记录到表中
- 记录INT3断点处的机器码信息
- 将INT3断点处的机器码修改为0xCC
- 设置断点信息表
4、内存断点
内存断点用来监控内存,它可以对内存数据的访问和写入进行监控。内存断点的设置主要依靠两个API来完成:VirtualQuery和VirtualProtectEx。通过VirtualQuery来获取原内存页的属性,以便于还原;通过VirtualProtectEx修改内存页属性,以制造内存访问异常。被调试的目标进程发生异常后,首先处理这个异常的是调试器。因此调试器可以成功捕获这个异常。内存断点的处理过程是由异常处理部分来完成。
5、硬件断点
在寄存器中,有一些寄存器专门用于调试,称为调试寄存器,调试寄存器一共有8个:Dr0-Dr7;对于Dr0-Dr3四个寄存器,作用是存放中断的地址,Dr4和Dr5一般不使用,保留,Dr6和Dr7这两个寄存器的作用是用来记录Dr0-Dr3中下断的地址的属性,比如:对这个401000是硬件读还是写,或者是执行;是对字节还是对字,或者是双字。
关于硬件断点的详细信息,请参阅断点部分的硬件断点知识。
6、单步执行
SEH即结构化异常处理(Structured Exception Handling),当程序出现错误时,系统把当前的一些信息压入堆栈,然后转入我们设置好的异常处理程序中执行,在异常处理程序中我们可以终止程序或者修复异常后继续执行。
异常处理处理分两种,顶层异常处理和线程异常处理,下面介绍的是线程异常处理。每个线程的FS: [0]处都是一个指向包含异常处理程序的结构的指针,这个结构又可以指向下一个结构,从而形成一个异常处理程序链。当发生异常时,系统就沿着这条链执行下去,直到异常被处理为止。
下面以最常见的OllyDbg调试器为例讲解调试器单步执行时的工作方式。
当在调试器中选择“步过”某条指令时,程序自动在下一 条语句停下来,这其实也属于一种中断,而且可以说是最常用的一种形式了,当我们需要对某段语句详细分析,想找出程序的执行流程和注册算法时必须要进行这一 步。是80386以上的INTEL CPU中EFLAGS寄存器,其中的TF标志位表示单步中断。当TF为1时,CPU执行完一条指令后会产生单步异常,进入异常处理程序后TF自动置0。调试器通过处理这个单步异常实现对程序的中断控制。持续地把TF置1,程序就可以每执行一句中断一次,从而实现调试器的单步跟踪功能。
单步执行中包含StepIn和StepOver两种:
StepIn:
- StepIn即逐条语句执行,遇到函数调用时进入函数内部,其实现方式如下:
- 通过调试符号获取当前指令对应的行信息,并保存该行的信息。
- 设置TF位,开始CPU的单步执行。
- 在处理单步执行异常时,获取当前指令对应的行信息,与1)中保存的行信息进行比较。如果相同,表示仍然在同一行上,转到2);如果不相同,表示已到了不同的行,结束StepIn。
StepOver:
StepOver即逐条语句执行,遇到函数调用时不进入函数内部,其实现方式如下:
- 通过调试符号获取当前指令对应的行信息,并保存该行的信息。
- 检查当前指令是否CALL指令。如果是,则在下一条指令设置一个断点,然后让被调试进程继续运行;如果不是,则设置TF位,开始CPU的单步执行,跳到4)。
- 处理断点异常时,恢复断点所在指令第一个字节的内容。然后获取当前指令对应的行信息,与1)中保存的行信息进行比较,如果相同,跳到2);否则停止StepOver。
- 处理单步执行异常时,获取当前指令对应的行信息,与①中保存的行信息进行比较。如果相同,跳到2);否则停止StepOver。