读Function Type Signatures

1. Introduction

对可执行代码的二进制码分析是一个计算机安全的经典问题。COTS二进制码的源代码通常得不到。由于编译器是不会保存比如types之类的语言信息,在汇编过程中,reverse engineering(反编译)需要从二进制代码中反向得到源代码的语义信息。从机器代码里重新获得语义在code hardening, bug-finding, clone detection, patching/repair 和 analysis领域都很重要。二进制代码分析包括静态反编译运算指令和恢复流程、数据结构、全面的语义信息。想要的到越高的语义和更细节的分析就要更多的技术来支持。

商业化二进制代码分析工具在工业界的广泛应用依靠于编译器领域知识的专门的二进制代码分析。识别二进制码中的编程习惯用语和设计分析的流程需要依靠人类的大量经验。代码分析的工具也需要随着编译器和计算机系统结构持续更新。这个项目中我们通过训练机器来从二进制代码中直接提取出特征,不需要指定编译。这篇文章就是讨论是否可以跳过compiler idioms和instructions,通过deep learning直接分析二进制代码来得到特征,尤其是function types和signatures。这个问题在contro-flow hardening和data-dependency analysis里面都经常用到。

function type recovery主要分为两种:1.函数参数的数量和2.函数参数的类型。这篇文章主要讨论的是函数参数的数量和C语言里面的基本数据类型(int, float, char, pointers, enum, union, struct)。我们首先通过反编译二进制代码来得到函数(functions/bodies)。目标是"perform type recovery without explicitly encoding any semantics specific to the instruction set being analyzed or the conventions of the compiler used to produce the binary."

Approach:

我们用一个RNN模型来从反汇编的二进制代码里面学到函数类型。目标就是来证明,如果没有编译器或者instruction set的相关信息,neural network是不是可以学到函数类型。这个设计过程其实是实验性的并且有些随机,经历了多次的试错以及模型和参数的选择。比如我们尝试过把disassembled code直接用于input输入模型,作为one-hot encoded input输入,还尝试了不同的epoch和network depth。但这些都没有什么明显的效果。

所以我们选定最终模型的原则是它的可解释性(explicability):让模型学到的东西是“可解释的”和“可比较的”。为了证明结果是正确的,我们用了类比推论、降维和显著图的方法来展示训练的模型有意义的。EKLAVYA可以从整体上获取二进制代码分析的多个特征和特定的函数类型。模型的设计是模块化的,这样它的dependency指令集的获取和特定的函数类型的恢复过程就是相对独立的。EKLAVYA是第一个基于neural network的function signature recovery task,它对debugging和binary analysis 系统的设计都很有帮助。

Results

EKLAVYA的测试用到了在不同的optimization level编译过的大量的Linux x86和x64二进制代码,结果也是很显著的。在恢复函数的参数数量上和种类上分别取得了84%和81%的准确率。其次,EKLAVYA generalized了编译器,包括在clang和gcc编译的代码,以及x86和x64的二进制代码,而且效果不亚于之前需要编译器和instruction set的信息的方法。第三,它的模型是可解释的,比如哪些输入决定了它的输出结果。这些features和现有的工具得到的也match。

EKLAVYA的结构和现有的nlp的结构都很相似。尤其是word embedding还很有效。

Contribution

EKLAVYA是一个基于rnn的模型,他可以从一个x86/x64的函数机器码中恢复得到函数的类型。EKLAVYA可以不需要多余的信息,类比不同的编译器,训练不同instruction sets。而且它的训练方式是可解释的,准确率也不亚于传统的训练方法。

2. Problem Overview

function type recovery包括从二进制代码中识别函数的参数数量和参数的类型。这通常是构建contro-flow graph和进行内部数据依赖分析的一个步骤。经常用于binary analysis和hardening tools。

传统的函数恢复方法要找到每个指令的明确含义。比如某一个byte表示的是一个"push"的指令,这个指令可以操作任何register argument。(说白了就是我们要想定位这些参数,就需要知道每一个指令是干嘛的。这个就是semantics of instructions. 图1下面讲的就是那些rules)。

这个模型的input就是一个function,我们想要得到这个function的function type。这个function是用disassemble的形式表示的。每一个函数是一串instructions,每个instructions又是一串bytes。(我理解disassembled就是分散开的。比如我们看到一个完整的函数是有序的instructions和operands一行一行的,但disassemble之后它们就都打散了,变成一个一个单独的instructions。这个输入就是还是function的二进制代码)。

T_a: 是function a 的disassembled code

T_a[i]:是function a 的第i个byte

那么第k个instruction就是可以表示为:I_a[k] = <T_a[m], T_a[m+1], ..., T_a[m+l]>

一个包含p个instructions的function就可以表示为:T_a = <I_a[1], I_a[2], ..., I_a[p]>

