Fuzzable Emulation

Styx supports fuzzing with LibAFL

Building a Fuzzing Capable Processor in Styx

Two components are required for fuzzing to work. First, the FuzzerExecutor needs to be assigned as the executor for your processor. The FuzzerExecutor requires a configuration struct upon creation, the configuration options are covered in another section.

Second, the trace plugin with basic block events needs to be enabled. The trace plugin is how the fuzzer gets coverage data from emulation.

let proc = ProcessorBuilder::default()
        .with_endian(ArchEndian::LittleEndian)
        .with_variant(ArmVariants::ArmCortexM4)
        .with_executor(Executor::new_unlimited(Arc::new(FuzzerExecutor::new(
            COVERAGE_MAP_SIZE,
            StyxFuzzerConfig {
                timeout: Duration::from_secs(1),
                branches_filepath: String::from("./branches.txt"),
                exits: vec![0xba2],
                max_input_len: MAX_INPUT_LEN,
                input_hook: Box::new(insert_input),
                setup: Box::new(pre_fuzzing_setup),
                context_restore: Box::new(context_restore),
                context_save: Box::new(context_save),
            },
        ))))
        .with_plugin(StyxTracePlugin::new(false, false, false, true))
        .with_loader(RawLoader)
        .with_target_program(get_firmware_path())
        .build::<Kinetis21Cpu>()?;

Code Coverage

Styx measures code coverage at the basic block level, keeping track of how many times each basic block is hit. To achieve this, when building your processor you need to provide a file containing the addresses of each basic block in your firmware. We provide a Ghidra script that can generate this file, styx/plugins/styx-fuzzer/GetBranches.java.

Fuzzer Config

The StyxFuzzerConfig struct defines important configuration options for fuzzing, explained below.

pub struct StyxFuzzerConfig {
    /// timeout for fuzzing executions
    pub timeout: Duration,
    /// path to file containing branch information, after running GetBranches.java in ghidra
    pub branches_filepath: String,
    /// addresses at which to exit emulation
    pub exits: Vec<u64>,
    // max input length
    pub max_input_len: usize,
    /// By default the fuzzer just writes inputs into memory at the location specified
    /// in this config.  If other target specific actions need to be taken when inserting
    /// inputs (like setting the value of a register) then a custom function should be
    /// provided.
    #[derivative(Debug = "ignore")]
    pub input_hook: InputCallbackType,
    /// Pre-fuzzing setup
    #[derivative(Debug = "ignore")]
    pub setup: SetupCallbackType,
    /// functions for saving and restoring context
    #[derivative(Debug = "ignore")]
    pub context_save: ContextSaveCallbackType,
    #[derivative(Debug = "ignore")]
    pub context_restore: ContextRestoreCBType,
}

There are a few functions that need to be defined to handle certain actions.

setup
  • A function that takes a reference to a processor. This should do everything required to get the program ready to fuzz. This could include doing things like emulating your firmware up to a certain point, setting registers/memory to some desired initial state, or receiving an external input from a peripheral.

input_hook
  • A function that takes a reference to a cpu backend and a reference to the data to be inserted for an execution. This will most likely be just writing data to memory, but could do other things like setting a register to a certain value.

context_save/context_restore
  • These functions are responsible for producing a snapshot of the cpu state to restore from, as well as reseting the cpu state after an execution. context_restore will be called very frequently, so make sure you are doing only necessary actions to make fuzzing as fast as possible.