一个简单的LibAFL模糊器

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

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

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

警告

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

创建一个项目

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

$ cargo new baby_fuzzer
$ cd baby_fuzzer

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

[package]
name = "baby_fuzzer"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

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

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

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

[package]
name = "baby_fuzzer"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
libafl = { path = "path/to/libafl/" }

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
lto = true
codegen-units = 1
opt-level = 3
debug = true

被测试的函数

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


#![allow(unused)]
fn main() {
extern crate libafl;
use libafl::inputs::{BytesInput, HasTargetBytes};

let mut harness = |input: &BytesInput| {
    let target = input.target_bytes();
    let buf = target.as_slice();
    if buf.len() > 0 && buf[0] == 'a' as u8 {
        if buf.len() > 1 && buf[1] == 'b' as u8 {
            if buf.len() > 2 && buf[2] == 'c' as u8 {
                panic!("=)");
            }
        }
    }
};
// To test the panic:
// let input = BytesInput::new("abc".as_bytes());
// harness(&input);
}

生成和运行一些测试

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

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

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

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

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

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

// The Monitor trait defines how the fuzzer stats are displayed to the user
let mon = SimpleMonitor::new(|s| println!("{}", s));

// The event manager handle the various events generated during the fuzzing loop
// such as the notification of the addition of a new item to the corpus
let mut mgr = SimpleEventManager::new(mon);

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

// A queue policy to get testcasess from the corpus
let scheduler = QueueCorpusScheduler::new();

// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, (), ());

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

// Create the executor for an in-process function
let mut executor = InProcessExecutor::new(
    &mut harness,
    (),
    &mut fuzzer,
    &mut state,
    &mut mgr,
)
.expect("Failed to create the Executor");

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

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

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

use libafl::generators::RandPrintablesGenerator;

// Generator of printable bytearrays of max size 32
let mut generator = RandPrintablesGenerator::new(32);

// Generate 8 initial inputs
state
    .generate_initial_inputs(&mut fuzzer, &mut executor, &mut generator, &mut mgr, 8)
    .expect("Failed to generate the initial corpus".into());

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


#![allow(unused)]
fn main() {
extern crate libafl;

use std::path::PathBuf;
use libafl::{
    bolts::{current_nanos, rands::StdRand},
    corpus::{InMemoryCorpus, OnDiskCorpus, QueueCorpusScheduler},
    events::SimpleEventManager,
    executors::{inprocess::InProcessExecutor, ExitKind},
    fuzzer::StdFuzzer,
    generators::RandPrintablesGenerator,
    inputs::{BytesInput, HasTargetBytes},
    monitors::SimpleMonitor,
    state::StdState,
};
}

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

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

用反馈来进化语料库

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

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

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

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


#![allow(unused)]
fn main() {
extern crate libafl;
use libafl::{
    inputs::{BytesInput, HasTargetBytes},
    executors::ExitKind,
};

// Coverage map with explicit assignments due to the lack of instrumentation
static mut SIGNALS: [u8; 16] = [0; 16];

fn signals_set(idx: usize) {
    unsafe { SIGNALS[idx] = 1 };
}

// The closure that we want to fuzz
let mut harness = |input: &BytesInput| {
    let target = input.target_bytes();
    let buf = target.as_slice();
    signals_set(0);
    if buf.len() > 0 && buf[0] == 'a' as u8 {
        signals_set(1);
        if buf.len() > 1 && buf[1] == 'b' as u8 {
            signals_set(2);
            if buf.len() > 2 && buf[2] == 'c' as u8 {
                panic!("=)");
            }
        }
    }
    ExitKind::Ok
};
}

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

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

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

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

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

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

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

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

extern crate libafl;
use libafl::{
    bolts::{current_nanos, rands::StdRand, tuples::tuple_list},
    corpus::{InMemoryCorpus, OnDiskCorpus},
    feedbacks::{MapFeedbackState, MaxMapFeedback, CrashFeedback},
    fuzzer::StdFuzzer,
    state::StdState,
    observers::StdMapObserver,
};

// The state of the edges feedback.
let feedback_state = MapFeedbackState::with_observer(&observer);

// Feedback to rate the interestingness of an input
let feedback = MaxMapFeedback::new(&feedback_state, &observer);

// A feedback to choose if an input is a solution or not
let objective = CrashFeedback::new();

// create a State from scratch
let mut state = StdState::new(
    // RNG
    StdRand::with_seed(current_nanos()),
    // Corpus that will be evolved, we keep it in memory for performance
    InMemoryCorpus::new(),
    // Corpus in which we store solutions (crashes in this example),
    // on disk so the user can get them after stopping the fuzzer
    OnDiskCorpus::new(PathBuf::from("./crashes")).unwrap(),
    // States of the feedbacks.
    // They are the data related to the feedbacks that you want to persist in the State.
    tuple_list!(feedback_state),
);

// ...

// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);

实际的模糊处理

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

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

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

use libafl::{
    mutators::scheduled::{havoc_mutations, StdScheduledMutator},
    stages::mutational::StdMutationalStage,
    fuzzer::Fuzzer,
};

// ...

// Setup a mutational stage with a basic bytes mutator
let mutator = StdScheduledMutator::new(havoc_mutations());
let mut stages = tuple_list!(StdMutationalStage::new(mutator));

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

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

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

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

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