本文参考:
[1]方磊,武泽慧,魏强.二进制代码相似性检测技术综述[J].计算机科学,2021,48(05):1-8.
(信息工程大学数学工程与先进计算国家重点实验室, 国家重点研发课题,北大核心)
摘要
代码相似性检测常用于代码预测、知识产权保护和漏洞搜索等领域,可分为源代码相似性检测和二进制代码相似性检测。软件的源代码通常难以获得,因此针对二进制代码的相似性检测技术能够适用的场景更加广泛。
根据关注的代码信息的不同,当前的二进制代码相似性检测技术分为4类:基于文本、基于属性度量、基于程序逻辑、基于语义的检测技术。
需要解决的难题:跨编译器、跨编译器优化配置、跨指令架构检测等。
代表性方法和工具:Karta, discovRE,Genius, Gemini,SAFE等。
1 研究背景及其意义
代码相似性的概念源于软件分析技术,目前还没有标准和权威的定义。通常认为,如果一段代码是由另一段代码复制或经过一定规则变换而来的,则认为它们是相似的。二进制代码的相似性是指由同一或相似的源代码编译得到的不同二进制代码是相似的。
代码相似性检测通常应用于但不限于代码预测(即根据现有的代码预测可能的代码修改和更新,提供代码模板推荐)、知识产权保护(即发现代码中未经授权的代码的复用和克隆,协助软件所有者保护知识产权或规避潜在的侵权行为)、漏洞搜索(如脆弱代码搜索、软件漏洞追踪和定位)。
2 相关研究
2.1 代码相似性检测的基本流程
whale于1988年提出代码相似性检测过程分为两个阶段:代码格式转换和相似度确定。具体展开又有4个部分:代码预处理、中间表示转换、比较单元生成和度量算法匹配。
- 代码预处理的目的是剔除部分与代码相似性无关的或影响较小的信息,同时规范和统一输入格式以便于处理,其操作通常包括:统一代码布局、去除无关字符(如空行、注释)、替换名称(如自定义函数名、数据名和通用寄存器名)、程序切片等。
- 中间表示转换的目的是在尽量不改变代码携带信息的前提下,提取感兴趣的信息(如字节流、字符串、可度量属性、控制流和数据流,以及代码的词法、语法和语义等),并将其转换成自动化程序可处理和比较的其他形式,该步骤一般借助于编译器、反编译器、程序分析器或自定义规则等进行转换。
中间表示形式有线性结构(字符串或序列)、树形结构(抽象语法树或XML文档等)、图形结构(控制流图或数据流图等)和其他非线性结构(多维特征向量或特征矩阵)。 - 比较单元生成是对代码或其中间表示进行切分或聚合,得到便于相似性度量的最小单元。常见的比较单元有字符串、标识符(Token)序列、特征向量、树的结点及其子树、图的节点及其子图等,也可以是中间表示本身。
比较单元还可以分为固定粒度和自由粒度。针对固定粒度,如果粒度太大,那么检测的准确度会降低;如果粒度太小,那么检测的工作量会增加。在实际操作中需要根据中间表示的形式和任务目标选择合适的检测粒度。 - 度量算法匹配是根据不同形式的比较单元,采用与之相适应的相似性度量算法(如串搜索、向量距离计算、子树和子图的匹配、聚类等),度量检测对象之间的相似性。
2.2 二进制代码相似性检测的难题
二进制代码通常由源代码编译而来,源代码级的复制和修改也会部分体现在相应的二进制代码中。代码克隆(也称代码抄袭)和混淆手段虽然多应用于源代码的复制和修改,但也可以用于可执行二进制程序或组件的篡改或修补。除此之外,同一源代码经过不同的编译器和优化配置,针对不同的硬件平台,编译得到的二进制代码不尽相同,因此二进制代码相似性检测会遇到其特有的难题,且该难题较源代码检测更为复杂。
在检测由同一源代码编译得到的二进制代码时,可能会遇到以下难题:
难题1 跨编译器问题。由于设计目的和采用的算法不同,不同的编译器产生的二进制代码不尽相同。例如,虽然寄存器的选择过程是通过各种启发方式和复杂的代码来传递驱动的,但在某些情况下,即使存在公共约定,编译器也不会完全遵守。这些都导致了寄存器选择的任意性。
难题2 跨编译优化配置问题。同一版本的编译器采用不同的编译优化配置所产生的二进制代码不尽相同。例如,可执行程序的调试版本相比发行版本会增加许多与调试相关的信息和调用。不同的优化等级,其区别在于增加或减少了对堆栈的检查和清理等操作,会导致最终的可执行文件的长度发生显著变化。再比如在x86-64架构下的GCC编译器,只有在-O0不优化的编译条件下,寄存器rbp才具有帧指针的特殊用途。
难题3 跨指令架构问题。不同指令架构的二进制代码不同。不同架构的指令集、寄存器、机器字长都有较大差异,从而导致相应的二进制代码差异巨大。
这些二进制代码来自同一源代码,因此具有相同的功能且语义等价,但其汇编指令和语法可能略有差异甚至截然不同。二进制代码相似性检测需要根据不同应用场景来屏蔽噪声干扰,提取代码中需要关注的信息。
3 二进制代码相似性检测技术的分类
3.1 基于文本
基于标识符的检测:(需要反汇编)首先模糊通用寄存器名和内存地址,然后提取指令的操作码和操作数,或者提取字符串以生成标识符序列,最后通过子序列匹配来判断不同代码是否相似。
- Karta是著名的静态反编译程序IDAPro的二进制程序匹配插件,首先利用唯一的数值常数和字符串等标识符来设置库的锚点函数,然后通过在二进制文件中查找锚点函数来缩小搜索范围,最后利用标识来进一步确定静态编译的开源库的确切版本并匹配函数符号,其也可以在这一步骤使用函数调用列表来匹配一组相似函数。
- DarunGrim2是基于指纹哈希匹配的二进制文件比较工具,它忽略了汇编代码中的操作数,将程序基本块的指令序列的哈希值作为相似特征,此外它还以程序中符号名称的匹配作为辅助手段。
基于字节流的检测:不对二进制代码进行反汇编,直接比较程序的二进制字节流是否相近。
- αDiff 检测算法:利用二维卷积神经网络(Convolutional Neural Network, CNN)对程序的二进制字节进行嵌入(嵌入既可以指神经网络将其他形式的输入转换为向量形式的输出的过程,也可以指神经网络的输出,即高维向量),并综合函数出入度和调用表的统计特征来构建特征向量,用向量之间的距离来度量程序间的相似性。
基于文本的检测因为不考虑程序的语法和语义等信息,所以其原理和实现较其他技术更简单,其时空复杂度也较低。该类技术主要针对未应用复杂混淆手段的代码克隆和复用,该类检测方法由于容易实现对抗检测,因此常作为一种高效的辅助检测手段。
3.2 基于属性度量
基于属性度量的检测技术关注程序汇编代码中可度量的属性和特征,提取属性和特征的度量值,构成多维向量(该方案认为特征向量能够从不同维度标识代码段),以特征向量之间的距离来度量代码段之间的相似度大小。这类方法需要确定检测的最小粒度(如基本块或函数),在最小粒度范围内通过对属性和特征的统计计数来获得度量值。度量值可能是但不限于是特定常量和指令等标识、函数的输入和输出等对象的计数。
- discovRE:基于跨架构的统计特征缩小相似函数的搜索范围。该方法首先通过人工筛选的特征来构建函数的简单特征向量(这些特征包含算术指令、函数调用、重定向、转移指令、局部变量、基本块、导入函数、所有指令和参数等对象的统计个数),利用kNN(k-Nearest Neighbors)算法计算函数的相似距离,从而实现预筛选可能相似的函数,降低后续基于函数控制流图(Control Flow Graph, CFG)的最大子图同构匹配方法的计算量。
- BinDiff:二进制文件比较工具,能快速查找汇编代码中的差异性和相似性。基于图形和指纹理论开发的 IDAPro 插件,其函数匹配主要是基于从 CFG 中得到的 NEC 值,即基本块数(Nodes)、边数(Edges)、调用数(Calls)。
基于属性度量的检测方法因为不考虑程序的逻辑结构,所以其统计特征易于实现,但其提取的信息十分有限,因此常作为一种辅助方法。主要问题是,属性特征是人为设计和筛选的,难以保证各属性都处于不同的维度,不同属性之间可能存在拟合问题,因此盲目地增加属性特征的种类,不仅可能无法有效提高检测的准确度,而且会增加计算开销。
3.3 基于程序逻辑
基于程序逻辑的检测技术:利用列表、树形或图形等数据结构来记录和描绘程序的数据流或控制流信息(该类技术认为所得的中间表示能够捕获程序中一定的语法和语义信息),通过匹配相似的序列、节点或边、公共子树或子图来寻找逻辑或功能相似的程序基本块或函数。
程序的数据逻辑在函数内部体现为数据的流向和运算,在函数外部体现为函数的输入和输出。
- 例如,Multi-MH系统通过基本块的输入和输出行为来掌握其语义,利用签名来查找具有类似行为的漏洞代码。二进制搜索引擎Bingo利用从符号表达式中生成的输入和输出样例来匹配语义相似的函数。
程序的控制逻辑在函数间可以用函数调用序列来描述,在函数内部可以用CFG来描绘,在基本块级可以用逻辑树来表示。
- 例如,工具HAWK实现了一种基于系统调用依赖图(System Call Dependence Graph, SCDG)胎记的动态检测方法;Pewny等提出的TEDEM方法利用表达式树的编辑距离来度量基本块级别的相似性;discovRE和Genius[23]是比较著名的基于属性优化CFG的二进制相似性检测系统,其采用了各自经优化的图匹配算法;Esh[24],BinHunt[25],iBinHunt[26]等工具在基于CFG的基础上,利用了符号执行来确定基本块或函数的相似性。
基于程序逻辑的检测方法的优点是准确度较高,可以根据不同的任务采用不同的匹配算法和策略,可伸缩性强。其缺点是时空复杂度高:一方面,数据流和CFG的提取过程代价非常昂贵;另一方面,相似性度量所依赖的图匹配算法的时间复杂度缺少多项式解,图匹配算法又多是两两匹配算法,因此在面对大规模查询任务时,计算量随代码库规模成几何倍数式增长。
针对该问题,一种方法是从中间表示CFG入手,利用其他辅助检测技术来为CFG引入轻量级且易于度量的属性,从而简化CFG的节点,例如discovRE利用轻量级的统计特征来筛选候选函数,降低图匹配算法的任务量。另一种方法是从图匹配算法入手,采用近似图匹配算法来提高检测效率,在准确度和速度之间进行折中,但该方法受限于图匹配算法的效率,对检测效率的提升有限。近年来神经网络技术被用于该领域,以解决传统图匹配算法遇到的效率瓶颈问题。具代表性的是Qian等提出的Genius系统,该系统利用机器学习的谱聚类算法[27]来对函数的CFG进行分类并生成码本(Codebook),再对码本进行编码,将相似函数的搜索问题转化为特征编码的搜索问题,极大地提高了效率并兼顾了结果的准确度。
3.4 基于语义
“语义”的概念来自自然语言,若将汇编语言的指令看作“单词”,基本块看作“句子”,则可得到相应的二进制代码的“词法”和“语法”概念,从而函数具有完整的“语义”,但是CFG主要包含程序的控制流信息,该信息仅是函数所包含的丰富语义信息的一部分。
本文中的基于语义的检测技术,通过捕获程序汇编代码中的语义信息,来比较函数或组件的语义差异,以实现相似性度量。这类方法通常借鉴自然语言处理(Natural Language Processing, NLP)、图像识别或其他领域的技术,利用深度神经网络来实现程序语义的嵌入,通过对嵌入向量的比较或查询操作来实现大规模任务的处理。其二进制代码的中间表示包括规范化的汇编文本、其他中间语言或CFG,神经网络模型多采用暹罗架构(Siamese Architecture)[30],利用程序代码的大规模特点构造来训练样本库,相关领域的专家和先验知识可能不是必须的。
- Xu 等[28]于2017年提出了一种基于神经网络生成的二进制函数 CFG 嵌入的方法,其在名为 Gemini 的系统中,利用改进的 Structure2Vec [31]模型(Structure2Vec 结构感知模型的灵感来自图形模型推理算法,该算法根据图的拓扑结构递归地聚合得到特定于顶点的特征向量)来构成暹罗架构网络,将函数的CFG嵌入高维向量,通过计算向量的余弦距离来度量函数间的相似性。
- Ding 等[32]提出的Asm2vec模型,是基于改进的 PV-DM[33]模型(PV-DM神经网络模型是为文本数据而设计的,基于文档中的标识来学习文档表征)的汇编代码表征学习模型。该模型利用自定义的函数内联和随机漫步机制,将函数的CFG建模为汇编指令的线性序列,以该汇编文本为输入,不需要任何先验知识,学习指令的语义,构建指令嵌入向量,最终得到函数的语义嵌入向量。Asm2vec是第一个将表征学习作为汇编代码构建特征向量的方案,具有优秀的抗混淆和抗编译器优化特性,但其不能用于跨架构比较。
- Luca 等提出的 SAFE 网络,先利用 NLP 的 word2vec[34] 模型(word2Vec 是谷歌于2013年推出的一款 NLP 工具,其特点是能将单词向量化,定量度量单词之间的关系[35])来实现汇编语言的指令嵌入,再利用循环神经网络(Recurrent Neural Network, RNN)来捕获指令序列的上下文关系[36],最终实现函数的嵌入,SAFE摒弃了CFG作为中间表示,汇编代码的语义信息直接由深度神经网络嵌入到高维向量中,这样既省去了会消耗大量时间的CFG提取过程,又避免了人为偏见的引入。SAFE模型利用RNN网络构建了暹罗架构并采用有监督的学习方法来训练模型,虽然其可以由程序自动随机构造正样本对和副样本对,但针对跨架构检测任务的训练,随着系统支持指令架构种类的增加,在理论上,其训练样本库的规模需要根据不同架构的组合成倍地扩大,这就限制了该模型的可扩展性。
- GeneDiff[37]是一种使用语义表征模型来学习二进制代码中间语言,从而实现跨架构克隆检测研究的方法。它借助动态分析VEX IR(VEX IR是动态分析框架Valgrind[38-39]的中间语言,是一种二地址形式的中间表示,支持多种指令架构)的中间语言消除了不同指令架构之间的差异,通过改进的PV-DM模型来为函数的VEX IR生成语义以嵌入向量。因为每一条汇编指令会被翻译成多条VEX指令,所以该模型将由同一条汇编指令翻译而来的多条VEX指令组合看作一个单词,将基本块看作句子,将函数看作段落,使用向量间的余弦距离来度量函数间的相似度。
除了检测速度和准确度的提升,将神经网络应用于相似性检测任务的优势还在于,传统检测方法所采用的匹配算法通常是固定不变的,神经网络可以针对不同任务进行再训练,应用场景更广阔;此外,神经网络不但可以自行学习和选择特征,还可以习得人工方法很难确定的不同特征对相似度影响的权重,从而降低甚至避免人工设计和筛选特征带来的拟合。但也存在难题如下:
- 神经网络训练需要大量的训练数据,如何针对不同任务构造高质量的训练集仍然充满挑战;
- 神经网络的输出是多维度向量,而向量中每一维度所代表的具体含义还无法进行科学的解释,且其结果无法用符号化的表达式或定理来证明。
3.5 方案对比
在实际应用中,单一检测技术取得的效果十分有限,因此往往采用多种检测技术相结合的方法来适应不同任务的需要。
- 技术分类的对比
- 二进制代码相似性检测技术及工具的对比(.代表解决,-代表未解决)
4 总结
跨编译器、跨操作系统、跨指令架构和海量数据查询等任务需求的提出,如何提高检测的通用性和效率成为更突出的问题。传统相似性检测方法由于多采用图形结构的中间表示,受限于图匹配算法的效率,只能选择在准确度和速度之间做平衡,对相似性度量算法效率的提升逐渐陷入瓶颈。
近几年,越来越多的研究借助于DNN突破了这一瓶颈。神经网络的优势在于不仅可以自动学习和筛选影响相似性的特征,还可以确定不同特征对相似性影响的大小,该方法的关键在于如何处理原始代码和选择哪种中间表示更利于神经网络的训练和学习,以及哪种神经网络模型更适合程序相似度模型的构建。