Rusty-Fuzzer - A Multi-Threaded Mutation Fuzzer in Rust

 

Rusty Fuzzer

Rusty fuzzer is a very simple mutation fuzzer written and multi-threaded with Rust. It was created for the purpose of learning.

Contents

Introduction


I wanted to learn fuzzing, and have also been learning Rust at University. I figured I would combine the both into a project. I’m also using this post as practice for writing blogs in the future.

This is very heavily based off of the blog-post/tutorial written by Hombre: Fuzzing Like a Caveman Part 1. The key differences is that my fuzzer is written in Rust, but also that the implementation uses Rust multi-threading. I won’t repeat everything that Hombre has already detailed very well in their own blog, but I will briefly summarise the key points, and then discuss the multi-threaded soup at the end.

Rusty_fuzzer gets it’s name from not only being written in Rust, but because nobody has probably used a fuzzer this simple in the field in over a decade. However, it’s a great learning opportunity.

Mutation Fuzzing


Mutation fuzzing is the act of making small changes (mutations) to the input to whatever binary we’re targeting.

Bit Flipping

The following functions constitute the bit-flipping mutation, where a given number of bytes in a file are randomly selected for a random bit to be flipped. select_indexes randomly selects the bytes of the byte array to be operated on using the rand crate. flip_bits enumerates the selected bytes, and flips a randomly-chosen bit using an XOR operation with a bit-shifted 0x1.

Note that it’s important to exclude the start of image and end of image magic bytes from the possible bits to be flipped. This is to avoid corrupting the input to the point where it gets (correctly) rejected by the target.

fn select_indexes(n_indexes : i32, n_selections: i32) -> Vec<i32>{
    // Excludes start of image and end of image markers from index range
    let index_range : Vec<i32> = (2..(n_indexes - 2)).collect();
    let mut selected_indexes = Vec::new();

    while selected_indexes.len() != n_selections as usize {
        let chosen_i = index_range.choose(&mut rand::thread_rng());
        match chosen_i {
            Some(i) => {
                selected_indexes.push(*i);
            },
            None => {
                panic!("Not enough indexes to choose given number of flips");
            }
        }
    }
    return selected_indexes;
}

fn flip_bits(mut bytes : Vec<u8>, byte_indexes : Vec<i32>) -> Vec<u8>{
    for i in byte_indexes{
        let index_range : Vec<i32> = (0..8).collect();
        let bit_i_opt = index_range.choose(&mut rand::thread_rng());
        match bit_i_opt {
            Some(bit_i) => {
                bytes[i as usize] ^= (1 as u8) << (*bit_i as u8);
            },
            None => {
                panic!("Could not randomly select bit index of chosen byte");
            }
        }
    }
    return bytes;
}

Gynvael’s Magic Numbers

GynvaelColdwind mentions some magic numbers which target data size and arithmetic errors.

0xFF
0x7F
0x00
0xFFFF
0x0000
0xFFFFFFFF
0x00000000
0x80000000
0x40000000
0x7FFFFFFF

As a second mutation technique, it’s possible to randomly choose a byte in the image to overwrite with any of the above. I implement this with the following functions.

fn get_magic_number() -> (i32, i32){
    // Format: (length in bytes, value of leading byte)
    let magic = [
        (1, 255),
        (1, 255),
        (1, 127),
        (1, 0),
        (2, 255),
        (2, 0),
        (4, 255),
        (4, 0),
        (4, 128),
        (4, 64),
        (4, 127)
    ];

    let magic_opt = magic.choose(&mut rand::thread_rng());
    match magic_opt{
        Some(num) => {
            return *num;
        },
        None => {
            panic!("Could not select magic number");
        }
    }
}

