-
Notifications
You must be signed in to change notification settings - Fork 2
Enabling more complex mutations
In a previous tutorial we have seen how we can create a minimal FTP fuzzer. However, this fuzzer is not really effective since its mutators only shuffle packets around and never really modify their contents. This tutorial explains how to modify the contents with the more complex mutators of butterfly.
Recall that our input is a vector of FTPCommand
s:
enum FTPCommand {
USER(BytesInput),
PASS(BytesInput),
CWD(BytesInput),
PASV,
TYPE(u8, u8),
LIST(Option<BytesInput>),
QUIT,
}
butterfly offers 4 mutators to modify an FTPCommand
:
Mutator | Description |
---|---|
PacketCrossoverInsertMutator | Inserts the contents of one packet into another of the same seed |
PacketCrossoverReplaceMutator | Replaces the contents of one packet with the contents of another in the same seed |
PacketSpliceMutator | Splices two packets in the same seed together |
PacketHavocMutator | Executes havoc mutators on a packet |
These mutators work on arbitrarily complex packet types, so let's see how we can implement them for FTPCommand
.
This mutator only works if our packet type implements HasCrossoverInsertMutation
. This adds a method mutate_crossover_insert()
to our packet type that is called by the mutator. The concrete implementation of the crossover-insert mutation is left to the user.
There exists a default implementation of HasCrossoverInsertMutation
for BytesInput
which does the heavy lifting of copying the bytes. Often times custom mutate_crossover_insert()
implementations just act as a proxy to BytesInput::mutate_crossover_insert()
.
This can be seen in the following example, where we implement HasCrossoverInsertMutation
for FTPCommand
:
impl<S> HasCrossoverInsertMutation<S> for FTPCommand
where
S: HasRand + HasMaxSize,
{
fn mutate_crossover_insert(&mut self, state: &mut S, other: &Self, stage_idx: i32) -> Result<MutationResult, Error> {
match self {
// Crossover path names
FTPCommand::CWD(dir) |
FTPCommand::LIST(Some(dir)) => {
match other {
FTPCommand::CWD(other_dir) |
FTPCommand::LIST(Some(other_dir)) => {
return dir.mutate_crossover_insert(state, other_dir, stage_idx);
},
}
},
// ...
}
Ok(MutationResult::Skipped)
}
}
Note how the entire logic of FTPCommand::mutate_crossover_insert()
only consists of filtering which commands can be combined and not of byte-copying procedures.
Basically the same as PacketCrossoverInsertMutator
except that it does not concatenate but replace the bytes.
Implement the trait HasCrossoverReplaceMutation
and define the function mutate_crossover_replace()
like above, proxying BytesInput::mutate_crossover_replace()
.
This mutator splices two packets of the same seed together at a random midpoint. The implementation is pretty similar to the mutators above except that we have to implement the trait HasSpliceMutation
and will use BytesInput::mutate_splice()
.
impl<S> HasSpliceMutation<S> for FTPCommand
where
S: HasRand + HasMaxSize,
{
fn mutate_splice(&mut self, state: &mut S, other: &Self, stage_idx: i32) -> Result<MutationResult, Error> {
match self {
// Splice pathnames together
FTPCommand::CWD(dir) |
FTPCommand::LIST(Some(dir)) => {
match other {
FTPCommand::CWD(other_dir) |
FTPCommand::LIST(Some(other_dir)) => {
return dir.mutate_splice(state, other_dir, stage_idx);
},
}
},
// ...
}
Ok(MutationResult::Skipped)
}
}
This mutator enables us to apply libafls havoc mutators to our packets.
We simply need to implement HasHavocMutation
like this:
impl<MT, S> HasHavocMutation<MT, S> for FTPCommand
where
MT: MutatorsTuple<BytesInput, S>,
S: HasRand + HasMaxSize,
{
fn mutate_havoc(&mut self, state: &mut S, mutations: &mut MT, mutation: usize, stage_idx: i32) -> Result<MutationResult, Error> {
match self {
// Mutate username
FTPCommand::USER(name) => name.mutate_havoc(state, mutations, mutation, stage_idx),
// Mutate password
FTPCommand::PASS(password) => password.mutate_havoc(state, mutations, mutation, stage_idx),
// and so on ...
}
}
}
Note how FTPCommand::mutate_havoc
also is just a proxy for BytesInput::mutate_havoc
as in the mutators above.
Then we can create a PacketHavocMutator
:
let mutator = PacketHavocMutator::new(supported_havoc_mutations());
It expects a tuple of havoc mutators from libafl but we cannot simply pass it havoc_mutations()
because some of the mutators operate on two inputs that implement HasBytesVec
which is incompatible with packet-based inputs. Thus we pass it supported_havoc_mutations()
, which contains all compatible mutators.
Now that our packet type implements all necessary traits we can construct our mutator:
let mutator = PacketMutationScheduler::new(
tuple_list!(
// Old mutators from previous tutorial
PacketReorderMutator::new(),
PacketDeleteMutator::new(4),
PacketDuplicateMutator::new(16),
// New mutators
PacketCrossoverInsertMutator::new(),
PacketCrossoverReplaceMutator::new(),
PacketSpliceMutator::new(4),
PacketHavocMutator::new(supported_havoc_mutations())
)
);
libafls mutation schedulers are not compatible with packet-based inputs so we use butterflys own mutation scheduler PacketMutationScheduler
, which always mutates only one packet to reach deeper program states.
You can find the full source code for this tutorial in the examples folder.