2 AI执行两个逆向工程任务

2.1 安全问题

逆向工程(RE)用于从二进制代码中恢复原始源代码的语义信息[16]。由于各种商业软件和恶意软件不提供其源代码,RE在网络安全中有各种应用。例如,为了在仅有二进制程序的情况下部署一些安全策略(例如,CFI [2]),采用RE来揭示二进制文件的层次结构(例如,代码部分、函数、基本块等)。另一个例子是,为了分析勒索软件并找到加密密钥,分析人员需要首先生成(有时是混淆的)勒索软件的流程图。

不幸的是,手动进行RE可能需要分析人员投入大量劳动。人工逆向工程师必须猜测程序的边界,跟踪这些程序中的控制和数据流,推断(经常访问的)数据的语义,并最终将所有这些信息汇总在一起,以开发对检查的可执行程序的程序逻辑的整体理解。因此,采用人工智能和深度学习技术实现更自动化的RE方法对分析人员非常具有吸引力。

在本章中,我们关注两个基本但至关重要的RE任务,它们是一些高级RE任务的基础:

  1. 函数边界识别。给定二进制代码中文本部分的原始字节,函数边界识别旨在检测函数的开始和结束。图2.1a显示了一个函数边界识别问题的示例。标签1和-1分别表示当前字节是函数的开始和结束。函数内的其他字节用0标记。Shin等人设计了一个模型[67],通过循环神经网络来识别函数边界。

  2. 函数签名生成。给定函数的二进制代码,函数签名生成是预测返回值的类型、参数的数量以及每个参数的类型。图2.1b显示了一个函数签名生成问题的示例。[16]采用了基于深度学习的方案来学习函数类型信息。

尽管这两个RE任务乍一看似乎很简单。但要通过传统的机器学习或数据挖掘方法实现高准确度并不容易。这主要是因为编译器可能以各种难以预测的模式在后端发出二进制代码的原因。

2.2 相关工作

研究人员已经开发了一些逆向工程(RE)工具,以减轻人工分析师的负担。例如,Hex-Rays开发的IDA [24]广泛被安全分析师使用,帮助分析恶意软件。

这些工具是基于领域知识开发的,即代码生成规范,用于进行反汇编和分析。然而,许多因素可能影响生成的二进制代码,例如使用的编程语言、编译器及其选项,以及不同的体系结构和操作系统。此外,一些商业软件和恶意软件会进行混淆,以隐藏其对逆向工程的逻辑。因此,开发基于规则的方法并非易事,将需要大量的人力。

近年来,逐渐引入了机器学习方法来解决各种逆向工程任务(例如,函数边界识别、函数原型推断等),并显示出巨大潜力。在这些方法中,深度学习取得了令人鼓舞的结果。研究人员观察到使深度学习在逆向工程中有效的两个特征:首先,与传统机器学习相比,深度学习具有强大的表示学习能力,能够发现高维数据中的复杂结构[41, 61]。其次,尽管深度学习通常需要大量数据集来训练高质量模型,但对于大多数逆向工程问题来说,生成大量训练样本并不是一个难题。

2.3 深度学习流程

在本节中,我们将遵循第1章介绍的统一深度学习流程(图1.1),来说明逆向工程的深度学习方法。不同的逆向工程任务通常共享一些共同的特征。因此,在以下各小节中,我们将首先介绍两个逆向工程任务共享的共同特征。在介绍了共同特征之后,我们将介绍每个逆向工程任务的独特特征。

2.3.1 原始数据

逆向工程通常始于剥离的二进制文件(即没有任何调试信息的二进制文件),其中包含指令和数据。收集原始数据并不具有挑战性,因为广泛可获得二进制文件和源代码(可以轻松编译为二进制文件)。在大多数情况下,研究人员会从一些实用程序(例如binutils和coreutils)和开源项目(例如编译器和GNU项目)中收集二进制文件。

例如,[67]采用了一个数据集,其中包含来自三个热门Linux软件包(binutils、coreutils和findutils)的二进制文件。该数据集包括2000个不同的二进制文件,这些文件是使用4个优化级别(O0-O3)和两个编译器(gcc和icc),针对两个指令集(x86和x64),从软件包中的程序编译而成的。对于x86二进制文件,有1,237,798个不同的指令,构成了274,285个函数。同样对于x64,有1,402,220个不同的指令,构成了274,288个函数。

