Skip to content

Commit 0ac407a

Browse files
committed
adapted to new feedback interface;;
2 parents 3fb4626 + 33e918f commit 0ac407a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+3266
-1542
lines changed

.github/FUNDING.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# These are supported funding model platforms
2+
3+
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4+
patreon: # Replace with a single Patreon username
5+
open_collective: AFLplusplusEU
6+
ko_fi: # Replace with a single Ko-fi username
7+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9+
liberapay: # Replace with a single Liberapay username
10+
issuehunt: # Replace with a single IssueHunt username
11+
otechie: # Replace with a single Otechie username
12+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

.github/workflows/build_and_test.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,9 @@ jobs:
5757
- name: Build Docs
5858
run: cargo doc
5959
- name: Test Docs
60-
run: cargo test --doc
60+
run: cargo test --all-features --doc
6161
- name: Run clippy
62-
uses: actions-rs/cargo@v1
63-
with:
64-
command: clippy
65-
args: --all
62+
run: ./clippy.sh
6663
windows:
6764
runs-on: windows-latest
6865
steps:

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ Check the [TODO.md](./TODO.md) file for features that we plan to support.
8787

8888
For bugs, feel free to open issues or contact us directly. Thank you for your support. <3
8989

90+
Even though we will gladly assist you in finishing up your PR, try to
91+
- use *stable* rust
92+
- run `cargo fmt` on your code before pushing
93+
- check the output of `cargo clippy --all` or `./clippy.sh`
94+
- run `cargo build --no-default-features` to check for `no_std` compatibility (and possibly add `#[cfg(feature = "std")]`) to hide parts of your code.
95+
96+
Some of the parts in this list may be hard, don't be afraid to open a PR if you cannot fix them by yourself, so we can help.
97+
9098
#### License
9199

92100
<sup>

TODO.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
# TODOs
22

