Kexin Pei Columbia University
Zhou Xuan University of California, Riverside
Junfeng Yang Columbia University
Suman Jana Columbia University
Baishakhi Ray Columbia University
检测语义上相似的功能——一种具有广泛现实世界安全用途的关键分析能力,包括漏洞检测、恶意软件分类和取证——需要理解函数的行为和意图。然而,这项任务具有挑战性,因为语义相似的函数可以以不同的方式实现,在不同的体系结构上运行,并使用不同的编译器优化或混淆进行编译。大多数现有的方法基于语法特征匹配函数,而不了解函数的执行语义。
我们提出了一个基于迁移学习的框架TREX,它可以从函数的微轨迹(一种欠约束的动态轨迹)中显式地自动学习执行语义,并将学习到的知识转移到语义相似的函数中。虽然已知这种微跟踪太不精确,无法直接用于检测语义相似性,但我们的关键见解是,这些跟踪可用于教授ML模型不同指令序列的执行语义。因此,我们设计了一个无监督的预训练任务,该任务训练模型从函数的微轨迹中学习执行语义,而无需任何手动标记或特征工程工作。然后,我们开发了一种新的神经结构,分层transformer,它可以在预训练阶段从微轨迹中学习执行语义。最后,我们对预训练模型进行微调以匹配语义相似的函数。
我们对来自13个流行软件项目的1,472,066个函数二进制文件进行了TREX评估。这些函数来自不同的体系结构(x86、x64、ARM和MIPS),并使用4种优化(0- 3)和5种混淆进行编译。TREX在跨架构、优化和混淆功能匹配方面分别比最先进的系统高出7.8%、7.2%和14.3%,同时运行速度快8倍。我们的研究表明,预训练任务显著提高了函数匹配性能,强调了学习执行语义的重要性。此外,我们广泛的案例研究展示了TREX的实际用例——在180个最新版本的真实固件映像上,TREX发现了以前任何研究都没有披露的16个漏洞。我们在https: //github.com/CUMLSec/trex上发布TREX的代码和数据集。
一句话:基于迁移学习的Trex,通过函数的micro-traces学习执行语义
当为安全关键应用程序(例如,漏洞发现)匹配语义相似的功能时,我们通常必须在二进制级别处理软件,例如商业现成产品(例如,固件映像)和遗留程序。然而,这项任务具有挑战性,因为函数的高级信息(例如,数据结构定义)在编译过程中被删除。当将函数编译为运行在具有各种编译器优化或使用简单转换混淆的不同指令集架构上时,建立语义相似性变得更加困难。
最近,基于机器学习(ML)的方法在解决这些挑战方面显示出了希望[25],[50],[77],通过学习可以识别不同架构,编译器优化甚至某些类型混淆的相似函数二进制文件的鲁棒性特征。具体来说,ML模型从函数二进制文件中学习函数表示(即嵌入),并使用两个函数嵌入之间的距离来计算它们的相似性。距离越小,函数之间越相似。这些方法已经取得了最先进的结果[25],[50],[77],优于传统的基于签名的方法[79],使用手工制作的特征(例如,基本块的数量)。这种基于嵌入距离的策略对于大规模的函数匹配特别有吸引力——搜索超过一百万个函数只需要大约0.1秒[30]。
尽管取得了令人印象深刻的进展,但这些方法仍然具有挑战性,无法将语义相似的功能与不同的语法和结构相匹配[51]。一个内在的原因是代码语义的特征是其执行效果。然而,所有现有的基于学习的方法都与程序执行语义无关,只在静态代码上进行训练。这样的设置很容易导致模型匹配简单的模式,当这些虚假模式缺失或改变时,会限制模型的准确性[1],[61]。
关注语法特征的ML模型可能会选择共同的子字符串(两个序列共享标记mov、eax、lea、ecx)来建立它们的相似性,这并没有编码语义等价的关键原因。如果不掌握近似的执行语义,ML模型可以很容易地学习这种虚假的模式,而不了解等效的内在原因:当eax为2时,[eax+eax*2]计算的地址与[eax+4]完全相同。
现有的动态方法试图通过直接比较函数的动态行为来确定相似性来避免上述问题。由于寻找到达目标函数的程序输入是一项极具挑战性和耗时的任务,因此先前的工作通过用随机值初始化函数输入状态(例如寄存器、内存)并直接执行目标函数来执行约束下的动态执行[27]。不幸的是,直接使用这种不受约束的执行轨迹来计算函数相似度往往会导致许多误报[25]。例如,向两个具有严格输入检查的不同函数提供随机输入可能总是会触发类似的浅层异常处理代码,并且可能看起来非常相似。
本文提出了TREX(迁移学习执行语义),它训练ML模型从受限的动态轨迹中学习近似的执行语义。与之前使用这些轨迹直接测量相似性的工作不同,TREX在不同的轨迹上预训练模型,以了解每个指令在其上下文中的执行效果。然后,TREX通过将从预训练中学习到的知识转移到匹配语义相似的函数来对模型进行微调(见图1)。我们的大量实验表明,预训练中近似学习到的执行语义知识显著提高了匹配语义相似函数二进制文件的准确性——TREX在匹配来自不同架构、优化和混淆的函数方面表现出色。
我们的主要观察是,虽然约束下的动态执行跟踪倾向于包含许多不可行状态,但它们仍然编码许多单个指令的精确执行效果。因此,我们可以训练一个ML模型来观察和学习从不同函数收集的大量欠约束动态轨迹中呈现的不同指令的效果。一旦模型对各种指令的执行语义有了大致的理解,我们就可以利用它学到的知识来训练它来匹配语义上相似的函数。因此,在推理过程中,我们不需要在匹配它们时实时执行任何函数,这节省了大量的运行时开销。此外,我们的训练模型不需要约束下的动态跟踪来匹配函数,它只使用函数指令,但它们被丰富的执行语义知识所增强。
我们在4种架构(x86、x64、ARM和MIPS)的13个流行开源软件项目中收集的1,472,066个函数上对TREX进行了评估,并使用4种优化(0- o3)和5种混淆策略进行了编译[78]。做出以下贡献。
(1) 提出了一种匹配语义相似函数的新方法:我们首先训练模型从微轨迹(一种欠约束动态轨迹)中学习近似的程序执行语义,然后将学习到的知识转移到识别语义相似的函数。
(2) 扩展了微执行来支持不同的体系结构,以收集微痕迹进行训练。然后,我们开发了一种新的神经结构-分层变压器-从微轨迹中学习近似的执行语义。
(3) 我们在13个流行的软件项目和库中的1,472,066个函数上实现了TREX并对其进行了评估。TREX在跨架构、优化和混淆功能匹配方面的性能分别比最先进的工具高出7.8%、7%和14.3%,同时运行速度提高了8倍。此外,TREX还帮助发现了180个真实固件映像中的16个漏洞,这些漏洞是以前的研究没有披露的最新版本。我们在https://github.com/CUMLSec/trex上发布TREX的代码和数据集。
我们使用三个语义等价但语法不同的函数对来演示仅从静态代码学习的一些挑战。图2显示了每个函数的(部分)汇编代码。
我们假定在比较二进制文件时不能访问调试符号或源代码。事实上,有许多方法可以从剥离的二进制文件中重构函数[4],[6],[24],[62],[72]。此外,我们假设二进制文件可以很容易地反汇编,也就是说,它不会被基于虚拟化的混淆器打包或转换[73],[74]。
我们认为两个语义相似的函数具有相同的输入-输出行为(即,给定相同的输入,两个函数产生相同的输出)。即认为编译自相同源码的函数为相似。
我们通过Godefroid[34]实现微执行来处理x64, ARM和MIPS,其中原始论文仅将x86描述为用例。在下文中,我们将简要解释如何微执行单个函数二进制,重点介绍处理不同类型指令的关键算法。
为了抽象出不同体系结构的汇编语法的复杂性,我们引入了一个低级的中间表示(IR)来对函数汇编代码进行建模。我们只包含语言细节的一个子集来说明实现算法。图3显示了IR的语法。这里的IR只是为了方便讨论我们的微跟踪实现。在我们的实现中,我们使用真实的汇编指令并将它们标记为模型的输入。
值得注意的是,我们用 l o a d ( e ) load(e) load(e) 和 s t o r e ( e v , e a ) store(e_v, e_a) store(ev,ea) 来表示内存读写。即,将值表达式 e v e_v ev 存储到地址表达式 e a e_a ea,这是从 load store架构 (即ARM, MIPS) 和寄存器内存架构(即x86)中推广出来的。这两种操作都可以接受 e e e 作为输入——一个表达式,它可以是一个显式十六进制数(表示地址或常量)、一个寄存器或两个寄存器上操作的结果。
我们使用jmp来表示一般跳转指令,它可以是直接跳转或间接跳转(即表达式 e a e_a ea 可以是常量 c c c 或寄存器 r r r),跳转指令也可以是无条件的或条件的。因此,jmp中的第一个参数是条件表达式 e c e_c ec,无条件跳转将 e c e_c ec 设置为true。我们通过call和ret表示函数调用和返回,其中call由表达式参数化,表达式可以是地址(直接调用)或寄存器(间接调用)。
算法1概述了微跟踪给定函数的基本步骤。首先,它初始化内存以加载代码和相应的堆栈。然后初始化除专用寄存器(如堆栈指针或程序计数器)以外的所有寄存器。如果指令访问内存(即读/写),我们按需映射内存地址。如果指令从内存中读取,则在特定的内存地址中进一步初始化一个随机值。对于调用/跳转指令,我们首先检查目标地址并跳过无效的跳转/调用,称为“forced execution”[63]。通过跳过不可达的跳转和调用,它可以一直执行函数直到函数结束,并暴露更多的行为,例如,跳过潜在的输入检查异常。因为nop指令可以作为函数内指令之间的填充,所以我们直接跳过nop。当微跟踪执行完所有指令、到达 ret 或超时时,我们终止微跟踪。
图13和14演示了实际函数的示例微轨迹。
形式上,给定函数 f f f (即汇编代码) 及其微跟踪 t t t (通过微执行 f f f),我们准备模型输入 x x x,由5种具有相同大小 n n n 的令牌序列组成。图4显示了模型输入示例以及分层 Transformer 如何屏蔽和处理它们以预测相应的输出作为预训练任务。
Micro-trace code sequence
第一个序列 x f x_f xf 是汇编码序列: x f = { m o v , e a x , + , . . . } n x_f = \{mov, eax, +, ...\}^n xf={mov,eax,+,...}n,通过对微跟踪中的汇编指令进行标记生成。我们将汇编指令中出现的所有符号都视为tokens。这样的标记化旨在保留汇编指令的语法和语义的关键提示。
我们对汇编代码中出现的数值进行特殊处理。将数值作为常规文本标记处理可能会导致非常大的词汇表大小,例如,在32位体系结构上有 2 32 2^{32} 232 个可能性。为了避免这个问题,我们将所有数值移动到微跟踪值序列中,并将它们替换为一个特殊的token num(例如,图4中输入的最后一个token)。通过所有这些预处理步骤, x f x_f xf 在所有体系结构中的词汇表大小为3300。
Micro-trace value sequence
第二个序列 x t x_t xt 是微跟踪值序列,其中 x t x_t xt 中的每个标记都是微跟踪相应代码的动态值。正如第二节所讨论的,我们在 x t x_t xt 中保留显式值(而不是现有方法使用的虚拟值)。值得注意的是,我们在执行指令之前使用每个token(例如,寄存器)的动态值。例如,在mov eax, 0x8; Mov eax, 0x3;第二个eax的动态值是0x8,因为我们在Mov eax之前取eax的值,0x3被执行。对于没有动态值的代码标记,例如mov,我们使用虚拟值。
Position sequences
每个代码和值标记的位置对于推断二进制语义至关重要。与自然语言不同,交换两个单词可以大致保持相同的语义,交换两个操作数可以显著改变指令。为了将位置的归纳偏置编码到我们的模型中,我们引入了指令位置序列 x c x_c xc 和操作码/操作数位置序列 x o x_o xo 来表示指令之间和每个指令内的相对位置。如图4所示, x c x_c xc 是一个整数序列,用于编码每条指令的位置。单个指令中的所有操作码/操作数共享相同的值。 x o x_o xo 是一个整数序列,用于在单个指令中编码每个操作码和操作数的位置。
Architecture sequence
最后,我们为模型提供一个额外的序列 x a x_a xa,描述输入二进制的指令集架构。 x a x_a xa 的词汇表包含4种体系结构 x a = { x 86 、 x 64 、 A R M 、 M I P S } n x_a = \{x86、x64、ARM、MIPS\}^n xa={x86、x64、ARM、MIPS}n。这个设置有助于模型区分不同体系结构的语法。
Encoding numeric values
如上所述,将具体值视为独立的标记可能导致词汇表过大。我们设计了一种分层输入编码方案来解决这一挑战。具体来说,设 x t i x_{t_i} xti表示 x t x_t xt 中的第 i i i 个值。我们将 x t i x_{t_i} xti 表示为(填充的)8字节固定长度字节序列 x t i x_{t_i} xti = {0x00,…, 0xff } 8 \}^8 }8以大端序排序。然后,我们将 x t i x_{t_i} xti提供给一个双层双向LSTM(bi-LSTM),并将其最后一个隐藏单元的嵌入作为值表示 t i = b i − L S T M ( x t i ) t_i = bi-LSTM(x_{t_i}) ti=bi−LSTM(xti)。
给定一个函数对,我们将每个函数的静态代码(而不是第1节讨论的微迹)提供给预训练的模型 g p g_p gp,并获得 g p g_p gp 的最后一个自关注层产生的嵌入序列对:
我们还使用Hikari[78]在x64(基于clang-8的混淆器)上使用5种类型的混淆(OBF)对所有项目进行混淆。这些混淆包括伪造控制流(bcf)、控制流平坦化(cff)、基于寄存器的间接分支(ibr)、基本块分割(spl)和指令替换(sub)。由于我们在使用Hikari(基于Clang)[78]和基线系统(即Asm2Vec[25])的交叉编译中遇到了一些错误,我们只比较了x64上的计算结果,因此我们只对x64限制了混淆的二进制文件。结果,有1,472,066个函数,如表1所示。
我们通过Unicorn实现微跟踪,Unicorn是一个基于QEMU的跨平台CPU模拟器。我们用不同的初始化寄存器和内存微执行每个函数3次,生成3个微跟踪(包括静态代码和动态值)用于预训练(掩码LM)。我们利用多处理来并行微执行每个函数,并设置30秒作为每次运行的超时,以防任何指令卡住(即无限循环)。对于每个函数(有3个微跟踪),我们附加一个额外的虚拟跟踪,它只由虚拟值(##)组成。这种设置鼓励模型利用其学习到的执行语义(来自其他具有具体动态值的跟踪)来预测仅使用代码上下文的掩码,这有助于微调任务,因为我们只将函数的静态代码作为输入。
[1] Hojjat Aghakhani, Fabio Gritti, Francesco Mecca, Martina Lindorfer, Stefano Ortolani, Davide Balzarotti, Giovanni Vigna, and Christopher Kruegel. When malware is packin’ heat; limits of machine learning classifiers based on static analysis features. In 2020 Network and Distributed Systems Security Symposium, 2020.
[4] Dennis Andriesse, Asia Slowinska, and Herbert Bos. Compiler-agnostic function detection in binaries. In Proceedings of the 2017 IEEE European Symposium on Security and Privacy, 2017.
[6] Tiffany Bao, Jonathan Burket, Maverick Woo, Rafael Turner, and David Brumley. Byteweight: Learning to recognize functions in binary code. In 23rd USENIX Security Symposium, 2014.
[24] Alessandro Di Federico, Mathias Payer, and Giovanni Agosta. rev.ng: A unified binary analysis framework to recover CFGs and function boundaries. In 26th International Conference on Compiler Construction, 2017.
[25] Steven HH Ding, Benjamin CM Fung, and Philippe Charland. Asm2vec: Boosting static representation robustness for binary clone search against code obfuscation and compiler optimization. In 2019 IEEE Symposium on Security and Privacy, 2019.
[27] Manuel Egele, Maverick Woo, Peter Chapman, and David Brumley. Blanket execution: Dynamic similarity testing for program binaries and components. In 23rd USENIX Security Symposium, 2014.
[30] Qian Feng, Rundong Zhou, Chengcheng Xu, Yao Cheng, Brian Testa, and Heng Yin. Scalable graph-based bug search for firmware images. In 2016 ACM SIGSAC Conference on Computer and Communications Security, 2016.
[34] Patrice Godefroid. Micro execution. In 36th International Conference on Software Engineering, 2014.
[50] Luca Massarelli, Giuseppe Antonio Di Luna, Fabio Petroni, Roberto Baldoni, and Leonardo Querzoni. Safe: Self-attentive function embeddings for binary similarity. In International Conference on Detection of Intrusions and Malware, and Vulnerability Assessment, 2019.
[51] Derrick McKee, Nathan Burow, and Mathias Payer. Software ethology: An accurate and resilient semantic binary analysis framework. arXiv preprint arXiv:1906.02928, 2019.
[61] Mathias Payer, Stephen Crane, Per Larsen, Stefan Brunthaler, Richard Wartell, and Michael Franz. Similarity-based matching meets malware diversity. arXiv preprint arXiv:1409.7760, 2014.
[62] Kexin Pei, Jonas Guan, David Williams King, Junfeng Yang, and Suman Jana. Xda: Accurate, robust disassembly with transfer learning. In Proceedings of the 2021 Network and Distributed System Security Symposium (NDSS), 2021.
[63] Fei Peng, Zhui Deng, Xiangyu Zhang, Dongyan Xu, Zhiqiang Lin, and Zhendong Su. X-force: force-executing binary programs for security applications. In 23rd USENIX Security Symposium, 2014
[72] Eui Chul Richard Shin, Dawn Song, and Reza Moazzezi. Recognizing functions in binaries with neural networks. In 24th USENIX Security Symposium, 2015.
[73] VMPROTECT SOFTWARE. VMProtect Software Protection. http://vmpsoft.com.
[74] Xabier Ugarte-Pedrero, Davide Balzarotti, Igor Santos, and Pablo GBringas. SoK: Deep packer inspection: A longitudinal study of the complexity of run-time packers. In 2015 IEEE Symposium on Security and Privacy, 2015.
[77] Xiaojun Xu, Chang Liu, Qian Feng, Heng Yin, Le Song, and Dawn Song. Neural network-based graph embedding for cross-platform binary code similarity detection. In 2017 ACM SIGSAC Conference on Computer and Communications Security, 2017.
[78] Naville Zhang. Hikari – an improvement over Obfuscator-LLVM. https://github.com/HikariObfuscator/Hikari, 2017.
[79] Zynamics. BinDiff. https://www.zynamics.com/bindiff.html, 2019
(1) unicorn 随机三个输入获取traces
(2) 使用architecture信息训练序列模型对不同架构的识别能力