您的位置:首页 > 其它

如何使用AFL进行一次完整的fuzz过程

2016-12-06 15:38 417 查看
第一次写,不知从何入手,就把最近在研究的AFL的一些文章翻译先发出来吧。。。。

原文地址https://foxglovesecurity.com/2016/03/15/fuzzing-workflows-a-fuzz-job-from-start-to-finish/ 

近年来,随着越来越多像AFL这种易用的工具的出现,降低了门槛,给了初学者以希望,使得很多人都开始对fuzzing产生了兴趣。许多网站都对AFL的功能进行了简单的介绍,eg:如何对给定的软件进行fuzzing,但是没有介绍过当决定结束的时候要做什么。
     在本文中,将会进行一个完整的fuzz过程。首先,找到一个合适的软件来进行fuzzing看起来并不容易,但是你可以根据一定的规则来使得你能更容易地开始fuzz。一旦选好了软件,最好地fuzz方式是什么?选择哪一个测试用例来作为seed?我们怎么知道做得怎么样,我们遗漏了目标程序中的哪些代码路径?
我们希望能够覆盖这一切,对如何有效地和有效地从头到尾完成一个完整的Fuzz工作流程提供一个全方面的观点。在这里,以AFL为例。

一、What should I fuzz? Finding the right software

     AFL对于C/C++应用效果最好,所以这是一条我们找软件的规则。以下还有一些问题:

1.有没有范例代码可用?

这个项目具有的功能太多,为了fuzz这些功能能不能被修剪。如果一个项目有一个简单的(bare-bones)范例代码,这会使得我们的fuzz过程变得简单。
2.能不能自己编译软件?(是否开源,能得到源码)
当你能自己编译源码时,AFL最有效。尽管它支持QEMU的userland模式来进行黑盒的二进制插桩,但是这会大大影响AFL的效果。在理想环境下,应该能够使用afl-clang-fast or afl-clang-fast++来编译软件。
3.Are there easily available and unique testcases available?有没有可用的unique测试用例

我们可能会fuzzing一个文件格式(通过修改,也可以fuzz网络应用),并且拥有一些unique、interesting的测试用例来作为seed,使我们有一个好的开端。如果项目有文件类型的测试用例来进行单元测试(或者保持具有已知bug的文件来进行回归测试),这将是巨大的成功。

     这些基本问题会帮你在以后的过程中节省很多时间,避免很多问题,如果你刚刚开始的话。

The yaml-cpp project
     github是能帮你快速解决上述问题的地方。在github上你能很容易地找到用C/C++的写的项目。例如,搜索C++和200star将会显示yaml-cpp这个项目。大致从这个项目中看一下之前的三个问题,并看看能多简单地开始fuzzing。
1.Can I compile it myself?
     yaml-cpp使用cmake作为build system。【CMake 是一个跨平台的,开源的构建系统】这看起来非常棒,因为我们可以自己选择用什么编译器,afl-clang-fast++将会Just Work。在yaml-cpp的README.txt中有一个有趣的记录,它默认构建一个静态库,这对我们来说是完美的,因为我们想给AFL一个静态编译、插桩的二进制文件来进行fuzz。
2.Is there example code readily available?
     在项目的根目录下的util文件夹中,有一些小的cpp文件,它们是展示yaml-cpp库某些特性的简单( bare-bones)工具。特别有趣的是parse.cpp文件。这个文件是完美的,因为它已经写好了从stdin接收数据的功能,我们可以很容易地改写来使用AFL的持久模式(persistent
mode),这将会有显著的速度提升。

3.Are there easily available and unique/interesting testcases available?
     在项目的根目录下的test文件夹中是一个specexamples.h文件,里面有大量的unique和interesting的YAML的测试用例,每一个都执行yaml库中的一段特定代码。这对fuzzer用来作为种子生成测试用例是非常好的。

二、Starting the fuzz job

     这里不会将AFL的安装配置,因为有很多文章已经介绍过了。我们假设这些已经完成,afl-clang-fast和afl-clang-fast++也已经配置完成。{export CC=afl-clang;export CXX=afl-clang++;可以使用env来查看环境变量}afl-g++工作起来没什么问题,但是afl-clang-fast++肯定是首选。下面开始获取yaml-cpp的代码,并使用AFL来构建它。

# git clone https://github.com/jbeder/yaml-cpp.git
# cd yaml-cpp

