Skip to content

Enabling more complex mutations

Patrick D edited this page Jul 16, 2022 · 1 revision

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.

Mutators

Recall that our input is a vector of FTPCommands:

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.

PacketCrossoverInsertMutator

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.

PacketCrossoverReplaceMutator

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().

PacketSpliceMutator

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)
    }
}

PacketHavocMutator

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.

Putting it all together

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.