+-
DIRE: 一种用于反编译标识符命名的神经网络方法


...

引用: Lacomis J, Yin P, Schwartz E, et al. Dire: A neural approach to decompiled identifier naming[C]//2019 34th IEEE/ACM International Conference on Automated Software Engineering (ASE). IEEE, 2019: 628-639.

1 摘要

反编译器是在没有相应源代码的情况下检查二进制文件的最常用工具之一,它可以将二进制文件转化成高级语言,实现编译过程的反转。反编译器可以重新构建编译过程中丢失的大部分信息(如结构和类型信息)。不幸的是,它们不能重建语义上有意义的变量名,而这些变量名增加了代码的可读性。我们提出了反编译标识符重命名引擎(DIRE),这是一种新的基于概率的变量名恢复技术,它同时使用了反编译程序恢复的词法和结构信息。我们还提出了一种适用于训练和评估反编译代码重命名模型的语料库生成技术,我们用它创建了一个包含 164642 个唯一的 x86-64 二进制文件的语料库,这些文件来自于 Github 的 C 语言项目。我们的结果表明,在这个语料库上,DIRE 预测与原始源程序相同的变量名的概率高达 74.3%。

2 介绍

软件逆向工程是指在没有访问源代码的情况下理解程序行为的问题,常用于预测恶意软件行为、发现漏洞、以及修补遗留软件中的错误等。

逆向工程师用来检查程序的主要工具是反汇编器—一种将二进制代码转换成低级汇编代码的工具。最近,逆向工程师使用诸如 Hex-ray 和 Ghidra 反编译器来降低理解汇编代码的认知负担,这些反编译器通过将反汇编器的输出转换成高级语言(如 C 语言)的代码来进行逆向编译。这些先进的工具能够使用程序分析和启发式方法重建有关程序变量、类型、函数和控制流结构的信息。

尽管反编译器的输出比原始汇编程序更容易理解,但反编译常常是不完整的。出于对二进制文件的大小、执行时间甚至混淆程度的考虑,编译器会丢弃源代码级别的信息并降低其抽象级别。注释、变量命、用户定义类型和惯用结构都会在编译时丢失,因此通常在反编译器的输出中不可用。特别是对于代码理解和可读性非常重要的变量名,变成了任意的占位符,比如 VAR1 和 VAR2。

在本次工作中,我们提出了 DIRE,一种为反编译代码中的变量分配有意义名称的神经网络方法。为了创建 DIRE 的环境,我们依赖于两个关键的洞察。第一个洞察是软件是自然的,即程序员倾向于编写相似的代码,并在相似的上下文中使用相同的变量。因此,基于这种重复性,如果给一个足够大的训练语料库,就可以学习适合特定上下文的变量名。

现有的方法可以从源代码和已编译的可执行文件中预测变量名。然而,从可执行文件预测变量名的方法要么直接操作二进制语义,要么操作反编译器的词法输出。前者忽略了现代反编译器能够恢复的丰富抽象。后者是前者的一种改进,但是从本质上来说,词法程序表示是顺序的,并且缺少可用于改进预测的丰富结构信息。相比之下,DIRE 使用反编译器内部的抽象语法树(AST)表示的反编译二进制文件所提供的扩展上下文,它能对附加的结构信息进行编码。

第二个关键的洞察是,与其他领域需要手动管理和创建训练数据不同,该项目可以自动生成大量的训练数据以进行标识符名称的预测。为此,我们挖掘出了来自 GitHub 的开源 C 语言代码,并使用调试信息进行编译,以使二进制文件保留原始名称,然后反编译这些二进制文件,以使输出包含原始名称。之后我们去除了调试符号,再次对二进制文件进行反编译,并在两个版本的反编译器输出中确定标识符之间的对齐方式。

有了这些洞察,我们在一个从 Github 上挖掘出的大量 C 代码数据集上训练和评估 DIRE,结果显示,我们预测的变量名与原始开发人员选择的变量名相同的概率高达 74.3%。

简而言之,我们的贡献如下:

反编译标识符重命名引擎(DIRE),这是一种为反编译变量分配有意义名称的技术,其性能优于以前的方法。

一种新的语料库生成技术,生成适合训练反编译代码中变量名的词法和基于图的概率模型的语料库。

