Fuzzing101 with LibAFL – 第 IV 部分:第 I 部分的速度改进

链接: https://epi052.gitlab.io/notes-to-self/blog/2021-11-07-fuzzing-101-with-libafl-part-1.5/

Twitter 用户安东尼奥·莫拉莱斯 (Antonio Morales)于 2021 年 8 月创建了Fuzzing101存储库。在该存储库中,他创建了练习和解决方案,旨在向想要学习如何在实际软件项目中查找漏洞的任何人教授模糊测试的基础知识。该 repo 侧重于AFL++ 的使用,但本系列文章旨在解决使用LibAFL的练习。我们将探索库并在 Rust 中编写模糊器,以便以与建议的 AFL++ 用法紧密结合的方式解决挑战。

由于本系列将着眼于 Rust 源代码和构建模糊器,为简洁起见,我将假设在这两个领域都有一定的知识水平。如果您需要简要介绍/复习/关于覆盖率引导的模糊测试,请查看此处。与往常一样,如果您有任何疑问,请随时与我们联系。

这篇文章将介绍一些提高本系列第一部分模糊器速度的方法。本练习的配套代码可以在我的fuzzing-101-solutions 存储库中找到

以前的帖子: –第一部分:模糊测试 Xpdf


快速参考

这只是即将发布的帖子中使用的不同组件的摘要。它旨在稍后用作确定在哪些帖子中使用哪些组件的简单方法。

{
  "Fuzzer": {
    "type": "StdFuzzer",
    "Corpora": {
      "Input": "InMemoryCorpus",
      "Output": "OnDiskCorpus"
    },
    "Input": "BytesInput",
    "Observers": [
      "TimeObserver",
      "HitcountsMapObserver"
    ],
    "Feedbacks": {
      "Pure": ["MaxMapFeedback", "TimeFeedback"],
      "Objectives": ["MapFeedbackState", "TimeoutFeedback"]
    },
    "State": "StdState",
    "Monitor": "MultiMonitor",
    "EventManager": "LlmpRestartingEventManager",
    "Scheduler": "IndexesLenTimeMinimizerCorpusScheduler",
    "Executor": "TimeoutExecutor<InProcessExecutor>",
    "Mutators": ["havoc_mutations"],
    "Stages": ["StdMutationalStage"]
  }
}

介绍

@domenuk对本系列的第一篇文章发表了评论:

如果你想在 fuzzing 过程中非常快,你通常需要进程内执行器而不是 forkserver

对于第一篇文章,我想让事情相对简单。在我看来,与进程内执行的工作方式相比,一个进程在孩提时代执行另一个进程要容易一些,尤其是当您不熟悉所有这些模糊测试时。此外,除非您启用持久模式(这是“进程内执行程序”的另一种说法),否则 afl++ 将使用 forkserver。

在@domenuk 提出建议之前,我已经在考虑撰写关于提高第 1 部分模糊器性能的文章,但他的评论决定了我的命运。所以,我们开始了,我们将通过以下方式来提高我们的第一个模糊器的性能:

  • 换出afl-clang-fastafl-clang-lto编译过程中
  • 通过共享内存而不是通过磁盘上的文件将输入传递给程序
  • 实现进程内执行程序而不是 forkserver

我们走吧!

第 1 步:编译器交换

本节将处理使用afl-clang-lto而不是afl-clang-fast. 但为什么?我很高兴你问了!以下是afl-clang-lto 上afl++ 文档中TL;DR 的摘录:

  • 使用 afl-clang-lto/afl-clang-lto++,因为它比 AFL 世界中的其他任何东西都更快并且提供更好的覆盖范围
  • 您可以将它与 llvm_mode: laf-intel 和仪器文件列表功能一起使用,并且可以与 cmplog/Redqueen 结合使用
  • AUTODICTIONARY 功能!

如果您不熟悉向 fuzzer 添加字典,这里是同一文档的另一个摘录:

AUTODICTIONARY 功能:在编译时,会自动生成基于字符串比较的字典并将其放入目标二进制文件中。这本词典在启动时转移到 afl-fuzz。这将统计覆盖率提高了 5-10%

因此,通过切换到afl-clang-lto,我们获得了更快的模糊器,并增加了代码覆盖率。如果您需要更多的说服力,这也是afl++ 文档所说的使用,如果您的系统和目标支持它。

好的,现在我们知道为什么要交换编译器了,让我们实现吧!

构建.rs

目前,构建脚本用于afl-clang-fast检测 Xpdf,因此我们将开始在那里进行更改。我们不只是更换编译器,而是构建两个 Xpdf,这样我们就可以对两者进行比较,看看我们的更改是否提高了速度。

如果你阅读本系列第一篇文章,你可能还记得,我们的构建脚本将执行我们的configuremakemake install步骤建立的xpdf。我们要做的就是执行这些步骤两次,每个编译器执行一次。然后我们将构建存储在单独的文件夹 ( built-with-(lto|fast)) 中。

for (build_dir, compiler) in [("fast", "afl-clang-fast"), ("lto", "afl-clang-lto")] {
    // configure with `compiler` and set install directory to ./xpdf/built-with-`build_dir`
    Command::new("./configure")
        .arg(&format!("--prefix={}/built-with-{}", xpdf_dir, build_dir))
        .env("CC", format!("/usr/local/bin/{}", compiler))
        .env("CXX", format!("/usr/local/bin/{}++", compiler))
        .current_dir(&xpdf_dir)
        .status()
        .expect(&format!(
            "Couldn't configure xpdf to build using afl-clang-{}",
            compiler
        ));

    // make && make install
    Command::new("make")
        .current_dir(&xpdf_dir)
        .status()
        .expect("Couldn't make xpdf");

    Command::new("make")
        .arg("install")
        .current_dir(&xpdf_dir)
        .status()
        .expect("Couldn't install xpdf");
}

我们还需要更新我们的make clean命令来处理新的构建目录。

// clean doesn't know about the built-with-* directories we use to build, remove them as well
Command::new("rm")
    .arg("-r")
    .arg("-f")
    .arg(&format!("{}/built-with-lto", xpdf_dir))
    .arg(&format!("{}/built-with-fast", xpdf_dir))
    .current_dir(&xpdf_dir)
    .status()
    .expect("Couldn't clean xpdf's built-with-* directories");

现在负责构建脚本,让我们看看接下来会发生什么。

主文件

接下来让我们跳到 fuzzer 的源代码。在其中,我们可以看到在第一部分中,我们将路径硬编码pdftotext到我们的 ForkserverExecutor 中。

let fork_server = ForkserverExecutor::new(
    format!("./xpdf/install/bin/pdftotext", compiler),
    &[String::from("@@")],
    -------------8<-------------
)

由于我们正在编译 的两个版本pdftotext,如果我们可以在不重新编译模糊器的情况下在它们之间切换会更酷。为了让这个梦想成为现实,让我们添加一个命令行选项来控制 pdftotext 的路径。

即使我们在 main.rs 部分,我们也需要添加 [clap crate]() 作为依赖项,所以让我们快速绕道而行。

练习 1/Cargo.toml

-------------8<-------------
[dependencies]
libafl = {version = "0.6.1"}
clap = "3.0.0-beta.5"

好的,回到 main.rs;让我们编写一个快速函数,该函数将解析ltofast从命令行解析,并将选择作为String.

use clap::{App, Arg};

-------------8<-------------

/// parse -c/--compiler from cli; return "fast" or "lto"
fn get_compiler_from_cli() -> String {
    let matches = App::new("fuzzer")
        .arg(
            Arg::new("compiler")
                .possible_values(&["fast", "lto"])
                .short('c')
                .long("compiler")
                .value_name("COMPILER")
                .about("choose your afl-clang variant (default: fast)")
                .takes_value(true)
                .default_value("fast"),
        )
        .get_matches();

    String::from(matches.value_of("compiler").unwrap())
}

编写函数后,我们可以从 调用它main,并更新 ForkserverExecutor 中的路径。