[16]扩展了第一个数据集,添加了5个更多的软件包,总共有8个软件包:binutils、coreutils、findutils、sg3utils、utillinux、inetutils、diffutils和usbutils。从二进制文件数量上来看,这个数据集扩展到了5168个不同的二进制文件。对于x86二进制文件,有1,598,937个不同的指令,构成了370,317个函数;而对于x64,总共有1,907,694个不同的指令,构成了370,145个函数。这些数据集的位置将在第2.8节中列出。

函数边界识别。对于函数边界识别,原始数据是等长度的字节序列,通过在二进制文件的文本部分使用帧窗口进行切割而生成。短序列通过填充(通常用零填充)扩展到所需长度。

函数签名生成。函数签名生成假设已知函数的边界,因此原始数据是函数中的所有指令的字节。

2.3.2 数据注释

大多数逆向工程任务可以从源代码中获取它们的实况。也就是说,在训练模型时,假定源代码是可用的,然后直接使用训练好的模型来处理二进制,而无需任何源代码的辅助。在逆向工程任务中,如果源代码可用,可以轻松获取所需的高级信息,并且将此信息从源代码传递到二进制并不是非常具有挑战性(例如,通过解析DWARF调试信息 [71])。

函数边界识别问题的标签。对于每个字节序列,有一个标签序列来指示相应的字节是否是函数的起始。

函数签名生成问题的标签。函数签名生成问题是一系列通常通过多个模型解决的小任务。因此,对于每个函数有三种标签(参数数量、每个参数的类型和返回值的类型)。

2.3.3 数据处理

数据处理从二进制文件中提取指令序列。有几个工具(如objdump、readelf、pyelftools)可以帮助实现这个目标。对提取的指令序列进行进一步处理取决于采用的嵌入方法。

2.3.3.1 嵌入方法

当将深度学习应用于这些逆向工程任务时,首要问题是:应该采用什么样的编码方法来表示数据?鉴于指令序列是原始数据,有必要将指令编码成深度学习友好的数据结构,以便深度学习算法能够将其作为输入并学习重要特征。

传统上,二进制指令可以以两种形式表示:机器码和汇编码。机器码是CPU可执行的指令表示,而汇编码是指令的字符串表示,更容易为人类理解。以下示例显示了一条 sub 指令的机器码和汇编码。

83 ec 0c sub $0xc, %esp

正如[44]总结的,最近研究人员尝试了许多不同的嵌入方法,可以分为三类:1)直接采用原始字节作为输入嵌入,2)从汇编码中选择编码特征,或3)通过一些表示学习模型自动学习为每个指令生成矢量表示。我们将在本节中简要介绍它们,并展示它们的优缺点。

原始字节编码:最简单的编码方法之一是对每条指令的机器码应用简单的编码,然后将编码后的指令(序列)输入深度神经网络。例如,DeepVSA [36] 采用的一热编码可以将每个机器码字节转换为一个256维的向量。向量中的一个维度为1,其余维度均为0。与一热向量不同,Shin等人 [67] 直接采用指令机器码的二进制编码,即将每个字节编码为一个8维向量,其中每个维度表示该字节的二进制表示中的相应位。图2.2显示了如何通过这种方法对上述指令的原始字节进行编码。代码2.1显示了原始字节编码的代码片段。

总体而言,原始字节编码是最直接和简单的方法,不需要进行反汇编和其他计算昂贵的过程。另一方面,其缺点是每个指令的语义意义隐藏在原始字节编码中。例如,X86指令集体系结构(ISA)采用可变长度编码,这意味着通常指令的编码较短,因此它们在指令缓存中需要更少的空间;而较不常见的指令的编码较长。因此,不同指令中的字节所代表的语义含义是不同的,这对深度神经网络理解指令构成了很大的挑战。因此,即使原始字节包含与反汇编代码一样多的信息,值得质疑神经网络是否能够有效地从这样的原始字节编码中学习语义意义。

反汇编指令的编码:由于汇编代码以更易理解的方式携带指令的语义意义,一些研究人员选择并从汇编代码中编码特征。例如,[45]采用从汇编代码中提取的操作码,并通过一热编码进行编码,但该方法完全忽略了操作数的信息。[42]提出的Instruction2Vec采用了来自指令的操作码和操作数,并将每个指令编码为一个九维特征向量。

指令的表示学习:另一种方法是通过表示学习学习一个指令嵌入。例如,Asm2Vec [21] 利用PV-DM神经网络 [40] 共同学习指令嵌入和函数嵌入。具体而言,该模型学习标记之间的潜在词法语义,并将汇编函数表示为集体语义的内部加权混合。该学习过程不需要有关汇编代码的任何先验知识,例如编译器的优化设置或汇编函数的正确映射 [21]。实验证明,与前述两种嵌入方法相比,Asm2Vec对于代码混淆和编译器优化更具鲁棒性。

