使用rellic-headergen揭秘C/C++数据结构内存布局

本文介绍了rellic-headergen工具如何将复杂的C++类型转换为等效的C结构定义,通过解析LLVM位码中的DWARF调试信息,揭示数据结构的内存布局。工具处理了填充对齐、继承关系等挑战,为逆向工程提供了强大支持。

使用rellic-headergen解析C数据结构

你是否曾想过编译器是如何看待你的数据结构的?Compiler Explorer可以帮助你理解源代码和机器码之间的关系,但在数据布局方面提供的支持有限。你可能听说过填充、对齐和"普通旧数据类型"的概念。也许你甚至尝试过通过在C语言中嵌入一个结构体来模拟继承。但你能在不查看平台ABI参考或标准库源代码的情况下,猜出所有这些类型的确切内存布局吗?

1
2
3
struct A { int x; };
struct B { double y; };
struct C : A, B { char z; };

具备必要的ABI知识后,推理C结构体相对简单。然而,更复杂的C++类型则完全是另一回事,特别是当模板和继承参与其中时。理想情况下,我们能够将所有复杂类型转换为简单的C结构体,以便更容易地推理它们的内存布局。这正是rellic-headergen的确切目的,这是我在Trail of Bits实习期间开发的一个工具。

rellic-headergen的工作原理

rellic-headergen的目的是生成与LLVM位码文件中包含的类型定义等效的C类型定义,这些类型不一定是从C源代码生成的。这有助于分析包含复杂数据布局的程序。

该工具基于一个假设:被分析的程序可以编译为包含完整调试信息的LLVM位码。工具首先构建所有可用调试信息的类型列表,从函数定义开始。

工具需要决定类型定义的顺序,但考虑到C语言的要求,这并非易事:当引用尚未定义的类型时,C语言需要显式的前向声明,而且结构体不能包含仅被前向声明类型的字段。

解决方案之一是预防性地前向声明所有现有类型。但这还不够。例如,结构体不能包含类型未完全定义的字段,但可以包含指向前向声明类型的指针字段。

因此,工具从类型定义形成一个有向无环图(DAG),并找到拓扑排序。按照这个顺序检查类型时,可以确信任何字段的类型都已完全定义。

结构体的处理技巧

DWARF元数据提供了几条信息,可用于恢复其描述类型的C结构定义:

  • 类型的大小
  • 每个字段的类型
  • 每个字段的偏移量
  • 类型最初是结构体还是联合体

rellic-headergen的重建算法首先按偏移量递增顺序排序字段,然后定义一个新结构体来添加每个字段。元数据没有提供原始定义是否声明为打包的信息,因此rellic-headergen首先尝试直接按照元数据的指定生成布局。如果结果布局与输入不匹配,工具会重新开始并生成打包布局,根据需要手动插入填充。

结构体与继承

在处理更复杂的用例时,我遇到了一些ABI工作方式不明显的情况。例如,处理C++继承需要谨慎,因为简单的方法并不总是正确的。

将:

1
2
struct A { int x; };
struct B : A { int y; };

转换为:

1
2
3
4
5
struct A { int x; };
struct B {
    struct A base;
    int y;
};

似乎是个好主意,在实践中也有效,但这种方法扩展性不好。

此外,空结构体在C语言中技术上无效。它们可以通过GCC和Clang扩展使用,在C++中有效,但会带来问题:空结构体的sizeof永远不会是0,通常是1。

rellic-headergen的未来发展

rellic-headergen有进一步改进的机会。工具的目标之一是能够从经过优化的代码中恢复字段访问模式。

另一个值得探索的未来功能是改变输出语言的可能性:有时候你确实希望保留继承信息作为C++!

结语

虽然rellic-headergen目前适用范围很窄,但在处理C和C++代码库时已经非常强大,它能够为rellic本身提取类型信息,包括LLVM和Clang。在导航带有调试信息构建的二进制文件时,它已经提供了有用的见解,但扩展其功能集以能够从更多样化的代码库中提取信息,将使它在处理更大项目时更加有用。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计