一个数据集,包含了 3,195,962 个反编译的 x86-64 函数和用黄金标准变量名注释的解析树。

3 背景

在深入讨论我们方法的技术细节之前,我们先介绍反编译,源代码的统计模型以及我们所依赖的两类特殊的深度学习模型——循环神经网络(RNNs)和门控图神经网路(GGNNs)的一些背景知识。

3.1 反编译

在较高的级别上,编译器使用处理阶段的管道从源代码生成二进制文件,反编译器尝试使用各种技术来反转此过程。通常情况下,二进制文件首先通过特定于平台的反汇编编译器传递,接下来,通常使用二进制到 IR 转换器将汇编代码转换为与平台无关的中间代码(IR)。下一个阶段是反编译器的核心,在这个阶段中,许多程序分析被用来恢复变量、类型、函数和控制流等抽象,这些抽象最终被组合起来,以重构对应于惯用程序的抽象语法树(AST)。最后,代码生成器将 AST 转换为反编译输出。

反编译比编译更困难,因为编译器的每个阶段都会丢失有关原始程序的信息。例如,编译器的语法分析阶段不会将代码注释传播到 AST。类似地,从 AST 转换为 IR 也会损失额外的代码注释信息。这种信息丢失允许多个不同的源代码程序编译为同一个汇编代码。例如,图 1 中的两个循环被简化为相同的汇编指令。反编译器无法知道哪个源代码是原始的,但它会尝试生成习惯用法的代码,使用启发式方法来提高代码的可读性。例如,循环之类的高级控制流结构(如 while 循环)优先于 goto 语句。

...

图 1 两个不同的 C 循环编译成相同的汇编代码

3.2 源代码的统计模型

基于软件的自然性,已经提出了多种表示源代码的统计模型。这个关键属性说明源代码在给定的上下文中是高度重复的,因此是可预测的。统计模型捕获隐藏在代码中的隐含知识,并将其应用于构建新的软件开发工具和程序分析,例如,用于代码完成、文档生成和自动类型注释。

预测变量名也不例外。研究表明,在源代码语料库上训练的统计模型可以预测先前不可见的程序变量的描述性名称,前提是给定变量所用代码的上下文特征。这些命名模型可以帮助提取编码规范或分析混淆代码。

3.3 神经网络模型

我们的方法创建在表示源代码的统计模型的两项改进之上:循环神经网络模型(RNNs)和门控图神经网络(GGNNs)。

循环神经网络:RNNs 是节点之间连接形成序列的网络,它们通常通过一次读取一个元素来处理输入序列,这使得它们非常适合序列,比如源代码标记。在这项工作中,我们使用长-短期记忆(LSTM)模型,这是在文本处理中广泛使用的 RNNs 的变体。

我们在 DIRE 中使用两种类型的 LSTMs:编码 LSTMs 和解码 LSTMs。编码器 LSTM 读取输入序列并将其编码为连续变量,而解码器 LSTM 获取这些向量并生成输出序列。

门控图神经网络:虽然 LSTMs 对序列建模很有用,但它不会捕获任何额外的结构信息,在反编译的任务中,AST 提供的结构化信息是关于变量名选择的自然信息源,为此,我们还使用 GGNNs 对代码进行结构编码,GGNNs 是一种将图映射到输出的神经网络模型,在高层次上,GGNNs 是有向图上的神经网络。 最初,我们将每个顶点与一个经过学习或计算后包含顶点信息的隐藏状态关联起来。GGNNs 根据初始节点信息和图结构计算每个节点的表示形式。

4 DIRE 结构

4.1 概述

我们将 DIRE 设计为在反编译器上运行的插件,它可以自动提供更多信息的变量名。我们使用 Hex-Rays,一种行业最先进的反编译器,我们的方法并没有完全与 Hex-Rays 耦合,因此可以适应其他反编译器。

...

图 2 我们方法的高级概述

图 2 给出了我们工作流程的一个高级概述。首先,将二进制文件传递给反编译器,反编译器对二进制文件中的每个函数进行反编译。对于每个函数,我们的插件遍历 AST,在变量节点插入占位符。这将产生两个输出:AST 和标记化代码。这些输出将作为我们的神经网络模型 DIRE 的输入,DIRE 为输入中的每个占位符生成唯一的变量名。然后,可以重写反编译器的输出,其中包含了建议使用的变量名。

