多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
## 2.4 断点 当我们需要让被调试程序暂停的时候就需要用到断点。通过暂停进程,我们能观察 变量,堆栈参数以及内存数据,并且记录他们。断点有非常多的好处,当你调试进程的时候 这些功能会让你觉得很舒爽。断点主要分成三种:软件断点,硬件断点,内存断点。他们有 非常相似的工作方式,但实现的手段却各不相同。 ### 2.4.1 软件断点 软件断点具体而言就是在 CPU 执行到特定位置的代码的时候使其暂停。软件断点 将会使你在调试过程中用的最多的断点。软件断点的本质就是一个单字节的指令,用于暂停 被执行程序,并将控制权转移给调试器的断点处理函数。在搞明白它是如何工作之前你必须 先弄清楚在 x86 汇编里指令和操作码的差别。 汇编指令是 CPU 执行的命令的高级表示方法。举个例子: ``` MOV EAX, EBX ``` 这个指令告诉 CPU 把存储在 EBX 寄存器里的东西放到 EAX 寄存器里。相当简单,不 是吗?然而 CPU 根本不明白刚才的指令,它必须被转化成一种叫做操作码的东西。操作码(opcode)就是 operation code,是 CPU 能理解并执行的语言。前面的汇编指令转化成操作码 就是下面这样: ``` 8BC3 ``` 如你说见,幕后正在进行的操作相当的令人困惑,但这确实是 CPU 的语言。你可 以把汇编指令想象成 CPU 们的 DNS(一种解析域名和 IP 的网络服务)。你不用再一个个的 记忆复杂难懂的操作码(类似 IP 地址),取而代之的是简单的汇编的指令,最后这些指令都 会被汇编器转换成操作码。在日常的调试中你很少会用到操作码,但是他们对于理解软件断 点的用途非常重要。 如果我们先前讲解的指令发生在 0x4433221 这个地址,一般是这样显示的: ``` 0x44332211: 8BC3 MOV EAX, EBX ``` 这里显示了地址,操作码,和高级的汇编指令。为了在这个地址设置断点,暂停 CPU, 我们将从 2 个字节的 8BC3 操作码中换出一个单字节的操作码。这个单字节的操作码也就 是 3 号中断指令(INT 3),一条能让 CPU 暂停的指令。3 号中断转换成操作码就是 0xCC。 这里是设置断点前和设置断点后的对比: 在断点被设置前的操作码 ``` 0x44332211: 8BC3 MOV EAX, EBX ``` 断点被设置后的操作码 ``` 0x44332211: CCC3 MOV EAX, EBX ``` 很明显原操作码中的 8B 被替换成了 CC。当 CPU 执行到这个操作码的时候,CPU 暂停, 并触发一个 INT3(3 号中断)事件。调试器自身能处理这个事件,但是为了设计我们自己的调 试器,明白调试器是如何具体操作的很重要。当调试器被告知在目标地址设置一个断点,它 首先读取目标地址的第一个字节的操作码,然后保存起来,同时把地址存储在内部的中断列 表中。接着,调试器把一个字节操作码 CC 写入刚才的地址。当 CPU 执行到 CC 操作码的 时候就会触发一个 INT3 中断事件,此时调试器就能捕捉到这个事件。调试器继续判断这个 发生中断事件的地址(通过 EIP 指针,指令指针)是不是自己先前设置断点的地址。如果在调 试器内部的断点列表中找到了这个地址,就将设置断点前存储起来的操作码写回到目标地 址,这样进程被调试器恢复后就能正常的执行。图 2-3 对此进行了详细的描绘。 ![image](https://box.kancloud.cn/2016-03-11_56e236197e127.gif) 图 2-3:软件断点的处理过程 有两种类型的软件断点可以被设置:一次性断点和持续性断点。一次性断点意味着,一 旦断点被触发(命中)一次,它就会从内部中断列表清除掉。一个持久性断点在 CPU 触发 后会重新存储在内部的断点列表里,以后每次运行到这里还会中断。 然而软件断点有一个问题:当你改变了被调试程序的内存数据的时候,你同时改变了运 行时的软件的循环冗余码校验合(CRC)。CRC 是一种校验数据是否被改变的函数,它被广 泛的应用于文件,内存,文本,网络数据包和任何你想监视数据改变的地方。 CRC 将一定 范围内的数据进行 hash(散列)计算,在逆向工程中一般是对进程的内存数据进行运算, 然后将 hash 值和此前原始的 hash 值进行比较,以判断数据是否被改变。如果不同说明数据 被改动了,校验失败。这点很重要,因为病毒程序经常检测程序在内存中运行的代码的 CRC 值是否相同,不同说明数据被修改,则自动杀死自己。为了在这种特殊的情况下也能正常的 进行调试工作,就要使用硬件断点了。 ### 2.4.2 硬件断点 硬件断点非常有用,尤其是当想在一小块区域内设置断点,但是又不能修改它们的时候。 这种类型的断点被设置在 CPU 级别,并用特定的寄存器:调试寄存器。一个 CPU 一般会有 8 个调试寄存器(DR0 寄存器到 DR7 寄存器),它们被用于管理硬件断点。调试寄存器 DR0 到调试寄存器 DR3 存储硬件断点地址。这意味着你同一时间内最多只能有 4 个硬件断点。 DR4 和 DR5 保留。DR6 是状态寄存器,说明了被断点触发的调试事件的类型。DR7 本质上 是一个硬件断点的开关寄存器,同时也存储了断点的不同类型。通过在 DR7 寄存器里设置 不同标志,能够创建以下几种断点: + 当特定的地址上有指令执行的时候中断 + 当特定的地址上有数据可以写入的时候 + 当特定的地址上有数据读或者写但不执行的时候 这非常有用,当你要设置特定的断点(至多 4 个),又不能修改运行的进程的时候。 图 2-4 显示了与硬件断点的状态,长度和地址相关的字段。 ![image](https://box.kancloud.cn/2016-03-11_56e2361994966.gif) ![image](https://box.kancloud.cn/2016-03-11_56e23619a8d29.gif) 图 2-4:DR7 寄存器决定了断点的类型 0-7 位是硬件断点的激活与关闭开关。在这七位中 L 和 G 字段是局部和全局作用域的 标志。我把两个位都设置了,以我的经验用户模式的调试中只设置一个就能工作。 8-25 位 在我们一般的调试中用不到,在 x86 的手册上你可以找到关于这些字节的详细解释。16-31 位决定了设置在 4 个断点寄存器中硬件断点的类型与长度。 和软件断点不同,硬件断点不是用 INT3 中断,而是用 INT1(1 号中断).INT1 负责硬件 中断和步进事件。步进( Single-step )意味着一步一步的执行指令,从而精确的观察关键 代码以便监视数据的变化。在 CPU 每次执行代码之前,都会先确认当前将执行的代码的地 址是否是硬件断点的地址,同时也要确认是否有代码要访问被设置了硬件断点的内存区域。 如果任何储存在 DR0-DR3 中的地址所指向的区域被访问了,就会触发 INT1 中断,同时暂 停 CPU。如果没有,CPU 执行代码,到下一行代码时,CPU 继续重复上面的检查。 硬件断点极其有用,但是也有一些限制。一方面你同一时间只能设置四个断点,另 一方面断点起作用的区域只有 4 个字节(也就是检测 4 个字节的内存数据改变)。如果你想 跟踪一大块内存数据,就办不到了。为了解决这个问题,你就要用到内存断点。 ### 2.4.3 内存断点 内存断点其实不是真正的断点。当一个调试器设置了一个内存断点的时候,它其实是 改变了内存中某个块或者页的权限。一个内存页是操作系统处理的最小的内存单位。一个内 存页被申请成功以后,就拥有了一个权限集,它决定了内存该如何被访问。下面是一些内存 页的访问权限的例子: + 可执行页 允许执行但不允许读或写,否则抛出访问异常 + 可读页 只允许从页面中读取数据,其余的则抛出访问异常 + 可写页 允许将数据写入页面 任何对保护页的访问都会引发异常,之后页面恢复访问前的状态 大多数系统允许你综合这些权限。举个例子,你能有在内存中创建一个页面,既能 读又能写,同时另一个页面既能读又能执行。每一个操作系统都有内建的函数让你查询当前 内存页(并不是所有的)的权限,并且修改它们。参考图 2-5 观察不同权限的内存页面数据 是如何访问的。 ![image](https://box.kancloud.cn/2016-03-11_56e23619c638b.gif) 图 2-5: 各种不同权限的内存页 这里我们感兴趣的是保护页(Guard Page) 。这种类型的页面常被用于:分离堆和栈或者 确保一部分内存数据不会增长出边界。另一种情况,就是当一个特定的内存块被进程命中(访 问)了,就暂停进程。举个例子,如果我们在逆向一个网络服务程序,在其接收到网络数据 包以后,我们在存储数据包的内存上设置保护页,接着运行程序,一旦有任何对保护页的访 问,都会使 CPU 暂停,抛出一个保护页调试异常,这时候我们就能确定程序是在什么时候 用什么方式访问接收到的数据了。之后再进一步跟踪观察访问内存的指令,继而确定程序对数据做了什么操作。这种断点同时也解决了软件断点数据更新的问题,因为我们没有修改任 何运行着的代码。 到目前为止,我们已经讲解完了调试器的基础知识和工作原理,接下来我们要亲自动手 写一个 Python 调试器,这个基于 Windows 的轻量级调试器,将会用到我们目前学到的所有 知识。