基于覆盖率的Fuzzer和AFL

VSole2023-07-06 09:15:37
一
Fuzzer的类型

模糊测试器的分类方法方式有好几种,本文将着重介绍基于覆盖率的模糊测试器,因此只详细介绍根据fuzzing策略的分类。基于fuzzing的策略,可将fuzzer分为基于定向的fuzzing和基于覆盖率的fuzzing。

对于基于覆盖率的模糊测试工具来说,往往需要使用恰当的种子测试目标程序,基于目标程序的反馈引导种子的变异生成更多恰当的测试用例,以引导测试覆盖尽可能多的代码路径。之所以用覆盖率这一个指标做为衡量,是因为在实践中人们发现更高的覆盖率往往能够找到更多的bug。

然而在实际场景中,一个项目中大部分代码都是不包含bug的,而且实际过程中,对于覆盖率模糊测试,仅靠依靠模糊测试器自己盲目测试是几乎不可能达到极高的覆盖率。定向模糊工具正是基于上诉原因应运而生。因此对于定向模糊测试工具而言,其目的旨在快速的测试特定的目标位置是否可能存在bug。(定向模糊测试工具并非本文的着眼点,因此将略过。)

二
基于覆盖率的Fuzzer

基于覆盖率的模糊测试策略是最先被完整应用的,在长时间的应用中十分可靠,高效且有效。对于基于代码覆盖率的Fuzzer来说,我们先讲一个重要的概念,代码覆盖率(Code Coverage),其次我们再介绍基于覆盖率的Fuzzer的一般工作流程,最后我们简单的介绍以下fuzzing另一个重要的概念种子语料库(Seed Corpus)。

代码覆盖率及度量方法

代码覆盖是软件测试中的一种度量,描述程序中源代码被测试的比例和程度,简单的说即测试目标代码的覆盖程度,所得比例称为**代码覆盖率。**代码覆盖率的度量方式主要有基本快覆盖、边覆盖率、条件覆盖率。

基本块(Basic Block)是具有单一入口和单一出口的代码片段,即一组顺序执行的指令。如下图,IDA中CFG图的代码块即是一种基本块。

一个基本块从其中第一条指令执行开始,直到执行到最后一条指令才结束(跳转到其他块)。也即,单一入口是指只能从基本块的第一条指令进入基本块,单一出口是指只能从基本块的最后一条指令跳转离开。

所谓基本块覆盖率即统计代码中基本块执行的情况,那些基本块执行了,那些没有执行。用执行了的基本块除于总的基本块即是基本块覆盖率。

边的概念也是依托于基本块的概念,基本块之间的跳转若用一条线画出来,这条线就叫做边。如下图所示。

所谓边覆盖率则是统计基本块之间的分支跳转的情况,也即统计那些边被执行了,那些边没有。相信诸位眼尖的可能发现了,边覆盖率能够覆盖相比于基本块覆盖率,虽然代码执行语句上似乎并无区别,但覆盖了更多的代码逻辑。

我们用一个例子来说明。如下图所示,有BB1、BB2、BB3和BB4,四个基本块。若执行路径BB1->BB2->BB3->BB4,则以基本块覆盖率来说,我们可以理所当然的说,覆盖率已经达100%了。

但是我们却发现代码中许多隐含的逻辑并未被测试出来。例如,我们走BB1->BB3->BB4和BB1->BB2->BB4,显然用基本块覆盖率来衡量自然是100%,但是却展示了一种全然不同的代码逻辑。

我们发现,若我们以边的执行程度来衡量,当所有的边被访问时,此时基本块覆盖率必然100%,并且还挖掘出许多隐含的代码逻辑,也是说,不但代码测的全还测的更加彻底。

一般工作流程

我们以《Fuzzing:a survey》给出的覆盖率Fuzzer的一般工作过程的伪代码做为例子来解读。

首先,我们需要输入一个初始种子S,若没有给定初始种子S,则模糊器自己将构造一个。

随后,从初始种子选择种子进行变异生成测试用例,测试程序,若测试的结果为crash则加入Crash输出集。若并未产生crash则模糊器判断该测试用例是否有价值、足够有趣,若答案是肯定的则加入种子集合,再后续的主循环中利用。直到模糊器收到终止信号,则退出循环,并将运行中收集到的crash集输出。

我们可以从这样一个流程中看到,模糊器无外乎做了两件事情,一是根据种子生成测试用例,二是根据测试用例测试程序并追踪程序的状态,如此循环往复。可以简单的兑换为以下的流程图。

种子语料库