...

图 3 DIRE 的神经网络结构概述

图 3 给出了神经网络结构的概述。DIRE 遵循编码器-解码器体系结构:编码器神经网络首先对反编译器输出的反编译代码标记序列及其内部 AST 进行编码,并为每个标识符和代码元素计算分布式表示(如实值向量或嵌入)。然后,这些编码表示被解码器神经网络使用,解码器神经网络根据使用标识符的上下文来预测每个标识符的有意义名称。

关键的一点是,DIRE 既使用从标记化代码获得的词汇信息,也使用从相应的 AST 获得的结构信息。这是通过使用两个编码器分别捕获反编译代码中的词法信息和结构信息来实现的。正如我们将要展示的,这种词汇信息和结构信息的结合使得 DIRE 的性能优于仅依赖词汇信息的技术。

4.2 编码器网络

DIRE 中的每个编码器网络输出两组表示:

反编译器输出中每个元素的代码元素表示。根据编码器的类型,代码元素要么是表层代码中的一个标记(对于词法编码器),要么是反编译器内部 AST 中的节点(对于结构编码器)。

输入二进制中定义的每个唯一标识符的标识符表示。它是一个实值向量,表示神经网络中的标识符。然后将词法表示和结构表示合并,生成一个输入二进制的统一编码(图 3 中的虚线框)。通过计算代码元素和标识符的单独表示,DIRE 解码器可以更好地将上下文信息合并到单个代码元素的编码中,从而提高预测不同标识符名称的准确性。

4.2.1 词法代码编码器

词法编码器按顺序对标记化的反编译代码进行编码,将每个标记投影到固定长度的矢量编码中。具体地说,词法编码器使用子标记化代码作为输入,其中复杂代码标记(例如函数名 mystrcopy)使用 SentencePiece 自动分解为子片段(如 my,str 和 copy)。子标记化减少了编码器词汇表的大小(从而减少了训练时间),同时还通过将稀有或未知标记分解为更常见的子标记来减轻问题。我们将反编译器输出中的占位符和保留变量名(例如 VAR1,VAR2 和反编译器推断的名称 result)视为不应进行子标记化的特殊标记。

DIRE 使用 LSTMs 实现词法编码器,我们使用一个双向的 LSTM,前向 LSTM 按顺序处理标记化的代码,逆向 LSTM 反序处理输入的标记化代码,为每个标记生成一个逆向隐藏状态。直观地说,双向 LSTM 捕捉特定变量在其顺序位置前后的信息上下文。

4.2.2 结构化代码编码器

词法编码器只捕获代码标记中的顺序信息。为了学习反编译器 AST 中丰富的结构信息,DIRE 在 AST 上使用了门控图神经网络结构编码器。这需要计算初始节点状态的机制,以及在节点编码中需要考虑 AST 边的设计选择:

a)初始节点状态:节点 vi 的初始状态由三个独立的嵌入向量计算,每个嵌入向量都捕捉了 vi 的不同类型的信息:1)节点语法类型的嵌入。例如,图 3 中 AST 中的根节点的语法类型为 block。2)对于表示数据的节点(例如变量、常量)或对数据的操作(如数学运算符、类型转块换、函数调用),其数据类型的嵌入,通过对其子标记化类型的嵌入求平均来计算。例如,图 3 中的变量节点 VAR1 具有数据类型 char ;它的嵌入是通过求子标记化的类型 char 和的嵌入量的平均值来计算的。3)对于命名节点,节点名称的嵌入(例如,图 3 中的根节点的名称 mystrcopy),通过对其内容子标记化的嵌入求平均来计算。初始状态由三个独立的嵌入向量串联的线性投影得到。对于没有数据类型或名称的节点,我们使用零值向量作为相应的嵌入。

b)图边:我们的结构编码器使用不同类型的边来捕获 AST 中不同类型的信息。除了原始 AST 中简单的父-子边(图 3 中 AST 中的实心箭头)之外,我们还使用额外的边对其进行了扩充:

我们从包含函数名的根节点向每个标识符节点添加一条边。函数名可以通知其主体中标识符的名称。在我们的运行示例中,mystrcopy 函数中定义的两个参数 VAR1 和 VAR2 可能表示副本的源和目标。这种类型的链接(图 3 中的“函数名到参数”)捕获了这些命名依赖关系。

为了捕获相邻代码之间的依赖关系,我们从每个终端节点添加一条边到它的词法后续节点。

为了在所有提到的标识符之间传播信息,我们为每个唯一标识符 vi 添加一个虚拟的“超级节点”,以及从提到的 vi 到超级节点的边。

最后,我们为上面定义的所有边类型添加了一个反向边,对双向信息流进行建模。

c)表示:对于元素表示,我们使用节点 n**i**的 GGNN 的最终状态作为它的表示:

...

递归过程展开 T 次(对于我们所有的实验而言,T=8)。对于每个唯一标识符 vi 的标识符表示,其表示形式 vi 被定义为其超级节点的最终状态,即 vi 的编码。由于超级节点与所有提到的节点都有双向连接,因此它的状态是使用其所有提及的状态来计算的。因此,vi 捕获了 vi 在不同情况下使用的信息。

4.2.3 结合词法编码器和结构编码器的输出

词法编码器和结构编码器分别为每个标识符和代码元素输出一组表示。在编码的最后阶段,我们将这两组输出组合起来。代码元素通过将元素表示的词法集(代码标记)和结构集(AST 节点)联合起来,作为每个输入代码元素的最终编码;标识符通过使用线性转换作为其表示形式,合并每个标识符 v 的词法和结构表示来组合。

4.3 解码器网络

解码器网络使用编码器给出的表示来预测标识符的名称。如图 3 所示,解码器基于标识符的表示和编码元素的上下文信息预测名称。具体来说,与编码器一样,我们假设标识符名称由一系列子标记组成(例如,destAddr→dset,ADDr)。

解码器将预测惯用名称的任务分解为一系列时间索引决策,在每个时间步骤中,它预测标识符惯用名称中的子标记。例如,VAR1 和 destAddr 的惯用名称在三个时间步骤中(s**1 到 s**3)预测,使用子标记 dest、Addr 和(是表示预测过程结束的特殊标记)。一旦生成了一个完整的标识符名称,解码器在对 AST 进行预排序遍历之后继续预测其他名称。并不是反编译代码中的所有标识符都将被标记为相应的“标准答案”惯用名称,因为反编译器经常生成原始代码中不存在的变量。因此,DIRE 允许通过预测一个特殊的标记来保留标识符的反编译器分配的名称。因此,在生成子标记 y**t**时,生成名称的概率因式分解为每个局部决策的概率乘积:

...

其中 X 为输入代码,Y 为所有标识符的子标记的完整序列,y < t 为时间步长 t 前的子标记序列。

我们使用 LSMT 对 p(y**t**| y

...

)

其中[ : ]表示向量连接。解码器的输入包括两个表示:先前预测名称的嵌入向量y*t-1;编码器要预测的当前标识符表示v*t。

我们的解码器还使用注意力来计算上下文向量c*t,它是由相关代码元素的表示聚合上下文信息生成的。c*t 通过对当前每个子标记化名称 y**t**的 AST 节点和表面代码标记编码的加权平均值来计算的。然后,使用上下文向量更新解码器的隐藏状态,将上下文信息合并到解码器的状态:

...

其中 W 是权重矩阵。然后,生成子标记(y**t**)的概率为:

...

4.4 训练神经网络

由于 DIRE 是由神经网络构造的,因此需要训练数据来学习每个神经元的权值。我们的训练语料库是一个集合 D={},由代码 X 和子标记序列 Y 组成,表示解码器预测的标识符名称序列,对于每个训练示例 X,通过最大化预测黄金子标记序列 Y**i 的对数似然来优化 DIRE:

...

5 训练数据的生成

训练 DIRE 需要大量的注释数据。幸运的是,可以从现有 C 语言源代码的大型存储库开始自动创建这个语料库。在较高的层次上,我们语料库中的每个条目都对应一个源代码函数,并且包含训练模型所需的信息。

...

图 4 训练语料库的一个条目

