一个简单的LibAFL模糊器

本章讨论了一个使用 LibAFL API 构建的极其简单的模糊器。 你将学习基本的实体,如 StateObserverExecutor。 虽然下面的章节会详细讨论 LibAFL 的组件,但在这里我们介绍基本原理。

我们将对一个简单的 Rust 函数进行模糊处理,该函数在某个条件下会出现panic。这个模糊器将是单线程的,并在崩溃后停止,就像libFuzzer通常做的那样。

你可以在 fuzzers/baby_fuzzer 中找到本教程的完整版本,作为一个模糊器的例子。

警告

这个示例模糊器对于任何现实世界的使用来说都是太天真了。 它的目的仅仅是为了展示库的主要组件,如果想更深入地了解如何构建一个自定义的模糊器,请直接阅读 Tutorial chapter

创建一个项目

我们使用 cargo 创建一个新的Rust项目,将 LibAFL 作为一个依赖项。

  1. $ cargo new baby_fuzzer
  2. $ cd baby_fuzzer

生成的 Cargo.toml 看起来像下面这样:

  1. [package]
  2. name = "baby_fuzzer"
  3. version = "0.1.0"
  4. authors = ["Your Name <you@example.com>"]
  5. edition = "2018"
  6. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
  7. [dependencies]

为了使用 LibAFl,我们必须在 [dependencies] 下增添其依赖 libafl = { path = "path/to/libafl/" }。 如果你愿意,你可以使用 crates.io 的 LibAFL 版本,在这种情况下,你必须使用 libafl = "*" 来获取最新的版本 (或者将其设置为当前版本) 。

由于我们要对Rust代码进行模糊处理,我们希望崩溃不会简单地导致程序退出,而是引发一个 abort,然后可以被模糊器捕获。 为此,我们在 profiles 中指定 panic = "abort"

除了这个设置之外,我们还为在发布模式下的编译添加了一些优化标志,最终的 Cargo.toml 应该类似于下面的样子:

  1. [package]
  2. name = "baby_fuzzer"
  3. version = "0.1.0"
  4. authors = ["Your Name <you@example.com>"]
  5. edition = "2018"
  6. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
  7. [dependencies]
  8. libafl = { path = "path/to/libafl/" }
  9. [profile.dev]
  10. panic = "abort"
  11. [profile.release]
  12. panic = "abort"
  13. lto = true
  14. codegen-units = 1
  15. opt-level = 3
  16. debug = true

被测试的函数

打开 src/main.rs,我们有一个空的 main 函数。 首先,我们创建一个我们想要模糊处理的闭包。它接受一个缓冲区作为输入,如果它以 abc 开头,就会引起崩溃:

  1. extern crate libafl;
  2. use libafl::inputs::{BytesInput, HasTargetBytes};
  3. let mut harness = |input: &BytesInput| {
  4. let target = input.target_bytes();
  5. let buf = target.as_slice();
  6. if buf.len() > 0 && buf[0] == 'a' as u8 {
  7. if buf.len() > 1 && buf[1] == 'b' as u8 {
  8. if buf.len() > 2 && buf[2] == 'c' as u8 {
  9. panic!("=)");
  10. }
  11. }
  12. }
  13. };
  14. // To test the panic:
  15. // let input = BytesInput::new("abc".as_bytes());
  16. // harness(&input);

生成和运行一些测试

基于 LibAFL 的模糊测试器使用的主要组件之一是状态,这是一个在模糊测试过程中演变的数据容器。 包括所有的状态,如输入的语料库,当前的rng状态,以及测试案例和运行的潜在 Metadata。 在我们的 main 中,我们创建了一个基本的 State 实例,如下所示。

  1. // create a State from scratch
  2. let mut state = StdState::new(
  3. // RNG
  4. StdRand::with_seed(current_nanos()),
  5. // Corpus that will be evolved, we keep it in memory for performance
  6. InMemoryCorpus::new(),
  7. // Corpus in which we store solutions (crashes in this example),
  8. // on disk so the user can get them after stopping the fuzzer
  9. OnDiskCorpus::new(PathBuf::from("./crashes")).unwrap(),
  10. (),
  11. );

