在本系列的第一篇文章中,我们编写了一个小型进程启动器作为调试器的基础。在这篇文章中,我们将学习x86 Linux下断点的工作原理,并继续编写我们的工具增加设置断点的功能。
有两种类型的断点:硬件断点和软件断点。硬件断点通常需要设置处理器中寄存器的值以产生断点,而软件断点则需要修改正在执行的代码。本文将以软件断点为主,因为它更简单,而且想要多少都可以,在X86上,同时只能设置4个硬件断点,不过硬件断点既可以在读取或写入给定地址触发,也可以在执行时触发。
我上面说过,软件断点是通过修改执行代码来实现的,所以问题来了:
第一个问题的答案当然是ptrace
。我们之前使用它来跟踪和继续执行程序,现在也可以使用它来读写内存。并发送信号通知程序。 在x86上,是通过使用 int 3
指令覆盖这个地址上的指令来实现的。x86具有中断向量表(IDT),操作系统可以使用它来注册各种事件的处理程序,例如缺页异常,保护错误和无效操作码。这有点像注册错误处理回调,但是是硬件级别的。当处理器执行 int 3
指令时,系统会执行断点中断处理程序,在Linux系统下,进程会产生一个SIGTRAP
的信号。您可以在下图中看到此过程,其中我们用0xcc
覆盖mov
指令的第一个字节,这是 int 3
的指令编码。
最后一个需要解决的问题是调试器如何通知用户断点已经触发。如果您还记得上一篇文章,我们可以使用waitpid
来等待发送给调试程序的信号。我们在这里可以做同样的事情:设置断点、继续程序、调用waitpid
并等到SIGTRAP
信号发生。然后通过打印已经到达的源代码位置或者改变有界面的调试器中的选中行来将该断点已触发传送给用户。
我们将实现一个breakpoint
类来表示某个位置上的断点,我们可以根据需要启用或禁用断点。
上面代码大多只是获取一些程序状态,真正的难点在enable
和disable
函数中。
如上所述,我们需要使用编码为0xcc
的 int 3
指令来替换当前在给定地址处的指令。我们还想保存以前在该地址中的内容,以便以后可以恢复代码。我们不想忘记执行用户的代码。
ptrace
使用PTRACE_PEEKDATA
参数可以实现读取跟踪进程的内存。我们给它一个进程ID和地址,它给我们返回目前在该地址的64位。 (m_saved_data&〜0xff)将该数据的底部字节置零,然后按位或与我们的int 3
指令设置断点。 最后,我们通过使用PTRACE_POKEDATA
参数将新数据覆盖那部分内存来设置断点。
disable
函数更容易,但仍然有些麻烦。由于ptrace
内存请求是对整个字而不是字节操作,因此,我们需要首先读取要恢复的字,然后用原始数据覆盖低字节并将其写回内存中。
我们将对debugger
类进行三个更改,以支持通过用户界面设置断点:
我们会将断点存储在 std::unordered_map<std::intptr_t, breakpoint>
结构中,以便检查一个给定地址是否有断点,如果有的话获取断点对象信息是容易和快速的。
在set_breakpoint_at_address
函数中,我们将创建一个新的断点,启用它,将其添加到数据结构中,并为用户打印一条消息。如果你喜欢,可以考虑将所有信息打印提出封装成一个库和命令行工具,以便于调试器使用,现在为了简单起见,我们将它们先放在一起。
现在我们将扩充我们的命令处理程序来调用我们的新函数。
以上代码中,我简单地删除了字符串的前两个字符,并在结果上调用了std :: stol
,感觉这样使解析更加健壮了。 std :: stol
有选择的将一个基数转换为十六进制读取会比较方便。
如果你尝试这样做,你可能会注意到,如果你从断点继续运行程序,没有任何反应。这是因为断点仍然设置在内存中,所以它会不停重复触发断点。比较简单恢复程序运行的解决方案是禁用断点,单步,重新启用它,然后继续。 不幸的是,我们还需要修改程序计数器(x86下是EIP)以指出断点之前的位置,所以我们将在下一篇关于操作寄存器的文章中解决。
当然,如果你不知道要设置断点的地址,那么设置断点并不是很有用。在将来,我们将添加在函数名或源代码行上设置断点的功能,但是现在,我们可以手工完成它。
测试调试器的一种简单方法是编写一个hello world程序并调用函数std::err
(避免缓冲),然后在调用输出操作时设置断点。如果你继续进行调试,那么程序会暂停而不打印任何东西。然后您可以重新启动调试器,并在调用后设置一个断点,你应该会看到成功打印的消息。
找到地址的一种方法是使用objdump。如果您打开一个shell并执行objdump -d <您的程序>
,那么您应该看到程序的反汇编代码。然后,您应该能够找到main
并找到您想要设置断点的调用指令的地方。例如,我构建了一个hello world示例,将其反汇编,并将其main函数反汇编:
如您所见,我们希望在0x400944
上设置一个断点,以查看有无输出,0x400949
可以看到输出。`
到此,您现在应该有一个调试器,它可以启动一个程序,并允许用户在内存地址上设置断点。下次我们将增加读写能力,读写内存和寄存器。如果你有任何问题,请在评论中告诉我。
你可以在这里找到这篇文章的代码 。
使用命令objdump -f <文件名>
查看程序hello15pb
的入口地址 然后使用命令objdump -d <文件名>
查看程序hello15pb
的入口代码 我们可以使用自己的调试器加载程序,然后在入口地址下方400bc2
使用命令break 0x400bc2
下断点,然后运行程序,如果使用两次命令cont
才能显示hello 15pb
,说明断点生效了。 操作命令的截图如下:
跟着这篇文章写下来,可以调试设置断点了,只是还不能将断点去掉之后重置cip
,所以接着努力吧!加油!15PB的同学们! 原文来自:https://blog.tartanllama.xyz/writing-a-linux-debugger-breakpoints/ 翻译来自:lantie@15PB 专注于信息安全教育 http://www.15pb.com.cn
注意: Clion工程中的代码修改了unordered_map对象的使用。 将数组操作修改为函数调用,在函数debugger::set_breakpoint_at_address
中 完整Clion工程代码以及示例小程序:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: