李航宇 方浩然 曲彦文 郭 帆
(江西师范大学计算机信息工程学院 南昌 330022)(yu@jxnu.edu.cn)
目前最有效的自动化漏洞挖掘技术依然是模糊测试(Fuzzing)[1-2].随着软件规模日益庞大,结构复杂精妙,静态程序分析的成本变得越来越高,效果不尽人意.近几年来,Fuzzing凭借其自动化程度高、漏洞挖掘高效、不依赖程序源码等优点,受到安全研究员的青睐.截至2021年,著名的Google’s OSS-Fuzz[3]服务在650个开源程序上发现了超40 500个bugs,这是其他漏洞挖掘技术所不可比拟的.
如图1所示的Fuzzing基本框架图,Fuzzing只需要持续地对目标程序输入畸形的测试用例,通过追踪收集覆盖率的信息,变异出能够增加程序覆盖或触发漏洞(crash)的种子,将种子继续输入至目标程序,监测目标程序状态,捕获目标程序的异常行为来发现程序漏洞.Fuzzing挖掘漏洞的关键在于探索到更深的程序行为,发现更多的程序状态.目前,许多改进的模糊测试工具(如 Fuzzer)在种子选择[4-7]、种子变异[8-11]、种子能量分配[4,12]等模块进行优化,通过利用污点分析技术[5,13-16]、符号执行技术[17-20]来指导Fuzzing产生更高的程序覆盖率.尽管这些优化极大地提高了程序覆盖率,但是随着时间的增加,Fuzzing运行了相当多无覆盖率增加的测试用例,导致Fuzzer追踪了许多无意义种子的反馈信息,造成了极大的性能开销.
Fig.1 Fuzzing framework图1 Fuzzing框架
为了减少性能开销,Untracer[21]通过对源程序二进制插桩,生成2种不同的目标程序.每个基本块前插桩1个中断指令的Oracle[21]目标程序,以及覆盖率收集的tracer[21]目标程序.如图2所示,Untracer先将生成的测试用例运行在带有中断的Oracle文件上,当Untracer捕获到中断信号时,才会将该测试用例输入到tracer目标程序中,并追踪该测试用例的覆盖率信息,由此将传统主流的边覆盖变成了Untracer的基本块覆盖.虽然Untracer极大地减少了测试用例追踪的开销,提升了Fuzzing的运行速度,但是由于其基本块覆盖的局限性,只追踪产生新基本块覆盖的测试用例,导致Untracer错误地丢弃了许多能够产生新路径覆盖的种子,遗失了许多挖掘漏洞的潜力.
Fig.2 Untracer framework图2 Untracer框架
为了解决以上问题,本文提出了一种基于深度学习异常检测[22]模型的Fuzzing框架——ADFuzz,首次将异常检测模型用于指导Fuzzing,很大程度上解决了AFL (American fuzzy lop)的开销问题,也弥补了Untracer精度上的不足,做到了在速度和精度上的平衡.本文将异常检测的思想结合到Fuzzing中,将程序的低频路径作为异常点,这些异常点根据路径频率的划分非常容易收集.本文评估了ADFuzz对于传统主流的Fuzzer的优势(以AFL为代表),在相同覆盖率的情况下最多减少了78%的运行开销;在相同时间下最高提升了42.9%的运行速度,最多产生25.8%的覆盖率.
综上所述,本文的主要贡献有3方面:
1)提出将异常检测模型与Fuzzing相结合,将运行低频路径种子视为异常点;
2)通过异常检测模型筛选出低频路径种子,使得Fuzzing持续朝着低频路径方向探索,减少无效种子的追踪运行,提高Fuzzing效率;
3)开发了原型工具ADFuzz,并且通过实验证明了ADFuzz的有效性.
覆盖率引导的Fuzzing是目前使用最广泛且最有效的自动化漏洞挖掘技术.覆盖率引导的Fuzzing主要是通过覆盖率的变化来区分不同的输入,以产生的新覆盖作为追踪运行的信号,进而筛选测试用例.最著名的覆盖率引导的Fuzzer是2013年由谷歌安全研究员Zalewski[23]研发的AFL,AFL通过在目标程序编译期间对代码轻量级插桩,记录代码覆盖率,然后将输入队列的初始种子按一定顺序进行确定性变异和破坏性变异.确定性变异对输入进行小范围、局部的有规则的变异,如位翻转、字节加减、字节替换;破坏性变异则是对输入进行大范围的无序变异.AFL将变异生成的新输入丢入目标程序运行、监视程序行为、收集产生新覆盖以及触发程序异常行为的输入,并且按照种子的运行时间、运行的路径深度给予不同的变异能量,变异能量用于指导下一次变异的次数,最后将种子放入队列,以此往复.
整个流程以覆盖率变化作为筛选种子到队列的标准,并作为变异能量分配的依据,也是整个AFL的变异方向.但是随着时间的推移,拥有更多变异能量的种子将会产生更多的输入,并且始终主导着目标程序运行.当这个方向的路径已经探索完全后,覆盖率不再变化,陷入局部最优解,使得后续的Fuzzing变得低效.
异常检测[22],又称为离群点检测,是指能够检测出明显偏离大部分数据的过程.异常检测主要是检测那些少数的、不可预测的、罕见的数据,这些数据往往充满着不确定性、多样性和稀有性.近几十年,异常检测一直是一个活跃的研究领域,在许多领域都有应用,比如网络安全中的入侵检测、信用卡诈骗检测、癌细胞检测等.通过研究异常点,研究人员可以获取到许多重要的知识,有助于做出更好的数据决策.
基于深度学习的异常检测模型旨在学习数据的低维潜在特征,即ϕ(X)→Z和1个异常分数函数τ(X),异常数据通常由异常分数以及特征区分.但是由于异常数据难以收集,使得异常检测模型检测性能不高.针对异常数据难以收集、样本数据不平衡的问题,近年来使用自编码器[24]( AutoEncoder,AE) 这种特征学习方法,通过对正常数据进行训练,学习正常数据到潜在特征空间的映射关系,并且通过潜在特征进行数据重构,使重构数据具有较小的重构误差,并将重构误差用来作为异常评分筛选异常数据.
基于生成对抗网络[25](generative adversarial network,GAN)的异常检测模型一经问世,便迅速成为了最受欢迎的方法.这种方法主要基于对抗性特征学习的思想,生成网络学习给定数据的潜在特征表示,与判别器进行对抗训练,然后将真实数据与生成数据之间的误差,以及判别器误差定义为异常评分.GAN在生成数据方面表现出众,增强了模型对异常实例的检测能力.基于GAN的异常检测模型将在后续相关工作进行分析介绍.
在Fuzzing中,触发程序异常行为的输入显然可以作为异常点进行检测,但是能够触发漏洞的输入样本十分稀少,且与其他漏洞没有关联性,并不能够指导漏洞挖掘.如果仅仅将产生漏洞的输入作为异常点,对异常检测模型进行训练,显然是无意义的.
目前,学术界涌现出各式各样覆盖率引导的Fuzzer.这些Fuzzer在突破路径约束,增大覆盖率、漏洞挖掘上已经变得非常完善且有效.基于覆盖率引导的Fuzzer需要对所有测试用例的覆盖率信息进行跟踪分析.Fuzzer通过分析是否增加覆盖率、是否触发程序异常,来给予种子一定的变异能量.然而随着时间的流逝,覆盖率的增长逐渐平缓,增加覆盖率的测试用例将远小于总测试用例.根据统计分析,所有测试用例的执行路径频数、发现程序路径的执行次数并不是均匀的,它们主要集中在程序的高频路径[4].Fuzzer对于每个种子有着不同的能量分配策略,更多的变异能量带来了更多的变异运行的机会,但是并不是每次变异都是有效的变异(产生新覆盖),大部分变异后的测试用例路径与原种子路径一致,导致高频路径始终保持高次数运行;然而高频路径的冗余跟踪运行将会带来大量的开销,并且使得低频路径缺乏足够的变异能量进行路径突破.
Untracer[21]以及该团队后续的CSI-Fuzz[26]、国内清华大学的Zeror[27]都是通过尽可能地减少冗余测试用例的跟踪运行,以节省系统开销和缩短时间成本来加快发现新覆盖和crash.以Untracer为例,在目标程序的二进制文件上的每一个基本块头部都插入1个int3中断指令,Fuzzer先将测试用例运行在插入中断指令的文件.当Fuzzer监控到产生了中断或者程序异常信息时,则把该测试用例运行在另一个代码覆盖追踪文件.Zeror则有多个不同程度的目标程序二进制插桩文件,根据贝叶斯公式选择相对低开销的目标程序二进制文件运行,以此减少运行开销.当确定发现了新覆盖时,保留该种子到队列中,进行下一轮的变异,并且新目标程序二进制文件移除相应基本块头部的中断指令.然而Untracer是以基本块头部中断指令作为信息反馈,如图3所示,当前2个测试用例经过ABD,ACE新路径时,Untracer将会把ABCDE基本块前的中断移除,中断被移除后,如果有测试用例经过ABE或者ACD时,由于没有触发中断,Untracer便错误地认为测试用例没有产生新的覆盖率而丢弃它.针对此问题,后续CSI-Fuzz以及Zeror通过不同方法在各条边上插入中断来减少错误丢弃运行交叉路径种子.尽管如此,当所有的中断都被擦除后,很难再引导种子的变异方向,依然会导致高频路径过量运行,带来不必要的运行开销.
Fig.3 Cross paths图3 交叉路径
如何在路径识别精度与运行速度之间取得平衡,尽可能筛选出有效种子是解决此类问题的关键.根据RED-QUEEN[15]中提及的input-to-state correspondece思想,程序的状态与输入数据是有某种对应关系.比如输入流中的某些特定字节能够突破判断条件到达一些新的路径,而程序中有许多条件语句,如:
在这些条件语句的共同作用下产生了不同路径,而输入流中的特定字节就是产生不同路径的key-byte,这些key-byte也就是输入的潜在特征.
通过Fuzzing随机生成的输入能够覆盖大多数的路径,此时如果覆盖率不再增加,则可以通过符号执行的方法来求解路径约束.但是其计算复杂,导致求解效率低下,还会引发路径爆炸等问题.那么如何在覆盖率不增加的情况下筛选出更有潜力的输入,以及决定Fuzzing的后续变异方向是本文的研究关键.Ankou[28]中提出,种子选择函数首先能够区别每个测试用例,并且能够量化不同输入导致的程序差异.Ankou[28]引入了一种基于距离的种子选择函数,通过衡量二次程序的分支组合来量化2个程序输入的相似度,并且使用动态PCA[28]进行降维,把每一个种子输入到种子选择函数进行计算,筛选出低于阈值的种子,即更有可能产生新覆盖的种子.但是传统机器学习的PCA算法,以及Ankou改进的动态PCA算法仍然有许多弊端,如效率低下,对于输入数据的潜在特征把握不够精确,丢失数据潜在信息等.
受到文献研究的启发,本文提出使用异常检测模型GANomaly[29]来求解输入数据的潜在特征与路径之间的关系.ADFuzz结构如图4所示.首先将ADFuzz运行一段时间,收集一定数量跟踪运行后的测试用例,根据测试用例的Hash值分别放入不同的文件夹,将高频路径测试用例作为模型的训练数据.编码器、解码器通过数据集的分布,学习真实数据到潜在特征Z之间的映射,以及重建从潜在特征Z到数据的关系.然后将低频路径与触发crash的种子作为异常点,以及加上少许高频路径的种子作为模型的测试集来优化模型参数.因为模型学习高频路径种子的潜在特征,且低频路径的种子以及触发crash的种子并不适合该潜在特征,以至于模型能够筛选出这些异常种子,减少高频路径种子的运行开销,节省时间成本及加速Fuzzing.最重要的是,异常检测模型能够持续地引导Fuzzer朝着低频路径的方向运行.
Fig.4 ADFuzz framework图4 ADFuzz框架
Untracer[21],CSI-Fuzz[26],Zeror[27]这3个加速Fuzzer都将运行和追踪阶段分开.先是运行只有中断的二进制文件,触发中断之后,再将该种子运行在有覆盖追踪标记的二进制文件上,实际花费的时间是一次运行时间、一次追踪时间、开关forkserver[21]和去掉中断指令时间的总和.随着时间推移,后期能够触发新覆盖的种子相当少,大部分只需要一次运行时间.而ADFuzz实际花费的时间是一次运行追踪时间和模型推理时间之和,大部分是一次推理时间.因为高频路径种子占比逐渐增大,在模型训练完成后都将被过滤,使得ADFuzz速度具有高于其他加速Fuzzer速度的潜力.
ADFuzz基本工作流程以及主要部分如图4所示,ADFuzz采用C和Python混合编程.相比传统AFL类工具而言,增加了异常检测模型指导Fuzzing,本框架能够与其他非加速类Fuzzer工具对接,所以在后续的实验中,本文以比较AFL为主,如果ADFuzz有效,则对于其他AFL类工具也是有同等效益的.通常来说,深度学习模型包括模型的训练、模型的测试以及模型的推理阶段,如何使得Fuzzer与深度学习高效地交互是提高工具效率的关键.本节将介绍ADFuzz的各个功能,包括数据集采集策略、缓冲区算法、能量调度模块以及灵活的模型训练策略.
模型的训练阶段是模型的基础,数据集是否丰富决定了模型的训练好坏,如何有效地生成一个高质量的数据集是一个难点.目前,并没有针对Fuzzing的一个公共通用的程序数据集,所以ADFuzz在每个被测程序上都生成了一个数据集.首先将Fuzzer单独运行一段时间,收集一定数量跟踪运行后的种子.由于确定性变异的种子之间差异较小(几个比特,字节位的翻转),所以只收集Fuzzing破坏性变异阶段的种子,防止模型过拟合.根据input-to-state correspondence[15]思想,程序的输入与程序路径存在某种对应关系,所以将收集到的种子按bitmap的Hash值来作为路径标识分别放到不同的文件夹下,并筛选出高频路径的种子作为模型的训练集,低频路径的种子作为模型的测试集.数据集收集完成后,有2个策略:1)停止Fuzzer运行,等待模型的训练完成后再继续Fuzzing.这样会导致Fuzzer受到模型训练时间的影响,效率低下.2)不停止Fuzzer运行,将Fuzzer和模型的训练分别用不同的进程开启,互不影响之间的效率.由于Fuzzer和模型训练同时运行的情况下有可能存在不在数据集下的新路径,这些路径并不会影响模型的准确率,所以本文采取第2种策略.
当模型训练完成后,开启模型的推理阶段,需要将Fuzzing变异生成后的种子在跟踪运行前交给模型进行判断.考虑到模型与Fuzzer之间的交互效率,以及GPU并行计算的特点,本文不采用将单个种子输入到模型,而是采用将一批次的种子输入到模型中进行并行计算,使用共享内存进行Fuzzer与模型之间的信息交互.通过设置1个共享内存,将Fuzzer生成的一批次种子存放在共享内存中.当共享内存存满后,一次性输入到异常检测模型中,并将结果通过共享内存返回给Fuzzer.尽管如此,ADFuzz的运行速度仍然受到模型推理的效率限制,以及GPU档次的影响.当模型推理时间大于种子跟踪运行时间时,ADFuzz的效率将大打折扣,使得ADFuzz局限于只能够运行在单次运行时间较长的程序上.通过采用缓冲区算法,ADFuzz跳过共享内存种子的跟踪运行阶段,不等待模型的推理结果,继续跟踪运行新的种子,并且缓冲区算法还能够减少异常检测模型错误丢弃种子的情况.
算法1.缓冲区算法.
输入:种子s,模型训练标志success_train,变异次数mutate_num,种子计数seed_count,共享内存地址shm_addr,共享内存数量shm_num;
输出:模型推理结果.
① foriinmutate_num
②S=mutate(s);
③ if(success_train)/*判断模型训练*/
④seed_count++;
⑤ if(seed_count%shm_num==0)
⑥InitShmAddr(shm_addr[0]);/*初始化共享内存*/
⑦copy(shm_addr[0],S);/*将种子拷贝到共享内存*/
⑧ if(model_infer())
⑨ fornuminshm_num/*取出共享内存的种子*/
⑩copy(buf,shm_addr[num]);
⑪run_target(buf);/*运行种子*/
⑫ end for
⑬ end if
⑭ else
⑮temp_num=seed_count%shm_num;
⑯InitShmAddr(shm_addr[temp_num]);
⑰copy(shm_addr[temp_num],S);
⑱ end if
⑲ else
⑳run_target(s);
㉑ end if
㉒ end for
传统的AFL[23]能量调度策略中并没有考虑程序不同执行路径频率的差异,使得种子的能量分配极不合理,种子的能量分配决定了整个Fuzzing的变异方向.在Fuzzing一段时间后,能量多的种子得到了更多的变异机会,产生大量的测试用例,而这些测试用例很难再产生新路径,导致高频路径的无意义运行次数增多;部分低频路径种子却只有少量的变异能量,变异产生的测试用例过少,执行频率过低,使得低频路径得不到足够变异次数进行路径突破.长时间的Fuzzing使得低频路径对应的种子需要更多的变异能量,所以本文对传统能量调度策略进行了改进.对于模型预测出的异常点种子,如果其产生了新路径,将给予更多的能量,使得低频路径种子有更多变异机会,从而引导Fuzzer朝着低频路径方向变异,增大程序覆盖.
由于修改后的能量调度策略对于模型预测成功,且真实产生新覆盖的种子给予更多的变异能量,用于突破低频路径的旁路.当Fuzzing一段时间后,程序的路径频率发生变化,低频路径的频数逐渐增加,新的路径频率分布已经改变,之前训练的模型不再适用,需要重新训练合适的模型.如果路径长时间未发生变化或变化不大时,那么可以重新开启模型训练,使得ADFuzz能够灵活、及时地朝着低频路径方向Fuzzing.
算法2.模型训练调度策略.
输入:种子s,破坏性变异阶段havoc_stage,训练完成标志finish_flag,数据集收集完成标志collect_flag,路径活跃时间live_time;
输出:数据集.
① while True:
②run_target(s);
③ if(hacov_stage&&!finish_flag)/*收集种子,只收集havoc阶段的种子*/
④collect_flag=collect_dataset();
⑤ end if
⑥ if(collect_flag)/*收集完成后训练模型*/
⑦train_model();
⑧collect_flag=False;
⑨ end if
⑩ if(isfile(model_weights))/*模型的参数文件生成,说明模型已经训练完成*/
⑪finish_flag=True;
⑫ end if
⑬ if(last_new_path>live_time)/*当last_new_path的时间大于所给定的时间时,说明路径已经很久没增加了,需要重新训练模型*/
⑭kill_model_process();/*杀掉之前的模型进程,开启新的模型训练*/
⑮finish_flag=False;
⑯ end if
⑰ end while
本文实现了工具ADFuzz,整体框架主要通过C和Python实现,使用异常检测模型GANomaly,所有实验均在CPU Intel i7-6 700,16GB内存,NVIDIA GeForce GTX 1 660,操作系统为64位Ubuntu 20.04.本文选择的被测程序均来自Untracer的FoRTE-FuzzBench[30].为了减少实验的偶然性,额外补充4个目标程序,这些被测程序的输入覆盖了大型文本、图像、音频、视频等格式,这些复杂的输入结构使得工具更具有通用性和代表性.尽管更长时间的Fuzzing对ADFuzz更加有利,本文仍然将所有实验的运行时间设定在24 h.本文通过实验尝试回答3个方面的问题:
1)精度.ADFuzz能否发现更多的路径覆盖以及漏洞数量.
2)速度.对于全追踪运行模式下的AFL类Fuzzer以及选择性追踪运行模式下的Untracer,ADFuzz平均运行速度如何.
3)开销.ADFuzz的性能以及额外开销如何.
为了证明ADFuzz的有效性,本文将FoRTE-Fuzz-Bench与AFL,Untracer进行比较.所有程序均由aflclang[31]编译插桩,运行24h.由于确定性变异阶段产生的测试用例之间差异较小,只有局部的比特和字节变化,使得ADFuzz过滤了大部分的测试用例,因此ADFuzz在确定性变异阶段运行速度得到巨大提升,但是也会降低ADFuzz的覆盖率精度.为了平衡精度与速度,所有Fuzzer只进行破坏性变异,即运行命令加入-d选项.由于被测程序的多样性,被测程序的测试用例通常有文本、图像、音频、视频等格式,对于不同格式的测试用例采取统一的格式作为模型的输入,将输入流按一定的字节行列展开(8的倍数)成二维矩阵,即输入流图,方便使用GANomaly来提取输入样本的特征.
4.2.1 ADFuzz的代码覆盖以及漏洞挖掘能力比较
如图5所示,本文统计了ADFuzz与AFL在FoRTEFuzzBench上24h的覆盖率情况.可以看出ADFuzz在12个程序上的覆盖率均有明显提升,ADFuzz发现新路径的速度以及数量都持续领先于AFL,说明ADFuzz相比于AFL能够更加高效地发现新路径,并且对于结构明显的程序输入效果更好,其中mp42acc覆盖增加最多,增加了25.8%.
Fig.5 24 h path coverage图5 24 h 路径覆盖
Zeror[27]是目前最新的加速Fuzzer,由于Zeror没有公开源码,本文直接与文献[27]中的结果进行比较.如表1所示,本文统计了Zeror,ADFuzz,CSI-Fuzz,Untracer在各自benchmark的平均覆盖率,相比于AFL分别增加10.14%,11.78%,7.7%和降低10.7%.因此,ADFuzz覆盖率高于其他加速Fuzzer,并且发现ADFuzz与Zeror在各自benchmark上有2个共同目标程序libjpeg和libarchive.Zeror相比于AFL分别增加路径条数493[27]和410[27],而ADFuzz分别增加路径条数626和656,可以看出在相同目标程序下,ADFuzz比Zeror能够覆盖更多的程序路径,具有更强的代码覆盖能力.
Table 1 Improvement of Average Coverage Rate Compared with AFL表1 相比AFL的平均覆盖率提升%
如表2所示,对于24h crash发现数量,8个FoRTEFuzzBench程序中只有binutils,poppler,audiofile这3个程序在AFL,ADFuzz,Untracer下发现了crash,以及额外4个补充程序.图6是被测程序发现漏洞的时序图.可以看出ADFuzz相比于AFL,crash挖掘数量更多,且在poppler程序上AFL与Untracer均未发现crash,足以说明ADFuzz发现crash的能力更强.当异常检测模型将高频路径筛选出后,ADFuzz朝着低频路径的方向Fuzzing,许多crash隐藏在这些低频路径中,使得ADFuzz具有更强的发现crash的能力.
Table 2 The Number of crash表2 crash数量
Fig.6 Number of crashes图6 漏洞数量
Zeror,Untracer,CSI-Fuzz这3类加速Fuzzer的基本原理相同,都是只追踪触发中断的测试用例,并且对于触发中断的测试用例给予更多的变异能量,这决定了整个Fuzzing的方向.但是随着时间的推移,当测试用例不再触发新中断后,Fuzzer将会丢失变异方向,导致很难对种子进行更好地筛选和变异,这是这类加速Fuzzer的缺陷.如果长时间的Fuzzing不会带来更多的程序覆盖,单纯地运行无意义的测试用例只会增加Fuzzing的成本,将导致整个Fuzzing变得不再高效.ADFuzz高效的关键在于异常检测模型能够持续、智能地引导Fuzzer,将异常检测模型的推理结果作为种子的适应度函数[28],对种子进行有效地筛选,给予变异运行的方向,即低频路径方向,使得长时间的Fuzzing ADFuzz依然能够有效地发现新覆盖.
4.2.2 ADFuzz的单例执行速度比较
如图5所示,可以明显发现在Fuzzing早期、模型训练期间,ADFuzz与AFL的路径数量基本一致,说明并行运行Fuzzing以及模型训练不影响Fuzzing的效率.当模型训练完成,开启模型推理后,ADFuzz均更快、更早地发现了新路径.只有audiofile和cjson在早期就已经很难发现新路径.当在更长时间(大于24 h)的Fuzzing下,ADFuzz的路径逐渐收敛,此时需要重新收集新的高频路径数据,并且开启模型训练,及时引导Fuzzing朝着低频路径的方向运行.
表3统计了AFL,ADFuzz,Untracer单个测试用例的平均运行时间(根据总生成的测试用例以及24 h运行时间算出),可以看出ADFuzz与Untracer互有高低,并且单个测试用例时间均小于AFL.ADFuzz相比于AFL,平均速度提升23.8%.
Table 3 Average Running Time for Each Testcase表3 单个测试用例的平均运行时间μs
鉴于文献[27]中,各Fuzzer采用tmpfs[27],导致AFL与Untracer以及Zeror单个测试用例的平均运行时间大幅度增大,如果单纯与AFL对比提升的幅度则不太公平,所以本文将速度以Untracer为基准(加速Fuzzer中Untracer速度最快)进行对比.Zeror相比于Untracer速度减少0.46%[27],ADFuzz相比于Untracer减少2.4%,可以看出ADFuzz平均速度稍慢于Zeror.但是对于运行时间稍长的程序poppler,audiofile,ADFuzz采取的过滤不追踪策略使得速度有明显提升,运行速度均高于最快的Untracer.
Untracer类加速Fuzzer,包括Zeror和CSI-Fuzz只追踪运行触发中断的种子,不触发中断的测试用例只运行不追踪其轨迹.当已知路径的中断指令被移除后,丢失变异方向的Untracer便会节省很多路径的追踪运行时间,以至于Untracer运行速度更快.与Untracer的运行不追踪不同,ADFuzz过滤大部分高频冗余的测试用例,既不追踪也不运行,节省了大部分测试用例的运行与追踪开销,使得ADFuzz在长运行时间的程序下快于Untracer类Fuzzer.
4.2.3 ADFuzz的性能以及开销比较
在覆盖率引导的Fuzzer中,追踪运行测试用例为共同的开销成本.在Fuzzing过程中,相比于AFL,Untracer和Zeror增加了开关forkserver开销,去除中断指令开销.而在ADFuzz中,只引入了额外的模型训练推理开销.在相同条件下,与AFL相比, ADFuzz通过过滤部分冗余种子,节省了大量的种子运行和追踪运行开销.这些节省下来的时间成本,使得Fuzzer能够快速切换至种子的生成阶段,利于更多的种子生成,经过模型筛选出的种子,有较高的概率产生新覆盖,以此提高Fuzzing的有效性.图7显示了ADFuzz过滤的种子数量,即节省的Fuzzing开销,其表示在不影响覆盖率增加的情况下,减少Fuzzer对种子的追踪运行开销.图8显示的是ADFuzz相比于AFL所提升的性能比较,即生成的总种子数.
Fig.7 The filtration ratio of seeds图7 过滤种子比
Fig.8 Total number of seeds图8 总种子数
结果分析表明,在运行的24 h内,ADFuzz通过异常检测模型过滤了大部分无意义的高频路径种子,这些被过滤掉的种子并不需要跟踪运行程序状态,相比于AFL节省了大量的追踪运行开销.这些节省下来的运行开销分别配给了模型推理,以及追踪运行额外多生成的种子.通过对所有过滤种子占比进行统计分析发现,ADFuzz相比于AFL模型节省的平均开销为43%,平均多生成33.4%的种子数量,可以得出模型训练和推理所占开销为9.6%.额外的种子生成提高了发现新覆盖种子的概率,使得ADFuzz精度有所保证.
实验结果分析发现:1)在运行速度上,ADFuzz明显快于AFL,并且有着超越Untracer速度的潜力;2)在覆盖率上,ADFuzz相比于AFL平均覆盖率提升了11.78%,其得益于ADFuzz的速度提升以及持续引导低频路径方向Fuzzing;3)在漏洞数量上,ADFuzz漏洞发现数量高于AFL和Untracer,并且在AFL和Untracer都没有发现的poppler上发现了2个新的crash,更加证明了ADFuzz挖掘漏洞的潜力.
本文对12个程序的队列种子进行了标记,发现在模型开启后,90%以上的队列种子都是由模型筛选出的,说明异常检测模型能够筛选出覆盖率增加的种子,也证明了异常检测模型的有效性.
可能影响工具效果的原因包括:1)数据集的丰富性.在训练模型时,数据集的丰富性会影响模型的准确率,由于各个程序没有公开的数据集,所以只能边Fuzzing,边收集训练的数据集,并且只收集破坏性变异阶段产生的种子.2)推理时间的长短.推理时间受显卡档次决定,当显卡更好时,推理时间降低,使得ADFuzz能够更快地筛选种子,提高运行速度.当模型的推理时间小于测试用例在目标程序的运行时间时,ADFuzz的速度将会大幅度提升.3)异常检测模型参数选择.模型参数的选择主要影响模型训练时间,模型训练时间过长容易导致模型不适合当前的路径分布,使模型准确率降低.
目前国内外研究者针对AFL的各个模块优化,衍生了大量的Fuzzer工具.比如, Learn&Fuzz[32]使用了RNN来学习同样具有生成性的统计输入模型,根据学习模型的概率分布生成新的输入;GANFuzz[33]通过SeqGAN算法学习工控协议的语法,生成能够通过协议的高质量测试用例;FuzzGen[34]通过分析整个系统,依据抽象API依赖图生成有效的“Fuzzer stub”;NEUTAINT[13]使用神经网络,分析种子与路径之间的关系,找出种子的关键字节进行变异,减少变异的冗余,提高变异的效率;Ankou[28]引入了一种基于距离的种子选择函数,通过动态PCA算法来减少种子选择函数的计算,筛选出高质量种子;MPBFuzzer[35]使用逻辑回归函数建立路径预测模型,通过熵不确定性公式来计算种子是否执行的阈值,如果高于阈值,代表种子对模型具有很高的不确定性,表示该种子极有可能产生新的路径,从而过滤掉低于阈值的种子;Angora[16]采用字节级污点追踪,只变异影响路径走向的字节,并且使用梯度下降的方法求解路径约束;AFLFast[4]和EcoFuzz[12]通过建模Fuzzing的过程,灵活地调整能量分配策略;CollAFL[5]通过解决Hash边冲突问题,尽可能增大覆盖率;RED-QUEEN[15]发现input-to-state corre-spondence,输入与程序的状态同步,利用这种关系来处理程序的magic numbers和checksums问题.这类Fuzzer寻求突破路径约束来增大覆盖率,确实取得了不错的效果,但是会浪费大量的时间去追踪无意义的输入.Untracer以及后续的CSI-Fuzz分别插入基本块中断和边中断,只追踪这些能够触发中断的种子,极大地提升了Fuzzing速度和减少追踪开销.
近几年,GAN[25]已经成为异常检测领域最受欢迎的方法.通过训练生成网络G的潜在特征空间,使潜在空间能够很好地捕捉到给定数据的分布,然后将实际样本和生成样本之间的残差定义为异常评分.首先使用这种方法的是AnoGAN[36],通过生成网络G学习正常数据的分布,然后在生成网络G的潜在特征空间里随机生成向量z,使得G(z)与正常数据x尽可能地相似,判别网络D通过辨别G(z)与x的差异来更新参数z.但是它的一个明显缺陷是每次有新的图像出现都需要不断地去更新参数z,使得计算成本增加且浪费大量时间.解决这个问题的一种方法是额外添加一个网络来学习数据到潜在空间的映射,比如EBGAD[37].尽管如此,额外的网络也带来不少的计算量.如图2所示,GANomaly[29]进一步改进生成网络G,将生成网络改成编码解码再编码的网络,使得生成网络能够更好地捕获到训练数据以及潜在特征空间的分布,进一步提升性能,并且取得了更好的效果.路径频率是随着时间变化的,如果模型训练时间过长,会导致模型已经不适合新的路径分布,这样模型的有效性就会大幅度降低,所以本文选取GANomaly作为指导Fuzzing的异常检测模型,不仅因为它检测效果好,更重要的是模型训练时间远小于其他异常检测模型.
随着神经网络的发展,越来越多的人工智能模型运用于安全领域,比如IOT安全[38]、5G安全[39]、区块链安全[40]等.本文提出了一种基于深度学习异常检测模型的高效Fuzzing方法,并且实现了该工具ADFuzz.通过实验发现,ADFuzz的运行速度更快,覆盖率以及漏洞挖掘的能力有所提升,并且证明了异常检测模型持续指导Fuzzing朝着低频路径方向变异的有效性.
下一步工作,我们将继续提高异常检测模型的准确率,对数据作更加细腻的处理,比如在收集数据集时,统计路径频数后,只抽取大于10次的路径作为数据集训练,使得模型更加稳定准确;添加低频路径种子生成模块,通过异常检测模型学习到的潜在特征,经解码器重建种子做种子生成,辅助当前的变异算法,直接产生能够运行低频路径的种子,加快变异效率以及更加准确地指导变异方向;减少错误地丢弃高频路径能够产生crash的种子,学习Untracer的方法,将模型筛选出的高频路径种子只运行在没有插桩的目标程序上,不追踪覆盖率信息,提高发现crash的能力.
作者贡献声明:李航宇提出论文思路、完成工具开发和实验、撰写论文;方浩然提出实现意见;曲彦文和郭帆提出指导意见并修改论文.