是否有能够从任意汇编代码重构结构化代码的工具?

逆向工程 反编译
2021-06-15 22:06:12

从本质上讲,我正在寻找一种工具,它可以从任意 CPU 的汇编语言代码中重建带有条件运算符、循环、中断/继续等的伪代码,前提是对与控制流相关的汇编结构的理解有限,并采用任何它不理解为不透明斑点的东西。或者,如果可以将伪代码“if (condition) goto label”和“goto label”作为基本块边界,则不需要理解任何程序集。

例如,

    blob1
    CMP A, B
    BLE L1
    blob2
    CMP C, D
    BEQ L2
L1: 
    blob3
    CMP E, F
    BGT L3
L2: 
    blob4
    ...
    B L4
L3: 
    blob5
L4: 
    blob6

可以变成类似的东西

blob1;
if ( A > B && { blob2; C == D } || {blob3; E <= F} ) { 
    blob4;
} else {
    blob5;
}
blob6;

或者传统的 ifs、ifblob2is blob3“太大”

此外,变量 A 到 F 不必遵循任何寻址模式约定,它们可以是任意字符串,在输出中逐字复制。该工具只需要知道标签的样子,以及比较运算符和分支的样式(一个比较指令生成各种分支指令使用的条件代码,而各种比较指令生成真/假值)在寄存器中,由“如果为真则分支”和“如果为假则分支”指令等使用)。

当这些结构被嵌套、标签被重用、在源代码中使用中断/继续、偶然的非结构化控制转移可能发生等等时,事情变得有些重要。Switch/case 语句是单独的故事。为了充分认识它们,需要对基础 ISA 有更深入的了解。

看来我需要的是基本DREAM反编译算法的实现;我无法找到将文本格式的 CFG 作为输入的简单实现。

4个回答

初步意见:

  • 问题中描述的过程不是反编译
  • 与“反编译”标签相关的定义不正确

  • 由于以下几个原因,目前没有任何工具可以执行所描述的过程:

    1. 任何给定的汇编语言都必然与目标处理器的机器语言密切相关
    2. 由于汇编代码(文本)和目标代码(二进制机器语言)之间的密切关系,它是一种符号表示,首先似乎可以怀疑通用汇编语言翻译器的功能等效项
    3. 根据定义,伪代码不符合语言规范。这排除了词法分析,这是文本到文本转换的重要步骤
    4. 反编译器不接受任意文本作为输入,因为它们从解析的机器代码创建 CFG。此外,CFG 构造方法因反编译器而异,因为 CFG 用于创建中间表示,这些表示因反编译器而异。
    5. 由于反编译器使用与体系结构无关的中间表示来执行机器代码到 HLL 的转换,因此不需要开发以某种方式解析任意伪汇编的工具
  • 虽然汇编源文本指令通常与目标机器语言指令具有一对一的关系,但中间表示可以独立于语言和体系结构。这取决于中间表示的选择

反编译

1. 重要的是能够引用反编译的正确定义,以便对它的含义有所了解

如前所述,与“反编译”标签相关的定义是不正确的。这里是:

用结构化编程语言(如 C)翻译从二进制文件中提取的汇编代码的过程。

我们可以将此定义与学术文献中提供的正确定义进行比较:

反编译器是一种程序,它读取用机器语言(源语言)编写的程序,并将其翻译成用高级语言(目标语言)编写的等效程序。反编译器或反向编译器试图逆转编译器将高级语言程序翻译成二进制或可执行程序的过程。1

更直接:

从表面上看,反编译是仅在给定二进制文件的情况下恢复程序的源代码。在下面,反编译由一系列抽象恢复机制组成,例如间接跳转解析、控制流结构化和数据类型重建,这些机制恢复了二进制形式不容易获得的高级抽象。2

可以根据正确的定义做出某些推论:

  • 反编译器的输入应该是目标代码,而不是 ASCII 文本或通过助记符的目标代码再现。错误地建议汇编代码(机器代码的符号语言表示)是预期的输入是一个严重的概念错误。这就是需要更改标签定义的原因,因为此错误可能会导致混淆或误解
  • 反编译的目的是通过二进制编码的操作的高级语言作为目标代码创建语义近似。为了创建一个可接受的准确度的近似值,需要使用多种方法来补偿编译过程中发生的信息丢失。换句话说,关于抽象恢复的原则越少,反编译的源文本作为编码二进制目标代码的信息的近似值就越不准确

2. 反编译使用中间表示而不是汇编语言文本来创建 HLL 输出

以下是大致涉及的内容:

反向编译技术,图1-10

另一个例子:

控制流图恢复 第一阶段解析输入二进制文件的文件格式,反汇编二进制文件,并为每个函数创建一个控制流图 (CFG)。在高层次上,控制流图是一种程序表示,其中顶点表示基本块,边表示块之间可能的控制流转换。(有关更多详细信息,请参见第 2.1 节。)虽然在可执行文件中精确识别二进制代码是已知的在一般情况下很难,当前的算法已被证明在实践中运行良好 [4, 5, 24, 25]。已经有成熟的平台实现了这一步。我们使用 CMU 二进制分析平台 (BAP) [10]。BAP 将 CFG 中的顺序 x86 汇编指令提升为一种称为 BIL 的中间语言,其语法如表 1 所示(参见 [10])。正如我们将看到的,2