fn overwrite_with_magic(mut bytes : Vec<u8>, magic_n : (i32, i32)) -> Vec<u8>{
    match magic_n.0 {
        1 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -2).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    bytes[*i as usize] = magic_n.1 as u8;
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        2 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -3).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    bytes[*i as usize] = magic_n.1 as u8;
                    bytes[(*i + 1) as usize] = magic_n.1 as u8;
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        4 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -6).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    match magic_n.1 {
                        255 => {
                            bytes[*i as usize] = 255;
                            bytes[(*i + 1) as usize] = 225;
                            bytes[(*i + 1) as usize] = 225;
                            bytes[(*i + 1) as usize] = 225;
                        },
                        0 => {
                            bytes[*i as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        128 => {
                            bytes[*i as usize] = 128;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        64 => {
                            bytes[*i as usize] = 64;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        127 => {
                            bytes[*i as usize] = 127;
                            bytes[(*i + 1) as usize] = 255;
                            bytes[(*i + 1) as usize] = 255;
                            bytes[(*i + 1) as usize] = 255;
                        },
                        _ => {
                            panic!("Invalid magic byte {} {}", magic_n.0, magic_n.1);
                        }
                    }
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        _ => {
            panic!("Invalid magic number length");
        }
    }
    return bytes;
}

Combining Mutation Techniques

The combined implementation is as follows. The code chooses between two mutation methods: bit flipping or overwriting bytes with magic numbers. It then performs either mutation, and writes a mutated output image.

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 3 {
        println!("Usage: cargo run <jpg-path> <fuzzing-target-path");
        return;
    }

    let mut bytes = get_bytes(args[1].clone());

    // Randomly choose between bit-flipping or magic number mutation
    let options : Vec<i32> = (0..2).collect();
    match options.choose(&mut rand::thread_rng()){
        Some(0) => {
            // Only perform flips on 1% of bytes
            let n_flips = ((bytes.len() as i32) as f64 * 0.01).floor() as i32;
            let selected_i = select_indexes(bytes.len() as i32, n_flips);
            bytes = flip_bits(bytes, selected_i);
        },
        Some(1) => {
            let magic_n = get_magic_number();
            bytes = overwrite_with_magic(bytes, magic_n);
        },
        _ => {
            panic!("Could not choose between functions");
        }
    }

    write_jpg(bytes, String::from("output.jpg"));
}

Multi-Threading


This is where my implementation differs a bit from Hombre’s. Usually in fuzzing, you would want to write thousands of mutated inputs to the target script, increasing the chances of finding a crash and therefore increasing efficiency in finding bugs. I wanted to turn the simple fuzzer into a multi-threaded one, with several threads each creating hundreds of possible mutations.

I also wanted to combine the triage stage in a single script. This is the stage where the target script (in Hombre’s case and in this case, the exif data parsing binary at https://github.com/mkttanabe/exif, is run numerous times against different mutated inputs.

Distributing Mutation Across Multiple Threads

The implementation for the mutating threads is below. In rust, it’s possible to spawn a thread using the std::thread library, via thread::spawn. It’s also important to pay attention to Rust’s ownership rules. The outer for loop spawns 20 threads. It also ensures to clone the index and the bytes read from the original image.

Recall that each thread must actually mutate the vector of bytes (by flipping bits or overwriting entire bytes). This requires a mutable reference. However, Rust does not allow multiple mutable references to the same data. This prevents race conditions among threads by ensuring that multiple threads cannot edit the same data at the same time, and is enforced by the compiler.

Therefore instead, we create a clone of the bytes vector. Additionally, we want to move ownership of the cloned vector of bytes into the thread, instead of borrowing, such that the cloned vector of bytes now exists within the thread’s scope. This is because the thread may outlive the scope of the loop. When the loop’s scope closes, the variables defined within it are destroyed. This is why the move is necessary.

Additionally, within the thread, we have an additional loop which repeats 500 times, producing 500 mutations for each of the 20 threads. Note that it’s necessary to maintain a cloned copy of the original bytes, so that the stream of bytes can be reset to the original following each mutation to avoid chaining the mutations.

// Spawn 20 threads, each of which will create a mutated image 500 times
for i in 0..20{
    let mut bytes_clone = bytes.clone();
    let i_clone = i.clone();
    thread::spawn(move || {
        let original_bytes = bytes_clone.clone();
        for j in 0..500{
            bytes_clone = original_bytes.clone();
            // Randomly choose between bit-flipping or magic number mutation
            let options : Vec<i32> = (0..2).collect();
            match options.choose(&mut rand::thread_rng()){
                Some(0) => {
                    // Only perform flips on 1% of bytes
                    let n_flips = ((bytes_clone.len() as i32) as f64 * 0.01).floor() as i32;
                    let selected_i = select_indexes(bytes_clone.len() as i32, n_flips);
                    bytes_clone = flip_bits(bytes_clone, selected_i);
                },
                Some(1) => {
                    let magic_n = get_magic_number();
                    bytes_clone = overwrite_with_magic(bytes_clone, magic_n);
                },
                _ => {
                    panic!("Could not choose between functions");
                }
            }
            let filename = String::from(format!("output/{}-{}-output.jpg", i_clone, j));
            write_jpg(bytes_clone, filename.clone());
        }
    });
}

The Triage Phase

Now we want to create a thread that tests the target script against a queue of mutated jpegs. There are some slight changes that need to be made. For example, we need to get the target script as input for one.

let target_script = args[2].clone();

We want to structure the program as a pipeline, where there are 20 threads each performing 500 mutations, but also another separate thread testing the target script on mutated jpegs at the same time. We need a way for the mutating threads to notify the triage thread when they have written a mutated jpeg to disk, such that the triage thread can run the target script on said mutated jpeg.

We can use Rust channels to do exactly that. There is one channel connecting all mutation threads with the triage thread. Each mutation thread has a clone of the sender endpoint, so that all of them are able to notify the triage thread. The triage thread has ownership of the only receiving endpoint, and reads notifications from the channel like a queue.

The notification itself will simply be the string filename of the output jpeg that the thread has just written.

It is possible to define the channel endpoints using the std::sync::mpsc library as follows:

let (sender, reciever) = channel();

The mutation threads become redefined slightly, so that the sender endpoint is cloned and moved into each thread. Also, after writing the jpeg, the cloned sender endpoint sends the filename. There is also some error checking code.

// Spawn 20 threads, each of which will create a mutated image 500 times
for i in 0..20{
    let mut bytes_clone = bytes.clone();
    let i_clone = i.clone();
    let sender_clone = sender.clone();
    thread::spawn(move || {
        let original_bytes = bytes_clone.clone();
        for j in 0..500{
            bytes_clone = original_bytes.clone();
            // Randomly choose between bit-flipping or magic number mutation
            let options : Vec<i32> = (0..2).collect();
            match options.choose(&mut rand::thread_rng()){
                Some(0) => {
                    // Only perform flips on 1% of bytes
                    let n_flips = ((bytes_clone.len() as i32) as f64 * 0.01).floor() as i32;
                    let selected_i = select_indexes(bytes_clone.len() as i32, n_flips);
                    bytes_clone = flip_bits(bytes_clone, selected_i);
                },
                Some(1) => {
                    let magic_n = get_magic_number();
                    bytes_clone = overwrite_with_magic(bytes_clone, magic_n);
                },
                _ => {
                    panic!("Could not choose between functions");
                }
            }
            let filename = String::from(format!("output/{}-{}-output.jpg", i_clone, j));
            write_jpg(bytes_clone, filename.clone());
            let res = sender_clone.send(filename.clone());
            match res {
                Ok(_) => {
                    continue;
                },
                Err(e) => {
                    panic!("Error sending {} to triage thread: {}", filename, e);
                }
            }
        }
    });
}

The triage thread is defined below. It loops 10000 times (20 * 500), to ensure that it collects the correct number of files to collect from the channel before quitting. At each iteration, it takes a path to a mutated jpeg from the channel (sent by a mutation thread), constructs a shell command with the given target_script path (provided earlier), runs the command and checks the status code.

// Triage thread
let triage_thread = thread::spawn(move || {
    for i in 0..10000 {
        let filename = reciever.recv().unwrap();
        let mut cmd = Command::new(&target_script);
        let cmd_output = cmd.arg(&filename).stdout(Stdio::piped());
        match cmd_output.status() {
            Ok(status) => {
                match status.code() {
                    None => {
                        println!("Process terminated by signal: {}", filename);
                    },
                    _ => {
                        continue;
                    }
                }
            },
            Err(e) => {
                println!("Could not get status for {}: {}", filename, e);
            }
        }
    }
});

The documentation of the code() method on ExitStatus (returned by matching the Result of cmd_output.status()) states that “On Unix, this will return None if the process was terminated by a signal.” Therefore, we match for None to print out the filenames of mutated jpegs that caused the target script to quit according to a signal, which is very likely to be a Segmentation Fault.

Conclusion


This was a fun project that allowed me to try fuzzing for the first time, and also practice multi-threading with channels in Rust. I notice that there’s probably a few problems, especially with the error handling in the mutated threads (the triage thread is expecting 10000 filenames from the channel, but if a mutation thread fails to send a filename, it will panic and therefore destroy the thread, but the other threads will continue, and the triage thread will wait forever).

It would be interesting to pick a different target too, since I used the same target that Hombre used in their own blog. I’d like to look at code coverage and smart fuzzing techniques in the future as well. Thanks again to Hombre for their really well-written blog that helped me access fuzzing as a beginner.

Anyway, the source code can be found below and on github

Source Code


use std::env;
use std::fs::{File, read};
use std::io::Write;
use rand::seq::{SliceRandom};
use std::thread;
use std::sync::mpsc::channel;
use std::process::{Command, Stdio};

fn get_bytes(filename : String) -> Vec<u8>{
    let bytes_vector = read(filename).unwrap();
    return bytes_vector;
}

fn write_jpg(data : Vec<u8>, filename : String){
    let mut f = File::create(filename).unwrap();
    f.write_all(&data).unwrap();
}

fn select_indexes(n_indexes : i32, n_selections: i32) -> Vec<i32>{
    // Excludes start of image and end of image markers from index range
    let index_range : Vec<i32> = (2..(n_indexes - 2)).collect();
    let mut selected_indexes = Vec::new();

    while selected_indexes.len() != n_selections as usize {
        let chosen_i = index_range.choose(&mut rand::thread_rng());
        match chosen_i {
            Some(i) => {
                selected_indexes.push(*i);
            },
            None => {
                panic!("Not enough indexes to choose given number of flips");
            }
        }
    }
    return selected_indexes;
}

fn flip_bits(mut bytes : Vec<u8>, byte_indexes : Vec<i32>) -> Vec<u8>{
    for i in byte_indexes{
        // println!("{:#010b}", bytes[i as usize]);
        let index_range : Vec<i32> = (0..8).collect();
        let bit_i_opt = index_range.choose(&mut rand::thread_rng());
        match bit_i_opt {
            Some(bit_i) => {
                bytes[i as usize] ^= (1 as u8) << (*bit_i as u8);
            },
            None => {
                panic!("Could not randomly select bit index of chosen byte");
            }
        }
        // println!("{:#010b}", bytes[i as usize]);
    }
    return bytes;
}

fn get_magic_number() -> (i32, i32){
    // Format: (length in bytes, value of leading byte)
    let magic = [
        (1, 255),
        (1, 255),
        (1, 127),
        (1, 0),
        (2, 255),
        (2, 0),
        (4, 255),
        (4, 0),
        (4, 128),
        (4, 64),
        (4, 127)
    ];

    let magic_opt = magic.choose(&mut rand::thread_rng());
    match magic_opt{
        Some(num) => {
            return *num;
        },
        None => {
            panic!("Could not select magic number");
        }
    }
}

fn overwrite_with_magic(mut bytes : Vec<u8>, magic_n : (i32, i32)) -> Vec<u8>{
    match magic_n.0 {
        1 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -2).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    bytes[*i as usize] = magic_n.1 as u8;
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        2 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -3).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    bytes[*i as usize] = magic_n.1 as u8;
                    bytes[(*i + 1) as usize] = magic_n.1 as u8;
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        4 => {
            let indexes : Vec<i32> = (2..(bytes.len()) as i32 -6).collect();
            let index_opt = indexes.choose(&mut rand::thread_rng());
            match index_opt {
                Some(i) => {
                    match magic_n.1 {
                        255 => {
                            bytes[*i as usize] = 255;
                            bytes[(*i + 1) as usize] = 225;
                            bytes[(*i + 1) as usize] = 225;
                            bytes[(*i + 1) as usize] = 225;
                        },
                        0 => {
                            bytes[*i as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        128 => {
                            bytes[*i as usize] = 128;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        64 => {
                            bytes[*i as usize] = 64;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                            bytes[(*i + 1) as usize] = 0;
                        },
                        127 => {
                            bytes[*i as usize] = 127;
                            bytes[(*i + 1) as usize] = 255;
                            bytes[(*i + 1) as usize] = 255;
                            bytes[(*i + 1) as usize] = 255;
                        },
                        _ => {
                            panic!("Invalid magic byte {} {}", magic_n.0, magic_n.1);
                        }
                    }
                },
                None => {
                    panic!("Cannot choose index");
                }
            }
        },
        _ => {
            panic!("Invalid magic number length");
        }
    }
    return bytes;
}

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 3 {
        println!("Usage: cargo run <jpg-path> <fuzzing-target-path");
        return;
    }

    let bytes = get_bytes(args[1].clone());
    let target_script = args[2].clone();

    let (sender, reciever) = channel();

    // Spawn 20 threads, each of which will create a mutated image 500 times
    for i in 0..20{
        let mut bytes_clone = bytes.clone();
        let i_clone = i.clone();
        let sender_clone = sender.clone();
        thread::spawn(move || {
            let original_bytes = bytes_clone.clone();
            for j in 0..500{
                bytes_clone = original_bytes.clone();
                // Randomly choose between bit-flipping or magic number mutation
                let options : Vec<i32> = (0..2).collect();
                match options.choose(&mut rand::thread_rng()){
                    Some(0) => {
                        // Only perform flips on 1% of bytes
                        let n_flips = ((bytes_clone.len() as i32) as f64 * 0.01).floor() as i32;
                        let selected_i = select_indexes(bytes_clone.len() as i32, n_flips);
                        bytes_clone = flip_bits(bytes_clone, selected_i);
                    },
                    Some(1) => {
                        let magic_n = get_magic_number();
                        bytes_clone = overwrite_with_magic(bytes_clone, magic_n);
                    },
                    _ => {
                        panic!("Could not choose between functions");
                    }
                }
                let filename = String::from(format!("output/{}-{}-output.jpg", i_clone, j));
                write_jpg(bytes_clone, filename.clone());
                let res = sender_clone.send(filename.clone());
                match res {
                    Ok(_) => {
                        continue;
                    },
                    Err(e) => {
                        panic!("Error sending {} to triage thread: {}", filename, e);
                    }
                }
            }
        });
    }

    // Triage thread
    let triage_thread = thread::spawn(move || {
        for i in 0..10000 {
            let filename = reciever.recv().unwrap();
            // let cmd = String::from(format!("{} {}", target_script, filename));
            let mut cmd = Command::new(&target_script);
            let cmd_output = cmd.arg(&filename).stdout(Stdio::piped());
            match cmd_output.status() {
                Ok(status) => {
                    match status.code() {
                        None => {
                            println!("Process terminated by signal: {}", filename);
                        },
                        _ => {
                            continue;
                        }
                    }
                },
                Err(e) => {
                    println!("Could not get status for {}: {}", filename, e);
                }
            }
        }
    });

    triage_thread.join().unwrap();
}