-
Notifications
You must be signed in to change notification settings - Fork 3
First steps: Creating a minimal FTP fuzzer
This page introduces you to all important components in butterfly
by building a minimal, blackbox FTP fuzzer that we could already use to fuzz some targets.
In order to create working fuzzers with butterfly
we have to create two components from scratch:
- An Input type
- An Executor
Inputs must contain a vector of packets. The type of a packet can be anything. If you want a vector
of plain bytearrays use BytesInput
as the packet type. Otherwise you can use enums or structs to encode more
complex information.
In our case a packet is an FTP command that we send to the server, so we use the following enum:
// For simplicity only a few commands are listed
enum FTPCommand {
USER(BytesInput),
PASS(BytesInput),
CWD(BytesInput),
PASV,
TYPE(u8, u8),
LIST(Option<BytesInput>),
QUIT,
}
Note that we use BytesInput
wherever we expect raw data. This enables us to directly call LibAFLs mutators on these elements.
Now we can construct our input type:
struct FTPInput {
packets: Vec<FTPCommand>
}
But this struct alone is worthless. We have to implement a few traits to make it compatible with LibAFL and butterfly
.
This gives other components access to the packet vector.
impl HasPackets<FTPCommand> for FTPInput {
fn packets(&self) -> &[FTPCommand] {
&self.packets
}
fn packets_mut(&mut self) -> &mut Vec<FTPCommand> {
&mut self.packets
}
}
This tells other components how many packets an input has.
impl HasLen for FTPInput {
fn len(&self) -> usize {
self.packets.len()
}
}
This tells LibAFL that the struct is a fuzz input.
impl Input for FTPInput {
fn generate_name(&self, idx: usize) -> String {
// generally bad idea but for this example ok
format!("ftpinput-{}", idx)
}
}
And that's it for the input!
Let's move to the second part.
The sole purpose of stateful fuzzing is to build a state-graph of the target so we need to extract state information from the target. Since the standard executors from LibAFL don't do this we have to create custom ones.
One possible way to extract state information is to analyze the responses of the server. For FTP that means that we can use the status code (first few bytes) in a response.
We start by defining the struct:
struct FTPExecutor<OT, S>
where
OT: ObserversTuple<FTPInput, S>,
{
// needed by LibAFL
observers: OT,
}
Then we add a new()
method to the executor:
impl<OT, S> FTPExecutor<OT, S>
where
OT: ObserversTuple<FTPInput, S>,
{
fn new(observers: OT) -> Self {
Self {
observers,
}
}
}
And again, this struct alone is worthless. We need to implement some traits to make it compatible with LibAFL.
impl<OT, S> Debug for FTPExecutor<OT, S>
where
OT: ObserversTuple<FTPInput, S>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "FTPExecutor {{ }}")
}
}
Since the executor takes ownership over the observers, this gives other components access to the observers.
impl<OT, S> HasObservers<FTPInput, OT, S> for FTPExecutor<OT, S>
where
OT: ObserversTuple<FTPInput, S>,
{
fn observers(&self) -> &OT {
&self.observers
}
fn observers_mut(&mut self) -> &mut OT {
&mut self.observers
}
}
This tells LibAFL that this struct can act as an executor and primarily provides the run_target()
method.
In run_target()
we
- Connect to the server
- Send the commands
- Receive the replies and parse the status codes
- Somehow tell butterfly about the status codes. We will discuss that shortly.
impl<OT, S, EM, Z> Executor<EM, FTPInput, S, Z> for FTPExecutor<OT, S>
where
OT: ObserversTuple<FTPInput, S>,
{
fn run_target(&mut self, _fuzzer: &mut Z, _state: &mut S, _mgr: &mut EM, input: &FTPInput) -> Result<ExitKind, Error> {
// explained later
}
}
And that's it with the custom components! Finally we can get to the main()
function.
In main()
we just plug all our components together like this:
fn main() {
// Components from butterfly
let monitor = StateMonitor::new();
let state_observer = StateObserver::<u32>::new("state");
let mut feedback = StateFeedback::new(&state_observer);
let mutator = PacketMutationScheduler::new(
tuple_list!(
PacketReorderMutator::new(),
PacketDeleteMutator::new(4),
PacketDuplicateMutator::new(16)
)
);
// Components from LibAFL
let mut mgr = SimpleEventManager::new(monitor);
let mut objective = CrashFeedback::new();
let mut state = StdState::new(
StdRand::with_seed(0),
InMemoryCorpus::new(),
InMemoryCorpus::new(),
&mut feedback,
&mut objective
).unwrap();
let scheduler = QueueScheduler::new();
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
let mut stages = tuple_list!(
StdMutationalStage::new(mutator)
);
// Our custom executor
let mut executor = FTPExecutor::new(tuple_list!(state_observer));
// Start fuzzing
fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr).unwrap();
}
Since this is the butterfly wiki and not the LibAFL wiki we will focus only on the first block with the butterfly components.
let monitor = StateMonitor::new();
This creates a monitor similar to SimpleMonitor
that prints fuzzing stats to stdout. In addition to the usual information it also includes information about the shape of the state graph, namely the number of vertices and edges.
let state_observer = StateObserver::<u32>::new("state");
The most important component in stateful fuzzing: StateObserver
is responsible for building the state-graph. The states can be of any type that implements Ord
. Most prominent are integer types or outputs of hash functions [u8; N]
. Make sure to always give the concrete type when instantiating a StateObserver
because the rust compiler may have trouble infering it automatically. In our example states are FTP response codes that are represented by u32
s.
let mut feedback = StateFeedback::new(&state_observer);
The StateFeedback
determines whether any new states or state transitions have been seen in the last execution.
let mutator = PacketMutationScheduler::new(...);
PacketMutationScheduler
is a simple mutation scheduler that runs exactly one mutator in a tuple of mutators. The reason for that is that the mutators may implement a scheduling algorithm of their own.
tuple_list!(
PacketReorderMutator::new(),
PacketDeleteMutator::new(4),
PacketDuplicateMutator::new(16)
)
These are butterflys most basic mutators. They simply shuffle the packets around without modifying their contents. To use mutators that actually modify the contents see Enabling more complex mutations.
Experienced LibAFL users may have noticed that something is missing in the main()
function. We are never loading inputs from a corpus into the queue.
butterfly supports loading pcaps, which is discussed in Loading inputs from pcap files but for now we want to keep it as simple as possible, so we generate one initial input from scratch and load it manually:
let example_input = FTPInput {
packets: vec![
FTPCommand::USER(BytesInput::new("anonymous".as_bytes().to_vec())),
FTPCommand::PASS(BytesInput::new("anonymous".as_bytes().to_vec())),
FTPCommand::CWD(BytesInput::new("/".as_bytes().to_vec())),
FTPCommand::PASV,
FTPCommand::TYPE(b'A', b'N'),
FTPCommand::LIST(None),
FTPCommand::QUIT,
],
};
fuzzer.evaluate_input(&mut state, &mut executor, &mut mgr, example_input).unwrap();
The final problem that we have to solve is how to communicate state changes of the target to butterfly.
This has to be done in the run_target()
method of an executor.
In our case run_target()
roughly looks like this:
fn run_target(&mut self, _fuzzer: &mut Z, _state: &mut S, _mgr: &mut EM, input: &FTPInput) -> Result<ExitKind, Error> {
// buffer for response
let mut buf = vec![0; 1024];
// connect to the server
let cmd_conn = TcpStream::connect("127.0.0.1:2121");
for packet in input.packets() {
// send packet over cmd_conn ...
match packet {
// ...
}
// Receive the response
cmd_conn.read(&mut buf);
let status_code = extract_status_code(&buf);
// Tell butterfly about the status code
let state_observer: &mut StateObserver<u32> = self.observers.match_name_mut("state").unwrap();
state_observer.record(&status_code);
}
}
Only the last part isn't pseudo code: First we get the StateObserver
from the executors observer list with match_name_mut
. Note how in the harness we created a StateObserver
with name state
.
Then we call the function StateObserver::record()
that tells the observer what state the target currently is in.
And this is all we need to build a fuzzer.
In this tutorial we have seen how we can build a simple blackbox FTP fuzzer from scratch by creating two types, FTPInput
and FTPExecutor
,
and plugging them together with butterfly and LibAFL components.
The full source code for our fuzzer can be found in the examples folder.