在上一篇文章中,我们向调试器添加了简单的地址断点。这一次,我们将添加读取寄存器和内存的功能,有了这个功能我们就可以观察寄存器状态和利用程序计数器(RIP)改变程序的执行流程了。
在我们编写读取寄存器代码之前,首先需要确定调试器支持什么平台,我们选择x86_64(即64位)。除了通用寄存器和专用寄存器之外,x86_64还提供了浮点寄存器和向量寄存器。为了简单起见,我将省略后两者,但如果你愿意,可以选择支持它们。x86_64还允许你访问的一些64位寄存器作为32位、16位、8位寄存器访问,但在这里我只支持64位。由于这样的简化,对于每个寄存器,我们只需要保存其名称,其DWARF寄存器编号和从ptrace
返回的结构里的位置即可。我定义了一个枚举来引用寄存器,然后我编写了一个全局局存起描述符数组,其中元素的顺序与ptrace
返回的寄存器结构中的顺序相同。
如果你想自己查看寄存器的数据结构可以在/usr/include/sys/user.h中找到,DWARF寄存器编号取自System V x86_64 ABI 。
现在我们可以编写一连串的函数来与寄存器进行交互。我们希望能读取寄存器、修改寄存器,从DWARF寄存器编号中检索一个值,并按名称查找寄存器,反之亦然。我们从get_register_value
开始:
又一次,ptrace
让我们轻松访问到了想要的数据。我们只是构造一个user_regs_struct
的实例,并将PTRACE_GETREGS
参数传入了ptrace
就可以完成。
现在我们要根据请求的寄存器来读取regs
。我们可以写一个大的switch语句,但是由于我们按照与user_regs_struct
相同的顺序排列了我们的g_register_descriptors
表,所以我们可以检索寄存器描述符的索引,并将user_regs_struct
作为 uint64_t
类型的数组访问。[注解1]
由于user_regs_struct
是一个标准的布局类型(线性结构),所以转为 uint64_t
是安全的,但我认为指针计算在技术上是比较难看的。由于目前编译器还没有警告,再加上我也比较懒,所以就先这样做,但是如果您想保持最大的正确性,那么就编写一个大的switch语句吧。
set_register_value
是一样的,我们只需要获取位置,并在其位置上写入寄存器的值:
接下来是通过DWARF寄存器号查找。 这一次我会检查一个错误条件,以防万一我们得到一些奇怪的DWARF信息:
到这几乎完成,现在还有对注册的寄存器名称的查找:
最后,我们将添加一个简单的函数来转储所有寄存器的内容:
如你所见,iostreams有一个非常简洁的接口,可以很好地输出十六进制数据。 如果你喜欢的话,可以自由地对I/O输出做格式控制。[注解2]
这给了我们足够的支持来在调试器的其余部分轻松地处理寄存器,因此我们现在可以将它添加到我们的UI中。
我们需要在这里做的就是向handle_command
函数添加一个新命令。使用以下代码,用户将能够键入register read rax
,register write rax 0x42
,等等。
在设置断点时,我们已经从内存中读取和写入内存,因此只需添加一些函数来隐藏ptrace
调用即可。
你可能想要一次添加对读取和写入的支持,通过每次你想读另一个单词时递增地址即可。您还可以使用process_vm_readv
和process_vm_writev
或/ proc/<pid>/mem
而不是ptrace
。 现在我们将为UI添加命令:
在测试我们的更改之前,我们现在可以执行一个更合理的版本的continue_execution
。 由于我们可以得到程序计数器(RIP),所以可以检查我们的断点映射,看看我们是否处于断点。 如果是这样,我们可以在继续之前禁用断点并重新切断它。
首先,为了清晰简洁,我们将添加几个帮助函数:
然后我们可以写一个函数来跳过一个断点:
首先,我们检查是否为当前PC的值设置了一个断点。 如果有的话,我们先把执行返回到断点之前,禁用它,重新执行原来的指令,然后再重新启用断点。
wait_for_signal
将封装我们通常的waitpid
模式:
最后我们重写如下的continue_execution
:
现在我们可以读取和修改寄存器,可以使用我们的hello world程序进行调试测试。 作为第一个测试,请尝试再次在调用指令上设置断点,并从中继续。 你应该看到Hello world
被打印出来。 有趣的部分在输出调用之后设置一个断点,继续运行程序,然后将调用参数设置代码的地址写入程序计数器(rip
)并继续。 由于这个程序计数器的修改,你应该再次看到Hello world
被打印了。 为了防止你不确定断点的位置,以下是我最后一篇文章的objdump
输出:
你将要将程序计数器移回0x40093a
,以便正确设置esi和edi寄存器。
在下一篇文章中,我们将首先介绍DWARF信息,并在调试器中添加各种单步。 之后,我们编写的工具将拥有调试器的主要功能,我们可以通过单步代码,设置断点,修改数据等等使用工具。 和往常一样,如果您有任何疑问,请在下方发表评论!
你可以在这里找到这篇文章的代码 。
注解1:你也可以重新排序寄存器表,并将其转换为基础类型以用作索引,但是我以现在的方式编写了,懒得了改变它了。 注解2:哈哈哈哈哈哈哈哈
吐槽一下:原来原作者也比较懒哈哈~
原文来自:https://blog.tartanllama.xyz/writing-a-linux-debugger-registers/ 翻译来自:lantie@15PB 专注于信息安全教育 http://www.15pb.com.cn 到此原文结束,以下是实践部分
这一节,最大的收获就是设计了寄存器的结构,能够读取寄存器信息,并且可以修改程序计数器(RIP)完成程序下断点之后的恢复执行。调试器到目前已经实现了以下命令:
使用自己的调试器,感受一下效果o( ̄︶ ̄ )o。 首先,使用命令objdump -d hello15pb|grep main
查看例子程序hello15pb
的main函数地址。记下main函数附近的第一个地址,方便设置断点。
然后使用编译好的调试,调试hello15pb
,使用命令break <地址>
在main
函数上设置断点
然后使用命令cont
运行程序,此时程序应该会暂停到main
函数后一个字节上。使用命令register read rip
可以查看当前程序计数器的内容。
还可以使用命令register dump
查看所有寄存器的信息
并可以使用命令register write r15 1
,修改指定寄存器r15
的值为1。
最后当使用命令cont
时,程序会将断点恢复,并将rip
减一,执行被断点覆盖的指令,程序就会正常的跑起来了!哈哈,15PB的同学们加油了!
由于调试器的代码使用的是最新的C++ 14的标准编写了,其中使用了一些新特性,所以如果编译器版本太低,会导致编译无法通过,所以需要修改一些代码将其变为老的方式,比如我遇到的了一个问题是unordered_map容器的使用,上一节就遇到了问题,这一节继续发生问题。所以想要将代码编译通过,需要将编译器升级到最高,在这里我已经将ubuntu系统上的gcc升级到了5.2,但经过测试还是不行,不知何原因,最后只能修改代码才完成通过编译,大家在编译的时候,注意以上问题吧! 我将代码经过比较大的重构,将原先的代码重新组织和编排,修改了编译时unordered_map容器操作的问题。
我重构之后的Clion项目工程:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: