NEUZZ Efficient FuzzinG with Neural Program Smoothing · GitBook
总结:
NEUZZ利用神经网络对程序进行平滑处理来提高模糊测试的效率和性能。
以前的模糊测试,输入会被随机地变异生成不同的测试用例。这种随机变异的方法在测试效率和测试覆盖率上存在一定的局限性。NEUZZ引入神经网络来解决。
-
神经网络训练:NEUZZ使用一个基于RNN(循环神经网络)的神经网络来对程序进行学习和平滑处理。通过将输入样本和对应的代码路径作为训练数据,NEUZZ学习到输入样本在不同代码路径上的执行特征,并生成平滑的输入样本,从而提高测试用例的质量和多样性。
-
模糊测试:NEUZZ在生成的平滑输入样本的基础上,采用了一种基于贪心策略的搜索算法(文中梯度引导优化)来引导模糊测试的生成过程。这种策略会根据神经网络生成的平滑输入样本的特征,优先选择那些能够导致不同代码路径执行的测试用例。这样可以有效地提高测试用例的覆盖率和测试效率。
梯度引导算法在一定程度上可以类比于贪心策略,因为它在每次迭代中选择当前梯度信息最大的方向来生成新的输入样本。
NEUZZ 使用基于梯度信息的平滑算法来生成新的输入样本。在神经网络的在线微调阶段,NEUZZ 根据神经网络输出的梯度信息,对当前的种子输入进行平滑操作,生成新的输入样本。梯度信息可以指示当前输入样本中哪些部分对于覆盖率的提升具有更大的潜力,从而选择梯度较大的方向进行平滑。这样,生成的新输入样本将更有可能引导模糊测试走向未被探索的高覆盖率路径。
类似于贪心策略,梯度引导算法在每次迭代中选择当前梯度信息最大的方向来生成新的输入样本,从而在模糊测试过程中尽量引导生成新的输入样本以探索高覆盖率路径。梯度引导算法在选择平滑方向时考虑的是当前的梯度信息,而贪心算法在选择路径时通常只考虑当前状态的局部最优解,两者在具体实现上存在差异。
NEUZZ在多个实际程序上进行了实验证明了其高效性和有效性。与传统的模糊测试技术相比,NEUZZ在测试效率和测试覆盖率上都取得了显著的改进。
NEUZZ基于神经网络的模糊测试方法的步骤:
-
初始种子生成:从程序的输入空间中随机生成一组初始种子输入作为模糊测试的起点。
-
程序执行与覆盖率收集:使用初始种子输入运行目标程序,并通过动态分析收集执行路径和代码覆盖率信息。
-
样本生成与筛选:根据收集到的执行路径和覆盖率信息,采用贪心搜索策略生成新的模糊测试样本。贪心搜索策略根据当前种子输入的覆盖率信息,对输入进行变异、交叉、替换等操作,生成新的输入样本。然后通过目标程序的执行,计算每个新生成样本的覆盖率,并将高覆盖率的样本筛选出来,作为下一轮迭代的种子输入。
-
神经网络训练:将筛选出的高覆盖率样本输入到神经网络中进行训练。神经网络通过学习输入样本和对应的覆盖率之间的关系,从而学习到输入样本的表示。
-
输入样本平滑:利用训练好的神经网络对新的输入样本进行平滑操作,即对输入样本进行一系列微小的变化,以生成平滑的输入样本。
-
生成新的种子输入:将平滑后的输入样本作为新的种子输入,重复步骤2到步骤5的过程,形成循环迭代的过程。
通过上述步骤,NEUZZ 利用神经网络对输入样本进行平滑操作,并在每轮迭代中根据目标程序的覆盖率信息生成新的种子输入,从而引导模糊测试生成更具多样性和覆盖率的测试用例,提高模糊测试的效率和效果。在样本生成与筛选的步骤中,NEUZZ 使用了贪心搜索策略来生成新的输入样本,从而在探索输入空间时能够更加高效地找到导致高覆盖率的输入,从而优化了传统的随机模糊测试方法。
贪心算法在 NEUZZ 论文中主要出现在路径选择的阶段。具体而言,NEUZZ 使用贪心算法来选择生成新的输入样本,以引导模糊测试过程向着更高的覆盖率路径前进。
在 NEUZZ 中,生成新的输入样本的过程中,贪心算法会在每次迭代中选择一个具有潜在高覆盖率的输入样本作为种子输入。贪心算法根据之前训练好的神经网络的输出,对当前的种子输入进行平滑操作,生成新的输入样本。这个新的输入样本被用于下一轮的模糊测试,并根据覆盖率信息来评估其覆盖率是否提高。如果新的输入样本引导模糊测试走向了未被探索的高覆盖率路径,那么它将被接受并用作下一轮的种子输入;否则,它将被丢弃,继续在当前轮的种子输入中选择下一个高覆盖率的输入样本。
贪心算法在选择种子输入时基于神经网络的输出进行决策,从而在模糊测试过程中尽量引导生成新的输入样本以探索未被探索的高覆盖率路径,从而提高模糊测试的效率和效果。
NEUZZ 将模糊测试中的路径探索问题转化为了一个优化问题。使用神经网络来学习输入样本和覆盖率之间的关系,并通过对输入样本进行平滑操作生成新的种子输入。这种平滑操作可以看作是在输入空间中进行搜索,寻找一种在目标程序中能够导致更高覆盖率的输入变化。因此,NEUZZ 将路径探索问题转化为了在输入空间中寻找高覆盖率输入的优化问题。
NEUZZ 使用了一种两阶段的神经网络训练方法,包括离线预训练和在线微调。
-
离线预训练:首先,NEUZZ 利用生成的初始种子输入和对应的覆盖率信息进行离线预训练。具体而言,NEUZZ 将输入样本和对应的覆盖率信息作为训练数据,输入到神经网络中进行训练。训练过程中,神经网络通过学习输入样本和对应的覆盖率之间的关系,从而学习到输入样本的表示。离线预训练使得神经网络能够初步学习到输入样本的特征和模式。
-
在线微调:在模糊测试的迭代过程中,NEUZZ 利用筛选出的高覆盖率样本作为在线微调的数据。具体而言,每轮迭代中,NEUZZ 将筛选出的高覆盖率样本输入到神经网络中,根据神经网络的输出进行输入样本的平滑操作。然后,将平滑后的输入样本作为新的种子输入,进行下一轮的模糊测试。在这个过程中,神经网络会根据新的输入样本和对应的覆盖率信息进行在线微调,从而不断优化神经网络的表示能力。
通过离线预训练和在线微调相结合的方式,NEUZZ 能够逐渐优化神经网络的表示能力,使其能够更好地捕捉输入样本和覆盖率之间的关系,从而在模糊测试过程中引导生成更具多样性和覆盖率的测试用例。这种训练方法使得神经网络能够逐步适应目标程序的输入空间和覆盖率信息,从而提高模糊测试的效率和效果。
对于算法优化的思路:
-
改进神经网络模型:NEUZZ采用了基于RNN的神经网络模型,但其他类型的神经网络模型如CNN(卷积神经网络)或Transformer等也可以考虑。优化网络模型的结构、参数设置和训练策略,可能会进一步提高NEUZZ的性能。
-
加入强化学习:可以考虑将强化学习方法引入NEUZZ,通过与程序的交互来优化测试用例的生成过程。例如,可以使用Q-learning或Policy Gradient等强化学习算法来训练一个策略网络,从而自动地生成高质量的测试用例。
-
改进贪心搜索策略:NEUZZ采用了基于贪心策略的搜索算法来引导模糊测试的生成过程,但贪心策略可能会导致局部最优解。可以考虑引入更加智能和灵活的搜索策略,如遗传算法、模拟退火等,来提高测试用例的多样性和覆盖率。
-
结合静态分析技术:可以将静态分析技术与NEUZZ相结合,从程序的静态代码结构、类型信息等方面提取更多的特征,并将其用于神经网络的训练和测试用例生成过程中,从而进一步优化NEUZZ的性能。
-
引入多目标优化:可以考虑引入多目标优化的思想,例如同时优化测试用例的覆盖率、执行路径长度、代码行覆盖率等多个指标,从而在生成测试用例时综合考虑多个目标,进一步提高测试用例的质量和多样性。
-
对抗样本生成:可以考虑引入对抗样本生成的技术,如生成对抗网络(GANs),来生成更具挑战性和多样性的测试用例,从而更好地探测目标程序中的漏洞和安全隐患。
详细笔记
进化算法有可能陷入无效的随机突变序列,所以可以结合梯度引导优化来解决进化算法存在的问题。这个文章将模糊测试中的路径寻找问题近似为一个目标程序离散分支行为的平滑函数来讲解决。利用神经网络模型与梯度引导输入生成的方案进行结合提升fuzzing的效率,达到的效果是能让fuzzer达到更高的边缘覆盖率(也就是更深)。
本身模糊测试问题就是一个优化问题,变量是程序的输入,目标是规定时间内最大化发现漏洞数量。漏洞在程序中是稀疏分布且不规律的,所以模糊测试需要最大化测试的代码覆盖率,传统一般使用进化算法。即在种子能够触发新的代码覆盖时将它保留存入突变的语料库。但是,随着语料库的增大,其效果会被减弱。
梯度引导优化算法
梯度引导优化算法无法直接在模糊测试中部署,因为测试的过程中有大量的不连续行为所以无法计算梯度。所以要创建近似于目标程序和输入相关的分支行为的平滑函数才可以引入梯度引导优化算法。目前存在的平滑技术依赖于符号分析,其开销比较大,还会面临路径爆炸,环境建模缺失和符号内存建模消耗巨大,这些问题导致现有技术无法在大型程序上应用。
本研究提供的平滑技术
使用前馈神经网络(feed-forword Neural Networks ) , 来学习程序分支行为的平滑近似,从而预测control-flow edge。通过gradient-guided search strategy 梯度引导策略,计算平滑近似从而创建NN模型识别应该进行突变的位置,从而最大化目标程序中检测到的错误数量。研究通过在错误预测的程序行为上逐步重新训练模型来改进NN模型。文章中的实现实际上是对前馈神经网络的实现。
"The universal approximation theorem"(通用逼近定理)是指在一定条件下,具有足够多隐藏层神经元的多层前馈神经网络(MLP)可以以任意精度逼近任何连续函数,无论输入空间多复杂。文章中利用神经网络的函数逼近能力,通过训练神经网络来学习程序的行为,并利用神经网络平滑技术来生成更有效的测试输入,从而提高了模糊测试的效率和速度。这也间接地利用了神经网络的通用逼近定理特性,以在模糊测试中获得更好的结果。
本文主要贡献如下:
-
第一个确定程序平滑的重要性,采用有效的梯度引导技术进行模糊测试。
-
第一个使用替代神经网络的高效且可扩展的程序平滑技术,以有效地模拟目标程序的分支行为。进一步提出了一种增量学习技术,以在更多训练数据可用时迭代地改进替代模型。
-
证明了替代神经网络模型的梯度可用于有效地生成程序输入,从而最大化目标程序中发现的错误数量。
-
设计,实施和评估NEUZZ,并证明它在各种实际程序以及策划的bug数据集上明显优于10个最先进的fuzzer
优化基础
-
问题:
梯度引导优化算法比进化算法更加高效的原因是因为进化算法每次都是保留局部最优解而梯度引导算法可以通过计算梯度从而选择全局最优解吗?
梯度引导优化算法在选择搜索方向时可以利用梯度信息,从而可以更加快速地朝着全局最优解的方向前进。相比之下,进化算法通常基于群体搜索,通过生成新的解并经过选择、交叉和变异等操作来不断优化解的质量。但由于进化算法是一种随机搜索方法,其搜索过程可能会陷入局部最优解,从而导致搜索效率较低。
梯度引导优化算法(如基于梯度的优化方法,如梯度下降法等)通过计算函数梯度来指导搜索方向,从而可以在搜索空间中更快地找到梯度指示的全局最优解或者接近最优解的解。这在一些问题中可以显著提高搜索的效率,特别是在连续可导函数的优化问题中。
NEUZZ中使用的梯度引导优化算法通过训练神经网络来学习程序的行为,并通过计算神经网络的梯度信息来引导模糊测试的搜索方向,从而生成更加有效的测试输入。这种方式可以在模糊测试中更快地探索到有潜力的输入空间,从而提高了模糊测试的效率和速度。
所以,本研究的重点在于如何设计或生成更加平滑的目标/约束函数,从而使梯度优化算法准确地计算梯度或高阶导数,并使用它们系统地搜索整个参数空间。而又由于模糊测试的输入生成基本无约束,所以梯度引导算法可以明显比进化算法更高效的收敛至最优解。
模糊测试可以表达为无约束优化问题,只有一个最大化测试输入发现漏洞的数量的目标。模糊测试一般会尝试最大化edge的覆盖率,所以目标函数可以表示为
其中F'返回程序P的输入x所覆盖的新控制流边缘的数量。
技术思路
步骤:
-
初始的种子通过前馈神经网络进行平滑处理,即创建一个可以模拟程序分支行为且不会引入大错误的平滑函数。其中的前馈神经网络是经过现有的测试输入和语料库来训练的。
-
经过训练的前馈神经网络模型更加平滑,其梯度和高阶导数就可以被计算。利用梯度和导数更快的收敛至最优解。
-
优化后的输入再次用于训练NN模型并引导新输入的生成(返回AFL的测试输入与边缘覆盖信息)
方法:
程序平滑
将被测试的程序抽象成一个平滑的函数模型。一般不连续函数 f 的平滑是 f 和平滑掩模函数g之间的卷积运算产生的新的平滑输出函数。公式为
但是在模糊测试中不连续函数f没有闭合形式的表示,所以使用一般的方法无法计算积分,所以一般使用离散版本并计算卷积,公式如下:
但是,又由于f是计算机程序,无法通过分析计算卷积,所以可以使用黑盒和白盒平滑。黑盒方法从f的输入空间中选取离散样本,并使用这些样本以数字方式计算卷积。相比之下,白盒方法会查看程序语句/指令,并尝试使用符号分析和抽象解释来总结它们的效果。黑盒方法可能会引入大的近似误差,而白盒方法会产生令人望而却步的性能开销,这使得它们对于真实世界的程序来说是不可行的。
文中最后使用NN网络以灰盒模式学习程序行为的平滑近似(例如,通过收集边缘覆盖数据)。
使用神经网络对程序进行平滑(keras)
文中使用收集到的边缘覆盖数据传入NN模型来学习和迭代程序的平滑。
本文中用NN来建模目标程序的分支行为(即,预测由给定程序输入执行的控制流边缘)。使用神经网络对分支行为进行建模的挑战之一是需要接受可变大小的输入。与现实世界的程序不同,前馈NN通常接受固定大小的输入。可以设置最大输入大小阈值,并在训练期间使用空字节填充任何较小尺寸的输入。支持更大的输入不是主要问题,因为现代NN可以轻松扩展到数百万个参数。对于较大的程序,可以根据需要简单地增加阈值大小,但是并非阈值越大建模精度越好。
f(x) = f:{0x00,0×01,…, 0xff} --->{0,1} 地址对应其是否被执行,给定一组训练样本(X,Y),其中X是一组输入字节,Y代表相应的边缘覆盖位图,参数函数f(x,θ)= y
这个公式表达的就是这个训练是要找到
其中是指输出在加了其中的
为参数时对于y的损失,其是一个损失函数。我们要改变参数
以最小化损失。然后使用二进制交叉熵计算预测出的位图和真实覆盖位图之间的距离。
第一次训练的数据是AFL自带的语料库生成的输入和执行后的边覆盖率
在训练的时候,会对数据进行预处理,因为执行的测试用例的边缘覆盖信息有时候只包含一小部分边的标签。如,有些边是无论如何都会被执行的一组标签之间的这种类型的相关性在机器学习中被称为多重共线性,这些数据就会对模型影响,导致其无法收敛到一个小的值。通过将总是一起出现在训练数据中的边缘合并到一个边缘来进行降维。文中值考虑边被激活一次就可以的规则,将训练的标签大大缩小,且在每次生成的数据被测试后重新进行预处理,所以之前被合并的标签还是有机会进行分裂(当发现新边覆盖时)。
梯度引导优化
梯度下降需要设置学习率,学习率就是每次移动的距离,学习率过大则无法收敛,太小则梯度会太小。自适应学习率是在开始的时候使用大的学习率,靠近目标之后使用小的学习率。随机梯度下降从样本中随机抽出一组,训练之后按照梯度更新一次,然后再抽再更新,这样可能不用训练所有的样本就可以找到合适的损失值。
文中为梯度引导设置的目标是找到对应不同边缘的会改变最终层神经元输出的输入。
研究中对梯度形式的定义为每个输入字节改变的多少以影响NN中最终的输出。
每个输出神经元对应于特定边缘,并计算0和1之间的值,总结给定输入字节对特定边缘的影响。
给定如上面说的定义的参数NN y = f(θ,x),令 y_i 表示 f 的最后一层中的第i个神经元的输出,其也可以写为 f_i(θ,x) 。 f_i(θ,x)相对于输入x的梯度G可以定义为G = ▽_xf_i(θ,x)=δy_i/δ_x。注意,可以容易地计算 f 的梯度w.r.t到θ,因为NN训练过程需要迭代地计算该值以更新θ。因此,通过简单地将θ的梯度的计算替换为x的梯度,也可以容易地计算G。注意,梯度G的维数与输入x的维度相同,在我们的例子中,它是一个字节序列。
算法的解释:该算法是使用梯度引导输入的生成。识别具有最高梯度值得输入字节并对其进行改变。
从种子开始寻找新的测试输入,每次迭代的时候首先利用梯度的绝对值来识别可以导致未捕获边缘的输出神经元的最大变化输入字节。然后根据每个字节的梯度值来确定突变的方向(如增加,减少)从而最大化或者最小化目标值。通过6行和10行中对字节的修剪来保证其变异在正常值之内。算法中的K是变异的目标,其实是要编译的字节数。
使用增量学习进行细化(Refinement with incremental learning)
梯度引导的效率受NN的建模的准确程度的影响。该研究会在模糊测试的过程中持续检测程序的行为,当其发现行为与预期不符合时,对NN模型进行细化 Refinement,在触发了新的边的时候利用增量学习学习新的数据来更新NN模型。
文中仅使用能触发新分支的旧数据进行重新训练。在这些数据被训练的可用之后,将确定可以实现新的边缘覆盖的数据和刚才留下的旧数据集合在一起重新训练NN。这样可以防止训练样本数量的爆炸,结果是可以轻松的重复训练50次,并且每次时间都在几分钟之内。
实现:
NN架构。我们的NN模型在Keras2.1.3 [5]中实现,Tensorflow-1.4.1 [6]作为后端。 NN模型由三个完全连接的层组成。隐藏层使用ReLU作为其激活功能。我们使用sigmoid作为输出层的激活函数来预测控制流边缘是否被覆盖。 NN模型被训练50个时期(即,整个数据集的50次完整通过)以实现高测试准确度(平均约95%)。由于我们使用简单的前馈网络,所有10个程序的训练时间不到2分钟。即使在运行频率为3.6GHz的Intel i7-7700上进行纯CPU计算,训练时间也不到20分钟。
训练数据收集。对于每个测试的程序,我们在单个核心机器上运行AFL-2.5.2 [88]一小时,以收集NN模型的训练数据。为10个项目收集的平均训练输入数量约为2K。得到的语料库进一步分为训练和测试数据,比例为5:1,其中测试数据用于确保模型不会过度拟合。我们使用10KB作为阈值文件大小,用于从AFL输入语料库中选择我们的训练数据(平均90%的AFL生成的文件低于阈值)。
模型参数选择
由于成功率取决于训练模型和产生突变的不同参数的选择。所以文中实际上选择了几个训练出来结果最好的程序。
参数的选择主要是k,即之前提到的只需要变异的字节数。在经过尝试之后,文中最终选择了10作为突变字节数。之后还需要调整的时每个隐藏层中的层数和神经元数,也是在经过实验之后,最终选择了1层隐藏层模型。
评估
四个问题:
-
NEUZZ可以找到比现有模糊器更多的错误吗?
结果1:NEUZZ在6个不同的程序中找到了31个以前未知的错误,其他模糊器找不到。NEUZZ在寻找LAVA-M和CGC漏洞方面也优于最先进的模糊器
比较了NEUZZ和其他模糊器在24小时运行时发现的错误和崩溃的总数,给出相同的种子语料库。 NEUZZ和其他模糊器发现了五种不同类型的错误:内存不足,内存泄漏,断言崩溃,整数溢出和堆溢出。为了检测不一定会导致崩溃的内存错误,使用AddressSanitizer编译程序二进制文件。我们通过比AddressSanitizer报告的堆栈跟踪来测量发现的唯一内存错误。对于不会导致AddressSanitizer生成错误报告的崩溃,检查执行跟踪。通过手动分析触发无限循环的输入找到整数溢出错误。
AFL,AFLFast和AFL-laf-intel发现了3种类型的错误 - 它们没有找到任何整数溢出错误。其他模糊器只发现2种类型的错误(即内存泄漏和断言崩溃)。 AFL可以在程序size上出现堆溢出错误,而NEUZZ可以在程序nm上找到相同的错误和另一个堆溢出错误。总的来说,NEUZZ发现的错误比第二个最好的模糊器多2倍。
-
NEUZZ能否实现比现有模糊器更高的边缘覆盖?
结果2:与其他灰盒式模糊器相比,NEUZZ可以实现更高的边缘覆盖率(比AFL高4倍,比24小时运行的第二好的高出2.5倍)
-
NEUZZ能否比现有的基于RNN的模糊器表现更好?
NEUZZ,一个基于简单前馈网络的模糊器,通过在不同项目中实现3.7倍到8.4倍的边缘覆盖率,明显优于基于RNN的模糊器
-
不同的模型选择如何影响NEUZZ的性能?
结果4:NN模型优于线性模型,增量学习使NN随着时间的推移更加准确。