# mkdir build

# cd build

# cmake -DCMAKE_CXX_COMPILER=afl-clang-fast++ ..

# make
     我们成功构建之后,可以对源码进行一些修改,来让AFL能更快。从根目录的/util/parse.cpp中,我们可以使用AFL的trick来更新main()函数for持久模式。
     
int main(int argc, char** argv) {
  Params p = ParseArgs(argc, argv);
if (argc > 1) {
    std::ifstream fin;
    fin.open(argv[1]);
    parse(fin);
  } else {
    parse(std::cin);
  }
return 0;
}
     对于main()函数,我们可以修改其else语句,包含一个while循环和一个特殊的AFL函数__AFL_LOOP(),该函数通过一些memory wizardry使得AFL在进程中执行二进制fuzzing,( as
opposed to)而不是新开一个进程给每一个我们想要fuzz的测试用例。修改如下:

if (argc > 1) {

  std::ifstream fin;

  fin.open(argv[1]);

  parse(fin);

} else {

  while (__AFL_LOOP(1000)) {

    parse(std::cin);
  }
}
     注意,在else语句中新的while循环中,我们传递1000给__AFL_LOOP()函数。这告诉AFL在一个进程中fuzz1000个test cases,然后在新开一个进程执行相同的工作。你可以增加执行次数通过指定一个数字,以内存使用(内存泄漏)为代价;这是高度可调节的,基于你fuzzing的应用。添加这种类型的代码以启动持久模式也不总是很容易的。一些应用由于启动时产生的资源或其他因素,可能没有支持添加while循环的架构。due
to resources spawned during start up or other factors.

     重新编译。返回根目录下的build目录,输入‘make’来重新生成parse.cpp。

1、Testing the binary
     随着二进制程序被编译,我们可以使用AFL的afl-showmap工具来测试它。afl-showmap工具将会运行一个给定的插桩的二进制程序(通过stdin接收的任意输入通过stdin传递给插桩好的二进制程序),并打印在程序执行期间它看到的反馈的报告。

# afl-showmap -o /dev/null -- ~/parse < <(echo hi)

afl-showmap 2.03b by <lcamtuf@google.com>

[*] Executing '~/parse'...

-- Program output begins --

hi

-- Program output ends --

[+] Captured 1787 tuples in '/dev/null'.

#
     通过将输入更改为应该执行新代码路径的内容,您应该可以看到在报告结束时报告的元组数量增加或减少。

# afl-showmap -o /dev/null -- ~/parse < <(echo hi: blah)

afl-showmap 2.03b by <lcamtuf@google.com>

[*] Executing '~/parse'...

-- Program output begins --

hi: blah

-- Program output ends --

[+] Captured 2268 tuples in '/dev/null'.

#
     可以看到,发送一个简单的YAML key(hi)只表示1787个元组的反馈,但是具有value的YAML key(hi:blah)表示2268个元组的反馈。 我们应该很好地去使用插桩的二进制程序,现在我们需要测试用例来生成我们fuzzing需要的testcases。