它需要一个随机数发生器,这是模糊器状态的一部分,在这种情况下,我们使用默认的 StdRand,但你可以选择一个不同的。我们用当前的纳秒数作为种子。

作为第二个参数,它需要一个实现语料库特性的实例,本例中是 InMemoryCorpus。语料库是由模糊器演化出的测试案例的容器,在这种情况下,我们把它全部放在内存中。

我们将在后面讨论最后一个参数。第三个参数是另一个语料库,在这种情况下,用来存储被视为模糊器 “solutions” 的测试案例。对于我们的目的,solutions是触发崩溃的输入。在这种情况下,我们想把它存储在磁盘的 crashes 目录下,这样我们就可以检查它。

另一个必要的组件是 EventManager。它处理一些事件,如在模糊处理过程中向语料库添加测试案例。对于我们的目的,我们使用最简单的一个,它只是用一个 Monitor 实例向用户显示这些事件的信息。

  1. // The Monitor trait defines how the fuzzer stats are displayed to the user
  2. let mon = SimpleMonitor::new(|s| println!("{}", s));
  3. // The event manager handle the various events generated during the fuzzing loop
  4. // such as the notification of the addition of a new item to the corpus
  5. let mut mgr = SimpleEventManager::new(mon);

此外,我们还有 Fuzzer,一个包含一些改变状态的行动的实体。其中一个动作是使用 CorpusScheduler 为模糊器调度测试案例。 我们将其创建为 QueueCorpusScheduler,一个以先进先出方式向模糊器提供测试案例的调度器。

  1. // A queue policy to get testcasess from the corpus
  2. let scheduler = QueueCorpusScheduler::new();
  3. // A fuzzer with feedbacks and a corpus scheduler
  4. let mut fuzzer = StdFuzzer::new(scheduler, (), ());

最后,我们需要一个 Executor,它是负责运行我们被测试程序的实体。在这个例子中,我们想在进程中运行 harness 函数 (例如,不 fork 出一个子程序),因此我们使用 InProcessExecutor

  1. // Create the executor for an in-process function
  2. let mut executor = InProcessExecutor::new(
  3. &mut harness,
  4. (),
  5. &mut fuzzer,
  6. &mut state,
  7. &mut mgr,
  8. )
  9. .expect("Failed to create the Executor");

它需要一个 harnessstate 和 事件管理器 的引用。我们将在后面讨论第二个参数。 由于执行器期望约束函数返回一个 ExitKind 对象,我们在 harness 函数中添加 ExitKind::Ok

现在我们有4个主要的实体,可以运行我们的测试,但我们仍然不能生成测试案例。

为此,我们使用一个生成器,RandPrintablesGenerator,它可以生成一串可打印的字节。

  1. use libafl::generators::RandPrintablesGenerator;
  2. // Generator of printable bytearrays of max size 32
  3. let mut generator = RandPrintablesGenerator::new(32);
  4. // Generate 8 initial inputs
  5. state
  6. .generate_initial_inputs(&mut fuzzer, &mut executor, &mut generator, &mut mgr, 8)
  7. .expect("Failed to generate the initial corpus".into());

现在你可以在你的 main.rs 中添加必要的 use 指令,并编译模糊器。

  1. extern crate libafl;
  2. use std::path::PathBuf;
  3. use libafl::{
  4. bolts::{current_nanos, rands::StdRand},
  5. corpus::{InMemoryCorpus, OnDiskCorpus, QueueCorpusScheduler},
  6. events::SimpleEventManager,
  7. executors::{inprocess::InProcessExecutor, ExitKind},
  8. fuzzer::StdFuzzer,
  9. generators::RandPrintablesGenerator,
  10. inputs::{BytesInput, HasTargetBytes},
  11. monitors::SimpleMonitor,
  12. state::StdState,
  13. };

