模糊测试技术
作者:网络转载 发布时间:[ 2017/7/10 9:48:33 ] 推荐标签:模糊测试 软件测试
安全软件的重点是能使系统更加安全。在开发软件时,不想引入新的故障点,或者增加软件运行系统的攻击面。所以我们自然会认真对待安全的编码实践和软件质量。在这篇文章中,我们想解释一下我们在内部使用的用来发现漏洞和缺陷的模糊测试技术,以便在这些漏洞发生在客户那里以及我们亲爱的bug赏金猎人之前找到它们。
一种已经被漏洞赏金猎人证明是非常有效的发现软件安全漏洞的一种称为fuzzing的技术,这种技术需要在目标程序中注入意外或畸形的数据,以便导致输入错误处理,例如可利用的内存损坏。为了创建模糊测试用例,一个典型的模糊测试器将会改变现有的样本输入,或者根据定义的语法或规则集生成测试用例。一种更有效的模糊方法是覆盖引导模糊测试,程序执行路径被用于为测试用例生成更有效的输入数据。覆盖引导模糊测试会尝试大化程序的代码覆盖率,以便测试程序中存在的每个代码分支。随着一些覆盖引导模糊工具的开源,如American Fuzzy Lop (AFL),LLVM libFuzzer和HonggFuzz,使用覆盖引导模糊测试技术从未如此简单。你不再需要掌握深奥的技术,或者花费无数个小时编写测试用例生成器规则,或者是收集覆盖目标所有功能的输入样本。在简单的情况下,你可以使用不同的编译器编译现有的工具,或者分离出你想要的模糊测试功能,只需编写几行代码,然后编译并运行fuzzer。fuzzer将每秒执行数千甚至数万个测试用例,并从目标中的触发行为中收集一组有趣的结果。
如果你想要开始使用覆盖指导自己的模糊测试,下面会提供几个示例,描述如何使用我们内部所喜欢的两个Fuzzer:AFX和LLVM libFuzzer来构建一个被广泛用于XML解析的工具库——libxml2的模糊测试工具。
用AFL进行模糊测试
将AFL用于实际的模糊测试的例子很简单。在Ubuntu 16.04 Linux上,你可以通过系统的xmllint实用程序和AFL,并执行下面的七个命令来进行libxml2的模糊测试。
首先我们来安装AFL并获取libxml2-utils的源代码。
$ apt-get install -y afl
$ apt-get source libxml2-utils
接下来,我们对libxml2进行配置和构建,配置的时候使用AFL编译器并编译xmllint实用程序。
1. $ cd libxml2 /
2. $ ./configure CC=afl-gcc CXX=afl-g++
3. $ make xmllint
后,我们为AFL创建一个包含“<a> </a>”的示例文件,然后开始并运行afl-fuzz。
$ echo "" > in/sample
$ LD_LIBRARY_PATH=./.libs/ afl-fuzz -i ./in -o ./out -- ./.libs/lt-xmllint -o /dev/null @@
AFL将会不停地持续进行模糊测试,写入输入,并在./out/queue/中触发新的代码覆盖,在./out/crashes/中触发输入崩溃,在 /out/hangs/触发输入挂起。有关上图中的AFL运行状态的更多信息,请参阅:http://lcamtuf.coredump.cx/afl/status_screen.txt
使用LLVM libFuzzer进行模糊化
我们现在使用LLVM libFuzzer来对libxml2进行模糊测试。要开始模糊测试,你首先需要引入一个目标函数LLVMFuzzerTestOneInput,它从libFuzzer接收模糊测试输入缓冲区。代码看起来像下面这样。
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Non-zero return values are reserved for future use.
}
针对libxml2的模糊测试,Google的fuzzer测试套件提供了一个很好的模糊测试示例函数。
// Copyright 2016 Google Inc. All Rights Reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
#include
#include
#include "libxml/xmlversion.h"
#include "libxml/parser.h"
#include "libxml/HTMLparser.h"
#include "libxml/tree.h"
void ignore (void * ctx, const char * msg, ...) {}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
xmlSetGenericErrorFunc(NULL, &ignore);
if (auto doc = xmlReadMemory(reinterpret_cast(data), size, "noname.xml", NULL, 0))
xmlFreeDoc(doc);
return 0;
}
在编译我们的目标函数之前,我们需要使用clang和-fsanitize-coverage = trace-pc-guard来编译所有依赖关系,以启用SanitizerCoverage覆盖跟踪。 为了启用
AddressSanitizer(ASAN)和UndefinedBehaviorSanitizer(UBSAN),捕获许多可能难以找到的错误,还需要使用-fsanitize = address,这是一个很不错的主意 。
$ git clone https://github.com/GNOME/libxml2 libxml2
$ cd libxml2
$ FUZZ_CXXFLAGS = “-O2 -fno-omit-frame-pointer -g -fsanitize = address,undefined -fsanitize-coverage = trace-pc-guard”
$ ./autogen.sh
$ CXX="clang++-5.0 $FUZZ_CXXFLAGS" CC="clang-5.0 $FUZZ_CXXFLAGS" CCLD="clang++-5.0 $FUZZ_CXXFLAGS" ./configure
$ make
在这篇文章里面,libFuzzer没有附带预编译的clang-5.0软件包http://apt.llvm.org/,所以你仍然需要自己检查并编译libFuzzer.a,参考文档在这里:http://llvm.org/docs/LibFuzzer.html#get-started,但这个文档可能会在不久的将来发生变化。
第二步是编译我们的目标函数,使用相同的标志,并将其与libFuzzer运行时和我们之前编译的libxml2进行链接。
$ clang++-5.0 -std=c++11 $FUZZ_CXXFLAGS -lFuzzer ./libxml-test.cc -I ./include ./.libs/libxml2.a -lz -llzma -o libxml-fuzzer
现在我们准备好运行我们的fuzzer了。
$ mkdir ./output
$ ./libxml-fuzzer ./output/
我们没有使用任何样例输入,所以libFuzzer会从生成随机数据开始,以便找到在libxml2目标函数中触发新代码路径的输入。触发新覆盖的所有输入都将作为示例文件存储在./output中。由于libFuzzer在进程中运行,所以如果发现了错误,它会保存测试用例并退出。在配置高端的笔记本电脑上,libFuzzer的单一实例每秒可以达到超过5000次执行,一旦开始生成具有更多覆盖范围的测试用例,速度会减慢到2000左右。有关解释输出内容的更多信息,请参见:http://llvm.org/docs/LibFuzzer.html#output
创建语料库
如果你的目标是快速的执行模糊测试,比如每秒执行数百甚至数千次,那么你可以尝试生成一个基础语料库。即使使用更复杂的格式,如AFL作者Micha?Zalewski对JPEG文件进行模糊测试,使用覆盖引导模糊测试也可以做到这一点,但是为了节省时间,你应该获得尽可能小的应用程序的典型文件。文件越小,模糊测试越快。
当生成语料库时,AFL没有给出任何补充标记。只需要给出一个小的样本输入,例如“<a> </a>”作为XML示例,并像通常那样运行AFL。
使用libFuzzer可以有更多的标志来进行实验。例如,对于XML,你可能需要尝试使用“ -only_ascii = 1 ”。对于大多数格式的一个很好的技术是执行多个时间较短的运行,同时增加我们的Fuzzer的每一轮的大样本量,然后合并所有结果以形成输出的语料库。
$ for foo in 4 8 16 32 64 128 256 512; do
./libxml-fuzzer -max_len=$foo -runs=500000 ./temp-corpus-dir;
done
$ ./libxml-fuzzer -merge=1 ./corpus ./temp-corpus-dir
使用这种方法,我们首先需要收集大长度为4字节的有趣的输入,接下来运行分析4字节的输入,并将其用作8字节输入的基础等等。这样我们可以用更小的输入来发现“简单”的覆盖范围,当我们移动到较大的文件时,我们有了一个更好的初始设置。
为了获得这个技术的一些数字,我们用示例脚本进行了三次运行。
平均来说,运行语料库生成脚本在我们的笔记本电脑上花了大约18分钟。LibFuzzer在迭代结束时仍然经常发现新的coverage,其中-max_len大于8字节,这表明,对于这些长度,使用libFuzzer花费的时间也比较长。
为了比较,我们还采用了libFuzzer的默认设置,并运行了三次,大概用了18分钟。
$ ./libxml-fuzzer -max_total_time=1080 ./temp-corpus-dir
$ ./libxml-fuzzer -merge=1 ./corpus ./temp-corpus-dir;
从这些结果我们看到,我们运行的语料库生成脚本平均执行了更多的测试用例,生成了一组更大的文件,触发了比使用默认值生成的集合更多的覆盖和功能。这是由于libFuzzer使用默认设置生成的测试用例的大小导致的。以前的libFuzzer使用的是64字节的默认的-max_len,但是在编写libFuzzer时,刚刚更新了一个默认的-max_len为4096个字节。在实践中,由脚本生成的样本集已经非常有效地起作用了,但是在长时间连续模糊测试中,与默认设置相比,效果不同,并没有收集到数据。
生成语料库是一个令人印象深刻的壮举,但是如果我们将这些结果与W3C XML测试套件的覆盖范围进行比较,我们看到,将不同来源的示例文件包含在你的初始语料库中也是一个好主意,在你弄清目标之前,会得到更好的覆盖。
$ wget https://www.w3.org/XML/Test/xmlts20130923.tar.gz -O - | tar -xz
$ ./libxml-fuzzer -merge=1 ./samples ./xmlconf
$ ./libxml-fuzzer -runs=0 ./samples
#950 DONE cov: 18067 ft: 74369 corp: 934/2283Kb exec/s: 950 rss: 215Mb
将我们生成的语料库合并到W3C测试套件中将代码块覆盖率增加到18727,所以并不是那么多,但是我们仍然获得了83972个功能,从而增加了这些测试用例的总吞吐量。这两个改进有可能是由于小样本触发了W3C测试套件未涵盖的错误条件。
“修剪”你的语料库
在将目标模糊测试一段时间后,终会出现一大堆模糊测试文件。这些文件中的很多文件是不必要的,将它们“修剪”成更小的集合可以为你提供与目标相同的代码覆盖。为了实现这一点,这两个项目都提供了语料库小化工具。
AFL为你提供了可用于小化语料库的afl-cmin shell脚本。对于上一个示例,为了小化在./out目录中生成的语料库,你可以将生成的小化的文件集放在./output_corpus目录中。
$afl-cmin -i ./out/queue -o ./output_corpus -- ./.libs/lt-xmllint -o /dev/null @@
AFL还提供了另一个工具afl-tmin,可用于小化单个文件,同时可以保持前面看到的相同的覆盖率。请注意,在一大堆文件上运行afl-tmin可能需要很长时间,因此在尝试afl-tmin之前,首先要使用afl-cmin进行几次迭代。
LibFuzzer没有提供外部“修剪”工具 – 它具有内置的称为merge的语料库小化功能。
$./libxml-fuzzer -merge=1 <output directory> <input directory 1> <input directory 2> ... <input directory n>
LibFuzzer 的merge更容易使用,因为它是从任意数量的输入目录递归地查找文件。libFuzzer merge中的另一个不错的功能是 -max_len标志。使用-max_len = X, libFuzzer将仅使用每个样本文件的前X个字节,因此你可以收集随机样本,而无需关心其大小。没有max_len标志,libFuzzer在执行合并时使用的默认大长度为1048576字节。
使用libFuzzer merge,你可以使用与生成语料库相同的技术。
$ for foo in 4 8 16 32 64 128 256 512 1024; do
mkdir ./corpus_max_len-$foo;
./libxml-fuzzer -merge=1 -max_len=$foo ./corpus-max_len-$foo ./corpus-max_len-* <input-directories>;
done
$ mkdir output_corpus;
$ ./libxml-fuzzer -merge=1 ./output_corpus ./corpus-max_len-*;
通过这种“修剪”策略,libFuzzer将首先收集每个输入样本中触发2个字节块的新覆盖,然后将这些样本合并为4个字节的块,依此类推,直到所有不同长度的块中都具有优化集合。
简单的 merge 并不总是可以帮助你解决性能问题。有时,你的fuzzer可能会遇到非常慢的代码路径,导致收集的样本开始衰减你的模糊测试吞吐量。如果你不介意牺牲几个代码块来执行性能,则可以轻松的使用libFuzzer来从语料库中删除运行太慢的样本。当libFuzzer以文件列表作为参数而不是文件夹运行时,它将单独执行每个文件,并打印出每个文件的执行时间。
$ ./libxml-fuzzer /*
INFO: Seed: 3825257193
INFO: Loaded 1 modules (237370 guards): [0x13b3460, 0x149b148),
./libxml2/libxml-fuzzer: Running 1098 inputs 1 time(s) each.
Running: ./corpus-dir/002ade626996b33f24cb808f9a948919799a45da
Executed ./corpus-dir/002ade626996b33f24cb808f9a948919799a45da in 1 ms
Running: ./corpus-dir/0068e3beeeaecd7917793a4de2251ffb978ef133
Executed ./corpus-dir/0068e3beeeaecd7917793a4de2251ffb978ef133 in 0 ms
使用awk的代码片段,此功能可以打印出花费太长时间运行的文件的名称,在我们的示例中为100毫秒,然后我们可以删除这些文件。
$./libxml-fuzzer /* 2>&1 | awk '$1 == "Executed" && $4 > 100 {print $2}' | xargs -r -I '{}' rm '{}'
并行运行两个fuzzer
现在你有一个很好的基础语料库了,你知道如何维护它,你可以启动一些连续的模糊测试运行实例。你可以单独运行你喜欢的fuzzer,或单独运行两个fuzzer,但如果你有足够的硬件可用,你也可以在同一语料库中同时轻松运行多个fuzzer。这样,你可以结合两个fuzzer的佳优势,而fuzzer可以分享他们各自发现的所有新覆盖。
很容易可以实现一个简单的脚本,同时运行两个fuzzer,同时每小时重新启动fuzzer来刷新样本语料库。
$mkdir libfuzzer-output; echo "" > .libfuzzer-output/1
$while true; do
afl-fuzz -d -i ./libfuzzer-output/ -o ./afl-output/ -- ./libxml/afl-output/bin/xmllint -o /dev/null @@ 1>/dev/null &
./libxml/libxml-fuzzer -max_total_time=3600 ./libfuzzer-output/;
pkill -15 afl-fuzz;
sleep 1;
mkdir ./libfuzzer-merge;
./libxml/libxml-fuzzer -merge=1 ./libfuzzer-merge ./libfuzzer-output/ ./afl-output/;
rm -rf ./afl-output ./libfuzzer-output;
mv ./libfuzzer-merge ./libfuzzer-output;
done
因为示例脚本每次迭代只运行一个小时,因此AFL使用“快速和脏模式”以跳过所有确定性步骤。即使一个大文件可能会导致AFL在确定性步骤上花费几个小时甚至几天时间,因此在按预算时间运行的话运行AFL更可靠。确定性步骤可以手动运行,也可以在将新样本复制到“ ./libfuzzer_output ”的另一个实例上自动运行。
字典
你现在有你的语料库,并你快乐地进行模糊测试和修剪。那么接下来你从哪里去呢?
AFL和libFuzzer都支持用户提供的字典。这些字典应包含关键字或其他有趣的字节模式,这对于fuzzer来说很难确定。有关一些有用的例子,请查看Google libFuzzer的 XML字典和关于AFL词典的这篇 博客文章。
由于这些工具现在非常受欢迎,所以可以在网上找到一些好的基础词典。例如,Google收集了不少字典:https://chromium.googlesource.com/chromium/src/+/master/testing/libfuzzer/fuzzers/dicts。此外,AFL源代码包含很少的示例字典。如果你没有源代码,可以从github查看afl镜像:https://github.com/rc0r/afl-fuzz/tree/master/dictionaries
AFL和libFuzzer都在执行期间收集字典。当执行确定性的模糊测试步骤时,AFL收集字典,而libFuzzer的方法是进行插桩。
当运行libFuzzer的时候或测试用例限制时,libFuzzer将在退出时输出一个推荐的字典。此功能可用于收集有趣的字典条目,但建议对所有自动收集的条目执行手动合理性检查。libFuzzer会在发现新的覆盖范围时构建这些字典条目,因此这些条目通常建立在终关键字上。
1. “ISO-”
2. “ISO-1”
3. “ISO-10”
4. “ISO-1064”
5. “ISO-10646-”
6. “ISO-10646-UCS-2”
7. “ISO-10646-UCS-4”
我们测试了三个运行时长为10分钟的字典:没有字典,第一次运行的推荐字典和Google的libFuzzer XML字典。这三个测试的结果可以从下表中看出。
令人惊讶的是,没有字典的运行结果与第一次运行的推荐字典的测试结果没有显著的差异,但是使用“真实”字典,在运行期间发现的覆盖量发生了巨大变化。
字典真的可以改变模糊测试的效果,至少在短时间内是这样的,所以他们值得去做。Shortcuts,像libFuzzer推荐的字典,很有帮助,但你仍然需要额外的手动操作来利用字典中的潜力。
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11