本文参考:
[1]方磊,武泽慧,魏强.二进制代码相似性检测技术综述[J].计算机科学,2021,48(05):1-8.
(信息工程大学数学工程与先进计算国家重点实验室, 国家重点研发课题,北大核心)
代码相似性检测常用于代码预测、知识产权保护和漏洞搜索等领域,可分为源代码相似性检测和二进制代码相似性检测。软件的源代码通常难以获得,因此针对二进制代码的相似性检测技术能够适用的场景更加广泛。
根据关注的代码信息的不同,当前的二进制代码相似性检测技术分为4类:基于文本、基于属性度量、基于程序逻辑、基于语义的检测技术。
需要解决的难题:跨编译器、跨编译器优化配置、跨指令架构检测等。
代表性方法和工具:Karta, discovRE,Genius, Gemini,SAFE等。
代码相似性的概念源于软件分析技术,目前还没有标准和权威的定义。通常认为,如果一段代码是由另一段代码复制或经过一定规则变换而来的,则认为它们是相似的。二进制代码的相似性是指由同一或相似的源代码编译得到的不同二进制代码是相似的。
代码相似性检测通常应用于但不限于代码预测(即根据现有的代码预测可能的代码修改和更新,提供代码模板推荐)、知识产权保护(即发现代码中未经授权的代码的复用和克隆,协助软件所有者保护知识产权或规避潜在的侵权行为)、漏洞搜索(如脆弱代码搜索、软件漏洞追踪和定位)。
whale于1988年提出代码相似性检测过程分为两个阶段:代码格式转换和相似度确定。具体展开又有4个部分:代码预处理、中间表示转换、比较单元生成和度量算法匹配。
二进制代码通常由源代码编译而来,源代码级的复制和修改也会部分体现在相应的二进制代码中。代码克隆(也称代码抄袭)和混淆手段虽然多应用于源代码的复制和修改,但也可以用于可执行二进制程序或组件的篡改或修补。除此之外,同一源代码经过不同的编译器和优化配置,针对不同的硬件平台,编译得到的二进制代码不尽相同,因此二进制代码相似性检测会遇到其特有的难题,且该难题较源代码检测更为复杂。
在检测由同一源代码编译得到的二进制代码时,可能会遇到以下难题:
难题1 跨编译器问题。由于设计目的和采用的算法不同,不同的编译器产生的二进制代码不尽相同。例如,虽然寄存器的选择过程是通过各种启发方式和复杂的代码来传递驱动的,但在某些情况下,即使存在公共约定,编译器也不会完全遵守。这些都导致了寄存器选择的任意性。
难题2 跨编译优化配置问题。同一版本的编译器采用不同的编译优化配置所产生的二进制代码不尽相同。例如,可执行程序的调试版本相比发行版本会增加许多与调试相关的信息和调用。不同的优化等级,其区别在于增加或减少了对堆栈的检查和清理等操作,会导致最终的可执行文件的长度发生显著变化。再比如在x86-64架构下的GCC编译器,只有在-O0不优化的编译条件下,寄存器rbp才具有帧指针的特殊用途。
难题3 跨指令架构问题。不同指令架构的二进制代码不同。不同架构的指令集、寄存器、机器字长都有较大差异,从而导致相应的二进制代码差异巨大。
这些二进制代码来自同一源代码,因此具有相同的功能且语义等价,但其汇编指令和语法可能略有差异甚至截然不同。二进制代码相似性检测需要根据不同应用场景来屏蔽噪声干扰,提取代码中需要关注的信息。
基于标识符的检测:(需要反汇编)首先模糊通用寄存器名和内存地址,然后提取指令的操作码和操作数,或者提取字符串以生成标识符序列,最后通过子序列匹配来判断不同代码是否相似。
基于字节流的检测:不对二进制代码进行反汇编,直接比较程序的二进制字节流是否相近。
基于文本的检测因为不考虑程序的语法和语义等信息,所以其原理和实现较其他技术更简单,其时空复杂度也较低。该类技术主要针对未应用复杂混淆手段的代码克隆和复用,该类检测方法由于容易实现对抗检测,因此常作为一种高效的辅助检测手段。
基于属性度量的检测技术关注程序汇编代码中可度量的属性和特征,提取属性和特征的度量值,构成多维向量(该方案认为特征向量能够从不同维度标识代码段),以特征向量之间的距离来度量代码段之间的相似度大小。这类方法需要确定检测的最小粒度(如基本块或函数),在最小粒度范围内通过对属性和特征的统计计数来获得度量值。度量值可能是但不限于是特定常量和指令等标识、函数的输入和输出等对象的计数。
基于属性度量的检测方法因为不考虑程序的逻辑结构,所以其统计特征易于实现,但其提取的信息十分有限,因此常作为一种辅助方法。主要问题是,属性特征是人为设计和筛选的,难以保证各属性都处于不同的维度,不同属性之间可能存在拟合问题,因此盲目地增加属性特征的种类,不仅可能无法有效提高检测的准确度,而且会增加计算开销。
基于程序逻辑的检测技术:利用列表、树形或图形等数据结构来记录和描绘程序的数据流或控制流信息(该类技术认为所得的中间表示能够捕获程序中一定的语法和语义信息),通过匹配相似的序列、节点或边、公共子树或子图来寻找逻辑或功能相似的程序基本块或函数。
程序的数据逻辑在函数内部体现为数据的流向和运算,在函数外部体现为函数的输入和输出。
程序的控制逻辑在函数间可以用函数调用序列来描述,在函数内部可以用CFG来描绘,在基本块级可以用逻辑树来表示。
基于程序逻辑的检测方法的优点是准确度较高,可以根据不同的任务采用不同的匹配算法和策略,可伸缩性强。其缺点是时空复杂度高:一方面,数据流和CFG的提取过程代价非常昂贵;另一方面,相似性度量所依赖的图匹配算法的时间复杂度缺少多项式解,图匹配算法又多是两两匹配算法,因此在面对大规模查询任务时,计算量随代码库规模成几何倍数式增长。
针对该问题,一种方法是从中间表示CFG入手,利用其他辅助检测技术来为CFG引入轻量级且易于度量的属性,从而简化CFG的节点,例如discovRE利用轻量级的统计特征来筛选候选函数,降低图匹配算法的任务量。另一种方法是从图匹配算法入手,采用近似图匹配算法来提高检测效率,在准确度和速度之间进行折中,但该方法受限于图匹配算法的效率,对检测效率的提升有限。近年来神经网络技术被用于该领域,以解决传统图匹配算法遇到的效率瓶颈问题。具代表性的是Qian等提出的Genius系统,该系统利用机器学习的谱聚类算法[27]来对函数的CFG进行分类并生成码本(Codebook),再对码本进行编码,将相似函数的搜索问题转化为特征编码的搜索问题,极大地提高了效率并兼顾了结果的准确度。
“语义”的概念来自自然语言,若将汇编语言的指令看作“单词”,基本块看作“句子”,则可得到相应的二进制代码的“词法”和“语法”概念,从而函数具有完整的“语义”,但是CFG主要包含程序的控制流信息,该信息仅是函数所包含的丰富语义信息的一部分。
本文中的基于语义的检测技术,通过捕获程序汇编代码中的语义信息,来比较函数或组件的语义差异,以实现相似性度量。这类方法通常借鉴自然语言处理(Natural Language Processing, NLP)、图像识别或其他领域的技术,利用深度神经网络来实现程序语义的嵌入,通过对嵌入向量的比较或查询操作来实现大规模任务的处理。其二进制代码的中间表示包括规范化的汇编文本、其他中间语言或CFG,神经网络模型多采用暹罗架构(Siamese Architecture)[30],利用程序代码的大规模特点构造来训练样本库,相关领域的专家和先验知识可能不是必须的。
除了检测速度和准确度的提升,将神经网络应用于相似性检测任务的优势还在于,传统检测方法所采用的匹配算法通常是固定不变的,神经网络可以针对不同任务进行再训练,应用场景更广阔;此外,神经网络不但可以自行学习和选择特征,还可以习得人工方法很难确定的不同特征对相似度影响的权重,从而降低甚至避免人工设计和筛选特征带来的拟合。但也存在难题如下:
在实际应用中,单一检测技术取得的效果十分有限,因此往往采用多种检测技术相结合的方法来适应不同任务的需要。
跨编译器、跨操作系统、跨指令架构和海量数据查询等任务需求的提出,如何提高检测的通用性和效率成为更突出的问题。传统相似性检测方法由于多采用图形结构的中间表示,受限于图匹配算法的效率,只能选择在准确度和速度之间做平衡,对相似性度量算法效率的提升逐渐陷入瓶颈。
近几年,越来越多的研究借助于DNN突破了这一瓶颈。神经网络的优势在于不仅可以自动学习和筛选影响相似性的特征,还可以确定不同特征对相似性影响的大小,该方法的关键在于如何处理原始代码和选择哪种中间表示更利于神经网络的训练和学习,以及哪种神经网络模型更适合程序相似度模型的构建。