- 加密与解密 调试篇(一)
- 前言
- 调试器原理笔记
- 调试技术概览
- 断点
- 单步执行
- 输出调试信息
- 日志
- 事件追踪
- 栈回溯
- 《Python灰帽子》
- 软断点
- 硬件断点
- 内存断点
现在开始对《加密与解密》第二篇的内容开始学习回顾。第二部分被称为“调试篇”,从内容上看主要涵盖对常见调试器与反编译器的介绍与使用。根据自己的计划,在每部分把涉及的常见工具进行复习回顾,对使用不熟练的工具进行重点学习,对这些工具背后的技术原理进行学习剖析。原理剖析方面目前应该不会进行很深,一是自己积累达不到,二是自己也是在熟悉学习的过程中。之前编写的《CE入门笔记》也算是工具篇的一部分了。根据目前自己的安排,以及《加密与解密》的结构设计,首先对调试器的相关知识进行复习掌握,会把涉及的硬件相关知识、调试API、基础的Demo实例进行研究分析。涉及的参考资料包括软件调试、软件加密技术内幕、Python灰帽子等。关于工具的学习,目前考虑包括CE、OD(x64dbg包含其中)、WinDBG(重点)、Sysinternals等。
调试器原理笔记 调试技术概览断点此部分内容参考自软件调试
断点是使用调试器进行调试时最常用的调试技术之一。基本思想是在某一个位置设置一个“陷阱”,当CPU执行到这个位置时便“跌入陷阱”,即停止执行被调试的程序,中断到调试器中,让调试者进行分析和调试。
常见的断点种类有三种:代码断点、数据断点以及IO断点。
追踪点(tracepoint)是断点的一种衍生形式,基本思路是,设置追踪点之后,调试器将其认为是一个特殊的断点进行处理。当运行到该点时,系统向调试器报告断点事件,调试器收到后发现是追踪点,就按追踪点定义的方式进行处理,通常是进行日志记录。在这过程中调试器会迅速中断并迅速恢复,所以对用户来说感觉不太到这个追踪点的存在。
条件断点,其实原理和断点一致,不过它附加了其他的条件进行判断,每次中断后如果不符合条件就恢复,如果满足就中断
分为以下几种:
- 汇编级别的单步执行。它是靠在CPU相应的单步执行标志来实现的,以x86为例,它就是靠在EFLAGS寄存器的陷阱标识(Trap Flag,TF)位,使CPU每执行一步触发一个调试异常(INT 1),中断到调试器
- 源代码(高级语言)级别的单步。也是靠汇编级别的单步实现的,不过它在每一步运行结束后会检查高级语言对应的一条语句是否完成,通常是通过符号文件中的源代码行信息来判断的
- 每次执行一个程序分支,又称为分支到分支单步跟踪。x86中,通过设置DbgCtl MSR寄存器的BTF标识后,再设置TF标识,可以使CPU执行到下一个分支指令时触发调试异常。WinDBG中使用tb命令
- 每次执行一个任务(线程),即当指定任务被调度时中断到调试器。x86中,每当CPU切换到一个新的任务时,它会检查任务状态段(TSS)中的T标志。如果为1,那么产生调试异常。但是目前大多数调试器未实现该功能
这个是一种“古老”的调试方法,即在关键点上输出调试信息。
- Windows内核:DbgPrint以及DbgPrintEx
- Windows用户态:OutputDebugString
- Linux内核:printk
- Linux用户态:printf
一种常用方法
事件追踪事件追踪机制使用结构化的二进制形式来记录数据,观察时将格式文件转化为文本形式,适用于监视频繁而且复杂的软件过程,如监视文件访问与网络通信等
ETW(Event trace for Windows)是Windows中的事件追踪机制
栈回溯是记录和探索程序执行轨迹的极佳方法
《Python灰帽子》调试器能够跟踪一个进程运行时的状态,一般的调试器都具备以下几个功能:
- 运行, 可以启动一个进程
- 暂停执行,可以停止一个进程
- 单步执行,可以操作一个进程,使其当前运行线程按照opcode逐步运行
对于一个调试器而言,它的内部实际上是一个无限循环,并一直在等待调试事件的发生。当一个调试事件发生时,调试器将被激活,并调用相应的处理例程处理该事件。当事件处理例程被调用时,被调试的目标暂停执行并等待调试器做出如何继续的指示。一个调试器必须捕获以下几种常见的调试事件:
- 断点触发
- 非法内存操作
- 由被调试程序抛出的异常
软断点可以使目标进程运行到特定到某个位于特定位置的指令时暂停运行。它实质上一个单字节长的指令。该指令可以使被调试的目标进程暂停执行并将控制权转交给调试器的异常处理例程。在Intel系统中,这个常用的指令是int 3,opcode是单字节值0xcc。
- 当调试器被告知要在某一个内存地址上设置一个断点时,调试器会首先读取这个地址的第一个字节并将其存储到断点列表中,紧接着将0xcc写入那个内存地址
- 当CPU执行到int 3时,触发断点事件,调试器捕捉到该事件
- 调试器检查EIP是否指向之前设置了断点的内存地址,如果找到,将此处原始字节恢复
这里我们做了个实验,使用x32dbg下一个断点,然后使用CE查看断点处的内存信息,分为执行到断点前与执行到断点后两部分:
通过两者对比,我们可以清晰的看到,在未执行到断点处时,断点处的代码为“cc 62 f8 ff”,执行之后恢复为“e8 62 f8 ff”。
硬件断点在x86CPU中,存在一组称为调试寄存器的特殊寄存器,它们包括DR0-DR7。其中
- DR0 - DR3被用于存储所设硬件断点的内存地址,这意味每一时刻最多使用4个硬件寄存器
- DR4 - DR5保留使用
- DR6称为调试状态寄存器,记录了上次断点触发所产生的的调试事件类型信息
- DR7是硬件断点的激活开关,并存储着各个断点的触发条件信息
通过DR7的设置,可以为断点设定多种触发条件:
- 当位于一个特定内存地址上的指令执行时触发条件
- 当数据被写入一个特定内存地址时触发条件
- 当数据被读出或者写入(不包括执行)一个特定非可执行内存时触发断点
对于DR7而言,0-7位为硬件开关位,DR0-DR3每个寄存器对应两个。低位为局部断点,高位为全局断点。8-15位不用于普通调试
16-31位用于控制对应的断点类型与长度,DR0-DR3每个寄存器占4位,前两位对应断点类型,后两位对应断点长度
| 数值 | 类型 | 长度 |
|---|---|---|
| 00 | 仅当执行对应地址时中断 | 1字节长 |
| 01 | 仅当向对应地址写数据时中断 | 2字节长 |
| 10 | 当向对应地址进行IO操作时中断 | 8字节或者未定义(其他CPU) |
| 11 | 当向对应地址读写数据时都中断,但是非执行 | 4字节长 |
内存断点是本质不是真正的断点,当一个调试器设置一个内存断点时,调试器本质上是改变一个内存区域或者一个内存页的访问权限。
- 页可执行,但是读写会导致非法内存操作异常
- 页可读,进程只能从这个内存页中读取数据,写入或者执行都会导致异常
- 页可写,运行写入数据
- 保护页,对保护页上任何类型的访问将导致一次性异常,之后这个内存页会恢复到之前的状态