2、Seeding with high quality test cases
     你给fuzzer的初始testcase是一个非常重要的方面,关系到一次fuzz运行是否能到一些好的crashes。像前面所说的,test目录下的specexamples.h文件有很好的test cases来开始,但是它们可以更好。对于这项工作,我从头文件中手动复制这些范例并粘贴到要使用的test case,以节省读者的时间,链接在这,这是我是用的原始种子。(就是将yaml中test目录下的specexamples.h 文件中的那些例子https://github.com/jbeder/yaml-cpp/blob/master/test/specexamples.h,分成一个一个的test
case用于fuzzinghttps://github.com/brandonprry/yaml-fuzz/tree/master/raw_testcases
     
     AFL带有两个工具确保:
 测试语料库中的文件尽可能有效地唯一
 每个测试文件尽可能高效地表示其唯一的代码路径
     这两个工具是afl-cmin和afl-tmin,它们执行最小化minimizing。
     afl-cmin工具需要一个给定的包含可能的(potential)test case的文件夹,然后运行每一个并将收到的反馈与所有其他的test case进行对比,找到最有效地表示最unique的代码路径的最好的test case。最好的test case被保存到一个新的目录。
     afl-tmin工具只用于一个指定的文件。当我们进行fuzzing时,我们不想浪费CPU来处理一些相对于test case表示代码路径来说无用的bit或byte。为了使每一个test case达到表示与原始测试用例相同的代码路径所需的最小值,afl-tmin遍历test
case的实际字节,逐步删除很小的数据块,直到删除任意字节都会影响到代码路径表示。这很啰嗦,但是对于有效地fuzzing来说,这都是很重要的步骤,也是需要理解的重要概念。下面看一个例子。
     在作者创建的项目中,从specexamples.h文件中得到这些原始test case中,选择‘2’这个文件来开始操作:

# afl-tmin -i 2 -o 2.min -- ~/parse
afl-tmin 2.03b by <lcamtuf@google.com>

[+] Read 80 bytes from '2'.
[*] Performing dry run (mem limit = 50 MB, timeout = 1000 ms)...
[+] Program terminates normally, minimizing in instrumented mode.
[*] Stage #0: One-time block normalization...
[+] Block normalization complete, 36 bytes replaced.
[*] --- Pass #1 ---
[*] Stage #1: Removing blocks of data...
Block length = 8, remaining size = 80
Block length = 4, remaining size = 80
Block length = 2, remaining size = 76
Block length = 1, remaining size = 76
[+] Block removal complete, 6 bytes deleted.
[*] Stage #2: Minimizing symbols (22 code points)...
[+] Symbol minimization finished, 17 symbols (21 bytes) replaced.
[*] Stage #3: Character minimization...
[+] Character minimization done, 2 bytes replaced.
[*] --- Pass #2 ---
[*] Stage #1: Removing blocks of data...
Block length = 4, remaining size = 74
Block length = 2, remaining size = 74
Block length = 1, remaining size = 74
[+] Block removal complete, 0 bytes deleted.

File size reduced by : 7.50% (to 74 bytes)
Characters simplified : 79.73%
Number of execs done : 221
Fruitless execs : path=189 crash=0 hang=0

[*] Writing output to '2.min'...
[+] We're done here. Have a nice day!

# cat 2
hr: 65 # Home runs
avg: 0.278 # Batting average
rbi: 147 # Runs Batted In
# cat 2.min
00: 00 #00000
000: 00000 #0000000000000000
000: 000 #000000000000000
#
     这是一个很好的例子,说明了AFL的强大。AFL不知道YAML是什么,但是它能有效地清除所有不是用来表示键值对的特殊YAML字符的所有字符。它通过确定改变这些特定字符将会改变从插桩二进制程序得到的反馈,来保留它们。它也从原始文件中删除4个不影响代码路径表示的字节,节省CPU的浪费。
     为了快速最小化启动测试语料库,我通常使用快速for循环将每个文件最小化为一个具有.min特殊文件扩展名的新文件。
# for i in *; do afl-tmin -i $i -o $i.min -- ~/parse; done;
# mkdir ~/testcases && cp *.min ~/testcases
     这个for循环将会遍历该目录下的每一个文件,并使用afl-tmin来使它达到最小化为一个名字相同,多了.min扩展名的文件。这样我可以把*.min复制到我用来作为AFL的种子的文件夹。

3、Starting the fuzzers
     这一部分是大多数fuzzing演示的结束,但是我这只是一个开始。现在我们有一个高质量的测试用例集来作为AFL的种子,我们就可以开始了。Optionally,我们也可以利用the dictionary token functionality来生成AFL的种子,with用YAML特殊字符来增加一点potency能力,但这个作为一个练习留给读者。
     AFL有两种类型的fuzzing策略,一种是确定性的,一种是随机的、混乱的。当开始afl-fuzz实例时,你可以指定fuzz实例要遵循策略的类型。一般来说,你只需要一个确定性的(主)fuzzer,但是你可以有很多随机(从)fuzzer。如果你之前使用过AFL,并且不知道这是在说什么,那你之前可能只运行了一个afl-fuzz实例。如果没有指定fuzzing策略,afl-fuzz实例将会在每个策略间来回切换。

# screen afl-fuzz -i testcases/ -o syncdir/ -M fuzzer1 -- ./parse
# screen afl-fuzz -i testcases/ -o syncdir/ -S fuzzer2 -- ./parse
     首先,请注意我们如何在screen(linux命令)会话中启动每个实例。这允许我们连接和断开连接到运行fuzzer的screen会话,所以我们不会意外关闭运行afl-fuzz实例的终端! 还要注意在每个相应的命令中使用的参数-M和-S。通过传递-M fuzzer1参数给afl-fuzz,我告诉它是一个master fuzzer(使用确定性策略),并且fuzz实例的名称是fuzzer1。另一方面,传递给第二个命令的-S
fuzzer2参数,说明要使用随机,混乱的策略运行实例,名称为fuzzer2。 这两个模糊器将相互工作,当新的代码路径被找到时,来回传递新的测试用例。

三、When to stop and prune 何时停止和删除减少

     一旦模糊器运行相对较长的时间(我喜欢等到至少Master Fuzzer已经完成它的第一个周期,这时从fuzzer实例通常完成了许多周期),我们不应该停止工作并开始看崩溃。在fuzzing过程中,AFL创建一个包含新测试用例的巨大的语料库,其中可能仍然存在bugs。我们应该尽量最小化这个新的语料库,然后重新设置fuzzer的种子,让它们继续运行。这是一个其他介绍文章没有提过的过程,因为它是无聊,乏味,可能需要很长时间,但它是高效fuzzing的关键。耐心和勤奋是美德。
     一旦yaml-cpp的parse程序的master fuzzer完成了它的第一个周期(我花了大约10个小时,平均可能需要24个小时),我们可以继续并停止我们的afl-fuzz实例。我们需要合并和最小化每个实例的队列queue,并重新启动fuzzing。当使用多个fuzzing实例运行时,AFL将在根目录的syncdir目录里,根据传给afl-fuzz的参数(fuzzer的名称),为每个fuzzer维护一个独立的、同步目录。每个单独的fuzzer
syncdir目录都包含一个队列queue目录,其中包含AFL能够生成的所有导致新的代码路径被检测出来的测试用例。
     我们需要合并每个fuzz实例的队列目录,但是因为其中会有很多重叠,需要最小化这个新的测试数据集。

# cd ~/syncdir
# ls
fuzzer1 fuzzer2
# mkdir queue_all
# cp fuzzer*/queue/* queue_all/
# afl-cmin -i queue_all/ -o queue_cmin -- ~/parse
corpus minimization tool for afl-fuzz by <lcamtuf@google.com>

[*] Testing the target binary...
[+] OK, 884 tuples recorded.
[*] Obtaining traces for input files in 'queue_all/'...
Processing file 1159/1159... 
[*] Sorting trace sets (this may take a while)...
[+] Found 34373 unique tuples across 1159 files.
[*] Finding best candidates for each tuple...
Processing file 1159/1159... 
[*] Sorting candidate list (be patient)...
[*] Processing candidates and writing output files...
Processing tuple 34373/34373... 
[+] Narrowed down to 859 files, saved in 'queue_cmin'.
     一旦我们通过afl-cmin运行生成的队列,我们需要最小化每个结果文件,以使我们不在我们不需要的字节上浪费CPU周期。然而,现在我们有比最小化初始test cases多一些的文件。一个用于最小化数千个文件的简单for循环很可能需要几天。随着时间的推移,我写了一个小bash脚本,称为afl-ptmin,它将afl-tmin并行化到一定数量的进程中,并证明在最小化过程中显著地提升了速度。
#!/bin/bash

cores=$1
inputdir=$2
outputdir=$3
pids=""
total=`ls $inputdir | wc -l`

for k in `seq 1 $cores $total`
do
  for i in `seq 0 $(expr $cores - 1)`
  do
    file=`ls -Sr $inputdir | sed $(expr $i + $k)"q;d"`
    echo $file
    afl-tmin -i $inputdir/$file -o $outputdir/$file -- ~/parse &
  done

  wait
done
     与afl-fuzz实例一样,我建议仍然在screen会话中运行此操作,以便不会出现网络中断或终端关闭导致的问题。它的用法很简单,只需要三个参数:启动进程的数量,要最小化test cases的目录,以及写入最小化test cases的输出目录。
# screen ~/afl-ptmin 8 ./queue_cmin/ ./queue/
     即使有并行化,这个过程仍然需要一段时间(24小时+)。对于我们用yaml-cpp生成的语料库,它应该能够在一个小时左右完成。一旦完成,我们应该从syncdirs目录中各个fuzzer目录下删除以前的队列queue目录(/syncdirs/fuzzer1/queue/) ,然后复制/syncdirs/queue/文件夹以替换旧的队列文件夹。
# rm -rf fuzzer1/queue
# rm -rf fuzzer2/queue
# cp -r queue/ fuzzer1/queue
# cp -r queue/ fuzzer2/queue
     使用最新的最小化队列queue,我们可以在之前离开的地方继续fuzzing。
# cd ~
# screen afl-fuzz -i- -o syncdir/ -S fuzzer2 -- ./parse
# screen afl-fuzz -i- -o syncdir/ -M fuzzer1 -- ./parse
     如果你注意到了,你会发现我们这次仅仅在-i参数后传入了一个连接符‘-’(hyphen),而不是之前的testcase文件夹。这会告诉AFL就用syncdir目录下的queue/目录作为fuuzer的seed目录并从这开始备份??。
     整个进程启动Fuzz工作,然后停止去最小化队列和重新启动工作,可以做你想要的次数(通常直到你感到厌烦或停止寻找新的代码路径)。它也应该经常做,否则也会在字节上浪费你的电费,不会获得任何汇报。

四、Triaging your crashes 分类crash

     另一个传统上fuzzing生命周期拥有的很沉闷的部分就是对发现进行分类。幸运的是,一些伟大的工具可以帮助我们做这些。
     一个伟大的工具是crashwalk,by @rantyben (props!)。它自动化gdb和一个特殊的gdb插件来快速确定哪些崩溃是否可能导致可利用的条件。This isn’t fool proof(十分安全?) by any means(无论如何), but does give you a bit of a
head start(优势,有利条件) in which crashes to focus on first.虽然这不是十分安全,但是在应该首先关注哪个crash上给你开了一个好头。安装相对比较直接,但需要一些依赖库。
# apt-get install gdb golang
# mkdir src
# cd src
# git clone https://github.com/jfoote/exploitable.git
# cd && mkdir go
# export GOPATH=~/go
# go get -u github.com/bnagy/crashwalk/cmd/…

     crashwalk安装在~/go/bin/中,我们可以自动分析文件,看它们是否能导致可利用的bugs。
# ~/go/bin/cwtriage -root syncdir/fuzzer1/crashes/ -match id -~/parse @@
     

五、Determining your effectiveness and code coverage 确定有效性和代码覆盖率
     
查找crashes是非常有趣的和所有,但是由于不能量化在二进制文件中执行可用的代码路径的程度,你只是在黑暗中拍照,并希望一个好的结果。通过确定你没有到达代码库的哪些部分,你可以更好地调整你的测试用例种子,以击中你还没有能够到达的代码。
一个优秀的工具(由@michaelrash开发)称为afl-cov,可以帮助你解决这个确切的问题,通过观察你的fuzz目录,当你找到新的路径,并立即运行testcase来寻找任何新的代码库覆盖你可能已经击中。 它使用lcov实现这一点,所以我们必须使用一些特殊的选项重新编译parse二进制文件,然后继续。
# cd ~/yaml-cpp/build/
# rm -rf ./*
# cmake -DCMAKE_CXX_FLAGS="-O0 -fprofile-arcs -ftest-coverage" \
-DCMAKE_EXE_LINKER_FLAGS="-fprofile-arcs -ftest-coverage" ..
# make
# cp util/parse ~/parse_cov

     有了新的parse程序,afl-cov可以将在给定输入的二进制程序中采用的代码路径与文件系统上的代码库链接起来。???
# screen afl-cov/afl-cov -d ~/syncdir/ --live --coverage-cmd "~/parse_cov AFL_FILE" --code-dir ~/yaml-cpp/ 
     一旦完成,afl-cov在syncdir目录下的名为cov的目录中生成报告信息。 其中包括可以在Web浏览器中轻松查看的HTML文件,详细说明命中了哪些函数和哪行代码,以及未命中的函数和代码行。

六、In the end

    总共跑了三天时间,但是我没有在yaml-cpp中发现潜在的可利用的bugs。这意味不存在bugs和它不值得fuzzing吗? 当然不是。在我们的行业中,我不相信我们在寻找bugs中公布了足够的关于失败的信息。许多人可能不想承认,他们花费了大量的精力和时间来开发其他人可能觉得没用的东西。在开放精神下,下面链接的是所有生成的语料库(完全最小化),种子和代码覆盖结果(约70%的代码覆盖率),以便别人可以获取它们并确定是否值得进行fuzzing


作者开发的yaml-fuzz:https://github.com/bperryntt/yaml-fuzz 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