我们在0x02章中,工作流程这个小节中发掘,种子库对于fuzzer来说是一个至关重要的环节,而在实践过程中也的确证实,好的种子库对于fuzzer结果的影响和执行效率都十分巨大。

对于基于覆盖率的fuzzer来说,种子语料库的质量显著的影响fuzzing的质量。良好的初始种子可以显著提高fuzzing的效率和效果。

具体来说,提供目标测试程序输入要求良好格式的种子,可以避免因大量无用的CPU消耗。良好的种子输入格式,可以避免模糊器在该阶段盲目的变异猜测。良好输入的格式的种子可以抵达更深和盲目变异难以抵达的路径。

此外,种子语料库的制作是也是一门重要的技能。

三
AFL

AFL是由Google工程师开发的一款基于引导覆盖的模糊测试器,可以用于白盒测试与黑盒测试。在Github上,Google官方的AFL项目对AFL的描述如下:

American Fuzzy Lop is a brute-force fuzzer coupled with an exceedingly simple but rock-solid instrumentation-guided genetic algorithm. It uses a modified form of edge coverage to effortlessly pick up subtle, local-scale changes to program control flow.

AFL工作流程

AFL的工作流程可以体现为下图。根据Google对AFL主要算法工作原理的总结,可以归纳为下面几点:

1.从用户获取初始种子,加载到种子池队列中。

2.从队列中获取一个种子文件,并且优先取得最小和最快的种子。

3.对取得的种子文件进行变异,生成一系列测试例子。

4.将一系列的测试例子,基于AFL的模糊测试策略进行测试和追踪。

5.对于有利于提高覆盖率的测试例子将被筛选为好的例子,加入到种子队列中,同时记录Crash。

6.go to 2。

在AFL执行流程的每一步中,都有许多迫切的问题。例如,如何挑选种子、如何变异、如何测试、如何测量覆盖率等等,许多的问题都有行之有效的解决方法,但有效的人工干预仍然是一个好的选择。

再下面一节,我们将讨论再AFL中,对于上诉问题采取什么策略,是如何应对的。

AFL的工作原理

覆盖率度量与引导

AFL的覆盖率度量是一种混合度量,它将边覆盖率与在一次执行中相应边的次数相结合,并设置一个上限(一个2的幂次方),防止路径爆炸。

如果一个testcase访问到了一条新的边(edge),那么AFL则认为它是有趣的(GOOD),将加入到种子队列中。

那么AFL又是如何记录已经访问的边呢?AFL是这样计算的。如下图所示,有两个基本块BB1和BB2,当发生从BB1到BB2的跳转时,即可认为BB1和BB2之间的边被访问。AFL在会在编译插桩阶段对基本块BB1和BB2分别插入一个标识R1和R2,根据这个R1和R2计算出一个哈希值,更新至位图中相应索引的字节。

上诉逻辑可以兑现为如下伪代码。

cur_location = 
shared_mem[cur_location ^ prev_location]++;
pre_location = cur_location >> 1;

Seed Mutations

AFL的变异分为deterministichavoc两种类型。其中deterministic对testcase的内容进行确定性的突变,如位翻转、加法、一些特定值的替换(-1、INT_MAX)等。

havoc模式中,突变将具有更大的不可确定性,如可能会调整testcase的内容(添加或删除部分内容)。

ForkServer

最初AFL对于每次fuzz都会建立一个虚拟进程空间,显而易见,频繁的进程创建对CPU来说是一个巨大的开销,对于fuzzing的效率不可忍受。

为了避免execve()的开销,AFL设计了一套Forkserver机制。每当AFL需要fuzz新的testcase时,AFL会重新写入testcase,然后告诉target fork自己。在这种情况下,AFL不需要每次都支付新进程初始化的开销,而是fork一次后不断在在第一次fork的进程中复用进程空间的资源,直到发生write时。

使用AFL进行fuzzing

优先推荐使用AFL的增强版本,AFL++。在比较新的Linux发行版上,大都可以通过一条命令安装AFL++,以Ubuntu为例子,执行以下命令即可。

sudo apt install afl

然而,更加简单快捷的使用完整的新版本AFL++,可以选择容器构建。具体的只需要执行如下命令即可。

docker pull aflplusplus/aflplusplus
docker run -ti -v /location/of/your/target:/src aflplusplus/aflplusplus