fn main() {
    let compiler = get_compiler_from_cli();

    //
    // Component: Corpus
    //
-------------8<-------------
    let fork_server = ForkserverExecutor::new(
        format!("./xpdf/built-with-{}/bin/pdftotext", compiler),
        &[String::from("@@")],
        // we're passing testcases via on-disk file; set to use_shmem_testcase to false
        false,
        tuple_list!(edges_observer, time_observer),
    )
    .unwrap();
-------------8<-------------

还不错,现在我们可以在两个编译器之间构建和切换。让我们继续进行比较。

时间比较.sh

为了查看我们的更改是否产生任何影响,我们需要进行某种比较。我们可以编写一个快速的 shell 脚本来执行以下操作

  • 在给定的超时时间内运行每个模糊器几次
  • 对于每次运行,记下执行的总数
  • 将执行次数除以超时
  • 平均所有的运行在一起
  • 吐出结果

这就是代码中的样子

练习 1/time-comparison.sh

#!/bin/bash

function exec-fuzzer() {
  # parameters:
  #   fuzzer: should be either "lto" or "fast"
  #   timeout: in seconds
  #   cpu: which core to bind, default is 7
  fuzzer="${1}"
  timeout="${2}"
  declare -i cpu="${3}" || 7
  
  # last_update should look like this
  # [Stats #0] clients: 1, corpus: 425, objectives: 0, executions: 23597, exec/sec: 1511
  last_update=$(timeout "${timeout}" taskset -c "${cpu}" ../target/release/exercise-one-solution -c "${fuzzer}" | grep Stats | tail -1)

  # regex + cut below will return the total # of executions
  total_execs=$(echo $last_update | egrep -o "executions: ([0-9]+)" | cut -f2 -d' ')
  
  execs_per_sec=$((total_execs/"${timeout}"))

  echo $execs_per_sec
}

function average_of_five_runs() {
  # parameters:
  #   fuzzer: should be either "lto" or "fast"
  fuzzer="${1}"
  declare -i total_execs_per_sec=0
  declare -i total_runs=5
  timeout=120

  for i in $(seq 1 "${total_runs}");
  do
    current=$(exec-fuzzer "${fuzzer}" "${timeout}")
    total_execs_per_sec=$((total_execs_per_sec+current))
    echo "[${fuzzer}][${i}] - ${current} execs/sec"
  done

  final=$((total_execs_per_sec/total_runs))
  echo "[${fuzzer}][avg] - ${final} execs/sec"
}

average_of_five_runs fast
average_of_five_runs lto

顺便说一句,速度并不是模糊器性能的唯一衡量标准。此外,我们使用这个脚本来衡量执行的方式也充满了不完善之处。长话短说:不要太在意这个脚本,或者认为它真的是个好主意,我们只需要一种快速/肮脏的方式来证明我们对模糊器速度的影响。

好的,排除免责声明,这是结果。

./time-comparison.sh
════════════════════════════

[fast][1] - 1129 execs/sec
[fast][2] - 970 execs/sec
[fast][3] - 1050 execs/sec
[fast][4] - 1112 execs/sec
[fast][5] - 1096 execs/sec
[fast][avg] - 1071 execs/sec
[lto][1] - 1016 execs/sec
[lto][2] - 1246 execs/sec
[lto][3] - 1151 execs/sec
[lto][4] - 1208 execs/sec
[lto][5] - 1217 execs/sec
[lto][avg] - 1167 execs/sec

我们可以看到,在五次运行过程中,lto 模糊器​​的速度提高了约 9%!它可能看起来不多,但这是一个大问题。我们会将其视为胜利并继续进行下一个改进。

我们让事情变得更快的下一个尝试是从我们的模糊测试工作流程中删除文件系统。目前,我们的工作流程如下所示:

  • 从语料库中获取测试用例
  • 变异测试用例
  • 将变异的测试用例写入磁盘 (.cur_input)
  • fork/exec 新子进程 (./pdftotext ./.cur_input)
    • 孩子从磁盘读取 .cur_input
  • 重复

我们实现共享内存模糊测试的目标是删除对磁盘的读取和写入。相反,我们的测试用例将从 InMemoryCorpus 中提取,在内存中进行变异,并通过pdftotext共享内存映射传递到模糊目标 ( )。这个过程并不难,因为 afl 包含了一些有用的宏来帮助完成这项任务。归结为在源代码中找到我们可以插入以下宏的可能位置。

__AFL_FUZZ_INIT();  // after #includes, before main
-------------8<-------------
// typically in main
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
int len = __AFL_FUZZ_TESTCASE_LEN;

AFL ++文档说共享存储器起毛后观察到的通常的速度增加加入通常约为2倍的性能提升。让我们看看我们是否能达到这个目标。

主文件

我们将首先修改我们的 fuzzer,因为这是我们需要为此改进所做的最简单的更改。我们真正需要做的就是将 ForkserverExecutor 的use_shmem_testcase参数从 false更新为 true,然后删除pdftotext@@参数。

let fork_server = ForkserverExecutor::new(
    format!("./xpdf/built-with-{}/bin/pdftotext", compiler),
    &[],
    // we're passing testcases via shmem; set to use_shmem_testcase to true
    true,
    tuple_list!(edges_observer, time_observer),
)
.unwrap();

这真的是为了main.rs,很容易吧?

调查 Xpdf

为了让我们的 fuzzer 进入共享内存的未来,我们需要修改一些 Xpdf 源代码。我们在这里所做的修改一定是针对 Xpdf 的,但对于其他模糊目标,一般步骤应该是相同的。我们的首要任务是读取源代码,以找出我们的输入文件是如何解析的以及在何处解析的。目标是用unsigned *char buf我们之前看到的宏替换文件读取逻辑。

我们将开始打猎 inpdftotext.cc的主要功能。main 函数首先声明变量,解析命令行值,并通过配置文件设置其全局状态。这些对我们来说都不是很有趣(现在),但是在初始设置之后,我们会看到PDFDoc创建的位置。

回购源

int main(int argc, char *argv[]) {
  PDFDoc *doc;
  GString *fileName;
-------------8<-------------
  doc = new PDFDoc(fileName, ownerPW, userPW);
-------------8<-------------
}

fileName变量被传递到 PDFDoc 构造函数中,因此从磁盘读取文件很可能发生在 PDFDoc 代码中的某处。

在这里我们看到了 PDFDoc 构造函数 PDFDoc.h

回购源

class PDFDoc {
public:

  PDFDoc(GString *fileNameA, GString *ownerPassword = NULL,
	  GString *userPassword = NULL, void *guiDataA = NULL);
-------------8<-------------

以及在 PDFDoc.cc

回购源

PDFDoc::PDFDoc(GString *fileNameA, GString *ownerPassword,
	       GString *userPassword, void *guiDataA) {
  Object obj;
  GString *fileName1, *fileName2;
-------------8<-------------
  fileName = fileNameA;
  fileName1 = fileName;
-------------8<-------------
  if (!(file = fopen(fileName1->getCString(), "rb"))) {
-------------8<-------------
  // create stream
  obj.initNull();
  str = new FileStream(file, 0, gFalse, 0, &obj);

  ok = setup(ownerPassword, userPassword);
}

在实现中,我们可以一直跟踪fileNameA参数到FileStream构造函数。那个面包屑引导我们到Stream.cc. 对我们来说不幸的是,FileStream 是一个用户定义的类,它包装了 IO 流相关的功能。它unsigned char不像我们需要设置上面讨论的宏那样使用数组。

幸运的是,他们还实现了一个MemStream类,该类确实使用字符数组,呵呵!

回购源

MemStream::MemStream(char *bufA, Guint startA, Guint lengthA, Object *dictA):
    BaseStream(dictA) {
  buf = bufA;
  start = startA;
  length = lengthA;
  bufEnd = buf + start + length;
  bufPtr = buf + start;
  needFree = gFalse;
}

我们也很幸运地发现 MemStream 具有与 FileStream 相同的 API,这使其成为替代品。我们需要做的就是用 MemStream 构造函数替换 PDFDoc 中的 FileStream 构造函数,我们应该很高兴。让我们开始吧!

解析器.cc

随着分析的进行,不需要太多的改动。首先,我们需要添加一个包含,unistd因为其中一个宏最终需要它。当我们接近文件顶部时,我们还可以__AFL_FUZZ_INIT在 #includes 下方和 PDFDoc 构造函数上方插入宏。

#include <unistd.h>
-------------8<-------------
#define headerSearchSize 1024	// read this many bytes at beginning of
				//   file to look for '%PDF'

__AFL_FUZZ_INIT();

//------------------------------------------------------------------------
// PDFDoc
//------------------------------------------------------------------------

PDFDoc::PDFDoc(GString *fileNameA, GString *ownerPassword,
	       GString *userPassword, void *guiDataA) {
-------------8<-------------

完成后,我们可以更改构造函数以使用 MemStream。此外,还有一堆与编写输出文件相关的代码(我们没有使用,但无论如何都会被默认情况触发),所以我们将继续并删除它。去除输出文件代码后,整个构造函数如下所示。

PDFDoc::PDFDoc(GString *fileNameA, GString *ownerPassword,
	       GString *userPassword, void *guiDataA) {
  Object obj;
  GString *fileName1, *fileName2;

  ok = gFalse;
  errCode = errNone;

  guiData = guiDataA;

  file = NULL;
  str = NULL;
  xref = NULL;
  catalog = NULL;
#ifndef DISABLE_OUTLINE
  outline = NULL;
#endif

  unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
  int len = __AFL_FUZZ_TESTCASE_LEN;

  // create stream
  obj.initNull();

  str = new MemStream((char *) buf, 0, (Guint) len, &obj);
  ok = setup(ownerPassword, userPassword);
}

只需要做更多的工作来完成工作,所以让我们继续保持下去。

pdftotext.cc

现在,我们需要做的pdftotext.cc就是删除命令行解析逻辑,如下所示。我们可以将一个虚拟文件作为参数传递,但实际上不再需要它了,那么为什么不删除它呢?

  exitCode = 99;

  // parse args
  ok = parseArgs(argDesc, &argc, argv);
  if (!ok || argc < 2 || argc > 3 || printVersion || printHelp) {
    fprintf(stderr, "pdftotext version %s\n", xpdfVersion);
    fprintf(stderr, "%s\n", xpdfCopyright);
    if (!printVersion) {
      printUsage("pdftotext", "<PDF-file> [<text-file>]", argDesc);
    }
    goto err0;
  }
  fileName = new GString(argv[1]);

  // read config file
  globalParams = new GlobalParams(cfgFileName);

好的,到此为止。接下来,我们可以测试我们的更改!

结果

在重新编译 Xpdf 和我们的 fuzzer 之后,我们看到了相当大的速度提升!对输出进行随机抽样表明我们处于 2 倍加速的范围内,这正是我们所希望的。

cargo make clean
cargo build --release
taskset -c 6 ../target/release/exercise-one-solution -c lto
[Stats #0] clients: 1, corpus: 615, objectives: 0, executions: 567834, exec/sec: 1961
[Stats #0] clients: 1, corpus: 615, objectives: 0, executions: 567834, exec/sec: 2040
[Testcase #0] clients: 1, corpus: 616, objectives: 0, executions: 570189, exec/sec: 2261
[Stats #0] clients: 1, corpus: 616, objectives: 0, executions: 571831, exec/sec: 2270
[Stats #0] clients: 1, corpus: 616, objectives: 0, executions: 575641, exec/sec: 2203
[Stats #0] clients: 1, corpus: 616, objectives: 0, executions: 575641, exec/sec: 2185

但是等等,还有更多!我们可以从 pdftotext.cc 的 main 函数中去掉一些更多的代码,并且运行得更快。第一条注释和它下面的第一行代码让我们知道我们正在从磁盘读取文件,所以让我们摆脱它。

  // read config file
  globalParams = new GlobalParams(cfgFileName);
  if (textEncName[0]) {
    globalParams->setTextEncoding(textEncName);
  }
  if (textEOL[0]) {
    if (!globalParams->setTextEOL(textEOL)) {
      fprintf(stderr, "Bad '-eol' value on command line\n");
    }
  }
  if (noPageBreaks) {
    globalParams->setTextPageBreaks(gFalse);
  }
  if (quiet) {
    globalParams->setErrQuiet(quiet);
  }
  // get mapping to output encoding
  if (!(uMap = globalParams->getTextEncoding())) {
    error(-1, "Couldn't get text encoding");
    delete fileName;
    goto err1;
  }

还有这段代码将转换后的 pdf 写入其文本文件。让我们也把它放在阳光下。

  // write text file
  textOut = new TextOutputDev(textFileName->getCString(),
			      physLayout, rawOrder, htmlMeta);
  if (textOut->isOk()) {
    doc->displayPages(textOut, firstPage, lastPage, 72, 72, 0,
		      gFalse, gTrue, gFalse);
  } else {
    delete textOut;
    exitCode = 2;
    goto err3;
  }
  delete textOut;

可以清理 main 的其余部分,以删除与 PDFDoc 及其方法没有直接关系的任何内容,但我们现在将不做任何处理。重新编译 Xpdf 后,我们可以再次启动模糊器。

[Stats #0] clients: 1, corpus: 438, objectives: 0, executions: 54787, exec/sec: 3378
[Testcase #0] clients: 1, corpus: 439, objectives: 0, executions: 55233, exec/sec: 3430
[Stats #0] clients: 1, corpus: 439, objectives: 0, executions: 55233, exec/sec: 3478
[Testcase #0] clients: 1, corpus: 440, objectives: 0, executions: 55386, exec/sec: 3528
[Stats #0] clients: 1, corpus: 440, objectives: 0, executions: 55386, exec/sec: 3575
[Testcase #0] clients: 1, corpus: 441, objectives: 0, executions: 55458, exec/sec: 3621
[Stats #0] clients: 1, corpus: 441, objectives: 0, executions: 55733, exec/sec: 3581
[Stats #0] clients: 1, corpus: 441, objectives: 0, executions: 55733, exec/sec: 3542

还不错!除了初始分析之外,无需太多努力即可实现大约 3 倍的加速。事情现在看起来很不错,但我们可能会通过更换我们的执行程序做得更好,我们接下来会看看。

第 3 步:执行者交换

我们寻找@gamozolabs一直在谈论的难以捉摸的“性能”的最后一步是将我们的 ForkserverExecutor 换成 InProcessExecutor。进程内模糊器的结构将与我们目前使用的结构有很大不同。我们当前的模糊器是一个独立的二进制文件,它一遍又一遍地执行外部程序。在接下来的部分中,我们将把这种范式抛诸脑后。

我们的攻击计划是创建一个模糊目标(harness.cc)和一个 LibAFL 支持的编译器(compiler.rs),我们将用它来编译模糊目标。我们还将修改我们的独立模糊器,使其成为一个静态库,我们将使用我们的编译器链接到我们的模糊目标。

这些是我们将要更换执行器的大致步骤。正如本文开头所提到的,这通常会显着提高 fuzzer 的性能。让我们看看这是否适用于我们。

静态编译 Xpdf

我们将通过静态编译 Xpdf 开始我们的交换。我们从这里开始,因为这确实是成败的步骤。如果我们不能将 Xpdf 静态编译为一个库,我们可能最好探索其他替代方案,例如持久模式模糊测试。静态编译的 xpdf 库最终将链接到我们的模糊测试目标,因此我们可以练习我们对模糊测试感兴趣的代码。

为了开始工作,并且因为我们很懒惰(以良好的黑客方式),我们将在谷歌上搜索是否有人已经为我们完成了这项工作。原来github上有个项目叫libxpdf,听起来正是我们需要的,不错!

不幸的是,他们只提供版本 4.02,一个比我们的目标更新的主要版本。这意味着我们需要自己构建 Xpdf 3.02。嗯,有点,我们仍然可以严重依赖 libxpdf 存储库中完成的工作,它只需要我们做一些额外的工作。

制作成 CMake

如果我们在 4.0 版之后的任何时候检查 Xpdf 存储库,我们可以看到他们将构建系统从 Make 移到了 CMake。这是一个障碍(至少对我来说,如果你知道一种更简单的转换方法,我全神贯注),但不是一个巨大的障碍。我们可以简单地从 4.0+ 存储库中获取 CMake 相关文件并将它们塞入我们本地的 3.02 存储库中。

我们要查找的是 4.0 文件夹中所有与 CMake 相关的文件。我们需要首先找到所有这些文件并将它们放在我们 3.02 文件夹中相同的相对位置。由于数量太少,我只是手动完成了mv它们。

find xpdf | grep cmake
════════════════════════════

xpdf/cmake-config.txt
xpdf/splash/CMakeLists.txt
xpdf/xpdf/CMakeLists.txt
xpdf/goo/CMakeLists.txt
xpdf/CMakeLists.txt
xpdf/external/external.cmake
xpdf/cmake/mimick_find.cmake
xpdf/fofi/CMakeLists.txt

将这些文件放在 3.02 文件夹中后,我们可以尝试使用 CMake 进行构建。我们将使用 CMake 推荐的“out of source”构建策略,这意味着我们将在与目标无关的目录中构建,并为 cmake 提供 CMakeLists.txt 的位置作为参数。

fuzzing-101-solutions/exercise-1

mkdir build 
cd build
cmake ../xpdf

当我们这样做时,会出现一堆错误,主要是关于尝试编译不存在的文件。为了让事情正常工作,我们只需要迭代地构建/错误输出/修改 CMake 文件,直到我们可以构建我们的目标。最终,需要进行以下更改才能使一切正常。

对于下面的两个文件,需要删除每个突出显示的行。

xpdf-4.02/fofi/CMakeLists.txt

11include_directories("${PROJECT_SOURCE_DIR}")
12include_directories("${PROJECT_BINARY_DIR}")
13include_directories("${PROJECT_SOURCE_DIR}/goo")
14
15add_library(fofi_objs OBJECT
16  FoFiBase.cc
17  FoFiEncodings.cc
18  FoFiIdentifier.cc
19  FoFiTrueType.cc
20  FoFiType1.cc
21  FoFiType1C.cc
22)
23
24add_library(fofi
25

lt;TARGET_OBJECTS:fofi_objs> 26)

xpdf-4.02/xpdf/CMakeLists.txt

28add_library(xpdf_objs OBJECT
29  AcroForm.cc
30  Annot.cc
31  Array.cc
32  BuiltinFont.cc
33  BuiltinFontTables.cc
34  Catalog.cc
35  CharCodeToUnicode.cc
36  CMap.cc
37  ${COLOR_MANAGER_SOURCE}
38  Decrypt.cc
39  Dict.cc
40  Error.cc
41  FontEncodingTables.cc
42  Form.cc
43  Function.cc
44  Gfx.cc
45  GfxFont.cc
46  GfxState.cc
47  GlobalParams.cc
48  JArithmeticDecoder.cc
49  JBIG2Stream.cc
50  JPXStream.cc
51  Lexer.cc
52  Link.cc
53  NameToCharCode.cc
54  Object.cc
55  OptionalContent.cc
56  Outline.cc
57  OutputDev.cc
58  Page.cc
59  Parser.cc
60  PDF417Barcode.cc
61  PDFDoc.cc
62  PDFDocEncoding.cc
63  PSTokenizer.cc
64  SecurityHandler.cc
65  Stream.cc
66  TextString.cc
67  UnicodeMap.cc
68  UnicodeRemapping.cc
69  UnicodeTypeTable.cc
70  UTF8.cc
71  XFAForm.cc
72  XRef.cc
73  Zoox.cc
74)
75
76if (HAVE_SPLASH)
77  set(SPLASH_LIB splash)
78  set(SPLASH_OBECTS

lt;TARGET_OBJECTS:splash_objs>) 79 set(SPLASH_OUTPUT_DEV_SRC "SplashOutputDev.cc") 80else() 81 set(SPLASH_LIB "") 82 set(SPLASH_OBECTS "") 83 set(SPLASH_OUTPUT_DEV_SRC "") 84endif() 85 86add_library(xpdf STATIC 87

lt;TARGET_OBJECTS:xpdf_objs> 88

lt;TARGET_OBJECTS:goo_objs> 89

lt;TARGET_OBJECTS:fofi_objs> 90 ${SPLASH_OBECTS} 91

lt;TARGET_OBJECTS:${PNG_LIBRARIES}> 92

lt;TARGET_OBJECTS:${ZLIB_LIBRARIES}> 93

lt;TARGET_OBJECTS:${FREETYPE_LIBRARY}> 94 PreScanOutputDev.cc 95 PSOutputDev.cc 96 ${SPLASH_OUTPUT_DEV_SRC} 97 TextOutputDev.cc 98 HTMLGen.cc 99 WebFont.cc 100 ImageOutputDev.cc 101)

现在,我们应该能够使用 afl++ 静态编译 xpdf。

fuzzing-101-solutions/exercise-1/build

cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=afl-clang-lto -DCMAKE_CXX_COMPILER=afl-clang-lto++ ../xpdf/
make

当我们查看时/build,我们可以看到我们的库。

ls -al */*.a 
════════════════════════════

-rw-rw-r-- 1 epi epi   417288 Nov 13 20:02 goo/libgoo.a
-rw-rw-r-- 1 epi epi   898772 Nov 13 20:02 fofi/libfofi.a
-rw-rw-r-- 1 epi epi   964732 Nov 13 20:03 splash/libsplash.a
-rw-rw-r-- 1 epi epi 12133702 Nov 13 20:03 xpdf/libxpdf.a

不是太寒酸!现在我们有了一个可以在 fuzzing 时使用的检测静态库(稍后我们将替换我们自己的 afl 编译器)。在我们开始修改我们的 fuzzer 之前,让我们编写代码将使用我们新编译的库(也就是我们的 fuzz 目标/线束/我们最终会进行 fuzz 的代码)……继续前进!

线束.cc

首先,我们可以排除一些命名法。我们将要写的是(根据我的经验)通常称为线束。在 libFuzzer 的文档中,它被称为模糊目标。它们是一样的东西,但线束更容易打字,所以我们会坚持下去。

线束只是一个函数,它接受一个字节数组和字节数组的大小作为参数,然后使用它们来调用被测目标库。在构建线束(从libFuzzer 文档修改)时,我们需要记住以下几点:

  • fuzzing 引擎将在同一进程中使用不同的输入多次执行 fuzz 目标。
  • 它不能在任何输入上 exit() 。
  • 它必须很快。尽量避免三次或更高的复杂性、日志记录或过多的内存消耗。

因为我们的线束将在同一个进程中一遍又一遍地执行,所以我们需要确保我们不会泄漏内存或到达调用exit. 我们还希望将代码量限制为仅执行我们希望模糊器采用的路径所绝对必要的代码量。由于我们已经有一个我们知道存在漏洞的驱动程序 ( pdftotext),我们可以简单地查看一下我们的线束应该做什么。

我们在这里的目标是保留原始程序的语义,但撕掉它的内脏以使其更容易模糊(来自@h0mbre 的粗引用)。我们主要对在PDFDoc实例化对象上创建或调用方法的代码感兴趣。下面是我们需要在我们的线束中复制该行为的全部内容。

xpdf/xpdf/pdftotext.cc

  doc = new PDFDoc(fileName, ownerPW, userPW);

  if (!doc->isOk()) {
    -------------8<-------------
  }


  if (!doc->okToCopy()) {
    -------------8<-------------
  }

  if (lastPage < 1 || lastPage > doc->getNumPages()) {
    lastPage = doc->getNumPages();
  }

  delete doc;

提取出我们关心的代码后,就可以编写我们的线束了。LLVMFuzzerTestOneInput下面看到的函数签名是许多(全部?)主要模糊测试框架支持的函数签名。这意味着我们可以编写一个线束并将其与 libFuzzer、AFL++、Honggfuzz 等一起使用……

#include <fstream>
#include <iostream>
#include <stdint.h>
#include "PDFDoc.h"
#include "goo/gtypes.h"
#include "XRef.h"

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    int lastPage = 0;

    GString *user_pw = NULL;
    GString *owner_pw = NULL;
    GString *filename = NULL;

    Object obj;
    obj.initNull();

    // stream is cleaned up when doc's destructor fires
    MemStream *stream = new MemStream((char *)data, 0, size, &obj);

    PDFDoc *doc = new PDFDoc(stream, owner_pw, user_pw);

    if (doc->isOk() && doc->okToCopy()) {
        lastPage = doc->getNumPages();
    }

    if (doc) { delete doc; }

    return 0;
}

请注意,我们正在使用MemStream我们之前在分析中找到的对象将我们的字节数组保存在内存中。此外,我们将代码保持在最低限度,清理了所有分配,并调用了原始程序调用的所有构造函数/方法。这对我们的线束完成了,让我们转向编译器。

gmem.cc

我们还需要对 xpdf 代码进行一项小的修改。具体在xpdf/goo/gmem.cc. 回想一下上面的代码,线束中/使用的代码不能exit来自任何输入。好吧,恰好有一个我们的模糊器将执行的代码路径导致调用exit(1).

我们可以通过将 exit 的调用替换为 的调用来解决此问题std::abort()。调用 abort 将允许 fuzzer 捕捉到崩溃并重新启动,而调用 exit 只会让我们的努力白费。

164  if (objSize <= 0 || nObjs < 0 || nObjs >= INT_MAX / objSize) {
165#if USE_EXCEPTIONS
166    throw GMemException();
167#else
168    fprintf(stderr, "nObjs: %d objSize\n", nObjs, objSize);
169    fprintf(stderr, "Bogus memory allocation size\n");
170    // exit(1);
171    std::abort();
172#endif
173  }
174  return gmalloc(n);
175}
176
177void *greallocn(void *p, int nObjs, int objSize) GMEM_EXCEP {
178  int n;
179
180  if (nObjs == 0) {
181    if (p) {
182      gfree(p);
183    }
184    return NULL;
185  }
186  n = nObjs * objSize;
187  if (objSize <= 0 || nObjs < 0 || nObjs >= INT_MAX / objSize) {
188#if USE_EXCEPTIONS
189    throw GMemException();
190#else
191    fprintf(stderr, "p: %p nObjs: %d objSize %d\n", p, nObjs, objSize);
192    fprintf(stderr, "Bogus memory allocation size\n");
193    // exit(1);
194    std::abort();
195
196#endif
197

编译器.rs

编译器代码听起来很吓人,但它几乎完全是样板文件。不过,首先,我们需要对我们的项目结构进行一些更改以支持新代码。

我们需要添加libafl_cc作为项目依赖项,以及libafl_targets. 我们选择使用文件系统上的文件夹,以便我们可以合并 LibAFL 团队最近所做的一些较新的更改。具体来说,出于本文的目的,我们提交 23f02dae12bfa49dbcb5157aee6e0c6ddaeddcd0。我们还需要将 crate 类型更改为静态库。

fuzzing-101-solutions/exercise-1/Cargo.toml

[dependencies]
# commit 23f02dae12bfa49dbcb5157aee6e0c6ddaeddcd0
libafl = { path = "../LibAFL/libafl" }
libafl_cc = { path = "../LibAFL/libafl_cc" }
libafl_targets = { path = "../LibAFL/libafl_targets" , features = ["libfuzzer", "sancov_pcguard_hitcounts"] }


[lib]
name = "exerciseone"
crate-type = ["staticlib"]

此外,我们的编译器将是一个可执行的二进制文件。我们可以使用 rust 的bin文件夹约定来说明文件src/bin夹中的任何文件都应该编译为独立的可执行文件。

fuzzing-101-solutions/exercise-1/

很酷,现在我们可以添加编译器代码了。如果您查看 LibAFL 存储库中的模糊器示例,它们中的大多数都使用相同的编译器代码。为清楚起见,下面显示的内容略有修改。

fuzzing-101-solutions/exercise-1/src/bin/compiler.rs

use libafl_cc::{ClangWrapper, CompilerWrapper};
use std::env;

pub fn main() {
    let cwd = env::current_dir().unwrap();
    let args: Vec<String> = env::args().collect();

    let mut cc = ClangWrapper::new();

    let is_cpp = env::current_exe().unwrap().ends_with("compiler_pp");

    if let Some(code) = cc
        .cpp(is_cpp)
        .silence(true)
        .from_args(&args)
        .expect("Failed to parse the command line")
        .link_staticlib(&cwd, "exerciseone")
        .add_arg("-fsanitize-coverage=trace-pc-guard")
        .run()
        .expect("Failed to run the wrapped compiler")
    {
        std::process::exit(code);
    }
}

关于上述代码的一些注意事项: –"compiler_pp"将是我们的 c++ 编译器包装器的名称 – 我们将我们的 crate 静态库的名称作为参数传递给.link_staticlib调用 –"-fsanitize-coverage=trace-pc-guard"此处讨论的 SanitizerCoverage 选项,但基本上允许我们轨道边缘覆盖

好的,最后,我们只需要添加我们的 c++ 编译器,它会简单地调用上面的编译器代码。

fuzzing-101-solutions/exercise-1/src/bin/compiler_pp.rs

pub mod compiler;

fn main() {
    compiler::main()
}

甜的!我们有 ac 和 cpp 编译器,由 clang 支持,它将基于 SanitizerCoverage 的覆盖检测添加到它编译的任何内容中。

库文件

现在是时候真正进行执行程序交换了。首先,我们需要重命名main.rslib.rs,因为我们的模糊器将成为一个静态库。

fuzzing-101-solutions/exercise-1/src/

libafl_main

之后,我们可以开始对模糊器进行修改。与 binary->library switch 主题保持一致,我们需要重命名 main 函数并添加no_mangle属性。no_mangle 属性指示 rustc 按原样保留此符号的名称,否则它最终可能看起来像 _ZN6afl_main17heb3ea72ba341fa07E。

#[no_mangle]
fn libafl_main() -> Result<(), Error> {
-------------8<-------------

边缘覆盖

接下来,我们需要更新观察边缘覆盖的方式。在基于 ForkserverExecutor 的 fuzzer 中,我们__AFL_SHM_ID自动从环境变量中获得了一个指向共享内存的指针,但是由于这个 fuzzer 现在使用 InProcessExecutor,我们需要使用EDGES_MAP来自libafl_targetscrate 的覆盖模块。

当我们用于afl-clang-[fast|lto]检测时,编译器插入了 __AFL_SHM_ID 指向的边缘覆盖图,我们可以使用该变量来获取指向该图的指针。这一次,我们使用libafl_cc,它使用 SanitizerCoverage 后端。最后,__AFL_SHM_ID环境变量不会被填充,所以我们需要使用libafl_targets暴露的EDGES_MAP。

特别感谢来自 Awesome Fuzzing Discord 服务器的 @toka 花时间帮助我/解释这个

let edges = unsafe { &mut EDGES_MAP[0..MAX_EDGES_NUM] };
let edges_observer = HitcountsMapObserver::new(StdMapObserver::new("edges", edges));

由于我们使用的是 EDGES_MAP,我们不能使用我们自己的地图大小定义,所以我们将更新我们的 Objective_state。

let objective_state = MapFeedbackState::new("timeout_edges", unsafe { EDGES_MAP.len() });

统计/监控组件

因为我们将在与 fuzzer 相同的进程空间中运行线束,所以线束打印到 stdout/err 的任何内容都将出现在 fuzzer 中。我们不希望看到一堆垃圾混杂在我们的模糊器统计信息中,因此我们将旧的 SimpleStats 组件替换为MultiMonitor。该Monitor组件是旧 Stats 组件的新名称。Stats 和 State 组件的名称过于相似,所以现在我们使用 Monitor 组件代替。

MultiMonitor 将显示累积和每个客户端的统计信息。它使用 LibAFL 的低级消息传递协议 (LLMP) 在代理和客户端之间进行通信。代理在第一次运行模糊器时产生,并且在代理处于活动状态时启动的任何模糊器进程都被视为客户端。值得注意的是,在第一次客户端连接到代理时,输出将显示有 2 个活动客户端。

当被问及这种行为时,@domenukk 是这样说的:

第 0 个客户端是打开网络套接字并侦听其他客户端和潜在代理的客户端。从 llmp 的角度来看,它仍然是一个客户端,因此它或多或少是一个实现细节。

实际代码与我们要替换的 SimpleStats 一样简单。

let monitor = MultiMonitor::new(|s| {
    println!("{}", s);
});

但是随着这种变化,我们的代理实例打印我们的统计信息,而每个客户端的 stdout/err 将打印到他们各自的终端。

broker terminal
════════════════════════════

[LibAFL/libafl/src/bolts/llmp.rs:600] "We're the broker" = "We're the broker"
Doing broker things. Run this tool again to start fuzzing in a client.
[LibAFL/libafl/src/bolts/llmp.rs:2187] "New connection" = "New connection"
[LibAFL/libafl/src/bolts/llmp.rs:2187] addr = 127.0.0.1:36678
[LibAFL/libafl/src/bolts/llmp.rs:2187] stream.peer_addr().unwrap() = 127.0.0.1:36678
[Stats       #1]  (GLOBAL) clients: 2, corpus: 0, objectives: 0, executions: 0, exec/sec: 0
                  (CLIENT) corpus: 0, objectives: 0, executions: 0, exec/sec: 0, edges: 299/17128 (1%)
[4:39 PM]
client terminal
════════════════════════════

We're the client (internal port already bound by broker, Os {
    code: 98,
    kind: AddrInUse,
    message: "Address already in use",
})
Connected to port 1337
[LibAFL/libafl/src/events/llmp.rs:833] "Spawning next client (id {})" = "Spawning next client (id {})"
[LibAFL/libafl/src/events/llmp.rs:833] ctr = 0

事件管理器组件

在我们的 fuzzer 的 forkserver 版本中,我们使用了 SimpleEventManager。这一次,我们需要一个LlmpRestartingEventManager。LlmpRestartingEventManager 执行与 SimpleEventManager 相同的基本功能,但也可以重新启动其关联的模糊器,在单独的执行之间保存模糊器的状态。这意味着每次孩子崩溃或超时时,LlmpRestartingEventManager 将产生一个新进程并继续进行模糊测试。在对setup_restarting_mgr_std的调用中,我们传入MultiMonitor、代理将侦听的端口 (1337) 和EventConfig::AlwaysUnique. LlmpRestartingEventManager 仅使用 EventConfig 来通过配置区分各个模糊器。

我们想要重新启动行为的原因之一是从根本上“清除掉”线束的 1000 次旧执行中的“碎片”,因此我们可以从头开始。

let (state, mut mgr) = match setup_restarting_mgr_std(monitor, 1337, EventConfig::AlwaysUnique)
{
    Ok(res) => res,
    Err(err) => match err {
        Error::ShuttingDown => {
            return Ok(());
        }
        _ => {
            panic!("Failed to setup the restarting manager: {}", err);
        }
    },
};

状态组件

接下来,我们需要从 EventManager 中获取 State。在初始传递时,setup_restarting_mgr_std从上面返回(None, LlmpRestartingEventManager)。在每次连续执行时(即在模糊器重新启动时),它返回保存在共享内存中的先前运行的状态。下面的代码通过提供默认的 StdState 来处理初始 None 值。第一次重启后,我们将简单地解开Some(StdState)调用 setup_restarting_mgr_std的返回值。

let mut state = state.unwrap_or_else(|| {
    StdState::new(
        // random number generator with a time-based seed
        StdRand::with_seed(current_nanos()),
        input_corpus,
        timeouts_corpus,
        // States of the feedbacks that store the data related to the feedbacks that should be
        // persisted in the State.
        tuple_list!(feedback_state, objective_state),
    )
});

线束组件

下面的代码是一个 Rust 闭包。它负责接受一些被 fuzzer 改变的字节,并将它们发送到我们LLVMFuzzerTestOneInputharness.cc.

let mut harness = |input: &BytesInput| {
    let target = input.target_bytes();
    let buffer = target.as_slice();
    libfuzzer_test_one_input(buffer);
    ExitKind::Ok
};

执行器组件

这里我们有小时的组件,InProcessExecutor!我们需要传入所有组件,然后将其包装在TimeoutExecutor 中,以便我们可以保持与之前相同的超时行为。

let in_proc_executor = InProcessExecutor::new(
    &mut harness,
    tuple_list!(edges_observer, time_observer),
    &mut fuzzer,
    &mut state,
    &mut mgr,
)
.unwrap();

let mut executor = TimeoutExecutor::new(in_proc_executor, timeout);

模糊器组件

最后,我们有 Fuzzer 组件。不是fuzz_loop再次使用该方法,而是永远循环。我们将改为使用fuzz_loop_for,它在继续之前只会运行 10,000 次模糊迭代。这将允许 fuzzer 退出并重新启动,让我们每隔一段时间就清理一次。

由于在重启场景中使用这个 fuzz_loop_for 在退出前只运行 10,000 次迭代,我们需要确保我们调用on_restart并将其传递给我们当前的状态。这样,状态将在下一个重新生成的模糊器过程中可用。

fuzzer
    .fuzz_loop_for(&mut stages, &mut executor, &mut state, &mut mgr, 10000)
    .unwrap();

mgr.on_restart(&mut state).unwrap();

生成文件.toml

有了所有必要的更改,我们就可以编写使一切正常的粘合剂。我最近遇到了货物制造项目,它非常强大。我们将在这里使用它来管理我们的构建和清理步骤。我们使用这个项目的主要动机build.rs是 Rust 的构建脚本没有类似的清理脚本。在过去,我通常只是用 Makefile 来扩充我的构建脚本,但现在没有了!现在,它Makefile.toml或破产。

在高层次上,我们可以运行cargo make rebuild以清理所有内容,构建编译器,然后使用编译器编译 xpdf 和我们的工具。

练习 1/Makefile.toml

[tasks.clean]
dependencies = ["cargo-clean", "afl-clean", "clean-xpdf"]

[tasks.afl-clean]
script = '''
rm -rf .cur_input* timeouts fuzzer fuzzer.o libexerciseone.a
'''

[tasks.clean-xpdf]
cwd = "xpdf"
script = """
make --silent clean
rm -rf built-with-* ../build/*
"""

[tasks.cargo-clean]
command = "cargo"
args = ["clean"]

[tasks.rebuild]
dependencies = ["afl-clean", "clean-xpdf", "build-compilers", "build-xpdf", "build-fuzzer"]

[tasks.build-compilers]
script = """
cargo build --release
cp -f ../target/release/libexerciseone.a .
"""

[tasks.build-xpdf]
cwd = "build"
script = """
cmake ../xpdf -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=$(pwd)/../../target/release/compiler -DCMAKE_CXX_COMPILER=$(pwd)/../../target/release/compiler_pp
make
"""

[tasks.build-fuzzer]
script = """
../target/release/compiler_pp -I xpdf/goo -I xpdf/fofi -I xpdf/splash -I xpdf/xpdf -I xpdf -o fuzzer harness.cc build/*/*.a -lm -ldl -lpthread -lstdc++ -lgcc -lutil -lrt
"""

在我们运行之后cargo run rebuild,我们fuzzerexercise-1目录中留下了二进制文件。

fuzzing-101-solutions/exercise-1

ls -al fuzzer
════════════════════════════

-rwxrwxr-x  1 epi epi 24446960 Nov 13 20:03 fuzzer

结果

好的,要看看我们产生了多大的影响,我们需要两个终端窗口(如果您喜欢,也可以使用窗格)。我们将fuzzer在每个窗口中运行。

窗口 1:经纪人

./fuzzer
════════════════════════════

[LibAFL/libafl/src/bolts/llmp.rs:600] "We're the broker" = "We're the broker"
Doing broker things. Run this tool again to start fuzzing in a client.

窗口 1:客户端

taskset -c 6 ./fuzzer
════════════════════════════

We're the client (internal port already bound by broker, Os {
    code: 98,
    kind: AddrInUse,
    message: "Address already in use",
})
Connected to port 1337
[LibAFL/libafl/src/events/llmp.rs:833] "Spawning next client (id {})" = "Spawning next client (id {})"
[LibAFL/libafl/src/events/llmp.rs:833] ctr = 0
Awaiting safe_to_unmap_blocking
-------------8<-------------
We're a client, let's fuzz :)
First run. Let's set it all up
Loading file "./corpus/sample.pdf" ...
We imported 1 inputs from disk.
-------------8<-------------

一旦客户端启动并运行,我们就可以在代理窗口中检查我们的工作情况。

[Stats       #1]  (GLOBAL) clients: 2, corpus: 454, objectives: 7, executions: 195316, exec/sec: 13500
                  (CLIENT) corpus: 454, objectives: 7, executions: 195316, exec/sec: 13500, timeout_edges: 619/17129 (3%), edges: 614/17129 (3%)
[Stats       #1]  (GLOBAL) clients: 2, corpus: 454, objectives: 7, executions: 195316, exec/sec: 13500
                  (CLIENT) corpus: 454, objectives: 7, executions: 195316, exec/sec: 13500, timeout_edges: 619/17129 (3%), edges: 614/17129 (3%)
[Testcase    #1]  (GLOBAL) clients: 2, corpus: 455, objectives: 7, executions: 196431, exec/sec: 13569
                  (CLIENT) corpus: 455, objectives: 7, executions: 196431, exec/sec: 13635, timeout_edges: 619/17129 (3%), edges: 614/17129 (3%)
[Stats       #1]  (GLOBAL) clients: 2, corpus: 455, objectives: 7, executions: 196431, exec/sec: 13087
                  (CLIENT) corpus: 455, objectives: 7, executions: 196431, exec/sec: 12573, timeout_edges: 619/17129 (3%), edges: 614/17129 (3%)
[Stats       #1]  (GLOBAL) clients: 2, corpus: 455, objectives: 7, executions: 196431, exec/sec: 12092
                  (CLIENT) corpus: 455, objectives: 7, executions: 196431, exec/sec: 11641, timeout_edges: 619/17129 (3%), edges: 614/17129 (3%)

好的!我们已经将我们原来的 fuzzer 加速了一个数量级,无论是给予还是接受。你可以从输出中看到,我的机器上有很大的变化。更酷的是现在我们可以为机器上的每个可用内核运行另一个模糊器实例。

这就是这篇文章。在下一篇中,我们将解决 Fuzzing101 中的练习 #2!

其他资源

  1. Fuzzing101
  2. AFL++
  3. 自由职业者联盟
  4. fuzzing-101-solutions 存储库
  5. libxpdf
  6. libFuzzer 文档
  7. SanitizerCoverage – trace-pc-guard

Fuzzing101 with LibAFL – Part I

 

Twitter user Antonio Morales created the Fuzzing101 repository in August of 2021. In the repo, he has created exercises and solutions meant to teach the basics of fuzzing to anyone who wants to learn how to find vulnerabilities in real software projects. The repo focuses on AFL++ usage, but this series of posts aims to solve the exercises using LibAFL instead. We’ll be exploring the library and writing fuzzers in Rust in order to solve the challenges in a way that closely aligns with the suggested AFL++ usage.

推特用户 Antonio Morales 在2021年8月创建了 fuzzing101资源库。在回购协议中,他创建了练习和解决方案,旨在向任何想要学习如何在实际软件项目中发现漏洞的人传授模糊化的基本知识。回复主要关注 AFL + + 的使用,但本系列文章旨在用 LibAFL 来解决练习。我们将探索图书馆,并在 Rust 中编写模糊符号,以便以一种与建议的 AFL + + 使用密切结合的方式解决这些挑战。

Since this series will be looking at Rust source code and building fuzzers, I’m going to assume a certain level of knowledge in both fields for the sake of brevity. If you need a brief introduction/refresher to/on coverage-guided fuzzing, please take a look here. As always, if you have any questions, please don’t hesitate to reach out.

由于本系列将讨论 Rust 源代码和构建模糊,为了简洁起见,我将假定这两个领域都有一定的知识水平。如果您需要一个简短的介绍/复习/关于覆盖引导模糊,请看看这里。一如既往,如果您有任何问题,请不要犹豫,联系我们。

This post will cover fuzzing Xpdf in order to solve Exercise 1. The companion code for this exercise can be found at my fuzzing-101-solutions repository

为了解决练习1,这篇文章将覆盖 fuzzing Xpdf。这个练习的伴随代码可以在我的 fuzzing-101-solutions 存储库中找到


Quick Reference

快速参考

This is just a summary of the different components used in the upcoming post. It’s meant to be used later as a easy way of determining which components are used in which posts.

这只是在即将发布的文章中使用的不同组件的总结。这意味着以后可以用它作为一种简单的方法来确定哪些组件用于哪些帖子。

{
  "Fuzzer": {
    "type": "StdFuzzer",
    "Corpora": {
      "Input": "InMemoryCorpus",
      "Output": "OnDiskCorpus"
    },
    "Input": "BytesInput",
    "Observers": [
      "TimeObserver",
      "HitcountsMapObserver"
    ],
    "Feedbacks": {
      "Pure": ["MaxMapFeedback", "TimeFeedback"],
      "Objectives": ["MapFeedbackState", "TimeoutFeedback"]
    },
    "State": "StdState",
    "Stats": "SimpleStats",
    "EventManager": "SimpleEventManager",
    "Scheduler": "IndexesLenTimeMinimizerCorpusScheduler",
    "Executor": "TimeoutForkserverExecutor",
    "Mutators": ["havoc_mutations"],
    "Stages": ["StdMutationalStage"]
  }
}

LibAFL Background

图片来源: LIBAFL

LibAFL, the Advanced Fuzzing Library, is a collection of reusable fuzzer components written in Rust. It is fast, multi-platform, no_std compatible, and scales well over cores and machines. LibAFL is written and maintained by Andrea Fioraldi and Dominik Maier (they also maintain AFL++). You can learn more about the motivation behind LibAFL and the different components in their rC3 talk.

高级 Fuzzing 库 LibAFL 是用 Rust 编写的可重用模糊器组件的集合。它具有快速、多平台、无标准兼容性、可扩展性好于核心和计算机的特点。LibAFL 由 Andrea Fioraldi 和 Dominik Maier 编写和维护(他们也维护 AFL + +)。你可以在他们的 rc3演讲中了解更多关于 LibAFL 背后的动机和不同组件的信息。

Without further ado, let’s get started.

闲话少说,我们开始吧。

Exercise 1 Setup

练习1设置

Our first step is walking through the setup steps for Rust, Xpdf, and AFL++. If you’re wondering why we’ll need AFL++ when we plan to use LibAFL, it’s because we’ll use AFL++’s compiler for instrumentation once or twice before we try out some different instrumentation backends.

我们的第一步是完成 Rust、 Xpdf 和 AFL + + 的设置步骤。如果你想知道当我们计划使用 LibAFL 时为什么需要 AFL + + ,那是因为在我们尝试一些不同的插装后端之前,我们将使用 AFL + + 的编译器进行插装一次或两次。

As far as setup, my assumption is that you’re on some flavor of linux. Any linux package manager commands will be given as debian-flavor commands (apt).

至于安装,我的假设是你使用的是某种风格的 linux。任何 linux 包管理器命令都将作为 debian 风格的命令(apt)给出。

Install Rust

安装防锈

This one’s easy.

这个很简单。

Further information is here in case you need it.

如果你需要更多的信息,这里有。

Install AFL++

安装 AFL + +

I’m taking these steps directly from Exercise 1’s setup instructions.

我直接从练习1的设置说明中采取这些步骤。

Install dependencies

安装依赖项

sudo apt-get update
sudo apt-get install -y build-essential python3-dev automake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools
sudo apt-get install -y lld-11 llvm-11 llvm-11-dev clang-11 || sudo apt-get install -y lld llvm llvm-dev clang 
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-dev

Checkout and build AFL++

检查和构建 AFL + +

Test your installation

测试你的安装

afl-fuzz -h
════════════════════════════

afl-fuzz++3.15a based on afl by Michal Zalewski and a large online community

afl-fuzz [ options ] -- /path/to/fuzzed_app [ ... ]

Required parameters:
  -i dir        - input directory with test cases
  -o dir        - output directory for fuzzer findings

Execution control settings:
  -p schedule   - power schedules compute a seed's performance score:
                  fast(default), explore, exploit, seek, rare, mmopt, coe, lin
                  quad -- see docs/power_schedules.md
  -f file       - location read by the fuzzed program (default: stdin or @@)
-------------8<-------------

Project Directory Setup

项目目录设置

In order to keep us on track for the upcoming rust code/build setup, we’re going to deviate from the directory structure recommended by the Fuzzing101 README. Since we know that we’ll be creating multiple Rust projects (one for each exercise), we’ll start out with a Rust workspace.

为了保持我们对即将到来的 rust code/build 设置的跟踪,我们将偏离 Fuzzing101 README 推荐的目录结构。因为我们知道我们将创建多个 Rust 项目(每个练习一个) ,所以我们将从一个 Rust 工作区开始。

The first step in creating a workspace for our Rust project directories is to simply make a directory.

为 Rust 项目目录创建工作空间的第一步是简单地创建一个目录。

cd $HOME
mkdir fuzzing-101-solutions
cd fuzzing-101-solutions

fuzzing-101-solutions will be the top level directory that houses all of our different exercise specific projects. In the top level directory, we need to create a Cargo.toml file that tells Rust and cargo that this is a workspace. Since all of our projects in the workspace are going to be fuzzers, we’ll globally modify the release profile settings as well.

Fuzzing-101-solutions 将是顶级目录,存放我们所有不同的特定锻炼项目。在顶级目录中,我们需要创建 Cargo.toml 文件,它告诉 Rust 和 cargo 这是一个工作区。由于我们工作空间中的所有项目都将是模糊的,因此我们也将全局地修改发布概要文件设置。

fuzzing-101-solutions/Cargo.toml

[workspace]

members = [
    "exercise-1",
]

[profile.release]
lto = true
codegen-units = 1
opt-level = 3
debug = true
  • lto=true :: perform link-time optimizations across all crates within the dependency graph
  • 对依赖关系图中的所有板条箱执行链接时间优化
  • codegen-units=1 :: controls how many “code generation units” a crate will be split into; higher codegen-units MAY produce slower code (max value 256)
  • Codegen-units = 1: : 控制一个板条箱将被分解成多少“代码生成单元”; 较高的 codegen-units 可能生成较慢的代码(最大值256)
  • opt-level=3 :: controls the level of optimizations; 3 == “all optimizations”
  • Opt-level = 3: : 控制优化级别; 3 = = “ all optimizations”
  • debug=true :: controls the amount of debug information included in the compiled binary; true == “full debug info”
  • 控制编译后的二进制文件中包含的调试信息量; true = “完整的调试信息”

After that, we can create our first solution project.

之后,我们可以创建我们的第一个解决方案项目。

Having run the commands above, we’re left with a directory structure that looks like this.

在运行了上面的命令之后,我们得到了一个如下所示的目录结构。

fuzzing-101-solutions/
├── Cargo.toml
└── exercise-1
    ├── Cargo.toml
    ├── .git
    │   ├── description
    │   -------------8<-------------
    ├── .gitignore
    └── src
        └── main.rs

Now we can proceed with the fuzz target setup.

现在我们可以继续进行模糊目标设置。

Install Xpdf

安装 XPDF

Again, these steps are pulled directly from Exercise 1’s README. We’ll modify them a bit to take our slightly different folder structure into account.

同样,这些步骤直接来自练习1的 README。我们将对它们进行一些修改,以考虑到我们略有不同的文件夹结构。

Download Xpdf 3.02

下载 XPDF 3.02

The following set of steps will download Xpdf version 3.02 into a folder ultimately named xpdf.

下面的步骤将把 Xpdf 版本3.02下载到一个名为 Xpdf 的文件夹中。

Our directory structure now looks like this

我们的目录结构现在看起来像这样

fuzzing-101-solutions/
├── Cargo.toml
├── exercise-1
│   ├── Cargo.toml
│   ├── .git
│   │   ├── config
│   │   -------------8<-------------
│   ├── .gitignore
│   └── src
│       └── main.rs
└── xpdf
    ├── aclocal.m4
    ├── aconf2.h
    -------------8<-------------

The Fuzzing101 README recommends building Xpdf with gcc at this point as a test. Feel free to do so if you like.

Fuzzing101自述文件建议此时使用 gcc 构建 Xpdf 作为测试。如果你愿意,可以随时这样做。

Fuzzer Setup

模糊设置

The Goal

目标

In order to write our fuzzer, we should take a look at our goal.

为了编写我们的模糊器,我们应该看看我们的目标。

According to Fuzzing101:

根据 Fuzzing101:

the goal is to find a crash/PoC for CVE-2019-13288 in XPDF 3.02.

目标是在 XPDF 3.02中找到 cve-2019-13288的崩溃/PoC。

CVE-2019-13288 is a vulnerability that may cause an infinite recursion via a crafted file.

Cve-2019-13288是一个漏洞,可能通过一个精心制作的文件导致无限递归。

Since each called function in a program allocates a stack frame on the stack, if a function is recursively called so many times it can lead to stack memory exhaustion and program crash.

由于程序中每个被调用的函数都会在堆栈上分配一个堆栈帧,如果一个函数被递归调用太多次,就会导致堆栈内存耗尽和程序崩溃。

As a result, a remote attacker can leverage this for a DoS attack.

因此,远程攻击者可以利用这一点进行 DoS 攻击。

Alright, we know what we want to accomplish, and all of the dependencies have been gathered, let’s go!

好了,我们知道我们想要完成什么,所有的依赖都已经收集好了,我们走吧!

Cargo.toml

Our first stop will be the Cargo.toml file in the exercise-1 directory. We’ll begin by telling cargo that we plan to use a build script, as well as adding LibAFL as a dependency.

我们的第一站是 exercise-1目录中的 Cargo.toml 文件。首先,我们将告诉 cargo 我们计划使用构建脚本,并将 LibAFL 作为一个依赖项添加。

exercise-1/Cargo.toml

运动-1/Cargo.toml

[package]
name = "exercise-one-solution"
version = "0.1.0"
edition = "2021"
build = "build.rs"

[dependencies]
libafl = "0.6.1"

build.rs

北京建筑科技有限公司

Next up, we’ll add a build.rs file to our exercise-1 directory, resulting in a directory structure like what’s shown below.

接下来,我们将在 exercise-1目录中添加一个 build.rs 文件,生成如下所示的目录结构。

fuzzing-101-solutions/
├── Cargo.toml
├── exercise-1
│   ├── Cargo.toml
│   ├── build.rs
-------------8<-------------

Build scripts are useful when one needs to perform some set of actions at build time. Typical uses are building/linking to external C libraries or doing some sort of code generation before building a project. By placing a file named build.rs in the root of our project directory, we’re telling cargo to compile and execute build.rs just before building our project.

当需要在构建时执行一组操作时,构建脚本是非常有用的。典型的用途是构建/链接到外部的 c 库,或者在构建项目之前进行某种代码生成。通过在项目目录的根目录中放置一个名为 build.rs 的文件,我们告诉 cargo 在构建项目之前编译并执行 build.rs。

The build.rs file is where we’ll configure and build Xpdf using AFL++’s compiler. More specifically, we’ll use alf-clang-fast, since that’s what Fuzzing101 recommends for this exercise.

在 build.rs 文件中,我们将使用 AFL + + 的编译器配置和构建 Xpdf。更具体地说,我们将使用 alf-clang-fast,因为这就是 fuzzing101对这个练习的建议。

build.rs is essentially a program unto itself, so we’ll begin with its imports and its main function.

Rs 本质上是一个程序本身,所以我们从它的导入和主要功能开始。

use std::env;
use std::process::Command;

fn main() {
    // todo 
}

Within the main function, we can configure when the build script should be run (after the initial build). We’ll do that the the rerun-if-changed directive. These directives tell cargo to re-run the build script if any of the files at the given paths have changed. More specifically, if their mtime timestamp has updated or not.

在 main 函数中,我们可以配置何时应该运行构建脚本(在初始构建之后)。我们将执行重新运行 if-changed 指令。这些指令告诉 cargo,如果给定路径上的任何文件发生更改,则重新运行构建脚本。更具体地说,如果它们的 mtime 戳更新了或没有更新。

println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/main.rs");

After that, we’re going to programatically execute the same commands that we would have run if we were to manually build Xpdf from the command line. Those commands are listed below to give you an idea of what build.rs is trying to accomplish.

在此之后,我们将通过编程方式执行相同的命令,如果我们从命令行手动构建 Xpdf,我们将会运行相同的命令。下面列出了这些命令,可以让您了解 build.rs 试图完成的目标。

# these are example commands that will be executed automatically by build.rs
# and were taken almost verbatim from Fuzzing101's README
cd fuzzing-101-solutions/exercise-1/xpdf
make clean
rm -rf install 
export LLVM_CONFIG=llvm-config-11 
CC=afl-clang-fast ./configure --prefix=./install
make
make install

Here’s what the first of the commands above looks like when written as a Command in Rust.

下面是上面的第一个命令在 Rust 中作为 Command 编写时的样子。

let cwd = env::current_dir().unwrap().to_string_lossy().to_string();
let xpdf_dir = format!("{}/xpdf", cwd);

// make clean; remove any leftover gunk from prior builds
Command::new("make")
    .arg("clean")
    .current_dir(xpdf_dir.clone())
    .status()
    .expect("Couldn't clean xpdf directory");

The rest of the commands follow the same kind of pattern. The entirety of build.rs is shown below.

其余的命令遵循同样的模式。

exercise-1/build.rs

练习-建造

use std::env;
use std::process::Command;

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=src/main.rs");

    let cwd = env::current_dir().unwrap().to_string_lossy().to_string();
    let xpdf_dir = format!("{}/xpdf", cwd);

    // make clean; remove any leftover gunk from prior builds
    Command::new("make")
        .arg("clean")
        .current_dir(xpdf_dir.clone())
        .status()
        .expect("Couldn't clean xpdf directory");

    // clean doesn't know about the install directory we use to build, remove it as well
    Command::new("rm")
        .arg("-r")
        .arg("-v")
        .arg("-f")
        .arg(&format!("{}/install", xpdf_dir))
        .current_dir(xpdf_dir.clone())
        .status()
        .expect("Couldn't clean xpdf's install directory");

    // export LLVM_CONFIG=llvm-config-11
    env::set_var("LLVM_CONFIG", "llvm-config-11");

    // configure with afl-clang-fast and set install directory to ./xpdf/install
    Command::new("./configure")
        .arg(&format!("--prefix={}/install", xpdf_dir))
        .env("CC", "/usr/local/bin/afl-clang-fast")
        .current_dir(xpdf_dir.clone())
        .status()
        .expect("Couldn't configure xpdf to build using afl-clang-fast");

    // make && make install
    Command::new("make")
        .current_dir(xpdf_dir.clone())
        .status()
        .expect("Couldn't make xpdf");

    Command::new("make")
        .arg("install")
        .current_dir(xpdf_dir)
        .status()
        .expect("Couldn't install xpdf");
}

If everything is configured correctly up to this point, we should be able to run

如果到目前为止一切都配置正确,我们应该能够运行

After which, the xpdf/install directory should look like this.

在此之后,xpdf/install 目录应该如下所示。

xpdf/install
├── bin
│   ├── pdffonts
│   ├── pdfimages
│   ├── pdfinfo
│   ├── pdftops
│   └── pdftotext
├── etc
│   └── xpdfrc
└── man
    ├── man1
    │   ├── pdffonts.1
    │   ├── pdfimages.1
    │   ├── pdfinfo.1
    │   ├── pdftops.1
    │   └── pdftotext.1
    └── man5
        └── xpdfrc.5

The binaries in the xpdf/install/bin folder were all compiled with afl-clang-fast, and as such, are instrumented for fuzzing!

Xpdf/install/bin 文件夹中的二进制文件都是使用 afl-clang-fast 编译的,因此可以检测 fuzzing!

corpus

语料库

I promise, we’re getting to the fuzzer soon, but before we do, we need a few sample PDF files to populate our input corpus. It’s just a few wget commands, so nothing too onerous.

我保证,我们很快就会得到的模糊,但在我们这样做,我们需要一些样本 PDF 文件填充我们的输入语料库。它只是一些 wget 命令,所以没有什么太麻烦的。

That’s it! These three PDF files will make up the initial input for our fuzzer.

就是它! 这三个 PDF 文件将构成我们的模糊器的初始输入。

Writing the Fuzzer

编写模糊语言

Ok, now we’re closing in on the good stuff. main.rs will house our fuzzer’s logic. Ultimately, the fuzzer is going to be put together piece-by-piece using different components from LibAFL. The majority of this code was derived from the forkserver_simple example in the LibAFL repo. We’re using a forkserver along with afl-clang-fast to keep somewhat in-line with what one would expect to see when following along with Fuzzing101’s recommendation.

好了,现在我们接近好东西了。主要的是我们的模糊逻辑。最终,我们将使用 LibAFL 中的不同组件逐件地将模糊器组合在一起。此代码的大部分来自 LibAFL repo 中的 forkserver _ simple 示例。我们使用叉式服务器和 afl-clang-fast 来保持与 fuzzing101的建议一致。

As we go through main.rs, we’ll attempt to dig in to why certain components were chosen and how they map to different fuzzing concepts.

在我们浏览 main.rs 时,我们将尝试深入研究为什么选择某些组件以及它们如何映射到不同的 fuzzing 概念。

Ok, I think that’s enough preamble, let’s get started.

好了,我想已经说得够多了,我们开始吧。

Components: Corpus + Input

References:

参考文献:

We’ll start building our fuzzer by creating our corpora (yes, I had to google the plural of corpus, don’t judge me). First up is the input corpus. The input corpus holds all of our current testcases. We’re using an InMemoryCorpus to prevent reads/writes to disk. Keeping our corpus in memory and preventing disk access should improve the speed at which we manipulate testcases.

我们将通过创建语料库来开始构建模糊语言(是的,我必须用谷歌搜索语料库的复数形式,不要评判我)。首先是输入语料库。输入语料库保存了我们当前的所有测试用例。我们使用 InMemoryCorpus 来防止对磁盘的读写。保持内存中的语料库并防止磁盘访问应该可以提高操作测试用例的速度。

While creating the input corpus, we need to define the Input type. The Input represents data received from some external source. In this case, we’re using the BytesInput, which means that our input corpus should contain items that can be represented as arrays of bytes. Those byte arrays will eventually be mutated by the fuzzer before being passed to the program being fuzzed.

在创建输入语料库时,我们需要定义输入类型。Input 表示从某个外部源接收的数据。在这种情况下,我们使用 BytesInput,这意味着我们的输入语料库应该包含可以表示为字节数组的项。这些字节数组在传递给模糊化的程序之前,最终会被模糊器变异。

let corpus_dirs = vec![PathBuf::from("./corpus")];

let input_corpus = InMemoryCorpus::<BytesInput>::new();

After that, we’ll move on to the output corpus. Testcases from the input corpus that cause a timeout are considered “solutions”. Our output corpus, which is of the type OnDiskCorpus, is the corpus in which we store those solutions. Said another way: any generated PDF that causes the program to hang will be stored in the output corpus.

之后,我们将继续讨论输出语料库。输入语料库中导致超时的测试用例被认为是“解决方案”。我们的输出语料库属于 OnDiskCorpus 类型,是我们存储这些解决方案的语料库。换句话说: 任何生成的 PDF,导致程序挂起将被存储在输出语料库中。

let timeouts_corpus = OnDiskCorpus::new(PathBuf::from("./timeouts")).expect("Could not create timeouts corpus");

Component: Observer

References:

参考文献:

The next component for our fuzzer is the Observer. An Observer can be thought of as something that provides information about the current testcase to the fuzzer. We’ll start with a simple Observer: the TimeObserver. The TimeObserver simply keeps track of the current testcase’s runtime. For each testcase, the TimeObserver will send the time it took the testcase to execute to the fuzzer by way of a Feedback component, which we’ll discuss shortly.

我们的模糊器的下一个组件是观察者。观察者可以被认为是向模糊器提供关于当前测试用例的信息。我们从一个简单的观察者开始: 时间观察者。TimeObserver 只是简单地跟踪当前测试用例的运行时。对于每个测试用例,TimeObserver 将通过一个 Feedback 组件将测试用例执行的时间发送给 fuzzer,我们将很快讨论这个组件。

let time_observer = TimeObserver::new("time");

While the TimeObserver was simple, the next Observer is less so. In addition to execution time, we also want to keep track of the coverage map (this is a coverage-guided fuzzer after all). In order to build a coverage map, we need some shared memory. This piece of shared memory, AKA the coverage map, will be shared between the HitcountsMapObserver and the Executor (we’ll discuss the Executor a little later in the post).

虽然时间观察者很简单,但下一个观察者就不那么简单了。除了执行时间,我们还需要跟踪覆盖率映射(这毕竟是一个覆盖率引导的模糊器)。为了构建覆盖范围图,我们需要一些共享内存。这部分共享内存,又称覆盖率映射,将在 HitcountsMapObserver 和 Executor 之间共享(稍后我们将讨论 Executor)。

First, we create a new instace of a StdShMemProvider, which provides access to shared memory mappings. We then use the StdShMemProvider to create a new shared memory mapping that is 65536 bytes.

首先,我们创建一个新的 stdout shmemprovider,它提供对共享内存映射的访问。然后,我们使用 stdout/shmemprovider 创建一个新的共享内存映射,其大小为65536字节。

const MAP_SIZE: usize = 65536;
-------------8<-------------
let mut shmem = StdShMemProvider::new().unwrap().new_map(MAP_SIZE).unwrap();

After that, we need to save the shared memory id to the environment, so that the Executor knows about it.

之后,我们需要将共享内存 id 保存到环境中,以便 Executor 知道它。

shmem.write_to_env("__AFL_SHM_ID").expect("couldn't write shared memory ID");

Next, we get a mutable reference to the memory map. The reference we create is of type &mut [u8].

接下来,我们得到一个对内存映射的可变引用。

let mut shmem_map = shmem.map_mut();

Finally, we create our Observer, passing in the reference to the shared memory map and giving it the name shared_mem.

最后,我们创建观察者,传递对共享内存映射的引用,并给它命名为 shared _ mem。

A HitcountsMapObserver needs a base object passed in as part of its constructor. The base we’re using is a ConstMapObserver. A ConstMapObserver is an optimization layer over a MapObserver. It allows for some performance gains by using a map size that’s known at compile time when deciding if a testcase is “interesting” (more on this in the Feedback section).

HitcountsMapObserver 需要一个基本对象作为其构造函数的一部分传入。我们使用的基础是一个 constanmapobserver。Constanmapobserver 是 MapObserver 上的优化层。在判断一个测试用例是否“有趣”时,它使用在编译时已知的映射大小来提高性能(在 Feedback 部分有更多关于这一点的内容)。

let edges_observer = HitcountsMapObserver::new(ConstMapObserver::<_, MAP_SIZE>::new(
    "shared_mem",
    &mut shmem_map,
));

Phew! We’re done with Observers for now, let’s see what’s next.

我们现在不需要观察员了,看看接下来会发生什么。

Component: Feedback

References:

参考文献:

After the Observers comes our Feedback. The purpose of a Feedback component is to classify whether or not the outcome of a testcase is interesting. When a Feedback determines that a testcase is interesting, the input that was used in that testcase is typically added to the Corpus. Feedback components may have a FeedbackState component tied to them as well. FeedbackState components represent the state of the data that the Feedback wants to persist in the Fuzzer’s State (both of which are coming up later).

观察者之后是我们的反馈。Feedback 组件的目的是对测试用例的结果是否有趣进行分类。当一个 Feedback 确定一个测试用例是有趣的时候,在这个测试用例中使用的输入通常会被添加到语料库中。反馈组件也可能有一个反馈状态组件绑定到它们。FeedbackState 组件表示反馈希望保持在 Fuzzer 状态的数据的状态(这两个都将在稍后出现)。

For our fuzzer, we need to create a few different Feedback components. We’ll start with the Feedbacks that will keep track of our coverage map and execution time.

对于我们的模糊器,我们需要创建一些不同的反馈组件。我们将从跟踪覆盖范围图和执行时间的反馈开始。

The MapFeedbackState tracks the cumulative state of the coverage map while getting updates from its associated Observer.

MapFeedbackState 在从其关联的 Observer 获取更新的同时跟踪覆盖率映射的累积状态。

let feedback_state = MapFeedbackState::with_observer(&edges_observer);

The MapFeedbackState and its HitcountsMapObserver (discussed earlier) are passed into a MaxMapFeedback. The MaxMapFeedback determines if there is a value in the HitcountsMapObserver’s coverage map that is greater than the current maximum value for the same entry. If a new maximum is found, the Input is deemed interesting.

MapFeedbackState 及其 HitcountsMapObserver (前面讨论过)被传递到 MaxMapFeedback 中。MaxMapFeedback 确定 HitcountsMapObserver 的覆盖率映射中是否有一个大于同一条目的当前最大值的值。如果找到一个新的最大值,则认为 Input 是有趣的。

After creating the MaxMapFeedback, we also create a new TimeFeedback, which is then tied to the TimeObserver we saw earlier. You may be wondering how the TimeFeedback component helps to decide if an Input is interesting… Well, it doesn’t. TimeFeedback never reports an Input as interesting. However, it does keep track of testcase execution time by way of its TimeObserver. Due to the fact that it can never classify a testcase as interesting on its own, we need to use it alongside some other Feedback that has the ability to perform said classification.

在创建 MaxMapFeedback 之后,我们还创建了一个新的 TimeFeedback,它绑定到我们前面看到的 TimeObserver。您可能想知道 TimeFeedback 组件如何帮助确定一个输入是否有趣… … 好吧,它并不有趣。时间反馈从来不报告有趣的输入。但是,它通过其 TimeObserver 来跟踪测试用例的执行时间。由于它本身永远不能将一个测试用例分类为有趣的,因此我们需要将它与其他一些具有执行上述分类能力的 Feedback 一起使用。

With both of the new Feedback components created, we use the feedback_or macro to compile both Feedbacks into a single CombinedFeedback, which is joined together with a logical OR.

在创建了这两个新的 Feedback 组件之后,我们使用 Feedback _ OR 宏来将两个 Feedback 编译成一个 CombinedFeedback,这个 Feedback 与一个逻辑 OR 连接在一起。

The feedback variable below is essentially saying, if the current testcase’s input triggered a new code path in the coverage map, we should probably save that input to the corpus.

下面的反馈变量实际上是说,如果当前测试用例的输入触发了覆盖率映射中的新代码路径,我们可能应该将该输入保存到语料库中。

The true passed to new_tracking says that we want to track indexes. The false says we do not want to track novelties.

传递给 new _ tracking 的 true 表示我们想要跟踪索引。错误的说法是我们不想追踪新奇事物。

let feedback = feedback_or!(
    MaxMapFeedback::new_tracking(&feedback_state, &edges_observer, true, false),
    TimeFeedback::new_with_observer(&time_observer)
);

Alright, we’ve got the Feedbacks for coverage and time, now we’re going to add a second set of Feedbacks. These upcoming Feedbacks will also be used to determine if a testcase is interesting, but can be thought of more as a testcase’s solution or objective. They’ll be passed into our Fuzzer component later on in the code.

好了,我们已经得到了报道和时间的反馈,现在我们要添加第二组反馈。这些即将到来的反馈还将用于确定一个测试用例是否有趣,但可以更多地将其视为一个测试用例的解决方案或目标。它们将在稍后的代码中被传递到我们的 Fuzzer 组件中。

Similar to the first set, we need to create a MapFeedbackState. However, instead of tying it to an Observer, this one will create its own memory map of MAP_SIZE, consisting of u8’s.

与第一组类似,我们需要创建一个 MapFeedbackState。然而,不是将它绑定到一个观察者,这一个将创建它自己的 MAP _ size 内存映射,包括 u8的。

const MAP_SIZE: usize = 65536;
// -------------8<-------------
let objective_state = MapFeedbackState::new("timeout_edges", MAP_SIZE);

Also similar to before, we combine two Feedbacks using a boolean operator, but this time it’s a logical AND. We’ll combine them using the feedback_and_fast macro. The Feedbacks in question are the TimeoutFeedback and our old friend MaxMapFeedback from earlier.

同样与前面类似,我们使用布尔运算符合并两个反馈,但这次是逻辑与。我们将使用 feedback 和 fast 宏来组合它们。我们讨论的反馈是 TimeoutFeedback 和我们的老朋友 MaxMapFeedback。

Recall that our goal is to find an infinite recursion bug. Since we’re looking for something that causes infinite recursion, we’re mostly interested in looking for testcases that make the target program hang. The objective variable below is essentially saying, if the given input triggered a new code path in the coverage map, AND, if the time to execute the fuzz case with the current input results in a timeout, our testcase meets our objective.

回想一下,我们的目标是找到一个无限递归 bug。因为我们正在寻找导致无限递归的东西,所以我们最感兴趣的是寻找使目标程序挂起的测试用例。下面的目标变量实际上是说,如果给定的输入在覆盖范围图中触发了一个新的代码路径,并且,如果用当前输入执行模糊案例的时间导致超时,那么我们的测试案例符合我们的目标。

let objective = feedback_and_fast!(
    TimeoutFeedback::new(),
    MaxMapFeedback::new(&objective_state, &edges_observer)
);

That’s it for Feedbacks, moving on to…

这就是反馈,继续..。

Component: State

References:

参考文献:

Our next component is the State component, specifically the StdState. A State component takes ownership of each of our existing FeedbackState components, a random number generator, and our corpora.

我们的下一个组件是 State 组件,特别是 stdout 状态。状态组件拥有我们现有的每个 FeedbackState 组件、一个随机数生成器和我们的语料库的所有权。

let mut state = StdState::new(
    StdRand::with_seed(current_nanos()),
    input_corpus,
    timeouts_corpus,
    tuple_list!(feedback_state, objective_state),
);

Component: Stats

References:

参考文献:

The Stats component defines how the fuzzer’s stats are reported. For now, we’ll use the simplest Stats representation: SimpleStats. SimpleStats will call println with SimpleStats::display as input in order to send a report to the terminal.

Stats 组件定义如何报告 fuzzer 的状态。现在,我们将使用最简单的 Stats 表示: SimpleStats。将使用 SimpleStats: : display 调用 println 作为输入,以便向终端发送报告。

let stats = SimpleStats::new(|s| println!("{}", s));

Component: EventManager

References:

参考文献:

The EventManager component handles the various Events generated during the fuzzing loop. Some examples of an Event are finding an interesting testcase, updating the Stats component, and logging. Once again, we’ll use the simplest type available for our current Fuzzer.

EventManager 组件处理 fuzzing 循环期间生成的各种事件。事件的一些例子包括发现一个有趣的测试用例、更新 Stats 组件和日志记录。再一次,我们将使用当前 Fuzzer 可用的最简单的类型。

let mut mgr = SimpleEventManager::new(stats);

Component: Scheduler

References:

参考文献:

During the fuzz loop, our fuzzer will need to acquire new testcases from the input corpus. The Scheduler component defines the strategy used to supply a Fuzzer’s request to the Corpus for a new testcase. For our fuzzer, we’re using the IndexesLenTimeMinimizerCorpusScheduler. The name is kinda scary, but it boils down to a minimization policy backed by a queue that is used to get test cases from the corpus. It will prioritize quick/small testcases that exercise all of the entries registered in the coverage map’s metadata.

在模糊循环过程中,模糊语言学习者需要从输入语料库中获取新的测试用例。调度器组件定义了一种策略,用于向语料库提供 Fuzzer 对新测试用例的请求。对于我们的模糊器,我们使用 indexlentmeminimizer/corpusscheduler。这个名字有点吓人,但它可以归结为一个由队列支持的最小化策略,该队列用于从语料库中获取测试用例。它将对快速/小型测试用例进行优先排序,这些测试用例执行覆盖率地图元数据中注册的所有条目。

The QueueCorpusScheduler is used as the IndexesLenTimeMinimizerCorpusScheduler’s backing queue.

这个 QueueCorpusScheduler 用作 IndexesLenTimeMinimizerCorpusScheduler 的备份队列。

let scheduler = IndexesLenTimeMinimizerCorpusScheduler::new(QueueCorpusScheduler::new());

Component: Fuzzer

References:

参考文献:

The Fuzzer component contains our feedback, objectives, and a corpus scheduler. It’ll be the primary driver of our program, running the target program with the generated Input while triggering Observers and Feedbacks.

模糊组件包含我们的反馈、目标和一个语料库调度器。它将是我们程序的主要驱动程序,在触发观察器和反馈的同时,使用生成的输入运行目标程序。

let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);

Component: Executor

References:

参考文献:

The Executor component is one of the few remaining. We’ll use a TimeoutForkserverExecutor. The TimeoutForkserverExecutor wraps the standard ForkserverExecutor and sets a timeout before each run. This gives us an executor that that implements an AFL-like mechanism that will spawn child processes to fuzz.

Executor 组件是仅存的几个组件之一。我们将使用 TimeoutForkserverExecutor。TimeoutForkserverExecutor 包装标准的叉车执行器,并在每次运行之前设置一个超时。这为我们提供了一个执行器,它实现了一个类似于 afl 的机制,这个机制将把子进程生成为模糊。

Part of creating our Executor is telling it what we want it to execute. In our case, we want to run the following.

创建我们的 Executor 的一部分是告诉它我们希望它执行什么。在我们的例子中,我们想运行以下代码。

./path/to/pdftotext INPUT_FILE

We’ll pass the path to pdftotext to the ForkserverExecutor constructor, along with the args it needs to run. We’ll use the double-@ symbol to tell the ForkserverExecutor that we want the BytesInput (covered earlier) generated from each testcase to be written to a file, and that the same file’s path should overwrite the @@ in the final command. Because we’re passing our generated Input via an on-disk file, we’ll set the use_shmem_testcase to false. Finally, we’ll pass our Observers into the ForkserverExecutor’s constructor, rounding out the call.

我们将把 pdftext 的路径和它需要运行的 args 一起传递给 forcserverexecutor 构造函数。我们将使用 double -@符号来告诉 forcserverexecutor,我们希望从每个测试用例生成的 BytesInput (前面已经介绍过)被写到一个文件中,并且在最终命令中,同一个文件的路径应该覆盖@@ 。因为我们是通过磁盘文件传递生成的 Input,所以我们将 use _ shmem _ testcase 设置为 false。最后,我们将观察器传递到 ForkserverExecutor 的构造函数中,完成调用。

let fork_server = ForkserverExecutor::new(
    "./xpdf/install/bin/pdftotext".to_string(),
    &[String::from("@@")],
    false,  // use_shmem_testcase
    tuple_list!(edges_observer, time_observer),
).unwrap();

With that out of the way, we simply need to choose the length of our timeout, and pass the resulting Duration and ForkserverExecutor to the TimeoutForkserverExecutor’s constructor.

解决了这个问题之后,我们只需要选择超时的长度,并将结果的 Duration 和 ForkserverExecutor 传递给 TimeoutForkserverExecutor 的构造函数。

let timeout = Duration::from_millis(5000);

// ./pdftotext @@
let mut executor = TimeoutForkserverExecutor::new(fork_server, timeout).unwrap();

Components: Mutator + Stage

References:

参考文献:

The final pieces of the puzzle are the Mutator and Stage components. We’ll register our StdScheduledMutator as a Stage using the StdMutationalStage component. A mutational stage is the stage in a fuzzing run that mutates the Input. Mutational stages will usually have a range of mutations that are being applied to the input one by one, between executions. In our case, the range of mutations we’ve chosen are the ever popular Havoc mutations. Each mutation is scheduled by the StdScheduledMutator as part of the mutational stage.

拼图的最后一部分是 Mutator 和 Stage 组件。我们将使用 stdout 变换阶段组件注册我们的 stdout scheduledmutator 为 Stage。变异阶段是输入变异的 fuzzing 运行中的阶段。突变阶段通常会有一系列的突变,这些突变在执行之间一个接一个地应用到输入端。在我们的案例中,我们选择的突变范围是一直流行的浩劫突变。每个突变都是由 stdout/scheduledmutator 作为突变阶段的一部分进行安排的。

let mutator = StdScheduledMutator::new(havoc_mutations());
let mut stages = tuple_list!(StdMutationalStage::new(mutator));

Running the Fuzzer

运行 FUZZER

That’s it for the individual components. All that’s left is for us to run the fuzzer. Recall that the Fuzzer takes ownership of a bunch of different components and essentially makes everything run. Knowing that, we pass in our stages, executor, the state, and the event manager. The code to make that happen is shown below.

对于单个组件来说就是这样。剩下的就是我们来运行这个模糊器了。回想一下,Fuzzer 拥有一大堆不同组件的所有权,并且本质上让所有东西运行。知道这一点,我们通过了我们的阶段,执行者,国家和活动经理。下面显示了实现这一目标的代码。

fuzzer
    .fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)
    .expect("Error in the fuzzing loop");

Build the Fuzzer

建立模糊器

Ok, it’s been a long time coming, but the moment has arrived! With build.rs setup to take care of building Xpdf, we can build our fuzzer and the fuzz target with a single command.

好吧,这是一个漫长的时间来了,但这一刻已经到来!通过 build.rs 设置来处理 Xpdf 构建,我们可以使用一个命令来构建 fuzzer 和 fuzz 目标。

cd fuzzing-101-solutions
cargo build --release

Commence Fuzzing!

开始模糊

After the build completes, we can kick off our artisnal, hand-crafted fuzzer.

建立完成后,我们可以开始我们的艺术,手工制作的模糊。

cd exercise-1
../target/release/exercise-one-solution

On my machine, it took roughly 10 minutes to get a timeout. The objectives: 1 shows that we have 1 testcase that met the fuzzer’s objective (a timeout that also produced new coverage).

在我的机器上,大概花了10分钟才得到一次暂停。目标: 1表明我们有1个测试用例符合 fuzzer 的目标(超时也产生新的覆盖率)。

[Stats #0] clients: 1, corpus: 640, objectives: 1, executions: 568810, exec/sec: 1744

Results

结果

We can confirm that we’ve found a bug by using the PDF inside the timeouts folder (our output corpus).

我们可以通过在超时文件夹(我们的输出语料库)中使用 PDF 来确认我们发现了一个 bug。

./xpdf/install/bin/pdftotext ./timeouts/7e3a6553de5cce87
════════════════════════════════════════════════════════
Error: PDF file is damaged - attempting to reconstruct xref table...
Error (677): Illegal character <2c> in hex string
Error (678): Illegal character <2c> in hex string
Error (679): Illegal character <2c> in hex string
-------------8<-------------
Error (3245): Dictionary key must be a name object
Error (3248): Dictionary key must be a name object
Error (3255): Dictionary key must be a name object
Segmentation fault

Outro

 

女名女子名

\o/ Huzzah! We’ve created our own fuzzer using LibAFL that was specifically tuned to find a recursion bug in real-world software; pretty neat eh? The companion code for this exercise can be found at my fuzzing-101-solutions repository.

啊,万岁!我们已经使用 LibAFL 创建了我们自己的 fuzzer,它专门用于在现实世界的软件中发现一个递归错误,很棒吧?这个练习的伴随代码可以在我的 fuzzing-101-solutions 存储库中找到。

In the next post, we’ll tackle the second exercise. See you then!

在下一篇文章中,我们将处理第二个练习。到时候见!

Additional Resources

额外资源

  1. Fuzzing101 
  2. AFL++ 
  3. LibAFL
  4. LibAFL API Documentation 文档
  5. LibAFL Book
  6. forkserver_simple example fuzzer 简单的例子 fuzzer
  7. fuzzing-101-solutions repository Fuzzing-101解决方案库

 

【Fuzzing101】练习一

➜ pdf_examples git:(main) ✗ $FUZZ/fuzzing_xpdf/install/bin/pdfinfo -box -meta $FUZZ/fuzzing_xpdf/pdf_examples/helloworld.pdf
Tagged: no
Pages: 1
Encrypted: no
Page size: 200 x 200 pts
MediaBox: 0.00 0.00 200.00 200.00
CropBox: 0.00 0.00 200.00 200.00
BleedBox: 0.00 0.00 200.00 200.00
TrimBox: 0.00 0.00 200.00 200.00
ArtBox: 0.00 0.00 200.00 200.00
File size: 678 bytes
Optimized: no
PDF version: 1.7

cargo-fuzz tutorial

一.安装

github: https://github.com/rust-fuzz/cargo-fuzz

安装: cargo install cargo-fuzz

➜ fuzzing cargo install cargo-fuzz
Updating https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git index
Downloaded cargo-fuzz v0.10.0 (registry https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git)
Downloaded 1 crate (30.5 KB) in 2.33s
Installing cargo-fuzz v0.10.0
……
Finished release [optimized] target(s) in 47.02s
Replacing /root/.cargo/bin/cargo-fuzz
Replaced package cargo-fuzz v0.8.0 with cargo-fuzz v0.10.0 (executable cargo-fuzz)

➜ fuzzing cargo-fuzz
cargo-fuzz 0.10.0
A cargo subcommand for using libFuzzer! Easy to use! No need to recompile LLVM!

USAGE:
cargo-fuzz

FLAGS:
-h, –help Prints help information
-V, –version Prints version information

SUBCOMMANDS:
add Add a new fuzz target
build Build fuzz targets
cmin Minify a corpus
coverage Run program on the generated corpus and generate coverage information
fmt Print the std::fmt::Debug output for an input
help Prints this message or the help of the given subcommand(s)
init Initialize the fuzz directory
list List all the existing fuzz targets
run Run a fuzz target
tmin Minify a test case

注意: cargo-fuzz 是集成了libfuzzer,libFuzzer依赖LLVM的,所以只能是x86-64的linux或者macos才可以,而且还需要C++编译器和C++11的支持。

用法

  • 初始化: cargo fuzz init
  • 添加目标: cargo fuzz add <target>
  • 运行: cargo fuzz run <target>
  • 打印测试用户输出 :cargo fuzz fmt <target> <input>
  • 缩小到最小输出:cargo fuzz tmin <target> <input>
  • 缩小输入数据: cargo fuzz cmin <target>
  • 生成覆盖范围信息: cargo fuzz coverage <target>

官方文档: https://rust-fuzz.github.io/book/cargo-fuzz.html

二.实战

我们随机选了一个好fuzzing的库,Human Time, 一个parser 时间的这种库。

crate: https://crates.io/crates/humantime

github: https://github.com/tailhook/humantime

文档: https://docs.rs/humantime/2.1.0/humantime/

第一步: 把项目clone到本地

git clone https://github.com/tailhook/humantime

我们可以看到,这个库自己是没有fuzz文件夹的,说明并没有做fuzzing

➜ humantime git:(master) ls
benches bulk.yaml Cargo.toml LICENSE-APACHE LICENSE-MIT README.md src vagga.yaml

第二步 : 项目初始化

进行项目文件夹执行

cargo fuzz init

可以看到,多了一个fuzz的文件夹

fuzz的文件夹结构:



➜ fuzz git:(master) ✗ tree
.
├── Cargo.toml
└── fuzz_targets
└── fuzz_target_1.rs


1 directory, 2 files



fuzz_target_1.rs的代码:

#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    // fuzzed code goes here
});

第三步,开始编写测试用例

我们查看文档,看看这个库的具体用法

https://docs.rs/humantime/2.1.0/humantime/fn.format_duration.html

#![no_main]
use libfuzzer_sys::fuzz_target;

use humantime::parse_duration;

fuzz_target!(|data: &[u8]| {
    
    if let Ok(s) = std::str::from_utf8(data) {
        let _ = parse_duration(s);
    };    

});

我们把data变成字符串,看看parse_duration是怎么解析的。

第四步,运行

cargo fuzz list 可以列出所有的测试用例

➜  fuzz_targets git:(master) ✗ cargo fuzz list
fuzz_target_1

我们继续添加测试用例

#![no_main]
use libfuzzer_sys::fuzz_target;

use humantime::{parse_duration, parse_rfc3339, parse_rfc3339_weak};

fuzz_target!(|data: &[u8]| {
    // fuzzed code goes here
    if let Ok(s) = std::str::from_utf8(data) {
        let _ = parse_duration(s);
        let _ = parse_rfc3339(s);
        let _ = parse_rfc3339_weak(s);
    };
});




第五步 分析结果