通过call instruction,我们可以得到有哪些functions调用了函数a 。如果函数b直接调用了函数a, 我们可以得到函数b里面所有在call a之前的instructions。这些instructions就叫caller snippet C_{b, a}[j] = <I_b[0], I_b[1], ... , I_b[j - 1]>. (这里我觉得就是把Call这个instruction当成一个label,通过它来找到函数b里面调用函数a之前的那些instructions,所以下标都是b,因为这些都是b的instructions)。如果是indirect call,那就为空集。用这个方法找到所有的

通过call instruction,我们可以得到有哪些functions调用了函数a 。如果函数b直接调用了函数a, 我们可以得到函数b里面所有在call a之前的instructions。这些instructions就叫caller snippet C_{b, a}[j] = <I_b[0], I_b[1], ... , I_b[j - 1]>. (这里我觉得就是把Call这个instruction当成一个label,通过它来找到函数b里面调用函数a之前的那些instructions,所以下标都是b,因为这些都是b的instructions)。如果是indirect call,那就为空集。用这个方法找到所有的\mathcal D_a

所以现在的目标就是得到一个模型\mathcal M, 给定\mathcal D_a可以通过模型\mathcal M得到函数a的参数种类的数量。 

Design

 EKLAVYA分成两个模块。1. instructions embedding 和 2. 参数恢复模块。instruction embedding通过观察instructions的用法来学到semantics。其实合成一步也是可以的,但EKLAVYA先从binaries里面得到semantics,和后面的分析工作区分开。这样embedding就可以在不同的二进制分析工作中重复使用。而且输出semantics也可以让我们单独地看到每一步的正确性。

instruction embedding module 的输入是一串instructions,作为symbols。每一个instruction的输出是256维的vector。这样vectors之间的距离就可以反映instructions之间的联系。

recovery module是一个rnn模型,输入是一个函数的一串instructions用vector表示。这个module包括4个tasks:

  1. 通过函数的caller的instructions来计算函数参数的数量
  2. 通过函数的callee的instructions来计算函数参数的数量
  3. 通过函数的caller的instructions来恢复函数的类型
  4. 通过函数的callee的instructions来计算函数的类型

我们为每一个task训练一个模型。

Instruction embedding module

第一步就是要恢复每个instruction 的semantic information。这个算法的输入是函数的二进制形式,并且已知函数和instruction的边界。在这种表示下我们是不知道任何高级的语义信息的。我们希望通过contexual use来推断出instructions的语义。(这个地方就是说每个instructions都是01代码,看不出有什么含义。但我们可以通过每组01代码出现的位置/次数/前后关系来推断含义。)EKLAVYA用word embedding来将每个instruction的0/1代码转化成vector,vector之间的距离代表instruction之间的联系。比如"push %edi"和"pop %edi"之间的距离就与"push %esi"和"pop %esi"之间的距离很相似。

我们用skip-gram negative sampling来train 这个word embedding模型。(这里讲的太复杂了,举个例子就明白了。)比如instruction是push %ebp,这个instruction的16进制opcode是0x55. 这个0x55就是一个word,和“apple”是一回事,和数字没有任何关系。

Argument Recovery Module

这个部分要训练4个模型,就是刚才说的4个task。每个task都train一个rnn。训练的时候就用word embedding得到的sequence of vectors, 每个函数的参数个数, 和参数的类型。这里我们用一个RNN去学第一个参数,第二个RNN去学第二个参数, 等等。

Recurrent Neural Network:

这里用到的是3层GRU的RNN,并且用dropout来防止overfitting。

Data preprocessing & Implementation

input是target function的disassembly binary code。获取这个数据的第一步就是找到function的边界。函数边界的界定问题是一类单独的研究方向。这里我们假设可以得到正确的函数边界。我们下载了Linux packages 并且用clang 和gcc编译。function边界、参数数量和参数类型都是通过parsing DWARF entries得到的。这里主要用到了pyelftools去parse DWARF。然后我们写了一个python module来提取argument counts和types。function boundaries的开始和结束使用Linux objdump utility得到的。把这些得到的信息作为ground truth。在disassembly之后得到call sites 和caller snippets。用tensorflow去训练instruction embedding model 和RNN。

Explicability of models

Instruction Embedding

我们将instructions用一个多维的vector表示,但可视化这个vector非常困难。为了更好地理解这些vectors,我们用到了两个常见的技术:t-SNE和类比推论。

t-SNE是把高维度的vectors变成低维度的vectors,并且保留原有的vector里面信息。有很多linear transformation方法来降维,比如PCA。但PCA只保留了线性信息,有很多非线性的信息就丢失了。t-SNE就是可以保留这种非线性的关系,比如如果word embedding学习到两个instructions很相似,那么他们在高维度里面就邻近。

