7 AI检测代码相似度

7.1 安全问题

在实际应用中,二进制代码相似性通常用于搜索已知漏洞、检测代码抄袭和分析恶意软件家族。然而,对于二进制代码来说,测量相似性是具有挑战性的。在二进制级别,有几个因素可能影响测量代码相似性的效果,包括编译器的选择、优化级别和处理器架构。这些因素成为泛化代码相似性检测的主要挑战,即跨版本代码相似性检测。例如,一项研究工作[58]基于控制流图(CFG)创建签名,但在不同处理器架构之间可能不一致。

一方面,如果重要特征能够总结成高级抽象表示,那么可以启用跨版本代码相似性检测,从而使基于统计的方法变得非常有效。另一方面,深度学习也可以用来克服跨版本挑战,深度学习已被证明能够提取抽象特征。

本章介绍了基于深度学习的跨版本代码相似性检测方法。问题定义如下:给定两段二进制代码,测量它们以确定它们是否来自相同的源代码。请注意,程序语义的相似性(例如,为相同目的而以不同方式编写的代码)超出了本章的范围。语义克隆通常在源代码级别上检测。

在代码相似性检测中应用深度学习时,通常可以使用两种不同的输入数据表示:专用图表示(例如 Gemini [88])和原始字节序列表示(例如 α-Diff [46])。尽管两种表示都将被介绍,但本章的重点是图表示,因为序列模型类似于其他领域(例如计算机视觉)中的模型,因此在实施过程中需要较少的领域知识。

7.2 原始数据

尽管二进制相似性可以在不同级别(例如函数、循环)进行测量,但最常见的是在函数之间进行比较。因此,原始数据是函数的二进制代码。通常,为了训练目的,需要地面真实标签,但标记代码克隆是一项耗时的任务。因此,在二进制代码相似性检测中,为了启用监督式训练,生成数据集的一种常见方式是使用不同优化级别和不同编译器编译一段源代码。例如,代码7.1显示了用C语言编写的一个非常简单的循环,代码7.2和代码7.3显示了使用GCC 7.5.0在两个不同优化级别下生成的代码。

如图所示,生成的代码非常不同,确定这两个二进制代码片段是否来自相同的源代码并非易事。通过使用不同优化级别和编译器生成代码,可以在最小程度的人力投入下获得带有地面真实标签的训练数据。

总的来说,原始数据是通过使用不同编译器设置编译源代码获得的。编译的程序可以进一步分为函数、基本块和/或循环。在模型训练阶段,可以使用符号表识别函数,在生产阶段,可以使用现有工具(例如IDAPro)识别函数。

7.3 数据处理

本节介绍将原始数据转换为序列表示和图表示的过程。如第7.1节所述,这两种表示都可能很有用。

7.3.1 序列表示

序列表示本质上是使用指令序列的原始字节形成的向量,可以用作深度学习模型的输入。换句话说,基于领域知识的信息抽象将非常少,例如进行流分析和依赖性分析。最直接的方法是使用指令序列的原始字节形成一个向量(类似于子子节2.3.3.1中介绍的原始字节编码)。这种方法的优点显而易见,非常简单直观;但在一些极端情况下,由于样本长度不同导致的填充可能会太长。另一种方法是将序列变为2D,创建一个位图,使整个序列能够分布到2个维度。这种结构的优势是对于一些神经网络,如卷积神经网络(CNN),不仅可以学习相邻字节之间的关系,还可以学习更远的字节之间的关系。图7.3说明了上述两种结构。请注意,没有理论证据表明其中任何一种结构比另一种更好,应根据经验研究选择合适的表示。

原始字节可以自然地映射到十进制整数值,即0到255,以便进行深度学习模型的计算。如果需要填充,填充值可以映射到超出范围的整数。这种映射的缺点是数值特征将传递到深度学习模型,这是不希望的,因为不能断言字节0x10在数值上小于0xff。解决此问题的一种方法是将字节映射为独热向量,但这将大大增加模型的大小。

7.3.2 图表示

大多数传统的代码相似性检测方法通常会首先将程序信息抽象成控制流图(CFG),然后可以为每个基本块汇总一些通常基于领域知识选择的统计信息。随着图神经网络(GNN)的成熟,研究人员开始创建基于CFG的精心设计的图,作为深度学习模型的输入。优势是明显的:与序列表示相比,图结构隐含的信息得以保留,提供了结构信息以及其他信息。

在图表示中,结构信息是本地携带的,而所有其他信息都携带在节点中。标准CFG中的节点是基本块,它是可以按顺序执行而不需要任何跳转的指令序列。如果不加处理,这些指令可能不适合深度学习模型学习。通常,输入图中的节点是具有选定属性的固定长度向量,称为节点特征。节点特征可以是基于领域知识选择的属性,也可以是来自模型的嵌入向量。

