调试器基础0x2(调试器框架基础知识)

创建/附加

调试器首先需要实现的功能是能够附加到正在运行的进程或者创建一个被调试进程,当然,调试器必须与被调试进程具有交互的能力,这样调试器才能用来操作被调试进程。可以通过DebugActiveProcess函数并指定目标的进程标识符来实现的。或者,也可以通过调用CreateProcess来完成带有DEBUG_PROCESSDEBUG_ONLY_THIS_PROCESS创建标志。后一种方法将创建一个新进程并将其附加到该进程,而不是附加到已在系统上运行的进程。附加后,调试器可以使用DebugSetProcessKillOnExit指定有关是否在分离时终止进程的策略。最后,分离过程是对DebugActiveProcessStop的直接调用。

调试循环

调试器的核心组件是调试循环。 这是负责等待调试事件,处理所述事件,然后等待下一个事件(即循环)的部分。 在Windows平台上,这非常简单。 实际上,它非常简单,微软就此步骤需要完成的工作编写了一个简短的调试循环Demo页面。 大致步骤是(在一个循环中)调试器调用WaitForDebugEvent ,它等待来自进程的调试事件。 这些调试事件在此处列举,通常是与进程和线程创建/销毁,DLL的加载或卸载,引发的任何异常或调试器专用的调试字符串输出有关的事件。 该函数返回后,将使用与特定调试事件相关的信息填充DEBUG_EVENT结构。 至此,调试器的工作就是处理事件。 处理事件之后,处理程序必须向ContinueDebugEvent返回处理状态,dwContinueStatus是一个DWOR类型变量,用于告诉引发事件的线程在处理事件后如何继续执行。对于大多数事件,即CREATE_PROCESS_DEBUG_EVENTLOAD_DLL_DEBUG_EVENT等等,您想要继续执行,因为这些事件并未反映出程序行为的任何错误,而是通知调试器更改过程状态的事件。通过选择DBG_CONTINUE作为继续状态来完成此操作。对于异常情况,例如导致未定义程序行为的异常(例如访问冲突,非法指令执行,除零等),这是程序无法自己解决的问题。此时的调试器工作是收集和显示与崩溃有关的信息,并且在大多数情况下,终止进程。对于这些异常,终止可以在处理程序内部进行,或者调试器可以选择继续使用DBG_EXCEPTION_NOT_HANDLED进行调试事件返回。保持原状,表示调试器放弃处理此条异常。几乎所有情况下,程序随后会立即终止。但是,有时会出现一些极端情况,尤其是在恶意软件中,在该过程中,进程将安装其自己的运行时异常处理程序作为一种混淆技术,并生成自己的运行时异常以在该处理程序中进行处理以实现某些功能。在这种情况下继续执行异常不会导致崩溃,因为在调试器将其沿着异常处理程序链转发后,该进程便能够处理其自身的异常。

断点

断点可以简单地定义为正在执行的代码中导致故意中止执行的位置。在调试器的上下文中,这很有用,因为它允许调试器在什么也没有发生的时间(即过程实际上“中断”)时检查过程。当目标处于中断状态时,调试器中会看到的典型功能是查看和修改寄存器/内存,打印断点周围区域的反汇编列表,进入或移至汇编指令或源代码行的能力,和其他相关的诊断功能。断点本身可以有几种不同的形式。

硬件中断断点
硬件中断断点可能是在调试器中了解和实现的最常见和最简单的断点。这些特定于x86x64体系结构,并通过使用硬件支持的指令来实现,该指令专门用于生成调试器专用的软件中断。该指令的操作码为0xCC,它与INT 3指令匹配。大多数调试器实现此目的的方式是将期望地址(即断点地址)处的操作码替换为0xCC操作码。调用代码后,中断(EXCEPTION_BREAKPOINT),调试器将处理并为用户提供执行上述调试功能的选项。当用户完成检查该地址处的程序状态并希望继续时,调试器将替换原始指令,确保执行地址(EIPRIP寄存器,取决于体系架构)指向该原始地址,并且继续执行。

但是,出现了一个有趣的问题。当原始指令被替换回时,断点将丢失。如果断点仅被命中一次,这可能没问题,但这种情况很少发生。之后需要一种方法可以立即重新启用该断点。这是通过设置EFlags(或x64RFlags)寄存器将处理器设置为单步模式来完成的。幸运的是,通过启用第8位(即与0x100进行“或”运算)可以很容易地实现这一点。当EXCEPTION_BREAKPOINT异常被处理并恢复执行,将有另一个异常,EXCEPTION_SINGLE_STEP,引发下一条执行的指令。此时,调试器可以重新启用上一条指令的断点并恢复执行。

硬件调试寄存器
第二种断点实现技术也特定于x86x64体系结构,并利用了指令集提供的特定调试寄存器。这些是调试寄存器DR0DR1DR2DR3DR7。前四个寄存器用于保存硬件断点将断开的地址。使用这些意味着对于整个程序,最多可以有四个硬件断点。DR7寄存器用于控制这些寄存器的使能,位0、2、4、6对应于DR0,…,DR3的开/关。此外,位16-1720-2124-2528-29充当DR0,…,DR3的位掩码,用于触发这些断点的条件,执行时为00,读取时为01,写入时为11