3-
- [ ] Conditional composition of feedbacks (issue #24)
4-
- [ ] Other objectives examples (e.g. execution of a given program point)
53
- [ ] Objective-Specific Corpuses (named per objective)
64
- [ ] Good documentation
75
- [ ] LLMP compression
86
- [ ] AFL-Style Forkserver Executor
9-
- [ ] Restart Count in Fuzzing Loop
107
- [ ] LAIN / structured fuzzing example
118
- [ ] More informative outpus, deeper introspection (stats, what mutation did x, etc.)
129
- [ ] Timeout handling for llmp clients (no ping for n seconds -> treat as disconnected)
13-
- [ ] LLMP Cross Machine Link (2 brokers connected via TCP)
1410
- [ ] "Launcher" example that spawns broker + n clients
1511
- [ ] Heap for signal handling (bumpallo or llmp directly?)
1612
- [ ] Frida support for Windows
1713
- [ ] QEMU based instrumentation
1814
- [ ] AFL++ LLVM passes in libafl_cc
15+
- [x] LLMP Cross Machine Link (2 brokers connected via TCP)
16+
- [x] Conditional composition of feedbacks (issue #24)
17+
- [x] Other objectives examples (e.g. execution of a given program point)
18+
- [x] Restart Count in Fuzzing Loop
1919
- [x] Minset corpus scheduler
2020
- [x] Win32 shared mem and crash handler to have Windows in-process executor
2121
- [x] Other feedbacks examples (e.g. maximize allocations to spot OOMs)

clippy.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
#!/bin/sh
22
# Clippy checks
33
cargo clean -p libafl
4-
RUST_BACKTRACE=full cargo clippy --all -- \
4+
RUST_BACKTRACE=full cargo clippy --all --all-features --tests -- \
55
-D clippy::pedantic \
66
-W clippy::cast_sign_loss \
77
-W clippy::similar-names \
88
-W clippy::cast_ptr_alignment \
99
-W clippy::cast_possible_wrap \
1010
-W clippy::unused_self \
1111
-W clippy::too_many_lines \
12-
-A missing-docs \
13-
-A clippy::doc_markdown \
14-
-A clippy::must-use-candidate \
12+
-W clippy::option_if_let_else \
13+
-W clippy::must-use-candidate \
14+
-W clippy::if-not-else \
15+
-W clippy::doc-markdown \
1516
-A clippy::type_repetition_in_bounds \
1617
-A clippy::missing-errors-doc \
1718
-A clippy::cast-possible-truncation \
@@ -22,4 +23,3 @@ RUST_BACKTRACE=full cargo clippy --all -- \
2223
-A clippy::unseparated-literal-suffix \
2324
-A clippy::module-name-repetitions \
2425
-A clippy::unreadable-literal \
25-
-A clippy::if-not-else \

docs/src/baby_fuzzer.md

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,300 @@
11
# Baby Fuzzer
2+
3+
This chapter will teach you how to create a naive fuzzer using the LibAFL API, you will learn about basic entities such as `State`, `Observer`, and `Executor`.
4+
The following chapters will discuss in detail the components of LibAFL, while here we will just scratch the fundamentals.
5+
6+
We are going to fuzz a simple Rust function that panics under a condition. The fuzzer will be single-threaded and will stop after the crash like libFuzzer does normally.
7+
8+
You can find a complete version of this tutorial as an example fuzzer in [`fuzzers/baby_fuzzer`](https://github.com/AFLplusplus/LibAFL/tree/main/fuzzers/baby_fuzzer).
9+
10+
## Creating a project
11+
12+
We use cargo to create a new Rust project with LibAFL as a dependency.
13+
14+
```sh
15+
$ cargo new baby_fuzzer
16+
$ cd baby_fuzzer
17+
```
18+
19+
The generated _Cargo.toml_ looks like the following:
20+
21+
```toml
22+
[package]
23+
name = "baby_fuzzer"
24+
version = "0.1.0"
25+
authors = ["Your Name <you@example.com>"]
26+
edition = "2018"
27+
28+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
29+
30+
[dependencies]
31+
```
32+
33+
In order to use LibAFl we must add it as dependency adding `libafl = { path = "path/to/libafl/" }` under `[dependencies]`.
34+
You can use the LibAFL version from crates.io if you want, in this case, you have to use `libafl = "*"` to get the latest version.
35+
36+
As we are going to fuzz Rust code, we want that a panic does not simply cause the program exit, but an abort that can be caught by the fuzzer.
37+
To do that, we specify `panic = "abort"` in the [profiles](https://doc.rust-lang.org/cargo/reference/profiles.html).
38+
39+
Alongside this setting, we add some optimization flags for the compile when building in release mode.
40+
41+
The final _Cargo.toml_ should look similar to the following:
42+
43+
44+
```toml
45+
[package]
46+
name = "baby_fuzzer"
47+
version = "0.1.0"
48+
authors = ["Your Name <you@example.com>"]
49+
edition = "2018"
50+
51+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
52+
53+
[dependencies]
54+
libafl = { path = "path/to/libafl/" }
55+
56+
[profile.dev]
57+
panic = "abort"
58+
59+
[profile.release]
60+
panic = "abort"
61+
lto = true
62+
codegen-units = 1
63+
opt-level = 3
64+
debug = true
65+
```
66+
67+
## The function under test
68+
69+
Opening `src/main.rs` we have an empty main function.
70+
To start, we create the closure that we want to fuzz. It takes a buffer as input and panics if it starts with "abc".
71+
72+
```rust
73+
let mut harness = |buf: &[u8]| {
74+
if buf.len() > 0 && buf[0] == 'a' as u8 {
75+
if buf.len() > 1 && buf[1] == 'b' as u8 {
76+
if buf.len() > 2 && buf[2] == 'c' as u8 {
77+
panic!("=)");
78+
}
79+
}
80+
}
81+
};
82+
// To test the panic:
83+
// let input = "abc".as_bytes();
84+
// harness(&input);
85+
```
86+
87+
## Generating and running some tests
88+
89+
One of the main components that a LibAFL-based fuzzer uses is the State, a container of the data that is evolved during the fuzzing process, such as the Corpus of inputs.
90+
In our main so we create a basic State instance like the following:
91+
92+
```rust
93+
// create a State from scratch
94+
let mut state = State::new(
95+
// RNG
96+
StdRand::with_seed(current_nanos()),
97+
// Corpus that will be evolved, we keep it in memory for performance
98+
InMemoryCorpus::new(),
99+
(),
100+
// Corpus in which we store solutions (crashes in this example),
101+
// on disk so the user can get them after stopping the fuzzer
102+
OnDiskCorpus::new(PathBuf::from("./crashes")).unwrap(),
103+
(),
104+
);
105+
```
106+
107+
It takes a random number generator, that is part of the fuzzer state, in this case, we use the default one `StdRand` but you can choose a different one. We seed it with the current nanoseconds.
108+
109+
As the second parameter, it takes an instance of something implementing the Corpus trait, InMemoryCorpus in this case. The corpus is the container of the testcases evolved by the fuzzer, in this case, we keep it all in memory.
110+
111+
We will discuss later the third and fifth parameters. The fourth is another corpus, in this case, to store the testcases that are considered as "solutions" for the fuzzer. For our purpose, the solution is the input that triggers the panic. In this case, we want to store it to disk under the `crashes` directory so we can inspect it.
112+
113+
Another required component is the EventManager. It handles some events such as the addition of a testcase to the corpus during the fuzzing process. For our purpose, we use the simplest one that just displays the information about these events to the user using a Stats instance.
114+
115+
```rust
116+
// The Stats trait define how the fuzzer stats are reported to the user
117+
let stats = SimpleStats::new(|s| println!("{}", s));
118+
119+
// The event manager handle the various events generated during the fuzzing loop
120+
// such as the notification of the addition of a new item to the corpus
121+
let mut mgr = SimpleEventManager::new(stats);
122+
```
123+
124+
Last but not least, we need an Executor that is the entity responsible to run our program under test. In this example, we want to run the harness function in process, and so we use the InProcessExecutor.
125+
126+
```rust
127+
// Create the executor for an in-process function
128+
let mut executor =
129+
InProcessExecutor::new(&mut harness, (), &mut state, &mut mgr)
130+
.expect("Failed to create the Executor".into());
131+
```
132+
133+
It takes a reference to the harness, the state, and the event manager. We will discuss the second parameter later.
134+
As the executor expects that the harness returns an ExitKind object, we add `ExitKind::Ok` to our harness function.
135+
136+
Now we have the 3 major entities ready for running our tests, but we still cannot generate testcases.
137+
138+
For this purpose, we use a Generator, RandPrintablesGenerator that generates a string of printable bytes.
139+
The State's method used to generate and run tests needs a scheduling policy for the corpus. We create it as QueueCorpusScheduler, a scheduler that serves testcases to the fuzzer in a FIFO fashion.
140+
141+
```rust
142+
// A queue policy to get testcasess from the corpus
143+
let scheduler = QueueCorpusScheduler::new();
144+
145+
// Generator of printable bytearrays of max size 32
146+
let mut generator = RandPrintablesGenerator::new(32);
147+
148+
// Generate 8 initial inputs
149+
state
150+
.generate_initial_inputs(&mut executor, &mut generator, &mut mgr, &scheduler, 8)
151+
.expect("Failed to generate the initial corpus".into());
152+
```
153+
154+
Now you can prepend the following `use` directives to your main.rs and compile it.
155+
156+
```rust
157+
use std::path::PathBuf;
158+
use libafl::{
159+
corpus::{InMemoryCorpus, OnDiskCorpus, QueueCorpusScheduler},
160+
events::SimpleEventManager,
161+
executors::{inprocess::InProcessExecutor, ExitKind},
162+
generators::RandPrintablesGenerator,
163+
state::State,
164+
stats::SimpleStats,
165+
utils::{current_nanos, StdRand},
166+
};
167+
```
168+
169+
When running, you should see something similar to:
170+
171+
```sh
172+
$ cargo run
173+
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
174+
Running `target/debug/baby_fuzzer`
175+
[LOG Debug]: Loaded 0 over 8 initial testcases
176+
```
177+
178+
## Evolving the corpus with feedbacks
179+
180+
Now you simply ran 8 randomly generated testcases but none of them has been stored in the corpus. If you are very lucky, maybe you triggered the panic by chance but you don't see any saved file in `crashes`.
181+
182+
Now we want to turn our simple fuzzer into a feedback-based one and increase the chance to generate the right input to trigger the panic. We are going to implement a simple feedback based on the 3 conditions that are needed to reach the panic.
183+
184+
To do that, we need a way to keep track of if a condition is satisfied. The component that feeds the fuzzer with information about properties of a fuzzing run, the satisfied conditions in our case, is the Observer. We use the StdMapObserver, the default observer that uses a map to keep track of covered elements. In our fuzzer, each condition is mapped to an entry of such map.
185+
186+
We represent such map as a `static mut` variable:
187+
188+
```rust
189+
// Coverage map with explicit assignments due to the lack of instrumentation
190+
static mut SIGNALS: [u8; 16] = [0; 16];
191+
192+
fn signals_set(idx: usize) {
193+
unsafe { SIGNALS[idx] = 1 };
194+
}
195+
```
196+
197+
As we don't rely on any instrumentation engine, we have to manually track the satisfied conditions in a map modyfing our tested function:
198+
199+
```rust
200+
// The closure that we want to fuzz
201+
let mut harness = |buf: &[u8]| {
202+
signals_set(0);
203+
if buf.len() > 0 && buf[0] == 'a' as u8 {
204+
signals_set(1);
205+
if buf.len() > 1 && buf[1] == 'b' as u8 {
206+
signals_set(2);
207+
if buf.len() > 2 && buf[2] == 'c' as u8 {
208+
panic!("=)");
209+
}
210+
}
211+
}
212+
ExitKind::Ok
213+
};
214+
```
215+
216+
The observer can be created directly from the `SIGNALS` map, in the following way:
217+
218+
```rust
219+
// Create an observation channel using the signals map
220+
let observer = StdMapObserver::new("signals", unsafe { &mut SIGNALS });
221+
```
222+
223+
The observers are usually kept in the corresponding executor as they keep track of information that is valid for just one run. We have then to modify our InProcessExecutor creation to include the observer as follows:
224+
225+
```rust
226+
// Create the executor for an in-process function with just one observer
227+
let mut executor =
228+
InProcessExecutor::new(&mut harness, tuple_list!(observer), &mut state, &mut mgr)
229+
.expect("Failed to create the Executor".into());
230+
```
231+
232+
Now that the fuzzer can observe which condition is satisfied, we need a way to rate an input as interesting (i.e. worth of addition to the corpus) based on this observation. Here comes the notion of Feedback. The Feedback is part of the State and provides a way to rate input and its corresponding execution as interesting looking for the information in the observers. Feedbacks can maintain a cumulative state of the information seen so far, in our case it maintains the set of conditions satisfied in the previous runs.
233+
234+
We use MaxMapFeedback, a feedback that implements a novelty search over the map of the MapObserver. Basically, if there is a value in the observer's map that is greater than the maximum value registered so far for the same entry, it rates the input as interesting and updates its state.
235+
236+
Feedbacks are used also to decide if an input is a "solution". The feedback that does that is called the Objective Feedback and when it rates an input as interested it is not saved to the corpus but to the solutions, written in the `crashes` folder in our case. We use the CrashFeedback to tell the fuzzer that if an input causes the program to crash it is a solution for us.
237+
238+
We need to update our State creation including these feedbacks:
239+
240+
```rust
241+
// create a State from scratch
242+
let mut state = State::new(
243+
// RNG
244+
StdRand::with_seed(current_nanos()),
245+
// Corpus that will be evolved, we keep it in memory for performance
246+
InMemoryCorpus::new(),
247+
// Feedback to rate the interestingness of an input
248+
MaxMapFeedback::new_with_observer(&observer),
249+
// Corpus in which we store solutions (crashes in this example),
250+
// on disk so the user can get them after stopping the fuzzer
251+
OnDiskCorpus::new(PathBuf::from("./crashes")).unwrap(),
252+
// Feedbacks to recognize an input as solution
253+
CrashFeedback::new(),
254+
);
255+
```
256+
257+
## The actual fuzzing
258+
259+
Now, after including the correct `use`, we can run the program, but the outcome is not so different from the previous one as the random generator does not take into account what we save as interesting in the corpus. To do that, we need to plug a Mutator.
260+
261+
Another central component of LibAFL is the Fuzzer, an entity that holds a set of Stages that are actions done on individual inputs taken from the corpus. The MutationalStage mutates the input and executes it several times for instance.
262+
263+
As the last step, to have a proper fuzzer, we create a Fuzzer with a single MutationalStage that uses a mutator inspired by the havoc mutator of AFL.
264+
265+
```rust
266+
// Setup a basic mutator with a mutational stage
267+
let mutator = StdScheduledMutator::new(havoc_mutations());
268+
let stage = StdMutationalStage::new(mutator);
269+
270+
// A fuzzer with just one stage
271+
let mut fuzzer = StdFuzzer::new(tuple_list!(stage));
272+
273+
fuzzer
274+
.fuzz_loop(&mut state, &mut executor, &mut mgr, &scheduler)
275+
.expect("Error in the fuzzing loop".into());
276+
```
277+
278+
`fuzz_loop` will request a testcase for each iteration to the fuzzer using the scheduler and then it will invoke the stage.
279+
280+
After adding this code, we have a proper fuzzer, that can run a find the input that panics the function in less than a second.
281+
282+
```
283+
$ cargo run
284+
Compiling baby_fuzzer v0.1.0 (/home/andrea/Desktop/baby_fuzzer)
285+
Finished dev [unoptimized + debuginfo] target(s) in 1.56s
286+
Running `target/debug/baby_fuzzer`
287+
[New Testcase] clients: 1, corpus: 2, objectives: 0, executions: 1, exec/sec: 0
288+
[LOG Debug]: Loaded 1 over 8 initial testcases
289+
[New Testcase] clients: 1, corpus: 3, objectives: 0, executions: 804, exec/sec: 0
290+
[New Testcase] clients: 1, corpus: 4, objectives: 0, executions: 1408, exec/sec: 0
291+
thread 'main' panicked at '=)', src/main.rs:35:21
292+
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
293+
Crashed with SIGABRT
294+
Child crashed!
295+
[Objective] clients: 1, corpus: 4, objectives: 1, executions: 1408, exec/sec: 0
296+
Waiting for broker...
297+
Bye!
298+
```
299+
300+
As you can see, after the panic message, the `objectives` count of the log increased by one and you will find the crashing input in `crashes/id_0`.

0 commit comments

Comments
 (0)