让我们以[31]提出的属性控制流图(ACFG)为例,它在Gemini [88]中使用,Gemini是一种基于图神经网络的二进制代码相似性检测方法。ACFG建立在常规CFG之上,其中每个节点包含8个节点特征,包括6个基本块特征和2个图结构特征。基本块特征包括:字符串常量的数量、数字常量的数量、转移指令的数量、调用的数量、指令的数量和算术指令的数量;而图结构特征包括出边的数量和介数。图7.4展示了从标准CFG生成ACFG的过程的示例。如图7.4a所示,图中的节点是基本块,总是以传送指令结束。然后,可以通过简单的程序分析轻松提取上述6个基本块特征,通过图分析计算2个图结构特征。两种特征组合将形成一个包含8个元素的节点特征向量,对于每个节点。

生成的ACFG已准备好输入深度学习模型。ACFG是与架构无关的。换句话说,通过将信息抽象成ACFG,缓解了跨架构代码相似性检测的挑战。然而,缺点是由于基于启发式和领域知识选择的节点特征,可能会忽略一些隐含的模式。

7.4 模型

为了获得量化的相似性分数,这个用例应用深度学习来学习给定输入的嵌入向量。就像其他基于深度学习的相似性检测应用(例如人脸识别),在那里相同的人脸会产生相似的嵌入向量,从相同函数编译的二进制代码也应该产生相似的向量。为了实现这个目标,使用的最受欢迎的模型架构之一是siamese网络[10],最初是为了验证手写签名而提出的。因此,为了使用siamese网络来训练模型,需要有标记的数据。

关于深度学习模型的主体,它主要取决于数据表示。对于序列数据,可以使用卷积神经网络(CNN);而对于图数据,可以使用图神经网络(GNN)。这个用例的重点是图模型,因为CNN主体相对直观。模型架构的图示如图7.5所示,在本节的其余部分将介绍GNN主体和siamese网络。

7.4.1 图神经网络Backbone

这个小节介绍了Gemini [88] 在2017年采用的图嵌入网络。尽管这个模型是几年前提出的,但其思想与现代的空间图神经网络相同:汇总邻居节点的节点特征以创建节点嵌入。当然,在追求更好性能时,可以选择更新的图神经网络主体。

在解释细节之前,让我们先呈现正式的定义。给定一个ACFG G = {N, E},其中N是节点集,E是边集,GNN模型f输出一个嵌入向量y。深度学习模型首先通过对节点及其相邻节点的节点特征xn进行聚合,为每个节点n ∈ N计算节点嵌入utn,进行t个跳数。然后通过聚合所有节点特征来获得整个图的嵌入y。

这是一个乏味的正式定义。简单来说,基本工作流程是首先获取每个节点的嵌入,然后聚合节点嵌入以获取图嵌入。这里的聚合函数可以定制,但最流行的是简单的求和。换句话说,在获取所有节点的节点嵌入之后,图嵌入就是所有节点嵌入的简单逐元素加法。

显然,重要的是如何获取节点嵌入。这里让我们使用Gemini中的节点嵌入计算:

在方程7.1中,W是可训练权重,σ是非线性变换函数,An是节点n的相邻节点集合。非线性变换函数可以是可训练的,因此可以是一个全连接的神经网络。这里非常关键的一个变量是迭代次数t,每次迭代表示对图进行一次跳跃。例如,当t = 1时,节点嵌入汇聚了来自直接邻居的信息;当t = 2时,节点嵌入汇聚了来自直接邻居的邻居的信息。虽然在Gemini论文中没有提到,但现今的研究人员都意识到了过度平滑的问题,因此总跳数不应该太大。

与其展示生成图嵌入的正式算法,不如看一下Gemini作者发布的Python代码1。如图7.6所示,在这个实现中,变量X是包含所有节点的所有节点特征的张量,而所有边信息都存储在msg_mask中,它是一个二进制矩阵(例如,元素为0或1)。

第2行到第5行的代码对应于方程7.1中第一次迭代中产生当前节点嵌入的Wxn部分。然后从第6行开始,每次迭代(即跳跃)的循环开始。请注意,通过将消息掩码张量(msg_mask)乘以节点特征/嵌入张量(cur_msg)来完成消息聚合。消息聚合操作在第8行显示。循环体的其余部分,从第10行到第21行,都很简单,实现了方程7.1的其他部分。最后,在第23行,节点嵌入被加在一起,产生整个图的嵌入,在第24行,使用一个额外的线性层来传播信息。

正如前面所述,GNN主体的最后一层(例如,池化层)将通过聚合节点表示输出图的嵌入。下一节将介绍整个网络结构,从一对相似/不相似的二进制代码中学习。

7.4.2 孪生网络

孪生网络是一种流行的表示学习架构,用于学习嵌入向量,使得相似输入的输出向量之间的距离最小化。为了训练孪生架构,需要一对相似/不相似的数据样本,这与常规的分类和回归任务不同,在训练时只需要地面真值。本节首先解释孪生网络的整体架构,然后介绍训练过程。