具体的如何使用AFL进行fuzzing可以参考我的一篇文章,Fuzzing101-Exercise2 fuzz CVE-2009-3895和CVE-2012-2836 - 辰星-cxing - 博客园(https://www.cnblogs.com/cx1ng/p/17362871.html)

Fuzzer面临的挑战

fuzzer面临的挑战有许多,这里挑选了可以通过人工干预改进fuzzer效率和结果的几个问题。

第一个是关于变异种子的,有两个关键的问题,分别是在哪里变异、如何变异。因此只有少数关键位置上的恰当变异才会实质的影响程序的控制流。

第二个是关于验证。许多的程序中有各种各样的验证,诸如验证版本号、Magic值、特定字符串、特征值、输入格式等等。这个问题在黑盒测试过程中尤为重要,你不能期待fuzzer的盲目试探可以给你一个很好的结果,由于上诉验证的存在,盲目的变异将极大降低fuzzing的效率。

对于fuzzer的使用者来说,这两个问题实际上都指向一个问题,即如何制作一个行之有效的种子语料库,正确的意识到这两个问题对于我们制作特定程序高质量的种子库有十分巨大的帮助。

具体的,对于验证我们可以制作有通过验证与没有通过验证的种子,这样我们即可以测试验证代码又可以绕过验证代码继而测试更深处的代码,这将大幅提高fuzzing的效率。对于这种子变异问题,我们也可以人工干预,在某些我们感兴趣的位置制作一些指向目标代码区域的种子。

此外,除了制作种子之外,我们可以通过修改目标程序的一些代码,使fuzzing容易的略过一些我们不感兴趣或充分fuzzing的区域,使得fuzzing可以更加容易的深入,但需要注意。

程序测试覆盖率
本作品采用《CC 协议》,转载必须注明作者和本文链接
模糊测试是一种向程序提供随机意外的输入以测试可能的崩溃或者边缘情况的方法。通过模糊测试可以揭示一些逻辑错误或者性能问题,因此使用模糊测试可以让程序的稳定性和性能都更有保证。Go 从1.18 版本开始正式把模糊测试加入到了其工具集中,不再依靠三方库就能在程序代码中进行模糊测试。为什么引入模糊测试大家看文章开头第一段的解释,那就是Go官方要引入模糊测试的原因。
也许能让你的工程鲁棒性再上一个台阶,并或多或少缓解你的担忧。其核心思想是将自动或半自动生成的随机数据输入到一个程序中,并监视程序异常,如崩溃,断言失败,以发现可能的程序错误,比如内存泄漏。模糊测试常常用于检测软件或计算机系统的安全漏洞。
编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。go test命令是一个按照一定约定和组织的测试代码的驱动程序。
入侵检测与威胁分析系统的研发为对抗攻击提供了更直接、响应速度更快的新方法。
随着开发团队越来越有信心在项目中引入安全,随着安全人员学会在不影响创新的情况下履行职责,新的关注重点落在了安全左移上。通过在早期阶段修复问题,防止其演变为需花费巨资加以修复的灾难性漏洞,安全左移可达到节省时间和金钱的目的。安全左移需要成熟的团队第一个挑战就是该方法需要成熟的团队。实现安全左移的团队必须对过程中彼此的功能有基本的了解。
最全的Python开发库!
2022-07-01 08:24:52
Web 框架主要用于网站开发,可以实现数据的交互和业务功能的完善。使用 Web 框架进行 网站开发的时候,在进行数据缓存、数据库访问、数据安全校验等方面,不需要自己再重新实现,而是将业务逻辑相关的代码写入框架就可以。
模糊测试探索者之路
2021-07-03 16:58:01
模糊测试探索者姜宇:由于分布式系统固有的复杂性,保障分布式系统安全充满挑战;模糊测试是具有良好扩展性、适用性以及高准确率的漏洞挖掘技术;模糊测试在分布式系统上的应用还存在局限性;解决高效模糊测试三大关键挑战为国产数据库软件安全保驾护航。
为了在对抗中保持领先地位,这是一场与攻击者的持续战争。多年来,首席信息安全官向董事会成员提供安全指标一直是一项挑战。这些评估着眼于事件响应,并为每天寻找入侵和响应事件的分析师提供宝贵的“实弹”演习机会。DumpsterFire工具集将在启动时自动检测您的自定义Fire模块并启用他们。预先记录的数据不仅表示特定的已知恶意事件,还表示围绕该事件发生的其他上下文/事件。此存储库保存由已知的“恶意”对抗活动生成的数据,项目的名称即
模糊测试器的分类方法方式有好几种,本文将着重介绍基于覆盖率的模糊测试器,因此只详细介绍根据fuzzing策略的分类。基于fuzzing的策略,可将fuzzer分为基于定向的fuzzing和基于覆盖率的fuzzing。定向模糊工具正是基于上诉原因应运而生。因此对于定向模糊测试工具而言,其目的旨在快速的测试特定的目标位置是否可能存在bug。
VSole
网络安全专家