因为最近研究执行路径探索,需要用到fuzzing的思想,而之前对fuzzing的了解很浅,故先阅读一篇survey来对这个领域有一个全面的认识。由于笔者水平有限,部分暂时无法准确理解之处使用原文表示,敬请谅解。
论文原文:The Art, Science, and Engineering of Fuzzing: A Survey
本文参考:
eunsummeo
Chary Liu
PUT:Program Under Test
待测试的程序
Fuzz inputs:an input that the PUT may not be expecting
待测试的程序所不期望的输入,比如无法正确执行或者触发程序开发者意料之外的行为
Fuzzing
使用预期输入空间之外(fuzz input space)采样的输入执行待测试程序PUT。需要注意的是,fuzz input space无需包含expected input space,这是因为fuzzing的意义就在于特殊、边界样例的测试。其次,fuzzing实际中需要多轮迭代执行,根据大数定律,期间所产生的inputs必然会包括expected inputs。最后需要说明的是,采样过程不一定是随机的,执行次数较少的路径更有可能是属于fuzz input space。
Fuzz testing:the use of fuzzing to test if a PUT violates a security policy
使用fuzzing技术测试PUT是否违反了安全策略
Fuzzer:a program that performs fuzz testing on a PUT
在PUT上执行fuzz tesing的程序
Fuzz Campaign:a specific execution of a fuzzer on a PUT with a specific security policy
fuzzer根据指定安全策略在PUT上的某个执行过程,目的是为了寻找违反指定安全策略的bug
Bug Oracle: a program, perhaps as part of a fuzzer, that determines whether a given execution of the PUT violates a specific security policy.
判定PUT的某次执行是否违反了给定安全策略的独立程序或者fuzzer的组件
Fuzz algorithm:the algorithm implemented by a fuzzer
fuzzer的核心算法,依赖于PUT外的参数
Fuzz Configuration: comprises the parameter value(s) that control(s) the fuzz algorithm
fuzz algorithm的参数的取值,类型取决于fuzz algorithm的类型,简单的比如随机字节流{(PUT)};复杂的比如使用遗传算法来更新参数集合 ( P U T , s 1 , r 1 ) , ( P U T , s 2 , r 2 ) , . . . {(PUT,s_1,r_1),(PUT,s_2,r_2),...} (PUT,s1,r1),(PUT,s2,r2),...,其中 s i s_i si是seed, r i r_i ri是突变率(mutation ratio),这样做的好处是可以减少重复执行,从而提高fuzz testing的效率
Seed:a (commonly well-structured) input to the PUT, used to generate test cases by modifying it
用于构造PUT输入的发生器
Seed pool:a collection of seeds
fuzzer维护的种子集合
根据fuzzer观察到的语义粒度,fuzzer被分为黑盒fuzzer、灰盒fuzzer和白盒fuzzer。
根据PUT输入可分为file, network, UI, web, kernel I/O, or threads fuzzer
预处理通常是检测被测程序,包括插桩,去除冗余配置(种子选择),修剪种子,生成驱动程序,还有准备生成输入的模型。
白盒和灰盒fuzzer可以通过插桩获得执行反馈或者执行时的内存镜像,获取信息的多少取决于fuzzer的类型。除了插桩之外,通过指令流和API序列也可以获得PUT的内部信息。但相比较而言,插桩能够获得更好的反馈信息。插桩分为动态插桩和静态插桩。静态插桩发生在编译之前,PREPROCESS这个阶段,也就是在PUT运行之前,而动态插桩则在程序运行的时候发生,也就是每个INPUTEVAL阶段。因此,静态插桩相较于动态插桩有更优的开销,而动态插桩则更加容易对DLL进行插桩。除了基于源码的插桩,还有基于二进制文件的插桩,即未知源码的插桩技术。常见的动态插桩工具有DynInst 、Dynamo、RIOPIN、Valgrind、QEMU等。
灰盒fuzzer通常用执行反馈来更新测试用例。
路径覆盖:AFL及其变体(bit向量存储覆盖信息)、CollAFL(使用路径敏感的hash函数解决路径冲突)
节点覆盖:LibFuzzer,Syzkaller
在处理启动时间较长或者有分析大程序的部分功能的需求的情况下,每次fuzzing都要启动一遍程序的话效率很低,并且会带来不必要的开销,例如GUI程序在接收输入之前通常需要很多秒的处理时间,网络应用中客户端和服务器的交互很频繁。一种可行的解决方案可以通过拍摄启动后快照的方法来对这种类型的程序fuzzing,这种方法称为in-memory fuzzing(e.g. GRR)。AFL采用类似于in-memory Fuzzing的做法,使用一个fork server来避免启动PUT所需要的开销。部分 fuzzer采用in-memory API fuzzing,这种技术不需要在恢复PUT状态的情况下执行in-memory fuzzing(e.g. AFL persistent mode,重复调用一个函数)。
缺点:
1. 主要依赖于被测程序的入口函数,这种函数难以寻找
2. 难以构造这样的输入能够触发bug或者crash,可能无法复现
3. 忽略了多函数调用之间的作用
Race condition(由于两个或者多个进程竞争使用不能被同时访问的资源,使得这些进程有可能因为时间上推进的先后原因而出现问题,这叫做竞争条件)的bugs比较难以触发,因为这种bugs触发条件为不确定的行为。但是,通过显式地控制线程的调度方式,插桩技术也可以用来触发不同的非确定性程序行为。现有的工作已经表明,即使随机调度线程可以有效地发现竞争条件错误。
如何减小种子池大小的问题称为Seed Selection。
找到最小的种子集,以使覆盖率(例如节点覆盖率)最大化,此过程称为计算最小集(minset)。
e.g. 两个种子s1,s2覆盖了PUT中的地址{s1 → {10, 20} , s2 → {20, 30}},如果第三个种子为s3 → {10, 20, 30}。
这一步可为CONFUPDATE的一部分。
较小的种子可能会消耗更小的内存并引发更高的吞吐量,fuzzer在进行fuzzing之前减小种子的大小,这就是Seed Trinmming。Seed Trimming可以在PREPROCESS或者CONFUPDATE中完成。AFL的种子修剪使用基于代码覆盖率的工具迭代地删除一部分种子,同时保证了修改后的种子有相同的覆盖率。
直接进行fuzz遇到困难的时候,准备一个driver是很有必要的。例如要fuzz一个库,我们写一个调用了库中函数的driver程序,然后才能对这个库进行fuzz测试。可以理解为提供一个与PUT相连接的接口,便于fuzzer调用进行fuzz campaign。
为下一轮fuzzing选择fuzz configuration的行为称为scheduling。
scheduling的目标是分析当前可用的配置信息,并选择可能导致最有利结果的信息,例如,找到最多的bug,或者最大化覆盖率。从根本上说,每种调度算法都面临着相同的探索与利用冲突,可以将时间花在收集每种配置的更准确信息以帮助未来的决策(exploration)上,也可以花在fuzz当前被认为会导致更有利结果(exploitation)的配置上。这种fuzzing过程中exploration和exploitation的冲突称为The Fuzz Configuration Scheduling (FCS) Problem。
shceduling选择configuration的依据:
黑盒fuzzer的FCS算法所能使用的唯一信息就是使用给定配置所得到的fuzz结果,包括crash、bug的数量和时间开销。利用这种信息的方法的自然的想法就是优先选择具有更高成功率(更多的bug或者crash)的配置。使用这种scheduling算法替代均匀采样(uniform-sampling)之后,在CERT BFF Black-box fuzzer超过500万次的运行中发现了85%的新增crash。
这种算法得到了多方面的改进,并在相同的时间内,与现有的BFF相比,获得了1.5倍的效果提升:
Bernooulli trials -> The Weighted Coupon Collector’s Problem with Unknown Weights(WCCP/UW)。Bernooulli trials假设每一个configuration对应一个固定的成功概率,并且随着时间推移学习;WCCP/UW则直接维持一个衰减的成功概率的上界。
Multi-armed bandit(MAB)。MAB通常用于解决exploration vs. exploitation的问题。使用MAB能够发现还没有衰减的configuration。
除此之外,一个更加快速的configuration使得fuzzer能够收集更多的bug或者更加快速地减小成功率的上界,由此自然想到根据时间开销对成功概率进行正规化。
固定的fuzz iteration->固定的总时间。这样一来,fuzzer无需在一个配置上花费很多时间。
灰盒fuzzer的FCS算法所能使用的信息更为丰富,除去黑盒fuzzer所能获取的信息之外,灰盒fuzzer还能获取到覆盖率。e.g. AFL使用遗传算法EA,EA维护一代configuration,每一个都有一些适应值。EA选择适当的配置并且对它们进行扰动和重组来产生后代,后代中的部分会成为新的configuration。使用这种算法的假设前提是遗传产生的新的configuration更加适合产生bug或者crash。
为了理解在FCS的情景下EA算法是如何使用的,需要定义如下三点:
AFLFast就是根据以上三方面对AFL的性能进行改进的:
AFLGo:对AFLFast的优先级分布进行修改,使得其能够针对特定的程序地址
Hawkeye:为directed fuzzing的seed scheduling和input generation引入静态分析
FairFuzz:为每对种子和rare branch引入mutation mask来加强rare branch的覆盖
QTEP:使用静态分析分析PUT的更可能导致PUT出现crash或者bug的部分,增加覆盖这一部分的configuration的优先级
理论上而言,测试样例本身的内容能够直接决定bug是否会被触发,因此input generation自然而然地就成为了一个fuzzer设计过程中最有影响的一部分。根据INPUT GENERATION所使用技术的不同,fuzzer一般会被分为generation-based或者mutation-based两类。generation-based fuzzer根据描述PUT所期望样例的模型产生输入,也可称为model-based fuzzer。mutation-based fuzzer则通过对种子seed添加扰动产生测试样例,而不依赖于模型。seed无法描述PUT的input space,因此也可称为model-less fuzzer。
Peach, Protos, Dharma等: 允许用户指定输入语法
Autodafe, Sulley, SPIKE, SPIKEfile, LibFuzzer:有API能够让用户创建自己的input models
Travor: 允许EBNF(Extended Backus-Naur form)语法的输入说明
PROTOS, SNOOZE, KiF, TFuzz: 用户需要指定network protocal specification
Kernel API fuzzers:定义了system call templates,指定系统调用的参数的类型和个数
Nautilus: general-purpose; grammar-based input; grammar-based seed trimming for general-purpose fuzzing
cross_fuzz,DOMfuzz:random Document Object Model Objects
jsfunfuzz:random syntactically correct JavaScript code
QuickFuzz:Haskell libraries that describe file formats
Frankencerts,TLS-Attacker,tlsfuzzer,llfuzzer:pecific network protocols such as TLS and NFC
Dewey等: constraint logic programming,生成不仅语法(grammatically)正确而且语义(semantically)正确的test case
LangFuzz: 解析输入的种子并产生code fragments,然后随机组合fragments对seed添加扰动产生测试用例,已被应用于 JavaScript and PHP
BlendFuzz: 和LangFuzz类似,只不过针对的是XML和正则表达式的解析
inferred model vs predefined(or user-provided) model。
尽管有大量的有关于输入格式自动化和协议逆向的研究,仍然只有一小部分fuzzer使用到了这些技术。和插桩技术类似,模型推断可能会出现在PREPROCESS或者CONFUPDATE两个过程。
TestMiner:在待测程序中搜索例如常量之类的数据,用于预测可行的inputs
Skyfire:使用数据驱动(data-driven)的方法推断一个概率上下文敏感的语法(probabilitistic context-sensitive grammar),
然后使用这种语法产生一个新的seed的集合。关注点在于产生语义合理的输入。
IMF:通过分析系统的API日志,学习一个kernel API model,然后使用这个模型产生调用某个API序列的C语言代码
CodeAlchemist:分解JavaScript 代码成“code bricks”,然后计算组合约束(assembly constraints),
组合约束条件决定独立的code brick是否可以被组合到一起来产生语义合法的测试用例。
约束条件的计算包括静态分析和动态分析。
Neural and Learn&Fuzz:使用神经网络机器学习算法从一个给定的测试文件的集合学习一个模型,然后使用这个模型产生测试用例。
Liu et al.:与Neural and Learn&Fuzz类似,针对文本输入
PULSAR:从一系列捕获PUT产生的网络包中推断出一个网络协议模型,然后使用这个模型进行fuzz。PULSAR在内部构造了一个状态机,
将message token映射到一个状态。这种映射关系之后会被用于生成覆盖状态机中更多状态的测试用例。
Doupe et al:提出了一种通过观察I/O行为推断出web服务的状态机,然后使用这个模型去扫描web漏洞。
Ruiter et al.:与Doupe et al工作过类似,基于LearnLib实现,针对TLS协议
GLADE:从I/O样例集合中总结出上下文无关(context-free)的语法,然后使用这种语法对PUT进行fuzz
go-fuzz:灰盒fuzzer,根据添加到seed pool的seed建立一个模型,这个模型会被用于产生新的输入
fuzzing通常被用于测试解析特定文件格式的decoder程序。很多文件格式都有对应的encoder程序,encoder程序可以被认为是文件格式的隐含模型。
MutaGen:
问题:随机生成测试样例效率低下,对需要结构化输入的PUT尤为突出。
解决方法:构造一个PUT所能接受的结构化的输入,即Seed。然后仅仅对Seed的一部分进行扰动,就可能能够产生一个能够运行但是包含能够触发PUT异常值的新的测试样例。
flip固定数目或者随机数目的bit,用户可设置mutation ratio(扰动率)来决定多少bit位被flip。fuzzing的表现与扰动率有关,不同的PUT所需的扰动率都不一样。BFF和FOE为每一个seed使用指数级别的扰动率集合并且分配更多iteration给统计上有效的扰动率。SymFuzz使用一个白盒程序分析来为每一个种子推断出一个良好的扰动率。
AFL和honggfuzz包含另一种扰动的操作:考虑一个选定的字节序列作为一个整数,对这个整数实施简单的运算,将计算出的结果用以代替选定的字节序列。这样做直观上的感觉就是通过一个很小的数字限制扰动的效用。例如,AFL从seed中选择一个4字节的值,把这个值当作整数 i i i,然后用 i ± r i \pm r i±r代替 i i i,其中 r r r是一个随机生成的很小的整数。 r r r的范围取决于fuzzer,通常是可以由用户配置的。例如在AFL中,默认的范围是 0 ≤ r < 35 0 \leq r < 35 0≤r<35。
Block是seed的一个字节序列,Block-basd Mutation分为以下几种:
一些fuzzer使用预定义的值的集合来进行扰动,这些值具有潜在的重要的语义权重,比如说0、-1或者格式化字符串。例如AFL、honggfuzz、LibFuzzer使用0,-1,1来对整数进行扰动。Radamsa使用Unicode字符串、GPF使用格式化的字符比如%x和%s来对字符串进行扰动。
白盒Fuzzer也可被分类到model-based或者model-less的类别。例如,传统的动态符号执行不需要任何模型,因此可被分为mutation-based fuzzer;一些符号执行器利用比如输入语法一类的输入模型来指导整个符号执行器的运行。
不是所有的白盒fuzzer都是动态符号执行器。一些fuzzer利用白盒程序分析来寻找PUT所接受输入的信息以便在灰盒或者黑盒fuzzer中使用。
经典的符号执行是指使用符号化的值作为输入运行一个程序,这些符号化的变量代表所有可能的值。当符号执行器执行PUT时,它会建立一个符号表达式而不是计算实际的变量。当它遇到一个条件分支指令的时候,它会分为两个symbolic interpreter,一个代表正确分支一个代表错误分支。对每一条路径,symbolic interpreter会为执行过程中遇到的每一条分支指令建立一个路径公式(路径断言)。如果存在一个实际的输入,能够执行目标路径,那么就说该路径公式是可满足的。可以通过求解SMT solver来生成一个适用于路径公式的实际输入。动态符号执行是传统的符号执行的变体,在动态符号执行过程中,符号执行和实际的执行会同时进行。因此,动态符号执行通常被称为concolic(concrete+symbolic)测试。结合动态执行的优点是实际的执行可以减小符号约束的复杂度。
相比较于灰盒或者黑盒方法而言,动态符号执行是很慢的,这是由于它需要分析PUT的每一条指令并插桩。为了解决开销过大的问题,一种缩小动态符号执行范畴的通用策略被提出:让用户确定代码中不感兴趣的部分或者感兴趣的片段、交替使用conclic testing和灰盒fuzzing。
Driller, Cyberdyne: 交替使用conclic testing和灰盒fuzzing
QSYM: a fast concolic execution engine提高了conclic testing和grey-box fuzzing的性能
DigFuzz: 用灰盒测试确定每个分支执行概率,再使用白盒fuzzer对对于灰盒fuzzing比较challenging的路径进行fuzzing
一些fuzzer利用静态或者动态的程序分析技术来增强fuzzing的效果。通常分为两个步骤:
TaintScope:fine-grained taint analysis to find "hot bytes"
Dowser:a static analysis during compilation to find loops(contaning pointer dereferences),
compute the relationship between input bytes and the candidate loops with a taint analysis,
only the critical bytes to be symbolic
VUzzer and GRT:static and dynamic analysis,control and data-flow features
Angora and RedQueen:a costly instrumentation for lighter instrument,
use taint analysis to associate each path constraint to corresponding bytes,
a search inspired by gradient descent to guide mutations towards solving constraints
looking for correspondence between their operands and the given input to solve a constraint
fuzzing所面临的一个实际的挑战就是绕过检验和的验证。例如,当一个PUT在解析输入之前计算它的校验和,很多测试用例将不会被PUT所接受。
TaintScope:checksum-aware,污点分析技术识别校验和检验的指令,对PUT打补丁来绕过校验和的验证
Caballero et al.:stitched dynamic symbolic execution,这种技术能够在存在checksum的情况下生成测试样例。
T-Fuzz:渗透各种条件分支,找到能够被修改掉但是不会影响程序逻辑的branches(Non-Critical Checks)
当停止找到新paths的时候,程序会找到一个NCC,在目标程序中transforms it
对被修改后的程序再执行测试,如果发生崩溃,用符号执行去跑原版PUT。
生成一个输入后,fuzzer使用这个输入执行PUT,然后决定如何使用执行的结果来提升fuzzer的表现和性能,这个过程称为Input Evaluation。
fuzz所使用的经典的安全策略一般是将导致程序执行终止的致命性的信号(比如segmentation fault)作为violation。这个策略对于内存漏洞检测十分有效,这是因为覆盖一个数据或者给指针赋予以一个非法的值通常会导致segmentation fault。这个策略既高效又便于实现,因为操作系统允许fuzzer不需要任何插桩就能trap到这样的exception。
然而,传统的检测crash的策略不能检测到每一个被触发的内存漏洞。比如,如果一个堆缓冲区溢出覆盖指针,程序可能会以异常值正常结束而不是产生crash,fuzzer将不会检测到这个漏洞。为了解决这种情况,研究者们提出sanitizers来解决这种不安全、预期之外的行为并且终止程序。
内存安全错误通常可被分为两类:时间性的和空间性的。空间性的内存错误通常在指针在对象之外被间接引用指向它原本指向的对象之外的情况下,比如缓冲区溢出;而时间性的内存通常发生在指针失效后被引用,比如use-after-free的漏洞。
ASan: 快速的memory error detector。在编译时插桩,然后维护一个shadow memory,每当一块内存要被解引用的时候就去做有效性检查
MEDS: 维护objects之间或者内部不可以访问的memory red zones,如果被访问了,那很可能时memory crash
SoftBounds/CETS: 在编译时插桩,为每个pointer都结合bounds和时间信息,这样就从理论上能够探测到所有的内存问题
CaVer, TypeScan, HexType等: 在编译时插桩,检查c++的bad-casting,object被cast into一个不兼容的type,例如基类被cast为衍生类
Control Flow Integrity: 检测运行时原本不应当出现的control flow transition,这样就能找到不合法地篡改了程序本身的test case
C语言一类的编程语言有的时候会留下未定义行为。有时程序在不同编译器或者平台上的行为会产生不一致,很多因素都会影响一个编译器实现undefined behaviors,优化设置、架构、编译器甚至是编译器的版本都可能会导致crash或者bug。
Memory Sanitizer: 编译时插桩,用于检测C和C++中使用未初始化memory导致的undefined behaviors
Undefined Behavior Sanitizer: 在编译时修改程序,以此检测未定义行为。UBSan能够检测多种未定义的行为,
比如使用misaligned pointers, 除0,解引用空指针,整数overflows
Thread Sanitizer: 编译时修改程序,检测data races,平衡精确度和性能开销。
e.g. XSS and SQL injection
KameleonFuzz: 用真实的web browser解析测试用例,提取DOM树,使用模式比较来检测XSS attacks
μ4SQLi: 用浏览器检测SQL injections,用db proxy来检测是否确实是有害行为。
(Since it is not possible to reliably detect SQL injections from a web application response, μ4SQLi uses )
differential testing通过比较相似程序之间的行为来发现语义bug。一些fuzzer使用differential testing确定相似程序之间的差异性,这种差异性可能会导致bug出现。
black-box differential fuzz testing: map mutations from input to output
跳过PUT的载入时间能够减少时间开销。
AFL:forl-server,fork from an already initialized process
in-memory fuzzing
Xu et al.:a new system call that replaces fork()
the process of analyzing and reporting test cases that cause policy violations.
冗余数据的删除有助于节省磁盘空间,直观上也能看到出现的bug的数量,有助于防御针对某一类漏洞的攻击。
自动记录crash产生时候的stack backtrace,并根据stack backtrace的内容分配一个stack hash,记录的函数的个数以及信息都没有限制。一些stack backtrace仅仅hash函数名和地址,但是另外一些可能会hash函数名和偏移或者行号。一般有两种hash:major hash和minor hash。major hash将不相似的crash聚集在一起,minor hash则更为精确。Stack Backtrace Hashing基于相似的bug触发相似的crash这一假设,但是这一假设还没有得到验证。
AFL认为一个crash是一个新的crash,当它满足以下两点之一:
(i) crash包含了一条前所未见的edge
(ii) crash没有包含某条出现在前述所有path中都出现过的边
RETracer:
based on the semantics recovered from a reverse data-flow analysis
analyzing a crash dump (core dump),
recursively identifies which instruction assigned the bad value to it
finds a function that has the maximum frame level
“blames” the function to cluster crashes
Prioritization, a.k.a. the fuzzer taming problem ,determining the exploitability of a crash.
exploitability描述的的是攻击者能够实际上编写漏洞利用程序的可能性,攻击者和防御者自然都会更加关注这一类能够实际利用的漏洞。
!exploitable:simplified taint analysis,EXPLOITABLE > PROBABLY_EXPLOITABLE > UNKNOWN >
NOT_LIKELY_EXPLOITABLE,exploitable plugin for GDB and Apple’s CrashWrangler类似
保证触发violation的前提下尽可能减小测试用例的大小。test case minimization and seed trimming的区别在于minimizer可以利用bug oracle。
BFF:尽可能减小和原种子不同的bit的个数
AFL:适时设置字节为0,减短测试用例的长度
Lithium:a general purpose test case minimization tool,remove “chunks” of adjacent lines or bytes,
motivated by the complicated test cases produced by JavaScript fuzzers such as jsfunfuzz
其他非专门为fuzzing设计的:
format agnostic techniques,e.g. delta debugging
specialized techniques for specific formats, e.g. CReduce for C/C++ files
黑盒模型所能利用的信息太少,configuration基本不更新;白盒基本上每运行一个新的测试用例都会更新configuration。
A common strategy in EA fuzzers is to refine the fitness function so that it can detect more subtle and granular indicators of improvements.
AFL通过让fitness function考虑某个branch被覆盖的次数来增进效果
STADS:提出一个受到经济学方法启发的统计学框架来估计如果继续fuzz还能找到多少conf
LAF-INTEL:breaks multi-byte comparison into several branches,detect when a new seed passes an intermediate byte comparison
LibFuzzer, honggfuzz, go-fuzz, Steelix: 都会对复杂条件中的每个比较都插桩。
Steelix: 检查哪个input offsets会影响比较指令
Angora: 考虑每个branch的calling context
DeepXplore: 使用neuron coverage作为fitness function来测神经网络
VUzzer: 其fitness function依赖于每个基本块的权重,而这个权重是由一次程序分析确定的。
使用程序分析将基本块分为正常(normal)或者异常处理块(EH)
normal basic blocks的权重是CFG上随机游走到达它的概率的倒数
EH blocks的权重是负的,based on the hypothesis that traversing an EH block signals a lower chance of exercising a vulnerability
since bugs often coincide with unhandled errors.
解决产生配置过多的问题,最简单的办法是维持一个最大化覆盖率的Minset。
Cyberdyne:removing configurations that are not in the minset
AFL:a culling procedure to mark minset configurations as being favorable