交互式反编译利器:rellic-xref 技术解析

本文深入解析了基于LLVM的反编译框架Rellic及其交互式工具rellic-xref,详细介绍了其代码精炼过程、多种优化策略(如死语句消除、Z3条件简化等)以及如何通过Web界面实现LLVM模块与反编译C代码的实时关联展示。

交互式反编译与 rellic-xref

Rellic 是一个用于分析和将 LLVM 模块反编译为 C 代码的框架,实现了原始论文中提出的 Dream 反编译器及其后继者 Dream++ 的概念。最近,我在本博客中介绍了 rellic-headergen,这是一个从 LLVM 模块提取调试元数据并将其转换为可编译的 C 头文件的工具。在本文中,我将介绍我开发的一个工具,用于探索原始 LLVM 模块与其反编译的 C 版本之间的关系:rellic-xref。

rellic-xref 的界面

我被指派改进 Rellic 生成的“来源信息”(即关于每个 LLVM 指令如何与最终反编译代码相关的信息)的质量。然而,Rellic 的主要前端(一个名为 rellic-decomp 的工具,以“批处理”风格生成 C 源代码)并未提供太多关于底层发生的信息。最重要的是,它没有打印我感兴趣的任何来源信息。

为了解决这个问题,我开发了 rellic-xref 作为一个 Web 界面,以便 Rellic 可以以交互和图形化的方式使用。rellic-xref 启动一个本地 Web 服务器,并作为底层反编译器的接口。当用户上传一个 LLVM 模块时,模块的文本表示会显示在右侧窗格中。用户然后可以对 LLVM 位码运行各种预处理步骤,或继续进行反编译步骤。当模块包含反编译引擎尚未支持的指令时,有时需要进行预处理。

一旦模块被反编译,原始模块和反编译的源代码会并排显示:

原始模块和反编译的源代码

此时,该工具的主要吸引力已经可用:将鼠标悬停在反编译源代码的部分上会高亮显示导致其生成的指令,反之亦然。

精炼过程

直接从反编译器生成的源代码并不像它可能的那样美观。为了解决这个问题,Rellic 提供了一系列精炼过程,使代码更易读。这些精炼过程通常迭代执行,并且这些过程执行的顺序在 rellic-decomp 中是硬编码的。例如,按下“使用默认链”按钮会将 rellic-decomp 的默认过程配置加载到 rellic-xref 中。

Rellic 提供的精炼过程

默认链包括只执行一次的过程和迭代计算以搜索固定点的过程。然而,rellic-xref 使用户能够更改精炼过程的运行顺序和方式:可以移除过程(使用上图中的“x”按钮)并随意插入。

尽管 Rellic 过程操作于 Clang 抽象语法树(AST),但它们并不适用于任何通用的 C 程序,因为它们依赖于关于反编译过程如何生成它们的假设。特别是,反编译的代码最初形式类似于单静态赋值(SSA),反映了它直接从 LLVM 位码生成的事实,而 LLVM 位码本身也是 SSA 形式。

精炼过程

对于那些不熟悉 Rellic 精炼过程的读者,我们提供这些过程的描述,并附上它们如何精炼代码的示例。

死语句消除

此过程从 AST 中移除不引起任何副作用的语句。例如,“空”语句(单独的分号)和几种类型的表达式可以安全地移除。

原始

1
2
3
4
5
6
void foo() {
  int a;
  a = 3;
  a;
  bar(a);
}

精炼后

1
2
3
4
5
void foo() {
  int a;
  a = 3;
  bar(a);
}

Z3 条件简化

此过程使用 Z3 SMT 求解器通过尽可能减少其大小来提高 if 和 while 条件的质量。它以递归方式工作,检查每个条件的语法树,并修剪任何它发现明显为真或假的分支。

原始

1
2
3
if(1U && x == 3) {
  foo(x);
}

精炼后

1
2
3
if(x == 3) {
  foo(x);
}

原始

1
2
3
if(x != 3 || x == 3) {
  foo(x);
}

精炼后

1
2
3
if(1U) {
  foo(x);
}

嵌套条件传播

顾名思义,此过程将条件从父语句传播到其子语句。实际上,这意味着父 if 和 while 语句中的条件在嵌套的 if 和 while 语句中被假定为真。