CMU 的 Phoenix 反编译器所采取的步骤顺序与 Cristina Cifuentes 的“反向编译技术”(她的反编译器名为dcc的图表中描绘的顺序略有不同:Phoenix 为二进制文件创建了一个 CFG,然后将其转换为中间文件语言,而不是相反。

另一个例子,来自No More Goto: Decompilation Using Pattern-Independent Control-Flow Structuring and Semantics-Preserving Transformations

No More Goto: 使用独立于模式的控制流结构和语义保留转换的反编译

从第一句话可以明显看出,DREAM 的输入是二进制的。与 Phoenix 使用BAP 中间语言作为中间表示不同,DREAM 使用自己未指定的中间表示形式。

最后一个例子——基于Decompilers 和白皮书之外对 Hex-Rays 反编译器的过时讨论,一般的反编译过程如下所示:

  1. 微码生成
  2. 局部优化
  3. 全局优化
  4. 局部变量分配
  5. 结构分析
  6. 初始伪代码
  7. 伪代码转换
  8. 类型分析

简而言之,Hex-Rays 反编译器将目标代码从二进制转换为微代码,创建 CFG,使用 CFG 创建伪代码(一些未指定的,可能是专有的,中间表示),然后输出转换后的伪代码。

当检查这些不同的反编译器时,会出现一些清晰的模式。

编译器和反编译器都使用中间表示作为翻译过程的一部分有几个很好的理由。主要原因是 IR 可以独立于语言和体系结构,同时保留以源语言编码的信息。IR 的这一方面完全消除了对通用汇编语言到伪代码转换器的等效物的需要。CFG 到 IR 的二进制方法非常优越且成熟。

IR 与每个体系结构每个语言的编译器方法

来源:普林斯顿 CS320 中级表示讲义

对各种评论的回应

看来我需要的是基本DREAM反编译算法的实现;我无法找到将文本格式的 CFG 作为输入的简单实现。

不存在这样的实现。如前所述,DREAM 从二进制文件中包含的解析目标代码构建 CFG。

我已经有了自己的反汇编程序。使用以二进制作为输入的工具将是一种浪费。

将目标代码作为输入并生成汇编语言 ASCII 文本作为输出的工具与反编译无关。重新发明轮子将是一种浪费。顺便说一下,所有上述反编译器都将二进制文件作为输入。

我的观点是重构控制流结构实际上独立于架构。我可以将输入提高一步:如果有一个由“if (condition) goto”和“goto”连接的不透明基本块的图形,则应该可以恢复原始的 if-then-else 语句和循环,并且一个可以做到这一点的工具不需要任何反汇编能力

CFG 是通过目标代码分析生成的,以便尽可能多地保留信息。更多保留的信息=更准确的反编译。目标代码格式直接取决于体系结构。


1.反向编译技术

2.使用语义保留结构分析和迭代控制流结构的原生 x86 反编译

IDA Pro 是我所知道的最接近的。虽然它只有我认为 x86 和 PPC 的反编译器,但它处理更多的 CPU,并且似乎对控制流的理解有限,如在图形模式下查看函数所示。使用 IDA Python,您可以解析函数、遵循控制流并轻松输出文本文件,但它只会让您获得由真/假分支链接的 Blob 树。

反汇编我自己的和其他微控制器代码让我得出结论,一个好的反编译器需要关于编译器和 CPU 的特定信息。对于指针结构、间接寻址和有限的类型信息,仅手动跟踪某些内容可能很困难。

在不知道您要针对哪种 CPU 架构的情况下,很难说这样的工具是否存在。但是,我会推荐radare2有两个原因:

  1. 它支持很多架构,甚至是异国情调的架构
  2. 它是开源且可扩展的——如果您的 CPU 不受支持,您可以集成现有的反汇编器实现或自己编写一个。

但是,请记住,它只会生成流量控制图。如果反汇编程序实现了一些数据流分析,则它可能是可用的。

我将把我的抽象汇编代码表示为一个 C 程序,其中基本块作为外部定义的过程,条件中的变量作为外部定义的函数等。将其编译为优化的 x86 二进制文件,然后通过现有的 x86 反编译器传递它。

使用问题中的示例:

extern void blob1(), blob2(), blob3(), blob4(), blob5(), blob6();
extern int A(), B(), C(), D(), E(), F();
main() {
    blob1();
    if (A() < B()) goto L1;
    blob2();
    if (C() == D()) goto L2;
L1: blob3();
    if (E() > F()) goto L3;
L2: blob4();
    goto L4;
L3: blob5();
L4: blob6();
}

一个反编译器产生的:

_blob1();
int32_t v2 = _A();
int32_t v3;
if (v2 < _B()) {
    _blob3();
    v3 = _E();
    if (v3 > _F()) {
        _blob5();
        _blob6();
        return 0;
    }
    _blob4();
    _blob6();
    return 0;
}
_blob2();
int32_t v4 = _C();
if (v4 != _D()) {
    _blob3();
    v3 = _E();
    if (v3 > _F()) {
        _blob5();
        _blob6();
        return 0;
    }
}
_blob4();
_blob6();

这并不完美,但它会做到。