用Pwndbg恢复二进制文件调试信息:Binary Ninja集成与Go调试增强

用Pwndbg恢复二进制文件调试信息

当调试缺乏调试符号的"剥离二进制文件"时,GDB会丧失大量功能:函数和变量名变成无意义的地址,设置断点需要从外部查找函数地址,查看结构化值需要手动解析内存转储。为此,我在Trail of Bits实习期间扩展了Pwndbg——这是由我的导师Dominik Czarnota维护的GDB插件,新增两项功能使剥离二进制文件的调试体验接近IDE调试器。

Binary Ninja集成

通过在Binary Ninja内部安装XML-RPC服务器并从Pwndbg查询,我实现了Pwndbg与这款流行反编译器的集成。这使得Pwndbg可以访问Binary Ninja的分析数据库,用于同步符号、函数签名、栈变量偏移等,恢复大部分调试体验。

对于反编译功能,我直接从Binary Ninja提取语法标记而非序列化为文本,实现完全语法高亮的反编译,可配置使用Binary Ninja的3种中间语言级别。反编译结果直接显示在Pwndbg上下文中,当前行高亮显示,与汇编视图一致。

我还实现了在Binary Ninja中显示程序计数器(PC)寄存器箭头的功能,以及从Binary Ninja内部设置断点的功能,减少在两者间切换的频率。

最复杂的集成组件是栈变量名同步。Pwndbg中任何出现栈地址的地方(如寄存器视图、栈视图或函数参数预览),集成功能都会检查是否为Binary Ninja中的命名栈变量,若是则显示正确标签。该功能还会检查父栈帧,确保调用者的变量也能正确标记。

实现该功能的主要困难在于Binary Ninja仅提供相对于栈帧基址的偏移量,因此需要推导帧基址才能计算绝对地址。虽然x86等架构有帧指针寄存器,但编译器可能将其作为普通寄存器使用。幸运的是,Binary Ninja具有常量值传播功能,可以判断寄存器是否是帧基址的可预测偏移量。

Go调试增强

调试非C语言(有时甚至是C)编译的可执行文件时,复杂的内存布局使得值转储变得困难。例如转储Go切片需要一个命令转储指针和长度,另一个命令检查切片内容;而转储map对小map需要十余个命令,大map则需要数百个。

为此我创建了go-dump命令。参考Go编译器源码,我实现了所有Go内置类型的转储,包括整数、字符串、复数、指针、切片、数组和map。内置类型采用Go原生表示法,无需学习新语法。

该命令还能解析和转储任意嵌套类型,使得任何类型都只需一个命令即可完成转储。

解析Go运行时类型

虽然Go专用转储比手动内存转储更方便,但仍存在可用性问题:需要知道待转储值的完整类型,这在处理多字段或嵌套结构体时尤其困难。此外,结构体字段名和用户定义类型名等编译无关信息也无法获知。

Go编译器会为程序中使用的每个类型生成运行时类型对象(供reflect包使用),包含任意嵌套结构的布局、类型名、大小和对齐等信息。这些类型对象还可与对应值匹配:接口值存储类型指针和数据指针,堆分配值在分配函数(通常是runtime.newobject)中传入类型对象。

我编写了能递归提取这些信息的解析器,通过go-type命令显示给定地址的运行时类型信息。对于结构体,包括每个字段的类型、名称和偏移量。

这可通过两种方式转储值。第一种方式仅适用于接口值,因其直接存储类型指针和数据指针,便于自动检索。使用Go的any类型转储空接口(无方法的接口),用interface类型转储非空接口。转储时命令会自动检索和解析类型,无需输入类型信息。

第二种方式适用于所有值,但需要指定类型指针。虽然查找类型指针可能涉及猜测,但仍比手动推导类型布局简单,能转储最复杂的类型。我在Go编译器(最大最复杂的开源Go代码库之一)的剥离构建上测试了几个大型结构体类型,都能成功转储。

展望未来

这个夏天,我增强了Pwndbg与Binary Ninja的集成以获取丰富调试信息,并添加了go-dump命令转储Go值。所有功能已在Pwndbg开发分支和最新版本(2024.08.29)中提供。

未来还有更多改进空间:Binary Ninja集成采用模块化设计,便于未来支持更多反编译器;Go调试可增强对goroutine的支持,目前这是Delve调试器(专用于Go的调试器)相对于GDB/Pwndbg的主要优势。

致谢

感谢Trail of Bits提供这个绝佳的实习机会,特别感谢我的导师Dominik Czarnota对代码审查和反馈的快速响应,以及Pwndbg社区在开发过程中解答我的各种问题。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码

posted @ 2025-07-31 10:56  qife  阅读(6)  评论(0)    收藏  举报