用于指令嵌入的预训练模型:尽管指令表示学习的最近进展令人鼓舞,但仍然存在一些学习良好表示的挑战。正如[44]中提到的,指令的复杂内部格式不容易学习。例如,在x86汇编代码中,操作数的数量可以从零到三不等;一个操作数可以是CPU寄存器、内存位置的表达式、即时常量或字符串符号;一些指令甚至具有隐含的操作数等。

其次,一些隐藏在指令序列中的高级语义(例如数据流)在没有一些特定设计的深度学习模型的情况下很难被捕捉。因此,提出了具有特定的预训练任务和大型数据集以揭示复杂的内部结构和高级语义的预训练模型(例如[44]和[38])。这样一个训练良好的模型可以应用于许多下游任务并取得良好的结果。

2.3.3.2 指导选择嵌入方法的原则

正如[44]所述,“与前两个选择相比,自动学习指令级表示有两个原因更具吸引力:1)它避免了手动设计的努力,这需要专业知识并可能繁琐且容易出错;2)它可以学习更高级的特征而不仅仅是纯句法特征,从而为下游任务提供更好的支持。”

尽管自动学习指令表示的优点很多,我们观察到两个缺点:a)需要更大的训练集来自动学习指令的良好表示。b)为二进制代码学习表示将消耗更多计算资源。因此,提出了预训练模型来缓解这两个限制。首先,即使对于特定任务收集带标签的大型训练集是一个常见挑战,但生成没有任何标签的大型数据集并非不可能,因为互联网上有大量的程序可用。通过精心设计预训练任务(例如,掩码语言模型),可以纯粹通过大型无标签数据集对其进行良好的预训练。其次,经过良好训练的预训练模型可以被许多下游任务复用。这意味着其他研究人员可以直接采用预训练模型来生成指令表示。他们只需微调一些层次(例如,输出层)以获得用于特定任务的模型,这将需要相当小的资源量。

总之,首先,如果有可用的话,预训练模型是最佳选择。其次,当没有合适的预训练模型可用且用户有足够的训练数据和计算资源时,他们可以采用表示学习来学习指令嵌入。最后,如果没有大量的训练数据或计算资源,那么对已反汇编指令进行编码是最佳选择。

2.4 模型架构

大多数逆向工程工作的深度学习模型可以共享学习指令序列表示的相同组件。在本节中,我们首先介绍表示学习的模型架构设计,然后展示特定逆向工程任务的模型架构。

2.4.1 共同组件:学习指令嵌入

深度学习被认为擅长构建模型,能够从高维数据中学习复杂的结构和捕捉底层复杂的特征[41, 61]。然而,最好的情况是开发者能够以数据结构表示数据样本,该数据结构可以直接向深度学习模型展示其内部格式关系。

因此,为了捕捉指令的复杂内部格式和指令内部的操作码和操作数之间的关系,值得采用一种细粒度的策略来将指令分解,正如[44]所提到的。为了实现这个目标,PalmTree [44]将代码序列中的每个指令视为一个句子,并将其分解为基本标记,并采用Transformer [75]学习每个句子的嵌入。代码2.3显示了标记化的代码片段,图2.3左侧显示了标记化的汇编代码示例。将标记序列作为输入,PalmTree采用Transformer学习指令的嵌入。为了使设计的深度神经网络能够理解指令的内部结构和指令之间的依赖关系,PalmTree利用了图2.3中所示的三个预训练任务:

  1. 掩码语言模型(MLM)用于理解指令的内部结构。MLM预测指令内部的掩码标记。

  2. 上下文窗口预测(CWP)用于捕捉指令之间的关系。CWP通过预测控制流中滑动窗口内两个指令的共现来推断单词/指令的语义。

  3. 定义-使用预测(DUP)用于学习指令之间的数据依赖关系(或定义-使用关系)。DUP预测两个指令是否存在定义-使用关系。

代码2.4显示了预训练任务的代码片段。第6行,第3行和第2行分别计算MLM、CWP和DUP的损失。第9行聚合了总损失。

Transformer的编码器生成一系列隐藏状态作为输出,每个隐藏状态对应于输入的一个标记。PalmTree采用第二个最后一层的隐藏状态的平均池化来表示指令表示。然而,其他池化方法,如最大池化和总和池化,也可能实现类似的目的。

