Fuzzing这个事物大概可以上溯到1950年,当计算机还在读取打孔卡作为输入的时候。那时候的工程师会从垃圾箱里随机检出一些废弃卡片,或者在卡上随机打孔作为输入来测试自己的程序。在1988年,Barton Miller在课堂上将Fuzzing这个名词正式确定,从此拉开三十年波澜壮阔的序幕。
广义上的fuzzing并不是漏洞挖掘中的专属内容,而是DevSecOps和Continous Integration质量保证中必不可少的一环,甚至可以延伸到完备图灵自动机的美妙梦想。在起初,人们通常以monkey testing来指代最原始的fuzz,就像著名的无限猴子定理一样:让一只猴子在打字机上随机地按键,当按键时间达到无穷时,几乎必然能够打出任何给定的文字,比如莎士比亚的全套著作,当然也有可能包含一套Nginx RCE。
但很显然,随机化的输入虽然终究能覆盖所有的输入空间,在人类未来可预见的算力水平下近乎天方夜谭。刘慈欣在《诗云》中有一个宏大的故事:宇宙神级文明为了写出最优雅的诗,而把整个太阳系的物质作为存储器,采用枚举遍历的办法把所有文字的排列组合全部生成并存储了下来。但最后神却发现,即使理论上这里存储了所有的诗句,他也无法在可接受的时间内找出目标来,因为美妙这个词本身就是主观而无法定量界定的。以神的算力尚且无法完成,以人的算力就更不可能了。随着软件复杂度大爆炸式的增加,dumb fuzzing的随机生成方式相当于在东方明珠上向下扔一只钉子,寄希望它能刚好掉进楼下某个人举着的酒杯里。
为了解决以上问题,在学术界,结合动态执行的symbolic execution在一段时间成为了主流,KLEE[1]即是其中的扛鼎之作。在Z3 Solver和LLVM bitcode infrastructure的加持下,KLEE能够自动解析各式各样的条件代码语句并生成输入,进而辅助fuzzing。
而在工业界,一般分两种做法:
虽然这些方案看起来都非常home brew,在早期的软件复杂度和质量的前提下,人们依然能够取得较好的成果。但随着软件复杂度的摩尔式增长和SDL开发流程的全面引入,停留在农耕时代的fuzzer们发现,自己开始跟不上软件发展的潮流了。
所有信息安全面临的问题都能在军事界找到类似的场景。美军在越战之中投放了前所未有的火力和军队,但依然遭受了惨重的伤亡乃至最后停战。越战的问题集中反应在火力杀伤效率的低下,面对防御严密的目标,攻击方只有两种选择
这和fuzzing技术后期面临的问题是类似的。针对越来越复杂的目标,纯粹堆计算资源的dumb fuzz效果已经可以忽略不计,但程序分析本身是一个NP-hard问题,符号执行的方法极易引起路径爆炸,对于复杂的软件最终会撞到资源墙上。而工业界的方法过于强调样本或格式指引而非程序指引,要么需要投入大量的前期精力编制模板,或者在缺乏context的情况下两眼一抹黑撞大运。
直至80年代末期,人们所设想的现代战争形式依然是拼消耗的钢铁洪流对撞,两伊战争的结果更是给这个理论增加了注脚。萨达姆在海湾阴云密布时,仍然充满了信心,仗百万精兵要让多国部队血流漂橹。
然平静之下,暗流涌动。针对以上问题,电子技术的发展和成熟及应用催生着新一代的军事革命,并最终在海湾战争中完美登台亮相。多国联军以摧枯拉朽之势击败了伊拉克军队,彻底颠覆了过往的战争形势,震撼了全球:
漫长的黑夜预示着黎明,更精准、更敏捷的划时代fuzzer即将登场。
随着LLVM工具链的成熟和计算资源的大发展,基于编译器插桩的coverage-feedback driven fuzzer渐渐成为了答案之一。正如美军在海湾战争展示的新军事革命一样,AFL的诞生则宣告着这个永恒的问题出现了一个高分答案。
AFL (Americal Fuzzy Lop,logo是美洲本地可爱的长毛兔) 由Michał Zalewski在2014年发布并开源,并迅速像AK47统治枪械市场一样成为了fuzzing系列工具的de facto standard,数年间斩获上万CVE。
在先驱者探明道路之后,后继者雨后春笋般涌现出来。LLVM也推出了自己的实现libfuzzer,以in-process mode取代了fork mode。基于AFL和libfuzzer的体系,结合protobuf定义,模版化fuzz也以structure-aware fuzzing的名义重返舞台。内核领域的对应者syzkaller也取得了极大成功。这类方法统称为Coverage-based greybox fuzzing(CGF),如秋风扫落叶一般横扫各大系统软件,进而成功入关获得了学术界的注意。
CCS’16中Marcel Bohme, et al[2]将工业界的这项革命成果正式通过Markov Chain的方式予以理论化,即Fuzzing实际上是不断变化的马尔可夫链,马尔可夫链的各个节点代表了程序的各种状态,而种子选择和样本变异的策略将直接影响结点之间的转移概率。那么衡量CGF Fuzzer效率的指标自然就是发现新节点的概率是否足够大,以及节点之间转移的概率*次数(命名为能量
,注意这里的能量并不是指省电费降低服务器负载的概念,而是说对各个样本和路径的单位fuzz投入和需要生成的input数量,笔者认为称为FI
, fuzzing investment更合适)是否足够均衡。
在能量体系的武装下,Marcel等人提出了AFLFast对AFL设计中存在的能量问题做了进一步改进。Marcel等观测发现,AFL的默认调度算法存在着两个问题
FI
被浪费,影响启动效率FI
,导致低密度区域无法触达,造成恶性循环针对这些问题,AFLFast在FI调度算法和搜索策略上进行了改进,在真实Fuzz目标-binutils上获得了比AFL高1-2个数量级的发现,在24小时内发现了原始AFL未能发现的4个CVE,平均达到19倍的效率提升。Team Codejitsu在美国国防部和DARPA组织的CGC – 基于人工智能的自动化漏洞挖掘大赛上,使用AFLFast获得了漏洞发现数目单项挑战的亚军。
在系统层面,Wen Xu等在ACM CCS 2017[3]上提出了AFL原始实现中fork和多实例样本同步中存在的问题:
通过新增snapshot等syscall来解决以上问题,最终在多核机器上取得了至少40%的性能提升。
在Zalewski于2018年宣布挂印归隐后,社区接过了AFL发展的任务,并以AFL++的形式继续发布。AFL++引入了上述研究在内的多种优化,继续在各大服务器里冲锋陷阵,繁荣的新时代已然到来。
未来战争是什么形式?未来的fuzzing又是什么形式?笔者并无意断言也无法断言,但仍愿管窥一二抛砖引玉
在历史洪流的大背景下,漏洞军火化不再是遮遮掩掩的话题,而逐渐露出了真刀真枪国家对抗的原本面目。互联网设施的安全保卫和对等威慑能力的建设和实施,包括ClusterFuzz/OSSFuzz这样基础平台的建设,已经是时代摆在面前的课题。
在后续系列文章中,笔者将结合上述展望,尝试再度展开一二,敬请期待。
[1] KLEE – KLEE LLVM Execution Engine https://klee.github.io/
[2] Coverage-based Greybox Fuzzing as Markov Chain – https://www.comp.nus.edu.sg/~abhik/pdf/CCS16.pdf
[3] Designing New Operating Primitives to Improve Fuzzing Performance – https://gts3.org/~wen/assets/papers/xu:os-fuzz.pdf