Windows平台上设置这些断点有些棘手。它们必须在进程主线程上设置。这涉及获取主线程,使用至少THREAD_GET_CONTEXTTHREAD_SET_CONTEXT特权打开该线程的句柄,并使用带有新添加的调试寄存器的GetThreadContext / SetThreadContext获取/设置线程上下文,以反映更改。请注意,没有修改内存中的可执行代码来设置这些断点。这与之前必须替换操作码的情况不同。这些是通过更改硬件寄存器的内容来设置和取消设置的断点。设置这些参数后将发生的情况是,该过程将引发在地址处命中指令后会发生EXCEPTION_SINGLE_STEP异常,然后调试器将以与上一节中的方式几乎相同的方式对其进行处理。由于数量的限制,这些内容不会在本文的示例断点代码中显示,但是出于完整性的考虑,最终可能会在以后写出来。我(原文作者)已经写了自己的使用情况在API钩子以前的文章(请原谅在一开始死链接)。调试器的实现与那里的实现非常接近。

软件断点
最后一类断点完全在软件中执行,并且与操作系统的功能密切相关。它们的另一个名称是内存断点。它们结合了中断断点的一些最佳功能,即具有任意数量的能力,以及硬件断点的最佳功能,这就是执行代码中的任何内容都不需要覆盖。但是,存在一个主要缺点:由于其实现,它们使代码的执行速度大大降低。

这些不是在地址级别实现的,而是在页面级别实现的。这些工作原理是使用VirtualProtectEx将要设置断点的地址的页面权限更改为保护页面的页面权限。当页面上的任何指令将被访问时,将抛出EXCEPTION_GUARD_PAGE异常。调试器将处理此异常,并检查有问题的地址是否是断点地址的地址。如果是这样,调试器可以像执行其他任何断点一样执行通常的处理/用户提示。如果不是,则调试器必须执行一些额外的步骤(如抛出异常)。

根据文档,保护页保护将在提升后从页面上删除。这意味着一旦处理了异常并继续执行,此后的任何访问都将不会生成EXCEPTION_GUARD_PAGE异常。因此,如果所访问的指令不是所需的断点地址,则断点将丢失。为了解决这个问题,将使用与硬件中断断点部分介绍的技术类似的技术。处理器将进入单步模式并继续执行。在下一条指令上,将有一个EXCEPTION_SINGLE_STEP引发异常。这将由调试器处理,并且将在页面上重新启用保护页面属性。此实现也将不在本文中讨论,但将来可能会涉及到。我以前写过关于它,也是在API挂钩的情况下,在这里

调试符号

在最简单的定义下,调试符号是一条信息,它显示了已编译程序的特定部分如何映射回源级别。例如,调试符号可能会告诉您有关内存地址中变量名称的信息,或一系列汇编指令映射到的代码行以及在哪个文件中的信息。它们通常在调试构建期间生成,用于为正在调试(或逆向工程)一段代码的开发人员提供一些清晰度。一种语言没有通用的调试符号格式,并且它们在编译器之间可能会有所不同。在现代Windows平台上,调试符号以程序数据库PDB)文件的形式出现,以.pdb扩展名结尾。

初始化符号处理程序非常简单:仅涉及调用SymInitialize。该函数具有一个进程句柄,该句柄在附加时由调试器打开。用户搜索路径还有一个参数可以找到PDB文件,第三个参数可以指定调试器是否要枚举进程中所有已加载的模块并加载它们的符号。对于附加调试器,请指定此行为取决于情况。在某些情况下,例如调试器创建了要调试的目标进程,或者使用了延迟加载的DLL,可能会导致某些符号无法加载。此外,如果此第三个参数设置为true并且在接收到所有LOAD_DLL_DEBUG_EVENT事件之前初始化了符号处理程序,则可能不会加载某些符号。

调试器的一个有用功能可能是在内部枚举模块的所有符号。 这样可以在以后存储和快速查找。 或者,它可以为用户提供图形显示,并可以从其名称轻松导航到符号地址。 枚举符号是一个两步过程:首先调用SymLoadModuleEx来加载模块的符号表,然后可以使用模块的基地址来调用SymEnumSymbolsSymEnumSymbolsPSYM_ENUMERATESYMBOLS_CALLBACK类型的回调作为参数。 该回调将针对在模块符号表中找到的每个符号进行调用,并将具有SYMBOL_INFO结构,该结构显示有关该符号的信息,例如其名称,地址,是否为寄存器,如果其为常数则是什么值 等。

原文链接

http://www.codereversing.com/blog/archives/168

http://www.codereversing.com/blog/archives/169

http://www.codereversing.com/blog/archives/170

http://www.codereversing.com/blog/archives/171

http://www.codereversing.com/blog/archives/172

参考代码

https://docs.microsoft.com/en-us/windows/win32/debug/writing-the-debugger-s-main-loop?redirectedfrom=MSDN

https://github.com/lracker/-MyDebugger