运行时,你应该看到类似的东西:

  1. $ cargo run
  2. Finished dev [unoptimized + debuginfo] target(s) in 0.04s
  3. Running `target/debug/baby_fuzzer`
  4. [LOG Debug]: Loaded 0 over 8 initial testcases

用反馈来进化语料库

现在你只是运行了8个随机生成的测试案例,但其中没有一个被存储在语料库中。如果你非常幸运,也许你偶然触发了崩溃,但你在 crashes 中没有看到任何保存的文件。

现在我们想把我们的简单模糊器变成一个基于反馈的模糊器,增加产生正确的输入来触发崩溃的机会。我们将根据达到崩溃所需的3个条件来实现一个简单的反馈。

要做到这一点,我们需要一种方法来跟踪一个条件是否被满足。为模糊器提供模糊运行属性信息的组件,即我们案例中的满足条件,是观察者。我们使用 StdMapObserver,这是一个默认的观察者,它使用一个 map 来跟踪覆盖的元素。在我们的模糊器中,每个条件都被映射到这种 map 的一个条目。

我们将这样的 map 表示为一个 static mut 变量。 由于我们不依赖于任何插桩引擎,我们必须手动跟踪 map 中被测试函数的满足条件。

  1. extern crate libafl;
  2. use libafl::{
  3. inputs::{BytesInput, HasTargetBytes},
  4. executors::ExitKind,
  5. };
  6. // Coverage map with explicit assignments due to the lack of instrumentation
  7. static mut SIGNALS: [u8; 16] = [0; 16];
  8. fn signals_set(idx: usize) {
  9. unsafe { SIGNALS[idx] = 1 };
  10. }
  11. // The closure that we want to fuzz
  12. let mut harness = |input: &BytesInput| {
  13. let target = input.target_bytes();
  14. let buf = target.as_slice();
  15. signals_set(0);
  16. if buf.len() > 0 && buf[0] == 'a' as u8 {
  17. signals_set(1);
  18. if buf.len() > 1 && buf[1] == 'b' as u8 {
  19. signals_set(2);
  20. if buf.len() > 2 && buf[2] == 'c' as u8 {
  21. panic!("=)");
  22. }
  23. }
  24. }
  25. ExitKind::Ok
  26. };

观察者可以直接从 SIGNALS map 中创建,方法如下:

  1. // Create an observation channel using the signals map
  2. let observer = StdMapObserver::new("signals", unsafe { &mut SIGNALS });

观察者通常被保存在相应的执行器中,因为它们所记录的信息只对一次运行有效。然后我们必须修改我们的 InProcessExecutor 创建,以包括观察者,如下所示:

  1. // Create the executor for an in-process function with just one observer
  2. let mut executor = InProcessExecutor::new(
  3. &mut harness,
  4. tuple_list!(observer),
  5. &mut fuzzer,
  6. &mut state,
  7. &mut mgr,
  8. )
  9. .expect("Failed to create the Executor".into());

既然模糊器可以观察到哪个条件被满足,我们就需要一种方法,根据这种观察来评定一个输入是否有趣 (即值得添加到语料库中) 。这里有一个反馈的概念,反馈是状态的一部分,它提供了一种将输入及其相应的执行评为有趣的方式,在观察者中寻找信息。反馈可以在一个所谓的 FeedbackState 实例中保持到目前为止所看到的信息的累积状态,在我们的例子中,它保持了在以前的运行中满足的条件的集合。

我们使用 MaxMapFeedback,这个反馈在 MapObserver 的 map 上实现了新奇的搜索。基本上,如果观察者的 map 中有一个值大于迄今为止为同一条目记录的最大值,它就会将该输入评为有趣的输入,并更新其状态。

反馈也被用来决定一个输入是否是一个 “solutions”。做到这一点的反馈被称为目标反馈,当它将一个输入评为有趣时,它不会被保存到语料库中,而是被保存到解决方案中,在我们的例子中被写在 crash 文件夹中。我们使用 CrashFeedback 来告诉模糊器,如果一个输入导致程序崩溃,那就是我们的解决方案。

