论文学习_SemDiff: Binary Similarity Detection by Diffing Key-Semantics Graphs
摘要:二进制相似性检测是一项关键技术,广泛应用于源代码不可用的实际场景中,例如漏洞搜索、恶意软件分析以及代码抄袭检测。然而,现有方法在面对不同编译优化选项、编译器、源代码版本或混淆手段时,往往难以有效识别相似的二进制文件。研究背景
作者发现,尽管不同的优化级别、编译器、源代码版本或混淆策略会显著改变二进制程序的语法和结构,但并不会改变其关键代码行为。基于这一观察,作者提出从二进制中提取关键指令以刻画其核心行为,并通过比较关键指令之间的相似性,有效弥补现有方法在复杂变异场景下的不足。具体而言,每条关键指令被转换为自定义的关键表达式,并基于控制流构建关键语义图,其中节点表示关键指令,属性为对应的表达式。为了量化两个关键语义图之间的相似性,作者首先采用拓扑排序将图序列化为关键表达式序列,再进行分词和拼接生成标记列表,随后计算局部敏感哈希(LSH)值以衡量相似度。评估结果表明,SemDiff 在识别由不同优化选项、编译器和混淆方式生成的二进制程序时,相较现有方法表现更优,同时也适用于库版本搜索和固件中相似漏洞的发现。
1. 引言
二进制代码相似性检测(又称为二进制差分分析)在漏洞搜索、补丁生成与分析、恶意软件检测以及抄袭识别等领域具有重要意义。现有方法主要分为基于机器学习的方法和基于程序分析的方法两大类。
基于机器学习的方法通常通过提取二进制代码中的语法、结构和语义特征来训练相似性检测模型,或借助自然语言处理技术学习语义信息。尽管理论上这些方法具有一定有效性,但其性能高度依赖于训练数据的质量。现实中的二进制程序具有高度多样性,即相同的源代码在不同编译器或优化级别下会生成差异显著的二进制文件,这使得构建具有代表性的训练数据集变得困难。一旦训练数据质量不足,相关方法的效果将受到明显影响。无监督学习尝试通过上下文指令自动学习每条指令的表示,但编译器优化或差异可能会改变函数中的非平凡部分,导致方法准确性下降。此外,虽然部分机器学习方法试图通过预测汇编语法的嵌入向量来保留语义信息,但这些嵌入往往难以准确反映真实语义。基于机器学习的相似性检测高度依赖于训练数据的质量
基于程序分析的方法通常通过静态或动态分析提取二进制程序中的信息,如数据依赖或控制依赖,并基于预定义的规则量化相似性。然而,静态的函数级处理方法通常将基本块或指令序列作为最小相似性单位进行匹配。当对相同源代码使用不同的编译优化时,函数的基本块结构可能会因块切分而发生显著变化,指令也可能因替换而不同。因此,基于基本块或指令级的比较方法难以有效应对这类二进制相似性检测任务,并非理想选择。基于程序分析的方法难以实现有效的相似性比较
存在问题:基于程序分析的二进制代码相似性检测介绍部分,需要对现有工作进行更深入的分析。一方面介绍的工作比较旧,另一方面没有清楚的介绍现有工作如何实现。
为解决上述问题,作者提出并实现了一种新的语义感知方法 SemDiff,用于比较任意二进制代码,无需考虑开发者使用的编译器或优化等级。SemDiff 包含图生成与图对比两个核心模块。在图生成阶段,SemDiff 以一对二进制函数为输入,通过以下步骤为每个函数构建关键语义图:首先识别能够反映函数主要行为的关键指令(如函数调用、赋值操作);然后利用符号执行提取每条指令的符号表达式及其依赖关系;最后将符号表达式转换为自定义的关键表达式,并通过有向边连接所有相关指令。完成两个函数的关键语义图构建后,图对比模块将其作为输入,采用拓扑排序将图序列化为两个关键表达式序列,接着进行分词处理,并基于局部敏感哈希(LSH)计算每个图的哈希值。当两个图的哈希值相近时,说明对应的二进制函数具有较高的相似性。
图生成模块:以一对二进制函数为输入,构建关键语义图。具体步骤包括,(1)关键指令生成;(2)关键表达式提取;(3)关键语义图生成。
图对比模块:使用非机器学习的方式实现二进制代码相似性检测。具体步骤包括,(1)对语义图进行序列化;(2)对序列进行分词;(3)进行基于LSH计算图哈希值。
作者在九个常用库上对 SemDiff 的二进制相似性检测性能进行了评估,包括 openssl、libtomcrypt、coreutils、ImageMagick、libgmp、curl、sqlite3、zlib 和 Puttygen,并将其与五种先进工具进行对比,包括 Asm2Vec、Gemini、Palmtree、JTrans 和 UniASM。评估结果表明,无论是在相同编译器但不同优化级别,还是相同优化级别但不同编译器的条件下,SemDiff 的整体性能均优于对比工具。该优势主要得益于其语义保持性强的关键表达式设计以及基于局部敏感哈希的高效图对比机制。此外,作者还将 SemDiff 应用于漏洞检测和库版本检查等任务,发现其在依赖相似性判断的应用场景中同样能够显著提升检测效果。论文和核心创新包括:
- 作者提出了一种新颖的语义感知方法用于二进制相似性检测。该方法通过筛选关键指令对二进制代码进行抽象,并结合符号执行提取指令间的关联关系以辅助后续分析。该方法能够有效简化二进制代码,同时提炼出更加精确的语义信息,从而提升相似性检测的准确性与效率。
- 作者提出了一种将指令摘要转化为图结构的方法,并设计了基于局部敏感哈希(LSH)的机制,将图转换为序列以实现最终的相似性计算。该方法结构简单,无需训练,具备良好的可扩展性,适用于漏洞搜索、恶意软件检测等多种实际应用场景。
- 作者在52个软件包中使用共计2106个二进制文件(涵盖不同编译器、编译选项、源代码版本和混淆方式)对 SemDiff 的性能进行了评估,并与多种先进工具进行了对比。结果表明,SemDiff 不仅在二进制相似性检测中表现优越,还能够作为辅助工具,为其他依赖二进制比较的系统提供有效支持。
2. 背景
2.1 研究动机
作者通过上图中的编译示例展示了二进制多样性及编译优化对控制流图(CFG)的影响。具体而言,左侧为未优化的二进制片段,其中包含一个条件跳转(jle .L2)指令,跳转至两条不同的赋值指令;而右侧的优化片段则通过一条 sbb 指令替代了这三条指令。尽管两者源自相同的源代码,但在语法和结构上存在明显差异。监督学习方法的性能高度依赖于训练数据,因此对于使用未见过的编译器或优化策略生成的二进制代码,其效果往往不佳。无监督学习方法虽然能自动从上下文中学习指令嵌入表示,但当上下文因优化或编译器差异而发生实质性变化时,其准确性也会受到影响。此外,一些基于程序分析的方法在基本块粒度上进行相似性比较,但如示例所示,经过优化后基本块可能被合并,从而削弱了此类方法的有效性。还有部分方法通过最长公共子序列(LCS)对两个函数的指令或基本块序列进行对齐,但编译优化可能改变指令、基本块或其顺序,给基于序列对齐的方法带来了额外挑战。
2.2 准备工作
研究目标:二进制相似性检测可在不同粒度上进行,包括基本块级、函数级以及跨过程控制流图级。其中,基本块级用于量化两个基本块之间的相似性,函数级用于衡量两个函数的相似性,而跨过程控制流图则分析多个函数间基本块按控制流连接形成的图结构。在实际应用中,识别具有相似功能的易受攻击函数尤为关键,因此作者的目标是实现函数级的高效相似性检测。正如前文所述,二进制相似性检测方法主要分为机器学习与程序分析两类。本文采用基于程序分析的方法进行二进制相似性比较。实验程序分析的方法研究函数层面的二进制代码相似性检测
研究假设:作者在研究中提出了三项在实际场景中具有现实意义的前提假设。首先,二进制文件被默认去除调试信息,因为在现实中发布的软件产品通常以去符号形式发布,以保护其知识产权。其次,二进制文件可能经过混淆处理,这是一种常见的手段,用于增加代码的理解难度。第三,假设二进制文件已经被解包,因为解包问题是一个独立的研究方向,已有相关工作可以有效解决。
2.3 研究挑战
挑战1:由于相同源代码在不同编译器或优化选项下生成的二进制代码在语法上存在差异,从中提取等价语义是一项具有挑战性的任务。现有方法通常通过将二进制代码转换为中间表示(IR)后对符号表达式进行比较。然而,IR 并未简化二进制代码,反而由于语法复杂,往往将一条二进制指令拆解为多条 IR 指令,从而使分析变得更加繁琐。此外,即使转换为 IR,不同编译器或优化策略仍可能引入不匹配的变量,导致符号表达式无法准确对齐。因此,基于 IR 的符号表达式比较仍难以精准识别语义等价关系。因此,需要一种既能保留语义信息,又能简化二进制表示的方法来更有效地进行相似性检测。核心语义提取
挑战2:相较于基于文本的简单分析,许多方法通过语义图的构建与比较来研究代码相似性,这些方法通常提取控制流图或调用图,并将其转换为向量,再通过机器学习算法识别相似的二进制代码片段。然而,这类方法普遍将每条二进制指令表示为固定长度的向量,这是基于指令通常较短且结构类似的假设。但在作者的设计中,关键表达式可能包含多条指令的语义,其长度不但可能极长,而且变化显著,因此固定向量长度的要求并不适用。为此,作者采用了一种基于局部敏感哈希(LSH)的方法,对图中所有节点计算哈希值,并通过比较哈希值来量化相似性。LSH 能够以低维数据有效表示高维信息,同时保持数据间的相对距离,适用于处理这种表达式长度不确定的语义表示问题。变长输入嵌入
挑战3:为比较函数相似性,许多现有研究采用分而治之的策略,如最长公共子序列(LCS)方法,将每个函数视为指令序列或基本块序列进行比对。在最底层,该方法通过比较单条指令或单个基本块来判断相似性。然而,底层的比较策略需要人为定义,即如何判定两条指令或两个块的相似度,这就需要专家对二进制代码进行人工审查并制定比较规则。比较结果的准确性高度依赖于规则的质量,同时规则的制定过程耗时耗力,难以扩展。此外,LCS 问题属于 NP-hard,其计算开销较大,导致整体效率较低,更难以高效处理结构复杂的二进制图。序列比较方法效率低且准确性不足
2.4 解决方案
针对二进制比较中面临的挑战,作者提出了相应的解决方案。针对第一个挑战,作者提出一种基于符号执行的二进制翻译方法,用于从每个二进制中抽象出关键指令,并生成具有语义感知能力的表示以支持后续比较。通过对源自相同高级语言代码的二进制程序进行观察,作者发现指令可分为关键指令与非关键指令。关键指令主要体现函数的核心执行过程以及参数的传递行为,而非关键指令则多用于地址计算等预处理目的。
通过人工分析,作者将关键指令划分为四类:函数调用行为、比较操作、间接跳转以及内存写入。这些指令类型共同构成了函数执行过程中最核心的语义信息。
函数调用行为指的是调用指令,其操作数通常由前面的指令计算所得,作为函数参数传入;比较操作指令用于比较操作数,例如使用 cmp 或 test 指令,会影响程序在控制流交汇处的分支选择;间接跳转指令则包含目标地址作为操作数,例如 jmp eax,用于实现跳转逻辑;内存写入指令表示将某个值或地址存入内存中,例如 mov [edx], ebx,体现了程序对内存状态的修改。
因此,作者进一步定义了与四类关键指令相对应的关键表达式类型,如上表所示。对于函数调用行为的关键表达式,RET 表示调用指令,FuncAddr 为函数的起始地址,expi(i ∈ {1, ..., n})表示函数参数的符号表达式;对于比较操作,cmp 表示比较指令,exp1 和 exp2 为参与比较的两个操作数;对于间接跳转,branch 表示跳转指令,exp 为跳转目标的符号表达式;而对于内存写入,表示将 exp2 的值写入由 exp1 表示的内存地址。基于这些定义,作者首先对二进制程序进行符号执行,提取每条指令中每个操作数的符号表达式,并将标记出的关键指令转换为对应的关键表达式。
为应对第二与第三个挑战,作者引入了局部敏感哈希(LSH)算法。具体而言,在获得包含关键表达式的抽象二进制表示后,作者提出了一种基于 LSH 哈希的比较方法,用于计算两个抽象汇编程序之间的相似性。该方法首先构建关键语义图,以概括每个函数的主要行为。随后,参考图中节点之间的依赖关系,对所有节点进行拓扑排序,并通过 LSH 哈希算法将整个图映射为一个 LSH 值。最终,通过比较两个关键语义图的 LSH 值,推测它们之间的相似性。
存在问题:应该进一步详细阐述上述特征是实现二进制代码的充分必要特征。
3. 方法
3.1 方法概述
对于两个待比较的二进制程序,例如 BinA 和 BinB,分别包含函数集合 {FuncA1, ..., FuncAn} 和 {FuncB1, ..., FuncBm}。为了在函数级别上进行相似性检测,作者从中选取一对函数 FuncAi(i ∈ {1, ..., n})与 FuncBj(j ∈ {1, ..., m}),并将其输入至 SemDiff 中进行相似度计算。SemDiff 包含图生成与图对比两个模块,分别执行相应的处理流程以实现函数间的语义匹配与相似性量化:
- 图生成:首先,作者利用定制化的符号执行技术,从给定函数中提取关键指令的符号表达式;其次,将提取到的符号表达式转换为关键表达式;最后,根据函数的控制流结构,将这些关键表达式连接起来,构建出能够保留函数关键语义的图结构。
- 图比较:首先,作者通过拓扑排序将关键语义图序列化为一组关键表达式序列;其次,对每个关键表达式进行分词,生成对应的标记序列;然后,将所有关键表达式的标记序列拼接,并使用局部敏感哈希(LSH)对其进行哈希处理,生成函数对应的 LSH 哈希值;最后,通过计算两个函数的 LSH 哈希值之间的 Jaccard 相似度,完成函数间的差异度量与相似性判断。
通过上述过程,系统会为选定的函数对计算一个相似度得分。对于 BinA 中的每个函数,作者将其与 BinB 中的所有函数进行一对多比较,并选出得分最高的函数对作为最相似的一组。如果选中的函数对具有相同的函数名,则说明系统成功识别出了正确的匹配关系。
3.2 图生成
3.2.1 关键指令符号表达式提取
为高效提取函数中关键指令的所有符号表达式,作者对符号执行进行了定制化设计,引入了两项关键技术。首先,对给定函数进行符号执行,遍历其所有指令;其次,针对循环结构采用轻量级方式进行符号执行,而非反复执行直至循环条件不满足。在函数指令遍历方面,作者将函数视为一个控制流图,图中的节点表示指令,子节点为控制流路径上的后续指令。算法的输入为函数的首条指令,同时需要为函数的输入参数赋予符号值,具体做法是根据 x86-64 的调用约定,将符号值 VARi(i ∈ N)分配给相关寄存器。例如,在 x64 Linux 系统中,寄存器 rdi、rsi、rdx 和 rcx 分别对应函数的第1到第4个参数。
作者在算法1中实现了一个深度优先搜索函数 execute_next_node,用于实现完整的指令覆盖,而非穷尽所有执行路径,从而有效避免路径爆炸问题。具体而言,算法首先判断当前节点是否已被执行(第2行);若未执行,则对该节点进行符号执行(第3行)。由于一条指令可能包含一个或多个操作数,符号执行后每个操作数将对应生成一个符号表达式,因此系统需为每条指令的每个操作数维护相应的符号表达式记录。如果操作数引用的数据是可解析的(例如指令 mov esi, address 中,address 指向字符串 "Rtmin"),则使用该解析值继续符号执行;若数据不可解析(如 mov edi, cs:bio_err,其中 cs:bio_err 的值未知),则为该未知值分配一个尚未使用的符号变量 VARi(i ∈ N)进行表示,以保证符号执行的连续性与表达完整性。
当节点已被执行过时,算法会进一步判断该节点是否为循环的起始点(第8行)。如果是,则通过轻量级方式对循环进行处理,即调用 lightweight_loop_processing() 方法(第10行),该方法将在后文的“轻量级循环处理”部分中详细介绍。如果该节点虽然已被执行但并不属于循环结构,则直接返回(第11行),以避免对同一节点的重复执行。
轻量级循环处理:在符号执行过程中,直接执行循环体往往效率低下,因为循环可能会重复多次甚至无限执行。为此,作者提出了一种轻量级的循环处理方法。考虑到循环通常会在每次迭代中更新一个或多个变量(例如对计数器加一或减一),这些变量被称为循环计数器。当检测到循环结构时,系统仅执行循环体两次。若循环中包含分支路径,第一次执行时随机选择一个路径进行符号执行,第二次则沿相同路径继续执行。例如,在图3中,循环包含多个分支,第一次执行时随机选择路径 .L1 − .L3 − .L4,并为该路径上的每条指令生成符号表达式,这些表达式以 "1st:" 标记表示。如果指令含有多个操作数,其对应的符号表达式以逗号分隔,例如图中 L3 第5行的 "1st: 3,3" 表示两个操作数的符号表达式均为 3。第二次执行沿同一路径继续,符号表达式以 "2nd:" 标记表示。通过比较两次执行后操作数的符号表达式,系统可识别出发生变化的表达式,进而判断哪些变量为循环计数器。对于这类变量,作者在其符号表达式前添加标识 ITER,表示其在每次迭代中发生变化。例如,图3中第7和第8行中的 eax,在第一次执行时符号表达式为 VAR0,第二次为 VAR0+1,说明其每轮增加1,因此将其表示为 ITER(VAR0)。
存在问题:轻量级循环处理是关键指令符号表达式提取的重要步骤,需要列出其具体的算法对其进行说明。
3.2.2 关键表达式生成
由于关键指令的符号表达式已经被提取,但仍可能因编译技术如数据编码或混合布尔算术等造成语法差异和结构复杂,作者进一步采用两步转换策略将其统一为关键表达式。首先,利用表达式合成技术对符号表达式进行化简,将复杂冗长的表达式转化为更简洁的形式。该技术能有效提升表达式的可比性与表达效率。其次,依据预定义的规则(如上表所示),将每条关键指令转换为统一的关键表达式表示,从而为后续相似性计算提供稳定的语义基础。
3.2.3 关键语义图生成
函数中的关键指令已被转换为关键表达式,但此时它们仍是无序的列表,如图4中红色矩形所示。为此,作者根据函数的控制流关系,将关键表达式连接起来,构建关键语义图。该图中的每个节点代表一条关键指令,记为顶点集合 V = {V1, ..., Vn},每个顶点的属性 Attri(i ∈ {1, ..., n})即为对应的关键表达式。图中的边集合 E = {(i, j) | i, j ∈ V²} 则表示关键指令之间的控制流关系,从而完整刻画函数的语义执行路径。具体而言,该过程首先从给定的二进制函数中提取关键指令的符号表达式,然后将这些表达式转换为关键表达式,最后根据函数的控制流关系将关键表达式连接起来,从而生成对应的关键语义图。
存在问题:关键语义图生成部分介绍过于单薄。
3.3 图比较
为量化两个图之间的相似性,现有方法通常将每个节点的属性向量化,因此要求节点属性必须是简短的符号。但作者构建的关键语义图中,节点属性可能是较长的表达式,这使得传统向量化方法无法直接适用。
受相关工作的启发,作者将源代码转换为线性指令序列以遍历抽象语法树的思路应用于本研究,并通过三个步骤解决相似性量化的问题:首先,对关键语义图进行拓扑排序,将其序列化为节点序列;其次,对节点属性中的关键表达式进行分词处理;最后,将分词后的关键表达式拼接,并应用局部敏感哈希(LSH)生成哈希值,以实现图之间的相似性度量。将代码转换为线性指令序列
3.3.1 关键表达式序列序列化
作者采用拓扑排序技术对有向图 G = (V, E) 中的所有节点 V = {V1, ..., Vn} 进行线性排序,使得图 G 满足以下性质:每一条有向边 {(Vu, Vv) | Vu, Vv ∈ V²} 都是前向的,即 Vu 排在 Vv 之前。更具体地说,拓扑排序能够保留图中节点的结构和几何关系,使得结构相似的图在排序后得到的节点序列也具有较高的一致性。
然而,在函数中可能存在包含多个关键指令的循环结构,因此图生成过程中生成的关键语义图可能包含环路。而拓扑排序仅适用于无环的有向图,无法直接应用于含有循环的关键语义图。为解决这一问题,作者首先通过移除循环流程中的最后一条边,使关键语义图变为无环结构;然后,在循环起始节点中添加符号 WHILE,以标识该节点为循环的开始。如图3所示,从 .L4 到 .L1 的边是循环中的最后一条边,因此被移除,而循环体中的其他节点则被保留。
3.3.2 关键表达式分词
对于序列化图中每个节点的属性,作者将其关键表达式拆分为操作数与运算符,构成一个标记序列。例如,表达式 X + 3 − Y + 7 ∗ Z 会被拆分并标记为 X、3、−、7、∗、Z。若表达式中包含括号,在分词时会保留括号信息,例如关键表达式中的部分片段 [X + [Y + Z − 3] ∗ 2] 和 (X + (Y ∗ 6 + (Z − 3 + K))),将被分词为:[X]、[[Y]]、[[Z]]、[[−]]、[[3]]、[∗]、[2],以及 (X)、((Y))、((∗))、((6))、(((Z)))、(((−)))、(((3)))、(((K)))。此外,作者在处理过程中省略了加号(+),因为观察发现加号在分词后频繁出现,会导致本质不同的表达式在相似性计算中得分偏高。为了关联每个标记与其所属的关键表达式类型,作者在每个标记前添加前缀符号。
- 对于不同类型的关键表达式,作者为每个标记添加特定的前缀以标识其来源。对于函数调用行为,每个标记添加前缀 RET _(),表示该标记来自调用指令,例如关键表达式 RET _(3, VAR0 + 4) 被分词为 RET _(3)、RET _(VAR0)、RET _(4)。
- 对于比较操作,每个标记添加前缀 cmp,例如表达式 4 cmp [VAR1 + 18] 被分词为 cmp 4、cmp [VAR1]、cmp [18]。
- 对于间接跳转,每个标记添加前缀 branch,例如表达式 branch [[VAR2 + 10] + 16] 被分词为 branch [[VAR2]]、branch [[10]]、branch [16]。
- 对于内存写入操作,等号左侧的标记以“=”结尾,等号右侧的标记以“=”为前缀,例如表达式 [VAR2 + 18] = (VAR1 + 10) ∗ 3 被分词为 [VAR2] =、[18] =、= (VAR1)、= (10)、= ∗、= 3。
3.3.3 基于局部敏感哈希的相似性计算
LSH 方法在高维空间中的近邻搜索中表现出良好的效果,因为它能够在保持数据相对距离的前提下,将高维数据映射为低维表示。该方法通过将数据对象哈希到不同的桶中,使得相似的数据以较高概率落入同一个桶中。基于这一特性,作者利用 LSH 为序列化并分词后的关键语义图生成对应的哈希值。具体而言,作者首先将关键语义图中所有关键表达式的标记序列拼接为一个整体的标记序列,然后基于该序列计算对应的 LSH 哈希值,用以表示一个函数。当获得两个函数的 LSH 哈希值后,作者采用 Jaccard 相似度来度量它们之间的相似性,该相似度通过两个集合的交集元素数量除以并集元素数量来衡量相似程度,是衡量集合相似性或距离的常用方法。
存在问题:局部敏感哈希原理部分介绍过于单薄。
4. 实验
作者通过以下研究问题(RQs)对 SemDiff 的有效性进行了评估:
- • RQ1:生成的关键表达式的准确性如何?
- • RQ2:SemDiff 能否识别不同编译优化下的相似性?
- • RQ3:SemDiff 能否识别不同编译器下的相似性?
- • RQ4:SemDiff 能否识别不同混淆方式下的相似性?
- • RQ5:SemDiff 是否适用于跨版本的相似性检测?
- • RQ6:SemDiff 在漏洞检测任务中的表现如何?
4.1 实验设置
数据集:作者在九个广泛使用的开源项目上开展了实验,包括 openssl-3.3.0、libtomcrypt-1.18.2、coreutils-8.32、ImageMagick-7.1.0-10、libgmp-6.2.1、curl-7.80、sqlite3-3.37.0、zlib-1.2.11 和 Puttygen-0.74。从这些库中筛选出13个被厂商和研究者广泛使用的程序。这些程序的源代码分布于52个项目包中。通过不同的编译器、编译选项、源代码版本和混淆策略,最终生成了共计2106个二进制文件,包含超过41.6万个汇编函数。
环境:作者在一台配备 Intel NUC8i5BEH(处理器为 i5-8259U,内存为 16GB)的主机上搭建了实验环境。考虑到实验可能涉及深度学习任务,还使用了高性能计算集群作为加速器,该集群包含 456 张 NVIDIA Tesla P100 显卡、114 个双路 14 核 Xeon E5-2690 v4 计算节点,以及每节点 256GB 内存。
实验:作者将 SemDiff 与五种当前先进的二进制相似性检测工具进行了对比,包括 JTrans、UniASM、Asm2Vec、Gemini 和 Palmtree。其中,JTrans、functionsimsearch、Gemini 和 Palmtree 均为基于机器学习的方法。作者在高性能计算集群上对 JTrans、UniASM、Gemini 和 Palmtree 进行了模型训练,并在 Intel NUC 上对所有工具进行了验证。训练数据选自四个程序(busybox、coreutils、libgmp 和 libMagickCore)中共计7000个汇编函数,这些函数在不同优化等级下编译而成。参考现有研究,作者仅选择包含不少于五个基本块的函数作为训练与评估对象,因为小于五个基本块的函数通常不易包含漏洞,在相似性检测任务中意义较小。
相似性评估:针对一对采用不同优化等级编译的二进制程序(BinA 和 BinB)计算函数级的相似度分数。具体方法为:首先从 BinA 中选择一个函数,使用二进制相似性检测工具计算该函数与 BinB 中每个函数之间的相似度得分,并将得分最高的函数对视为最相似的一组。随后,通过启用调试信息并比对函数名,验证 BinA 和 BinB 中的函数是否为同一个函数。为量化检测效果,作者采用与现有研究一致的评估指标,即 precision@1,用于衡量 BinA 中的函数是否在 BinB 中准确匹配到函数名相同的目标函数,并且排名第一。在本评估场景下,precision@1 等价于 recall@1。
4.2 关键表达式的准确性
为验证关键表达式转换的准确性,作者从第4.1小节所述的数据集中随机选取了200个函数,并邀请三位有经验的程序员对其进行人工标注。表4列出了这200个函数。实验结果显示,SemDiff 能够正确转换约85%的指令。通过对错误案例的人工分析,作者发现部分错误源于 SemDiff 当前尚不支持一些较少见的 x64 指令变体(如 movzx);此外,由于 SemDiff 基于 IDA Pro 解析字符串变量内容,若 IDA Pro 错误地将常量识别为内存地址并尝试读取该地址的内容,也可能导致解析结果出现偏差。
存在问题:实验过于简单,是否可以给出不同项目关键表达式识别的准确率,分析不同项目表达式转换准确率的差异,对提取方法进行进一步分析。此外,并未分析未能成功转换的关
键表达式是否会对后续相似性比较产生影响。
4.3 跨编译优化级别实验
鉴于 GCC 是现实中最广泛使用的编译器之一,作者选择其作为跨优化等级评估的编译工具。在实验中,设置了五种优化等级:O0、O1、O2、O3 和 Os,并使用 SemDiff 及其他五种先进工具,对来自相同源函数但经不同优化等级编译所得的二进制代码进行相似性检测。此外,作者还引入了标准化压缩距离(NCD)得分作为指标,用于量化二进制代码对的语法相似性。其中,NCD 得分越高,表示该对二进制代码在语法上越不相似。
相似性检测结果如上表所示。由于篇幅限制,作者仅列出了三个差异较大的优化等级组合的检测结果,即 O3 对 O0、O1 对 O0 以及 Os 对 O0,其余检测结果及相关讨论已发布在作者网站上。从实验结果来看,SemDiff 平均可达到 73% 的检测精度,而其他工具的检测精度仅在 18% 至 74.9% 之间。尽管在某些特定情况下 JTrans、UniASM 和 Asm2Vec 的表现略优于 SemDiff,但 SemDiff 在面对语法差异最明显的二进制对时,仍能保持稳定且较高的检测性能。
4.4 跨编译器实验
同样地,作者还进行了基于不同编译器的实验,选用了 GCC 5.4.0 和 CLANG 3.8.0 两个编译器,在相同优化等级下评估 SemDiff 的相似性检测性能。通过检查二进制代码对的 NVD 得分,作者发现 GCC O0 与 CLANG O0 以及 GCC O1 与 CLANG O1 是差异最大的两组二进制代码对。因此,论文中列出了这两组的相似性检测结果(如表6所示),其余结果则发布在作者网站上。
实验结果表明,在相同优化等级但不同编译器的条件下,SemDiff 平均可达到 81% 的检测精度。此外,在部分特定场景中,JTrans、UniASM 和 Asm2Vec 的表现略优于 SemDiff。对于第4.3小节中使用不同优化等级或第4.4小节中使用不同编译器编译的相同源代码生成的二进制程序,其函数数量可能因内联优化而有所差异,该优化会将被调用的函数插入到调用函数中。此外,即使是同一个函数,其内部指令也可能在语法上存在差异,但语义保持不变。这些差异会导致函数属性变化,例如基本块数量、指令数量和助记符统计等。因此,依赖语法特征的方法(除 SemDiff 外)在检测过程中准确性较低。而在此情况下,大多数函数的核心语义仍被保留,使得 SemDiff 相较其他工具具有更高的检测效果。
4.5 混淆情况下实验
作者还通过不同的混淆选项对相似性检测性能进行了评估。具体而言,选取由 CLANG 3.8.0(优化等级为 O0)编译的程序与由 OLLVM 使用三种不同混淆策略(SUB、BCF 和 FLA)编译的程序进行比较。SUB 策略在基本块内部替换特定指令,对函数的控制流图影响较小;BCF 策略以固定模式向函数中插入额外的基本块;FLA 策略则通过将原始基本块拆分为更小的基本块或添加额外基本块,从而实现控制流扁平化。
对于每个程序,作者生成了三个二进制代码对,每对由一个使用 CLANG 编译的二进制文件和一个使用 OLLVM 并应用某种混淆策略编译的二进制文件组成。随后,作者将这些代码对分别输入至 SemDiff 以及两种具有代表性的机器学习方法(Gemini 和 Palmtree)中进行相似性量化。实验结果如表8所示,SemDiff 在所有混淆策略下的检测性能均显著优于另外两种工具。
为深入理解 SemDiff 失败案例的原因,作者对相关结果进行了人工分析。在第 4.3 和第 4.4 小节的实验中,当 SemDiff 未能将真实匹配的函数排在首位时,约有 50% 的情况中,匹配函数仍被排在前十名以内,作者认为这在实际应用中仍可有效辅助人工查找相似函数。而在剩余 50% 的失败案例中,SemDiff 无法将相似函数排在前列,主要原因包括:1)对某些使用频率较低的助记符(如 cvtss2sd)缺乏支持,导致语义信息提取不足,从而影响精度;2)部分函数调用被编译器优化为其他指令,例如 call strlen 被替换为 repne scasb,尽管语义相同,但其符号表达式差异较大,即使经过符号执行也难以匹配;3)展开循环与未展开循环之间在结构与符号表达式上存在较大差异,特别是当循环次数不一致(如一个展开循环对应多个未展开循环)时,匹配难度进一步加大。
对于第 4.5 小节的实验,作者推测尽管混淆选项在语法结构上对二进制程序进行了修改,但其核心语义仍被保留,SemDiff 能够有效提取这些关键语义。在所评估的三种工具中,SUB 混淆下的相似性得分最高,可能是由于该混淆策略并未改变程序的控制流结构。而在三种混淆策略中,FLA 的相似性得分最低,因为它通过控制流扁平化引入了更多语法和控制流上的变化。此外,除前文提到的三点原因外,混淆过程中新增的关键指令也是导致识别失败的一个重要因素。
存在问题:论文方法仅与Gemini和Palmtree两个方法进行比较,论文未阐明这两个baseline选择的原因。此外,从实验结果来看,论文所提的方法明显由于其他两个方法,应该解释为什么论文所提的方法在加壳情况下有更好的表现。
4.6 跨版本情况下实验
在实际应用中,二进制相似性检测可用于查找库文件或可执行文件的相似版本,因为漏洞往往会在多个版本间延续。因此,在本节中,作者针对同一程序的不同版本二进制文件,量化其相似性。具体而言,每个程序选取四个跨越数月至数年的版本,并使用 GCC 5.4.0 编译。其中一个版本被指定为基准版本(该版本已在第 4.3 和第 4.4 小节的实验中使用),然后将其与其余三个版本分别进行相似性比较,并使用 precision@1 作为评估指标,共生成 39 对二进制文件。由于篇幅限制,部分结果展示于表7,其余完整结果可在作者的 GitHub 仓库中查阅。
总体来看,SemDiff 的检测性能优于所有其他工具。在 13 个测试程序中,SemDiff 在其中 10 个程序中取得了最佳检测效果,在剩余的 coreutils、libgmp 和 sqlite3 三个程序中排名第二。对于 coreutils 和 sqlite3,SemDiff 的平均得分仅比 Bindiff 低 0.01;而在 libgmp 中,其得分仅比 Asm2Vec 低 0.03。作者认为,在这三个程序中 SemDiff 表现略逊的一个可能原因是:它们的版本差异较小,语法结构更为相似,这使得依赖语法和结构特征的工具更容易捕捉到相似性。而在其他版本差异较大的程序中,SemDiff 的表现则最为出色。本实验中的失败案例同样主要由于以下原因:缺乏对部分罕见助记符的支持、函数调用被等效指令替代,以及循环结构难以精确匹配等问题。
4.7 漏洞检测
二进制相似性检测的重要应用之一是识别相似的漏洞函数。为此,作者随机选取了18个具有公开漏洞编号(CVE)的函数,并尝试检测其相似的漏洞版本。对于每个漏洞函数,随机选择一个易受攻击的版本作为基准函数,并准备另一个随机选择的、在不同编译设置(如 O0、O1、O2、O3、Os)下生成的漏洞版本,作为目标函数,要求各工具能够将其识别为与基准函数相似。随后,作者将目标函数与二进制文件中的所有其他函数混合,并检查工具能否将目标漏洞函数准确排在第一位(即 top-1 分数)。如表9所示,Asm2Vec 的 top-1 检测成功数为 9 个(50%),而 SemDiff 成功检测了 10 个(55.6%)。
作者对 SemDiff 未能成功识别的 CVE 案例进行了人工分析。在 8 个失败案例中,有 6 个(占比 75%)中,SemDiff 将目标漏洞函数排在前十名以内,仍体现出其在漏洞函数识别方面的有效性。其余两个失败案例中,一个是由于 IDA Pro 无法识别间接跳转地址,导致 SemDiff 无法构建完整的关键语义图;另一个则是由于缺乏对某些低频助记符的支持,影响了语义信息的提取,从而导致识别精度下降。
存在问题:Asm2Vec 更倾向于成功检测出何种漏洞,本文提出的方法更倾向于检测出何种漏洞?为什么仅与Asm2vec进行对比?
5. 讨论
图生成:缺乏对低频助记符的支持会降低检测精度。因此,提升助记符支持的完整性将有助于提高准确率。由于时间限制以及助记符本身的复杂性,当前版本的 SemDiff 仅支持最常见的助记符。除了完善助记符支持外,向 SemDiff 中引入硬编码的优化知识也能提升其准确性。例如,call strlen 与 repne scasb 的符号表达式虽然不同,但语义相同,只有通过加入相关优化规则,才能将它们识别为等价指令。考虑到 GCC 和 Clang 是开源的,一个有前景的方向是自动解析其源代码,从中学习这类优化知识。
图比较:采用 LSH 算法将每个图转换为哈希值,因此在计算过程中默认每个关键表达式中的标记具有相同的重要性,不同类型的关键指令也被视为等同。然而,实际上某些标记和关键指令应具有更高的权重。例如,匹配较长的立即数值相比于常见的运算符(如 ∗)更能反映语义上的相似性;而两个具有四个参数的调用类指令的匹配,也应比只有一个参数的调用匹配赋予更高权重。这些标记与关键指令类型的权重可以通过机器学习方法自动学习,前提是具备充足的训练数据。
存在问题:依赖的静态二进制分析平台 IDA Pro 在某些情况下未能成功分析间接跳转目标,且展开与折叠循环的匹配问题尚未得到有效解决。针对这一问题,作者提出了两种可能的解决方案:一是将循环展开一定次数;二是将展开的循环合并为一个循环,并与折叠循环进行比较。至于关键表达式简化,SemDiff 使用了 msynth 工具,但该工具在时间效率和准确性方面可能存在一定的问题。
6. 相关工作
6.1 基于程序分析的方法
SMIT、BINCLONE 和 SPAIN 等方法使用哈希技术将各种指令序列映射为固定长度的哈希值并比较它们的相似性;IDEA、MBC 和 Expose 生成来自指令序列的嵌入表示;Exediff、Tracy 和 Binsequence 对两个序列进行对齐并决定它们的相似性;SMIT、Binslayer 和 Cesare 等方法将问题转化为在两个控制流图(CFG)之间寻找最小成本的映射;Beagle、Cesare、Rendezous 和 FOSSILE 将图划分为多个子图并进行子图相似性匹配;CoP、SIGMA 和 Binsequence 根据路径确定相似性;Beagle、FOSSILE 和 SIGMA 基于算术、逻辑或数据传输操作对指令进行分类;Binhash、MULTI-HM、Bingo、SPAIN、Kargén 和 IMF-SIM 检查输出是否与输入相同;Binhunt、Binhash、Expose、CoP、MULTI-MH 和 ESH 通过符号执行二进制文件,并通过约束求解器比较相似性;XMATCH 和 TEDEM 则确定符号公式的树或图的编辑距离。然而,这类方法在基本级别的比较或序列对齐中存在问题,主要是由于编译器和优化引起的差异。
6.2 基于机器学习的方法
Genius、Vulseeker、Gemini、Yu 等人、Cochard 等人以及 TIKNIB 等方法从图中提取特征并将其转化为特征向量,进而确定向量之间的相似性。QBinDiff 提取二进制代码特征,并使用图编辑距离和网络对齐方法来衡量相似性。αDiff、InnerEye、Asm2Vec、Kam1n0 和 Safe 等方法则自动学习每条指令的嵌入表示,并使用这些嵌入生成基本级别或函数级别的表示。然而,这类方法往往会受到训练数据和优化级别的影响,导致性能波动。
7. 总结
本文提出了 SemDiff,一种用于二进制相似性检测的新型语义方法。SemDiff 包含两个模块:图生成和图对比。在图生成模块中,作者提出了对给定函数进行完整指令遍历和轻量级循环处理,以生成关键指令的符号表达式。接着,作者将符号表达式转换为关键表达式,并形成关键语义图。通过利用 LSH 方法对两个关键语义图进行对比。在评估中,作者将 SemDiff 与五种先进的基准工具进行了对比,结果显示 SemDiff 在平均性能上超越了所有基准工具。虽然当前版本的 SemDiff 仅适用于基于 x86 的二进制文件,但作者计划在未来的工作中扩展 SemDiff,支持跨架构的相似性检测(如 x86 和 ARM)。特别地,作者将扩展图生成模块,提取并转换目标架构中的关键指令为关键表达式。