图 4 展示了训练语料库中的一个条目。每个条目包含三个元素:(a)标记化的代码,变量替换为在函数中唯一标识该变量的 ID;(b)将反编译器的 AST 修改为包含相同的唯一变量 ID;(c)将变量 ID 映射到反编译器和开发人员分配的名称的查找表。为每个变量指定一个唯一的变量名,以消除任何隐藏变量定义的歧义,这一点很重要。标记化的代码和 AST 表示在模型的输入和输出中都使用。输入表示使用反编译器分配的名称,而输出使用开发人员指定的名称。

生成占位符和反编译器选择的名称相对简单。首先,二进制文件被正常编译并传递给反编译器。接下来,对于每个函数,我们遍历其 AST 并用唯一的占位符标记替换每个变量引用。最后,我们指示反编译器从修改的 AST 中生成反编译的 C 代码,并对输出进行标记。因此,我们有标记化的代码、AST 和一个将变量 id 映射到反编译器选择的名称表。剩下的步骤,将开发人员选择的名称映射到变量 id,是自动生成语料库的核心挑战。按照我们之前的方法,当二进制文件中存在 DWARF 调试符号时,我们利用反编译器的功能将开发人员选择的标识符名称合并到反编译代码中。然而,仅凭这一点还不足以确定哪个开发人员选择的名称映射到第一步中生成的特定变量 ID。

具体来说是,由于反编译器使用调试信息以各种方式(例如改进类型信息)来丰富反编译器的输出,因此出现了一些挑战。反编译器经常在语义相同的结构之间做出选择:添加调试信息可以改变使用的结构。不幸的是,这意味着使用和不使用调试符号生成的代码之间的区别并不总是在于文件重命名。

...

图 5 显示了图 1 的代码通过反编译器生成的 AST

在实践中,代码的格式和结构在两种情况下可能有很大的不同。图 5 展示了一个示例。在这个例子中,反编译器的第一个过程在没有调试信息的情况下运行,并且反编译器为一个 while 循环生成一个 AST,其中包含两个自动生成的变量名为 v1 和 v2。接下来,向反编译器传递 DWARF 调试符号并再次运行,在右侧生成 AST。虽然反编译器能够使用开发人员选择的变量名 i 和 z,但它会生成一个与 for 循环对应的 AST。

另一个挑战是,反编译器生成的变量通常比在原始代码中使用的要多。例如,return (x + 5);通常被反编译为 int v1;v1 = x + 5;return v1;。反编译代码引入了一个临时变量 v1,它与原始源代码中的任何变量都不对应。在这种情况下,没有开发人员为 v1 分配的名称,因为它在原始代码中不存在。使用调试信息可以改变生成的额外变量的数量。

总之,为了生成我们的语料库,我们:1)反编译包含调试信息的二进制文件。2)收集每个函数中每个变量的签名以及相应的开发人员指定的名称。3) 剥离调试信息并反编译剥离后的二进制文件。4) 通过变量的签名来识别变量,并在 AST 中重命名它们,同时对反编译器和开发人员分配的名称进行编码。5) 从更新的 AST 生成反编译代码。6)处理更新的 AST 和生成的代码,以创建一个语料库条目。最终输出是一个 per-binary 文件,包含每个函数的 AST 和反编译代码以及相应的变量重命名。

6 评估

我们提出了以下的研究问题: RQ1:在反编译代码中给变量赋名的效果如何。

RQ2:DIRE 的每一个组成部分对它最终效果的贡献。

RQ3:数据的来源和数量如何影响 DIRE 的有效性。

RQ4:DIRE 是否比之前的方法更有效。

6.1 RQ1:总体效果

表 1 DIRE 性能评估

...

实验结果在表 1 中进行了总结。“Overall”行显示了我们的技术在整个测试集上的性能,最左边的列显示了 DIRE 的准确性。由此可以看出,在反编译代码中,DIRE 可以恢复原来变量名的 74.3%,说明它可以有效地为反编译代码中的标识符分配有意义的名称。

...

图 6 反编译函数(简化表示)、DIRE 的变量名和开发人员分配的名称