我们需要更新我们的状态创建,包括反馈状态和模糊器,包括反馈和目标。

  1. extern crate libafl;
  2. use libafl::{
  3. bolts::{current_nanos, rands::StdRand, tuples::tuple_list},
  4. corpus::{InMemoryCorpus, OnDiskCorpus},
  5. feedbacks::{MapFeedbackState, MaxMapFeedback, CrashFeedback},
  6. fuzzer::StdFuzzer,
  7. state::StdState,
  8. observers::StdMapObserver,
  9. };
  10. // The state of the edges feedback.
  11. let feedback_state = MapFeedbackState::with_observer(&observer);
  12. // Feedback to rate the interestingness of an input
  13. let feedback = MaxMapFeedback::new(&feedback_state, &observer);
  14. // A feedback to choose if an input is a solution or not
  15. let objective = CrashFeedback::new();
  16. // create a State from scratch
  17. let mut state = StdState::new(
  18. // RNG
  19. StdRand::with_seed(current_nanos()),
  20. // Corpus that will be evolved, we keep it in memory for performance
  21. InMemoryCorpus::new(),
  22. // Corpus in which we store solutions (crashes in this example),
  23. // on disk so the user can get them after stopping the fuzzer
  24. OnDiskCorpus::new(PathBuf::from("./crashes")).unwrap(),
  25. // States of the feedbacks.
  26. // They are the data related to the feedbacks that you want to persist in the State.
  27. tuple_list!(feedback_state),
  28. );
  29. // ...
  30. // A fuzzer with feedbacks and a corpus scheduler
  31. let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);

实际的模糊处理

现在,在包括正确的 use 之后,我们可以运行这个程序了,但结果与之前的并没有什么不同,因为随机生成器并没有考虑到我们在语料库中保存的有趣内容。要做到这一点,我们需要插入一个 Mutator

LibAFL 的另一个核心组件是状态,它是对来自语料库的单个输入所做的动作。例如,MutationalStage 对输入进行突变,并多次执行。

作为最后一步,我们创建了一个突变状态,它使用了一个受 AFL 的 havoc 突变器启发的突变器。

  1. use libafl::{
  2. mutators::scheduled::{havoc_mutations, StdScheduledMutator},
  3. stages::mutational::StdMutationalStage,
  4. fuzzer::Fuzzer,
  5. };
  6. // ...
  7. // Setup a mutational stage with a basic bytes mutator
  8. let mutator = StdScheduledMutator::new(havoc_mutations());
  9. let mut stages = tuple_list!(StdMutationalStage::new(mutator));
  10. fuzzer
  11. .fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)
  12. .expect("Error in the fuzzing loop");

fuzz_loop 将使用调度器为每个迭代向模糊器请求一个测试案例,然后它将调用状态。

加入这段代码后,我们就有了一个合适的模糊器,它可以在一秒钟内找到让函数崩溃的输入。

  1. $ cargo run
  2. Compiling baby_fuzzer v0.1.0 (/home/andrea/Desktop/baby_fuzzer)
  3. Finished dev [unoptimized + debuginfo] target(s) in 1.56s
  4. Running `target/debug/baby_fuzzer`
  5. [New Testcase] clients: 1, corpus: 2, objectives: 0, executions: 1, exec/sec: 0
  6. [LOG Debug]: Loaded 1 over 8 initial testcases
  7. [New Testcase] clients: 1, corpus: 3, objectives: 0, executions: 804, exec/sec: 0
  8. [New Testcase] clients: 1, corpus: 4, objectives: 0, executions: 1408, exec/sec: 0
  9. thread 'main' panicked at '=)', src/main.rs:35:21
  10. note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  11. Crashed with SIGABRT
  12. Child crashed!
  13. [Objective] clients: 1, corpus: 4, objectives: 1, executions: 1408, exec/sec: 0
  14. Waiting for broker...
  15. Bye!

正如你所看到的,在崩溃信息之后,日志的 objectives 计数增加 1,你会在 crashes/ 中找到崩溃的输入。