Fuzzable Emulation¶
Styx supports fuzzing single vcpu targets 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 mut proc = ProcessorBuilder::default()
.with_builder(Kinetis21Builder::default())
.with_backend(Backend::Unicorn)
.with_custom_executor(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),
..Default::default()
},
))
.with_ipc_port(16000)
.add_plugin(StyxTracePlugin::new(false, false, false, true))
.with_loader(RawLoader)
.with_target_program(get_firmware_path())
.build()?;
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,
/// Timeout on max instructions, if true then the fuzzer will
/// exit with timeout when the maximum number of instructions
/// has been executed.
///
/// If false then the fuzzer will continue to run until
/// the time-based timeout is reached.
pub timeout_on_max_insns: bool,
/// 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>,
/// Maximum number of instructions to execute per fuzzing run
pub max_insns: 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,
/// Function for saving context
#[derivative(Debug = "ignore")]
pub context_save: ContextSaveCallbackType,
/// Function for restoring context
#[derivative(Debug = "ignore")]
pub context_restore: ContextRestoreCBType,
/// Directory to use for the discovered crashes,
/// defaults to `./crashes`
pub crashes_dir: PathBuf,
/// Paths to any **read-only** input corpora not in [`Self::crashes_dir`]
///
/// **NOTE**: Any path added here is *read-only* and will
/// not be modified at runtime
pub corpus_paths: Vec<PathBuf>,
/// Input generator to use, defaults to `StyxFuzzerInputType::RandomBytes`.
///
/// **NOTE**: If you want to use an input generator that is not `RandomBytes`,
/// you must provide a custom fuzzer function via [`Self::fuzz_func`]. This is
/// due to a limitation of the underlying fuzzing library (LibAFL) which requires
/// the input generator (and essentially everything else) to be known at compile time.
///
/// If you want to use a grammar-based input generator, you should
/// set this to `StyxFuzzerInputType::Grammar(String)` with the
/// grammar string being the path to the grammar file. You can then
/// convert that into the proper grammar type in your custom fuzzer function
/// before beginning execution.
pub generator: StyxFuzzerInputType,
/// Execution stride, how many instructions to execute per call to `execute`
///
/// This should not be a large number (Default is 1000).
pub execution_stride: u64,
/// Maximum number of inputs fuzz cases and reasons
/// to keep in an in-memory cache
pub max_in_mem_corpus: u64,
/// Optional custom fuzzer function to use instead of the default
#[derivative(Debug = "ignore")]
pub fuzz_func: Option<FuzzerFuncType>,
}
There are a few functions that need to be defined to handle certain actions.
setup
A function that takes a mutable reference to the processor core and to the slice of vcpu cores. 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 mutable reference to a vcpu core 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.