图 6 显示了一个由 DIRE 生成的重命名示例。在这里,DIRE 生成了表中“DIRE”列中显示的变量名。开发人员选择的名称显示在“Dev”列中,DIRE 建议的三个名字中有两个与开发人员选择的名字完全匹配。虽然 DIRE 建议用 buf 代替 ret 作为 V3 的替代品,但是这个名称并不完全是误导: mmap 返回一个指针,指向一个可以写入或读取的内存映射区域。

RQ1 回答:我们发现,DIRE 建议的变量名与原来开发人员选择的变量名相同的概率为 74.3%。

6.2 RQ2:组件贡献

表 1 还显示了仅使用词法或结构编码器的模型的结果。我们发现,词法编码的预测准确率为 72.9%,结构编码的预测准确率为 64.6%。这些更简单的模型仍然运行良好,但通过将它们组合在一起,我们能够实现更好的性能。

...

图 7 DRIE 结合词法模型和结构模型的例子

图 7 说明了 DIRE 如何有效地结合这些模型来改进建议。占位符 V1、V2 和 V3 是应该分配名称的变量。“Lexical”、“Structural”和“DIRE”列显示来自每个模型的预测,“Developer”列显示最初由开发人员指定的名称。这个示例显示了子模型的贡献。例如,对于 V1,词法模型预测变量名为 file,而结构模型预测变量名为 fname。DIRE 结合它们,预测了与开发人员选择相同的变量名。对于 V2,词法模型和结构模型都不能预测出变量名为 mode,但是请注意,词法模型可以预测 V3 的变量名为 mode。通过对模型的组合,DIRE 生成了对 V2 的正确预测。

RQ2 回答:DIRE 中的每一个组件都对其整体准确度做出了独特的贡献。

6.3 RQ3:数据的影响

为了回答 RQ3,我们改变了训练数据的大小,并测量了模型性能的变化。训练数据以 1%、3%、10%、20%和 40%的比率二次抽样。实验的结果如图 8 所示。

...

图 8 训练语料库大小对 DIRE 性能的影响

图 8a 和图 8b 分别显示了 DIRE 的精度和 CER 的变化。训练数据的大小显示在 X 轴上,而精度和 CER 显示在 Y 轴上。

在低采样率下,DIRE 的 CER 仍然很高。这意味着在 DIRE 选择错误变量名的情况下,所选的名称与正确的名称完全不同。较高的采样率可以显着降低 CER,从而允许使用更接近开发人员选择的名称。在 40%的采样率下,DREE 与在全训练集上训练的模型的性能相当接近,总体准确率为 68.2%(对比 74.2%),CER 为 33.6%(对比 28.2%)。

图 8c 显示了训练集大小对 DIRE 和它的组件神经模型在非训练测试集上的性能的影响。请注意,在采样率为 10%或低于 10%时,这些模型具有相似的性能。在训练数据较少的情况下,只使用两个子模型中的一个子模型可以进一步缩短训练时间。

RQ3 回答:DIRE 是数据高效型的,仅使用 40%的训练数据就具有竞争力。DIRE 也是健壮的,在大多数二次采样的情况下,它的表现优于词法和结构模型。

6.4 RQ4:与先前工作的比较

在我们之前的工作中,我们使用了基于统计机器翻译(SMT)的纯词法模型,我们能够准确地恢复开发人员选择的原始变量名的 12.7%。相比之下,DIRE 在 74.3%的情况下能够预测相同的变量名。我们将这一改进归因于两个因素:1)语料库生成技术的准确性提高;2)使用了一个同时包含词汇和结构信息的模型。

RQ4 回答:与其他最先进的方法相比,DIRE 是一种更精确、伸缩性更强的变量名选择技术。

7 总结

众所周知,语义上有意义的变量名可以提高代码的可理解性,但反编译器通常无法恢复它们。在本文中,我们提出了反编译标识符重命名引擎(DIRE),这是一种新的概率性的变量名恢复技术,它使用了词法和结构信息。我们还提出了一种适合训练语言的语料库生成技术,利用该技术从 164,632 个惟一的 x86- 64 二进制文件中生成一个语料库。我们的实验表明,DIRE 能够预测与原始源代码中使用的名称相同的变量名称的概率高达 74.3%。

致谢

本文由南京大学软件学院 2020 级硕士吴青衡翻译转述。