类比推论也可以用来通过vectors来推断出instruction关系。在自然语言里面,类比通常由两个词组成一对。比如(女人,王后),(男人,国王)。这两个词对就是相似的。I_1 - I_2 \approx I_3 - I_4I_1, I_2, I_3, I_4就是四个vectors。或者可以写成I_3 - I_1 + I_2 \approx I_4。如果这种类比的方法推算出的conventions或者idiom和我们预期的二进制代码相符合,那我们就可以说EKLAVYA是可以推断相似性的。

RNNs for Argument Recovery

我们想要知道给定一个function,RNN模型认为里面的那些instructions在预测过程中是重要的。如果这些instructions和我们之前的认知相符,那就可以认为RNN的学习方向是正确的。我们通过Saliency map(显著图)来判断。

Saliency Map

显著图就是来判断input里面的那部分对于预测结果更为重要,就是input里面的一点点变化就能导致output很不一样。这里用到了gradient by back-propagation。计算"derivative of the output of the penultimate layer with respect to each input instruction"(就是针对input里面的每一个instruction)。然后得到一个Jacobian matrix,它告诉我们每一个维度的instruction vector对整个output有多大的影响。所以我们对output的所有项和对应的input做partial derivatives 然后求和。结果是一个256维的vector,就是告诉我们在input改变下对每一个纬度的影响程度。我们用一个矢量来表示整个input改变对output的相对影响。我们计算每个instruction的L2-norm。

Evaluation

实验是要证明1.判断函数参数的数量和类型的准确性,和2.训练模型学到的语义是不是符合我们已有的认知。

dataset

用到两个dataset。binaries都是通过高gcc 和clang两个compiler分别得到的。第一个dataset包括从binutils,coreutils和findutils这三个常见的Linux packages里得到的二进制代码。每个package都是用4个级别的optimization(O0-O3)编译得到的二进制代码。x86的二进制代码有274,285个functions,包括1,237,798个不同的instructions。x64有274,288个function,包括1,402,220个instructions。

第二个dataset是在第一个dataset基础上增加了5个packages,一共8个。就有更多functions和instructions。

sanitization

第二个dataset里面移除了重复的functions。

这里重复不仅是说function重复,还包括由不同compiler对于同一段代码编译出来的不同的二进制代码。比如直接寻址里面"je 0x98",就不看0x98。删除这些instructions里面的地址之后,再去做function层面的remove duplications。除了这个方法,还有一步就是派出所有4个instruction以下的functions,因为function太小了也不会有什么参数。

这样处理之后的dataset用80%training20%testing。training包括一个instruction set里面的所有二进制代码,这些二进制码是clang和gcc的不同optimization level compile出来的。因为EKLAVYA的任务是generalization。

Imbalanced classes

union和struct这种instructions就不包括了,因为太少了。9个参数以上的也是不包括。

Accuracy

还是用precision和recall来做performance measurement。因为imbalanced dataset,所以report数据的时候也是分labels的。当然也有总体的。

Findings

  1. 84% accuracy for count recovery and 81% for type recovery. 
  2. gcc 和 clang都能用
  3. 很少出现的类型也表现的很好
  4. x64比x86更好
  5. count recovery方面,optimization level越高,accuracy越差;type recovery都一样。

Explicability of Models

EKLAVYA的目的还是要learn semantics and similarity between instructions or instruction set. 所以用t-SNT来反应instructions之间的关系。

Instruction Semantics Extraction

t-SNE和上面说的一样,就是非线性的降维。这里把256维降到2维坐标上表示。首先可以看到相似的instructions都聚在一起。这部分主要是两个发现:1.相似的instructions在图上距离很近和2.instruction pairs之间的距离Relation between instructions: analogical reasoning - similarity between sets of instructions. e.g., the cosine distence between the instruction pair (push %edi, pop %edi) is nearly the same as the distance between pair (push %esi, pop %esi)

Auto-learning Conventions

这里用到的是之前提到的saliency map的方法,来判断input function里面instructions的重要性。通过观察这些相对重要的instructions,我们还发现EKLAVYA还自己学会别的了:比如argument passing conventions, "use-before-write" instructions,stack frame allocation instructions, and set up instructions for stack-based arguments。这些都是在那个训练计算参数个数的RNN model 上学到的。比如在一个函数里,EKLAVYA认为rdi,rsi,rdx,rcx,r8这几个instruction最重要,而这几个instructions就是用来pass arguments的,符合我们的认知。“use-before-write”就是当一个instruction被多次用到的时候,比如rcx,EKLAVYA会把写入之前用到的那个标记成最重要的。

NOTES

  1. direct/indirect call:字面意思就是直接调用拿到地址fixed address, 间接就是可能要去一个register里拿这个地址,或者从另一个地方里拿,比如memory。

最后的最后,是一个打赏链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值