注意:预训练任务的目的是通过无监督学习学习指令的良好表示。因此,如果下游任务的标记训练集足够大以学习良好的模型,则预训练任务并非总是必要的。

2.4.2 模型架构用于函数边界识别

在学习了指令嵌入后,解决函数边界识别问题变得非常直观。可以采用简单的输出层(例如,全连接层)来预测每条指令的标签,即该指令是否是函数的开始或结束。

2.4.3 用于函数签名生成的模型架构

PalmTree提出了基于预训练模型的函数签名生成模型,如图2.4所示。首先,该模型利用[44]中显示的预训练模型为目标函数中的指令学习嵌入。其次,采用递归神经网络(RNN)或其变种(LSTM或GRU)来聚合目标函数的指令嵌入。第三,采用输出层来预测目标函数的类型签名。代码2.5展示了模型的实现。

2.5 模型训练中的问题

在模型训练阶段可能会面临几个问题。首先,训练预训练模型通常需要大量的GPU资源,这对大多数用户可能不可承受。

其次,数据集可能极度不平衡。在解决函数边界识别问题时,只有一个函数有数千字节的开始字节和结束字节。在解决函数签名生成问题时,输入参数的数量在候选函数之间分布不均。有两种策略可以解决不平衡数据问题:1)通过对多数类别进行下采样平衡数据集;2)通过采用加权损失函数加强对少数类别的惩罚。

第三,机器学习中广泛注意到精度和召回率之间的权衡,可以通过加权损失函数进行调整。这可以通过在某些特定安全问题(例如二进制重写)上牺牲一些精度来实现较高的召回。

由于PalmTree [44]提供了在函数边界识别问题上的模型性能,表2.1直接复制了原始论文的结果。使用单热嵌入方法进行的函数边界识别的模型性能在[67]中得到评估,这里我们在表2.2中引用了它们的模型性能。

2.7 模型部署

用于逆向工程的模型可以在两种情景下部署:首先,它可以集成到逆向工程平台中,例如IDA Pro或Binary Ninja。然而,由于预训练模型的参数规模通常较大,通常需要大量的计算资源。 因此,模型部署还有另一个可能的情景 - 它可以部署为在线服务。具体而言,一家公司可以将模型与数据处理组件一起部署在云服务器上。用户可以将要分析的二进制文件上传到服务器,服务器将处理这些二进制文件,预测相应数据样本的标签,并将结果发送回用户。

2.8 源代码和数据集

PalmTree [44]的源代码可在https://github.com/palmtreemodel/PalmTree上公开获取。函数类型签名生成的原始实现[16]可在https://github.com/shensq04/EKLAVYA上找到。函数边界识别工作[67]没有公开可用的源代码。 我们在GitHub上实现了2.4.2小节介绍的模型,并发布了它们的源代码:https://github.com/PSUCyberSecurityLab/AIforCybersecurity。 此外,正如2.3.1小节所述,这两篇论文都使用了一些公开可用的数据集来准备它们的训练集。这些包含二进制程序的数据集可以在https://security.ece.cmu.edu/byteweight/上获取。此外,我们在我们的GitHub存储库中准备了一些可用的训练集。

2.9 剩余问题

我们注意到逆向工程中现有深度学习模型存在两个限制。首先,通常在一个平台上训练的模型通常不能推广到来自其他平台的二进制文件,具有不同的编译器工具链,甚至具有不同的编译器选项。尽管可能对每种类型的二进制文件训练一个模型是可能的,但由于平台的大量,这可能需要大量的人力和资源。例如,ARM [50]有10个版本的处理器和超过10个版本的架构。GCC [32]和LLVM [39]都包含4个优化级别。因此,值得进一步研究如何提高模型在进行逆向工程任务时的泛化能力。 其次,尽管现有研究在深度学习辅助的逆向工程中展现了巨大潜力,我们注意到目前的深度学习应用仅能用于进行非常简单的逆向工程任务,对于进行更复杂的逆向工程任务(例如CFG、DFG生成和指针分析)的能力有限。一个主要原因似乎是当前的方案不太能够学习高级语义信息。幸运的是,我们看到越来越多的研究人员开始从二进制代码中学习更高级的语义信息 [78, 79]。 最后,二进制文件包含代码和数据,它们有些被混合在一起(例如,由于冯·诺依曼体系结构的特性,跳转表 [6])。然而,目前逆向工程中深度学习应用主要集中在代码的逆向工程上。因此,如何学习程序的内存布局(例如,数据结构)值得深入研究。在文献中,对学习数据结构和内存布局的研究工作非常有限 [12, 79]。

Last updated