论文名称 | 发表时间 | 发表期刊 | 期刊等级 | 研究单位 |
BinaryAI: Binary Software Composition Analysis via Intelligent Binary Source Code Matching | 2024年 | ICSE | CCF A | 南方科技大学 |
1. 引言
软件成分分析(SCA):识别软件工件中包含的开源组件(重用的第三方库,即 TPL),以实现经济高效的开发。基于 SCA 结果,开发人员可以轻松跟踪 TPL 对软件工件引入的安全威胁,例如漏洞传播和许可证违规。考虑到目标软件项目和已识别的 TPL 的不同形式,现有的 SCA 技术分为多个类别(例如,二进制到二进制的 SCA 和二进制到源的 SCA )。
二进制到源 SCA 技术通过基于提取的二进制文件和大规模收集的TPL数据集来测量二进制文件和大规模收集的TPL数据集之间的相似性,将源代码项目识别为目标二进制文件中包含的重用 TPL。本文主要研究二进制到源(B2S)的 SCA
现存问题 :现有的二进制到源 SCA 技术利用编译后保持一致的基本语法特征(例如字符串文字)来执行二进制源代码匹配。虽然基本特征可以用来建立二进制和源代码之间的对应关系,但是现有的依赖于基本特征的二进制到源代码的SCA技术仍然存在两个局限性。
- 这些基本特征通常在大规模 TPL 数据集中表现出很大程度的冗余。这样的问题会导致收集的 TPL 之间出现特征重复,从而进一步限制 SCA 的精度,即在特征匹配过程中不可避免地会出现误报。误报问题
- 二进制代码与源代码有很大不同,并且很少有功能在编译后保持一致。当应用传统技术来匹配基本特征时,这种差异甚至会增加漏报。漏报问题
因此,有必要采用更细粒度的特征,例如与基本句法特征相比,函数级特征通常包含更多高级句法和语义信息,以提高 B2S 匹配的准确性。
研究内容:论文提出一种具有智能函数级二进制源代码匹配的二进制到源 SCA 技术,BinaryAI。考虑到编译带来的差异,论文采用基于 Transformer 的模型来捕获基于 token 的语法特征并生成函数嵌入来计算二进制函数和源函数之间的相似性。
具体来说,BinaryAI 使用 Pythia 套件中的大型语言模型作为起点,然后使用对比学习以监督方式预训练模型。基于训练好的模型,来自大规模 TPL 数据集的所有源函数的嵌入都会离线生成并存储在语料库(即向量数据库)中。对于目标二进制文件的在线 SCA 检测,BinaryAI 执行反编译以提取二进制函数,并将其进一步编码为嵌入作为查询,以从语料库中检索 top-k 相似的源函数。
此外,BinaryAI 在二进制源函数匹配的第二阶段采用局部驱动匹配。具体来说,我们利用链接时局部性和函数调用图作为附加结构化信息来捕获语义特征并从前 k 个相似函数中识别出完全匹配的源函数。最终,BinaryAI 利用匹配的源函数来计算重用函数的比率,作为收集的 TPL 与目标二进制文件之间的相似度得分,进一步识别相似度超过预定义阈值的组件以及潜在的安全风险。
2. 背景
2.1 软件成分分析
软件成分分析主要可以分为二进制到二进制(B2B)的 SCA 与 二进制到源码(B2S)的 SCA。
B2S:TPL 数据集由大规模爬取的开源 C/C++ 项目组成,其中大部分是来自 GNU/Linux 社区的 GitHub 存储库和源代码包。 通过匹配从 C/C++ 存储库中提取的源代码特征,二进制到源 SCA 可以识别目标二进制文件中重用的源代码级 TPL。 具体来说,二进制到源 SCA 的基本步骤是二进制源代码匹配,它将二进制代码映射到相应的源代码。 B2SFinder 是最先进的工具,它选择编译后仍然保持一致的基本语法特征(例如字符串文字)来匹配源代码和开源组件。 除了二进制SCA之外,二进制源代码匹配在软件安全的其他场景中也至关重要,例如逆向工程和恶意软件分析。据我们所知,由于二进制和源代码之间存在巨大差异,现有二进制源代码匹配的有效性通常会受到影响。
B2B:TPL 以从源包构建的二进制文件。通过利用公开可用的包管理器(例如 Nix),可以跨不同版本、架构和优化级别自动编译源包。许多现有的二进制到二进制 SCA 技术集成了先进的基于嵌入的方法来检测二进制文件之间的代码相似性,并根据 SCA 数据库进一步识别重用的库。具体来说,他们利用深度神经网络模型将二进制函数嵌入到向量的表示中,并通过测量函数嵌入之间的相似性来执行二进制代码克隆检测。除了基本的语法特征之外,这些技术通常还捕获每个二进制函数的控制流图 (CFG) 等语义特征,以增强代码克隆检测和下游 SCA 任务的准确性。
2.2 研究动机
二进制到二进制 SCA 可能会因 TPL 数据集的可扩展性差而受到损害。 特别是,由于与自动编译相关的复杂性,只有包管理器维护的源包的有限子集可以自动编译为多个版本的二进制文件并合并到SCA数据库中。大量的开源 C/C++ 项目(例如 GitHub 存储库)很难包含在 TPL 数据集中,这受到手动编译的大量开销的阻碍。 例如,最先进的技术 ModX 从 Nix 维护的总共 795 个 TPL 中选择 100 个最常重用的 TPL,以构建二进制文件作为数据库。 然而,现有最大的二进制到源 SCA 数据集中大约有 10K 个 TPL(与 ModX 相比约为 100 倍)。 请注意,TPL 数据集的有限规模可能会显着抑制 SCA 的实用性,因为可能无法识别所包含的 TPL 和相应的漏洞。B2B 数据集规模受限,存在实用性方面的问题
现有的二进制到源 SCA 工具利用基本语法特征(例如字符串文字)来建立二进制代码和 TPL 源代码之间的对应关系,这可能无法很好地推广到所有场景。 首先,这些基本特征在大规模 TPL 数据集中往往表现出很大程度的冗余。 例如,字符串“407 Proxy Authentication Need”(表示常见的 HTTP 错误)在我们收集的数据集中的 50 多个 TPL 中重复。 冗余句法特征的存在降低了其独特性和有效性,从而不可避免地产生误报,从而降低了 SCA 的精度。 此外,人们普遍观察到,重用的 TPL 和目标二进制文件之间很少甚至不存在共同的语法特征,尤其是那些被剥夺了字符串文字和导出函数名称等独特特征的二进制文件。 同时,现有的从 C/C++ 源代码中提取字符串的技术本质上并不稳健,例如,通过连接宏定义字符串和常量字符串生成的字符串丢失,从而与从相应 TPL 中的二进制文件中提取的字符串文字不匹配,从而导致召回 二进制到源 SCA 的功能也可能受到损害。 因此,有必要在二进制到源的SCA中采用细粒度的特征(例如功能级特征),以便可以处理高级语义信息,以减轻基本特征的冗余和不可靠性问题。
3. 研究方法
BinaryAI 的工作流程包括:特征提取(第3.1节)、基于嵌入的函数检索(第3.2节)、局部性驱动的匹配(第3.3节)以及第三方库检测(第3.4节),如下图所示。 具体来说,BinaryAI 是通过从 TPL 数据集中的大量存储库中提取 C/C++ 源代码函数以及通过反编译从目标二进制文件中提取类似 C 的伪代码函数来初始化的(标记为❶)。因此,BinaryAI 采用大型语言模型来生成源函数和二进制函数的嵌入。请注意,BinaryAI 中的二进制源代码匹配不是端到端过程,而是分为两个不同的阶段。 具体来说,BinaryAI 首先训练一个基于 Transformer 的模型来学习基于 token 的语法特征,并从每个查询二元函数的语料库中检索前 k 个最相似的源函数 (❷)。 接下来,BinaryAI 利用额外的结构化表示(例如,链接时间局部性)来捕获语义特征并匹配前 k 个候选者中的确切源函数 (❸)。 最终,当公共源函数与目标二进制文件的对应比例超过预先定义的阈值时,BinaryAI 会识别出重用的 TPL 组件(❹)。
3.1 特征提取
论文分别从 TPL 数据集和目标二进制文件中提取具有其他元信息的函数,为 BinaryAI 的后续阶段做好准备。具体来说,论文用源函数和二元函数来表征特征。
源函数:TPL 数据集中的所有开源项目都使用 git 作为版本控制系统。 对于每个项目,论文收集所有版本的 C/C++ 源文件(即 git 标签),并通过其内容的哈希值来区分每个文件。 然后,论文应用开源源代码解析器 tree-sitter,通过其内置的 C/C++ 语言解析器构建文件的抽象语法树,并提取所有独特的源函数。 同时,论文维护两个倒排索引将提取的源函数的对应关系存储到 SCA 数据库中,其中一个索引将每个源函数映射到包含它的所有文件,另一个索引将每个源函数映射到包含它的所有TPL。
二进制函数:论文利用国家安全局(NSA)开发的开源逆向工程框架 Ghidra 来分析二进制文件,其中涉及反汇编二进制代码并识别函数、数据结构和其他相关信息。 随后,Ghidra 执行反编译以生成函数的类 C 伪代码表示(即二进制函数)。 此外,论文利用 Ghidra 提取相对虚拟地址(表示为 bin_rva)作为表示二进制文件中链接时局部性的序号,以及作为函数间通信的函数调用图。 请注意,论文设计 BinaryAI 时假设输入的二进制文件已被剥离,即消除了所有调试和符号信息,这在现实场景中很常见。
3.2 基于嵌入的函数检索
BinaryAI的核心是基于函数嵌入来执行函数级 B2S 匹配。 特别是,论文的目标是训练一个模型,该模型可以在单个向量空间中学习二进制和源函数的有意义的向量表示,其中相似的二进制到源函数对预计会保持接近,而不同的则相距很远。 这样,可以使用它们相应的嵌入来计算它们的相似度。 典型的代码表示学习仅允许匹配对象的一种代码格式,即源到源或二进制到二进制代码匹配。 然而,对于二进制源代码匹配,C/C++语言特性(例如函数内联[23])和编译器优化(例如代码移动[30])可能会导致二进制代码和源代码之间存在实质性差异,例如 在设计 BinaryAI 时,差异可能相当具有挑战性。 为了填补这一空白,理想的模型需要准确捕获微妙的语法特征,以生成用于测量相似性的代码嵌入。 值得注意的是,现有的大型语言模型在学习自然语言的语法方面非常有效,并且这种能力也扩展到代码语言。特别是,用多种编程语言训练的模型可以潜在地识别不同代码格式中类似的基于标记的特征。 这可以帮助检测代码克隆,即使代码已被翻译成不同的语言。 为此,论文使用现有的大型语言模型作为基础模型,并使用标记的二进制源函数对的语料库进一步预训练模型以构建我们的模型。 具体来说,论文以监督方式应用对比学习方法来训练充当函数编码器的模型来生成嵌入。 这使模型能够学习二进制函数和源函数的代码表示,从而最小化相似正样本之间的距离,同时远离不相似的负样本。 在本文中,我们采用研究界广泛采用的 Pythia 作为训练模型的基础模型。 如前所述,我们使用 Pythia 套件中的 410M 参数初始化原始模型,然后使用对比学习进一步执行预训练。
请注意,作为对比学习的关键要素之一,扩大批内负样本可以有效帮助模型学习更多判别性表示,因为它需要区分每批中大量的正样本和负样本,从而导致更好的表示学习并提高下游任务的性能(即二进制 SCA)。 为此,论文利用 CLIP(对比语言图像预训练)的损失函数,该函数最初旨在对齐图像和文本标题的表示,作为我们的对比训练目标。 上图展示了基于 CLIP 对比学习方法的训练过程。 我们首先执行标记化,将函数转换为标记序列。 随后,我们将标记化输入传递给 Pythia 模型,通过提取最后一个隐藏层的输出来获得函数嵌入,其中我们将 (𝑒𝑏 𝑖 , 𝑒𝑠 𝑖 ) 表示为 𝑖𝑡ℎ 正样本的二进制和源函数嵌入。 因此,如果 𝑖 不等于 𝑗,则 (𝑒𝑏 𝑖 , 𝑒𝑏 𝑗 ) 表示一个负样本。 对于对比训练过程,一批由 𝑁 二进制到源对组成,CLIP 计算所有可能对之间的余弦相似度矩阵。 训练目标是通过矩阵上的对称交叉熵损失最大化 𝑁 正样本之间的相似性,同时最小化其余 𝑁 ∗ (𝑁 −1) 负样本的相似性。 下式给出了二进制到源损失函数 𝐿𝑏𝑖𝑛 和源到二进制损失函数 𝐿𝑠𝑟𝑐,其中 𝜏 是用于缩放 logits 的可学习参数。 请注意,这两个损失函数通过在计算相似度时交换二进制和源函数嵌入而有所不同。 因此,整体损失函数𝐿𝐶𝐿𝐼𝑃是𝐿𝑏𝑖𝑛和𝐿𝑠𝑟𝑐的平均值,表示为方程2。此外,我们将动量对比(MoCo)方法扩展到我们的对比预训练中,这进一步增加了负样本的数量和 通过为 CLIP 构建动态词典,实现更有效的对比学习。
我们在 BinaryAI 中部署经过训练的模型,首先为 SCA 数据库中的所有源函数(来自 12,013 个 TPL 的总共 56,342,179 个独特函数)离线导出函数嵌入,并将源函数嵌入作为语料库存储到向量数据库中。 然后,对于在线二进制到源 SCA 任务,我们从目标二进制文件中提取二进制函数,并执行二进制函数嵌入的实时推导。 这些派生的嵌入用作查询,从语料库中检索给定二进制函数的相似源函数。 图 3 显示了通过二元函数查询检索到的 top-1 最相似源函数。 这实际上是一个正样本,相似度为0.982。 最终,我们应用相对虚拟地址(表示为bin_rva)作为二进制函数的标识符,并获得它们的top-k最相似的源函数作为基于嵌入的函数检索的输出。 请注意,我们通过访问第 3.1 节中描述的倒排索引来附加包含相应函数的所有源文件(即 src_func ⇒ src_files)。
3.3 局部性驱动的匹配
理想情况下,我们可以直接选择在函数嵌入方面与二元函数相似度最高的源函数(即基于嵌入的函数检索的top-1)作为我们的匹配结果。 然而,由于不同版本中源函数的细微修改,在大型 TPL 源代码库中大量存在类似的函数。 因此,仅依靠语言模型生成的函数嵌入来捕获基于标记的句法特征不足以准确匹配源函数,因为检索到的 top-k 源函数可能非常相似。 为了解决这个问题,我们尝试利用链接时局部性[15](即第3.1节中描述的相对虚拟地址)和函数调用图作为代表结构化语义特征的补充函数间通信,这可以帮助进一步识别积极的 在二进制源代码匹配的第二阶段中从前k个相似源函数中采样。 在本节中,我们将介绍局部驱动匹配的基本原理和工作流程。
对于用于构建二进制文件的传统 C/C++ 工具链,源代码文件 (file.c) 最初由编译器编译为目标文件 (obj.o)。 随后,链接器解析目标文件之间的符号引用并将它们组合起来生成二进制文件,其中合并每个目标文件的代码部分。 通过分析编译过程,我们可以得出几个基本发现。 1) 同一源文件中的所有源函数都被编译到单个目标文件中,尽管它们与源文件的相对位置可能会发生变化。 2) 目标文件连续链接到二进制文件中,并且目标文件代码段内的所有函数(即机器代码格式的二进制函数)保留其相对局部性。 3)由于C/C++模板函数和条件编译,源文件中的一个源函数可以对应目标文件中的多个二进制函数(即从源函数到二进制函数的“1对n”映射)。 受这些发现的启发,我们可以进一步推导出从同一源文件编译的二进制函数的链接时位置在二进制文件中呈现连续。 相应地,给定二进制文件的地址空间,我们可以通过切割包含连续二进制函数的区间来进行逆向工程,恢复目标文件的边界[15],并进一步识别编译到二进制文件中的对应源文件,从而准确匹配 源函数。 为此,我们通过链接时局部性为每个源文件提取连续函数对,作为可以映射回二进制文件的地址空间的函数间隔。 请注意,我们将文件中的孤立函数对视为无效匹配,并在选择连续区间时将其消除。 与其他文件相比,编译成二进制文件的文件应具有较长的连续函数间隔。 因此,我们将文件选择形成为覆盖二进制文件地址空间内问题的区间,并进一步利用函数调用图来促进所选文件内的二进制源函数匹配。
3.4 第三方库检测
BinaryAI 获取匹配的源函数,并进一步对目标二进制文件执行 TPL 检测(即 SCA 任务),如算法 2 所示。基线技术是通过引用 SCA 数据库( 第 3-4 行),其中包含从源函数到 TPL 的对应关系的倒排索引,如第 3.1 节中所述。 然而,注意到一般来说,源函数在大型数据集中的各种 TPL [38、49、57、60] 中被广泛克隆,如果我们保留所有包含的 TPL,这种内部代码克隆 [13] 可能会导致不可避免的误报。 例如,假设二进制文件仅包含 TPL zlib 作为组件,则由于从 zlib 克隆的公共函数,其他重用 zlib 的 TPL 也被识别为组件。 为了缓解这个问题,我们遵循以前的工作[24,61,63]基于TPL依赖关系来过滤TPL,它展示了跨TPL的重用关系,并且只保留重用的TPL。 具体来说,我们利用 TPLite [24],这是一种基于函数诞生时间(即最早发布时间)和分层路径信息的最先进技术,提前生成 TPL 依赖项,作为附加输入 SCA 帮助识别重复使用的 TPL。