原始

1
2
3
4
5
if(x == 0) {
  if(x == 0 && y == 1) {
    bar();
  }
}

精炼后

1
2
3
4
5
if(x == 0) {
  if(1U && y == 1) {
    bar();
  }
}

嵌套作用域组合

这个很简单:任何出现在复合语句或明显为真的 if 语句中的语句都可以提取并放入父作用域。此过程基于所有局部变量已在函数开头声明的假设。

原始

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void foo() {
  int x;
  int y;
  {
    x = 1;
  }
  if(1U) {
    y = 1;
  }
}

精炼后

1
2
3
4
5
6
void foo() {
  int x;
  int y;
  x = 1;
  y = 1;
}

基于条件的精炼

此过程识别作用域中包含两个具有相反条件的相邻 if 语句,并将它们合并为一个 if-else 语句。

原始

1
2
3
4
5
6
if(a == 42) {
  foo();
}
if(!(a == 42)) {
  bar();
}

精炼后

1
2
3
4
5
if(a == 42) {
  foo();
} else {
  bar();
}

基于可达性的精炼

类似于基于条件的精炼过程,此过程识别连续的 if 语句具有互斥但非相反的可达条件,并将它们重新排列为 if-else-if 语句。

原始

1
2
3
4
5
6
if(a == 42) {
  foo();
}
if(!(a == 42) && b == 13) {
  bar();
}

精炼后

1
2
3
4
5
if(a == 42) {
  foo();
} else if(b == 13) {
  bar();
}

循环精炼

此过程识别应负责终止无限 while 循环的 if 语句。它重构代码以创建带条件的循环。

原始

1
2
3
4
5
6
while(true) {
  if(a == 42) {
    break;
  }
  foo();
}

精炼后

1
2
3
while(!(a == 42)) {
  foo();
}

表达式组合

此过程执行许多简化,例如将指针算术转换为数组访问和移除多余的转换。

原始

1
*&x

精炼后

1
x

原始

1
!(x == 5)

精炼后

1
x != 5

原始

1
(&expr)->field

精炼后

1
expr.field

条件规范化

这是默认精炼链中唯一未使用的过程。此过程的目的是将条件转换为合取范式,以揭示更多简化的机会。不幸的是,它往往会产生指数级大的条件——字面意思!——因此最好谨慎使用,并且仅作为使用 Z3 进行最终简化之前的最后一步。

我被说服了!如何使用它?

结束语

rellic-xref 将传统上批处理的 Rellic 转变为更交互式的工具,提供了对反编译和精炼过程的洞察。它也是对 Rellic 框架可能用途的一瞥。例如,如果用户对底层的 Clang AST 有更多控制权会怎样?例如,允许自定义变量重命名和重新类型化,将使 Rellic 感觉更像是逆向工程套件的适当组件。对 rellic-xref 的进一步工作(或开发类似工具)可以以这种方式给用户更多对 Clang AST 的控制。

Rellic 的过程直接在 C AST 级别操作,并大量使用 Clang 的 API。这在我使用 Rellic 时既是福也是祸。例如,将 Clang AST 称为“抽象”有点用词不当,因为它具有抽象和具体语法树的特征。例如,它包含关于标记和注释位置的信息,但也包含源文本中实际上不存在的内容,如隐式转换。我在 Rellic 上的经验告诉我,Clang AST 接口并不真正打算用作可变资源,它更多地具有写一次、读多次的语义。我们计划将 Rellic 迁移用于即将推出的 MLIR 项目,这可能在这方面有所帮助。然而,这超出了本博客文章的范围。

我要感谢我的导师 Peter Goodman 在我实习期间的指导,以及 Marek Surovič 对我与 Rellic 工作的宝贵反馈。在 Trail of Bits 工作继续证明是一次充满满足时刻的伟大经历。

如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

页面内容 rellic-xref 精炼过程 精炼过程 我被说服了!如何使用它? 结束语 最近的帖子 非传统创新者奖学金 劫持您的 PajaMAS 中的多代理系统 我们构建了 MCP 一直需要的安全层 利用废弃硬件中的零日漏洞 Inside EthCC[8]:成为智能合约审计员 © 2025 Trail of Bits。 使用 Hugo 和 Mainroad 主题生成。

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