如图7.5所示,孪生网络的最重要的思想是简单的:两个共享权重的子网络,每个子网络将在输入对中取一样本。损失函数是子网络输出之间的距离,如果输入是一对相似样本,则应该最小化,如果输入是一对不相似样本,则应该最大化。现代孪生网络使用所谓的对比损失,可以处理相似对和不相似对作为输入。对比损失的一个例子如方程7.2所示,其中D是两个子网络输出的距离,y是一个二进制标量,表示D是相似对还是不相似对的距离,m是不相似对之间的最大距离阈值。

请注意,在方程7.2中,如果y = 1,D来自相似对。距离D通常是欧几里得距离,但如果需要,它可以被任何距离或相似度测量替代。事实上,在原始的Gemini [88] 论文中,作者并没有使用任何对比损失,而只使用余弦相似度作为损失函数。通过最小化L,神经网络不仅可以学习两个样本如何相似,还可以学习它们如何不同。对于不相似的样本,尽管在标签和类别方面存在差异,但它们仍然属于同一领域,因此有必要有一个阈值m来限制距离。

对于孪生网络,有两个要实现的重要元素:损失函数和共享权重的子网络。方程7.2中损失函数的简单实现如代码7.4所示。第3行计算张量x1和x2中输出向量之间的欧几里得距离。第5行是方程7.2的实现。第6行通过对批次中的损失进行求和来计算标量损失值。

子网络的模型参数是共享的,因此在内存中实际上只有一组模型参数。梯度将通过反向传播相对于损失函数进行计算,这需要两个不同输入的两个不同输出。代码7.5显示了从Gemini存储库修改的代码片段。图_embed函数在图7.6中早些时候展示过。正如注意到的那样,由于子网络共享权重,每个权重只有一个副本,所有这些权重都在此代码片段的第5行、第7行到第10行、第12行和第14行定义。对于不熟悉Tensorflow 1.X版本的人来说,tf.placeholder是计算图上的一个节点,可以在运行时保存数据(例如输入数据),而这些变量如X1、msg1_mask将保存来自数据集的输入。在这里,一对中的两个不同样本将被相同地处理,如第20行和第25行所示。在获取了来自不同输入的两个嵌入向量(即embed1和embed2)之后,可以像代码7.4中所示那样获得损失函数。然后可以计算损失函数相对于参数的梯度。

7.5 代码、数据和其他问题

7.5.1 代码和数据资源

在非生产环境中训练和测试Gemini的源代码可以在https://github.com/xiaojunxu/dnn-binary-code-similarity找到。

7.5.2 模型性能

模型性能因实现、模型、调整和数据集而异。与推荐模型类似,代码相似性检测模型的性能通常使用Precision@k和Recall@k作为主要指标。

我们在这里介绍了α-Diff和Gemini的评估。α-Diff [46] 使用序列数据和CNN全面评估模型。平均Recall@1为0.953,Recall@5为0.996,明显优于大多数传统方法。在对coreutils进行跨版本二进制代码相似性比较时,与14年前的旧版本相比,最佳Recall@1为0.738,Recall@5为0.821。Gemini使用图数据结构评估GNN模型。不幸的是,Gemini没有使用Precision@k或Recall@k,但作者提供了ROC-AUC得分为0.971,可以视为ROC@1。

尽管基于报告的数字,图模型的整体性能似乎优于序列模型,但仅仅基于两篇论文报告的数字进行比较是不公平的。原因是α-Diff在程序级别上进行代码相似性检测,而Gemini是在函数级别上进行的。因此,读者应该根据自己的实验和需求选择模型。简而言之,图模型更依赖于程序分析领域知识,但与序列模型相比,更容易扩展和进一步定制。序列模型通常更容易实现和训练速度更快,但由于它采用编码的原始字节作为输入,可定制性较差。

7.5.3 主动学习

正如在第7.4节中提到的,训练孪生网络需要标记的数据。在大多数二进制代码相似性检测场景中,通过使用不同的编译器设置将相同的源代码编译成不同的二进制文件来标记数据。然而,正如Gemini的作者所展示的,通过包含一些在高级语义上相似但在源代码上不同的样本,性能可能会得到提高,因此可能偶尔需要手动标记数据。在这种情况下,由于手动标记数据的成本较高,主动学习变得有用。

在经典的分类任务中,模型输出对数或Softmax分数,即给定输入属于每个类别的概率。换句话说,在分类任务中,输出实质上是模型在进行预测时的置信度。基于这个事实,可以选择那些模型预测不太自信的样本进行标记,丢弃其他样本。通过这种方式,人为标记数据的工作可以更有效率。选择样本进行标记的这些技术是主动学习的一种形式。

然而,对于孪生网络,基于Softmax分数的主动学习技术并不太实用,因为孪生网络输出输入的嵌入,这并不提供有关模型是否在推断样本时有信心的信息。神经网络的一般主动学习的困难之处在于神经网络是黑盒,导致难以解释哪些样本对它们来说很重要。因此,解决这个问题的一种方法是使用另一个子网络来学习网络是否对某些样本感到自信,这在[91]中提出。基本上,可以创建一个神经网络来预测模型的损失值,给定输入数据。如果输入样本的预测损失很高,那么应该对该样本进行标记。

Last updated