EKLAVYA – 利用神经网络推断二进制文件中函数的参数
文章目录
- EKLAVYA -- 利用神经网络推断二进制文件中函数的参数
这一次介绍一篇文章,名为
Neural Nets Can Learn Function Type Signatures From Binaries来自于新加坡国立大学Zhenkai Liang团队,发在了Usenix Security 2017上
问题介绍以及形式化定义
该工作主要关注的问题是函数的参数推断,包含两个部分:
- 参数的个数
- 参数的类型,比如int, float等
传统方法通常会使用一些先验知识,将指令的语义,ABI惯例 (Application Binary Interface),编译器的风格等进行编码。
一旦编译器发生了改变,指令集合发生了改变,那么我们就需要重新引入一些先验知识。
如果我们可以摆脱,或者说是减少这些先验知识的利用,那么就不会受限了!
那么,使用神经网络来进行自动化的学习和推断,就是一种思路了。
前提假设
- 我们首先能知道一个函数的边界 (boundary)
- 在一个函数内部,我们知道它的指令边界
- 我们知道代表一个函数调用(function dispatch)的指令,比如call
通过反汇编工具,我们可以满足上述假设。
值得一提的是,函数边界也可以使用神经网络来做,有兴趣的读者可以参考 Dawn Song 发在Usenix Security 2015 的 Recognizing functions in binaries with neural networks.
这里,首先给出一些符号的定义:
-
我们定义我们的模型为 M(⋅)M(\cdot)M(⋅)
-
定义函数 aaa 反汇编出来的代码为 TaT_aTa ,Ta[i]T_a[i]Ta[i] 代表函数 aaa 的第 iii 个字节
-
函数 aaa 的第 kkk 条指令可以被写成 Ia[k]:&#61;I_a[k]:&#61; Ia[k]:&#61;<Ta[m],Ta[m&#43;1],...,Ta[m&#43;l]>
- 其中 mmm 是对应指令的起始字节的位置索引
- lll 是该指令所包含的字节数
-
一个包含 ppp 条指令的函数 aaa 可以被表示为 Ta:&#61;T_a:&#61;Ta:&#61;<Ia[1],Ia[2],Ia[p]>
-
如果一个函数 bbb 有一个直接调用 call 对于函数 aaa, 我们将该条call指令之前的所有指令拿出来&#xff0c;称为 caller snippet&#xff0c;可译为调用者片段。定义为 Cb,a[j]:&#61;C_{b,a}[j]:&#61;Cb,a[j]:&#61;<Ib[0],...,Ib[j−1]>
- 其中 Ib[j]I_b[j]Ib[j] 对应 call 函数 aaa 的那一条指令
- 如果 Ib[j]I_b[j]Ib[j] 是一个间接调用&#xff0c;我们令 Cb,a[j]:&#61;∅C_{b,a}[j]:&#61;\emptyCb,a[j]:&#61;∅
-
我们会收集函数 aaa 的所有调用者的调用者片段&#xff0c;记为 Da:&#61;Ta∪(⋃b∈Sa(⋃0≤j≤∣Tb∣Ca,b[j]))\mathcal{D}_a:&#61;T_a\cup(\bigcup_{b\in S_a}(\bigcup_{0\leq j\leq |T_b|}C_{a,b}[j]))Da:&#61;Ta∪(⋃b∈Sa(⋃0≤j≤∣Tb∣Ca,b[j]))
- 其中 SaS_aSa 是调用 aaa 的所有函数的集合
由于调用者片段的长度可能非常长&#xff0c;这里文章设置为不超过500条指令
我们的函数 M(⋅)M(\cdot)M(⋅) 接受输入 Da\mathcal{D}_aDa , 输出两个变量&#xff1a;
- 函数 aaa 的参数个数
- 函数 aaa 的每一个参数的类型
- C-风格的参数类型可以被定义为 int, char, float, void*, enum, union, struct
方法设计
我们这里先直接给出整体的方法流程图&#xff1a;
简单来说&#xff0c;可以划分成两个模块&#xff1a;
- 指令编码模块
- 首先&#xff0c;将输入的二进制文件进行函数抽取、指令的分割、调用点的抽取。
- 然后对指令进行Word Embedding编码&#xff0c;得到对应的向量表示&#xff0c;这一部分可以参照 NLP 中的 Word2Vec。
- 参数还原模块
- 将这些数据切分成训练集和测试集&#xff0c;分别使用4个递归神经网络(RNN)来从两个方面(调用者和被调用者)推断函数参数的个数以及类型&#xff0c;也就是对应上图中的4个任务&#xff08;Task2&#xff0c;Task4 对应被调用者&#xff0c;Task1&#xff0c;Task3对应调用者&#xff09;。
究竟是怎么推断多个参数类型的呢&#xff1f;
一个RNN&#xff0c;输入一个序列&#xff0c;只能推断出来一个类型。
所以这篇文章的实现是&#xff0c;训练多个RNN&#xff0c;每一个RNN独立推断固定位置的参数类型。
先用一个RNN推断出来参数个数&#xff0c;然后分别使用多个RNN来推断不同的位置的参数。
数据准备
该文章使用一些linux的包&#xff0c;然后使用clang和gcc来进行编译&#xff0c;通过设定debug模式&#xff0c;就可以直接在binary中的DWARF字段找到对应函数边界、参数个数以及类型&#xff0c;作为ground truth。
构建了两个数据集&#xff1a;
- 数据集1: 包含了3个流行的linux包&#xff08;binutils&#xff0c;coreutils 以及 findutils&#xff09;, 使用了O0到O3的优化等级进行编译
- 数据集2: 将数据集1包含的linux包进行扩展&#xff0c;多增加5个 (sg3utils, utillinux, inetutils, diffutils 和 usbutils), 也在4个优化等级上进行编译
训练集和测试集的划分比例为8:2
不平衡的数据
在数据集的构造中&#xff0c;会出现不同类的数据比例相距甚远的情况。比如参数为pointer类型的数据就是union类型的数百倍&#xff0c;大部分函数都是少于3个参数。该文章中并没有解决这个问题。
实验结果
这里贴出来Task1和Task2&#xff0c;也就是通过调用者和被调用者&#xff0c;推断参数个数的结果
可以看到&#xff1a;
- 优化级别越高&#xff0c;越难推断&#xff0c;但并没有严格的递增关系
- 参数个数越多&#xff0c;越难推断&#xff0c;和训练数据量也有关系
- 从调用者方面&#xff0c;更容易推断出来参数个数
上面是&#xff0c;关于参数类型推断的结果
可以看到&#xff1a;
- 优化级别似乎干扰性不强&#xff0c;甚至优化级别越高&#xff0c;推断类型越精确
- 参数的位置越靠后&#xff0c;越难推断出来了类型
- 从调用者和被调用者两方面来推断&#xff0c;差别不是很大