diff --git a/fuzzers/frida_libpng/Cargo.toml b/fuzzers/frida_libpng/Cargo.toml index 84ccdab240..ca77303189 100644 --- a/fuzzers/frida_libpng/Cargo.toml +++ b/fuzzers/frida_libpng/Cargo.toml @@ -28,7 +28,7 @@ reqwest = { version = "0.11.4", features = ["blocking"] } [dependencies] -libafl = { path = "../../libafl/", features = [ "std", "llmp_compression", "llmp_bind_public" ] } #, "llmp_small_maps", "llmp_debug"]} +libafl = { path = "../../libafl/", features = [ "std", "llmp_compression", "llmp_bind_public", "frida_cli" ] } #, "llmp_small_maps", "llmp_debug"]} capstone = "0.10.0" frida-gum = { version = "0.6.3", features = [ "auto-download", "event-sink", "invocation-listener"] } libafl_frida = { path = "../../libafl_frida", features = ["cmplog"] } diff --git a/fuzzers/frida_libpng/src/fuzzer.rs b/fuzzers/frida_libpng/src/fuzzer.rs index 489789fbeb..7c9119b774 100644 --- a/fuzzers/frida_libpng/src/fuzzer.rs +++ b/fuzzers/frida_libpng/src/fuzzer.rs @@ -4,19 +4,14 @@ use mimalloc::MiMalloc; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; -use clap::{self, StructOpt}; use frida_gum::Gum; -use std::{ - env, - net::SocketAddr, - path::{Path, PathBuf}, -}; +use std::path::PathBuf; use libafl::{ bolts::{ + cli::{parse_args, FuzzerOptions}, current_nanos, launcher::Launcher, - os::Cores, rands::StdRand, shmem::{ShMemProvider, StdShMemProvider}, tuples::{tuple_list, Merge}, @@ -46,7 +41,7 @@ use libafl::{ use libafl_frida::{ coverage_rt::CoverageRuntime, coverage_rt::MAP_SIZE, executor::FridaInProcessExecutor, - helper::FridaInstrumentationHelper, FridaOptions, + helper::FridaInstrumentationHelper, }; #[cfg(unix)] @@ -55,114 +50,16 @@ use libafl_targets::cmplog::{CmpLogObserver, CMPLOG_MAP}; #[cfg(unix)] use libafl_frida::asan::errors::{AsanErrorsFeedback, AsanErrorsObserver, ASAN_ERRORS}; - -#[derive(Debug, StructOpt)] -#[clap( - name = "libafl_frida", - version = "0.1.0", - about = "A frida-based binary-only libfuzzer-style fuzzer for with llmp-multithreading support", - author = "s1341 , - Dongjia Zhang , Andrea Fioraldi , Dominik Maier " -)] -struct Opt { - #[clap( - short, - long, - parse(try_from_str = Cores::from_cmdline), - help = "Spawn a client in each of the provided cores. Broker runs in the 0th core. 'all' to select all available cores. 'none' to run a client without binding to any core. eg: '1,2-4,6' selects the cores 1,2,3,4,6.", - name = "CORES" - )] - cores: Cores, - - #[clap( - short = 'p', - long, - help = "Choose the broker TCP port, default is 1337", - name = "PORT", - default_value = "1337" - )] - broker_port: u16, - - #[clap( - parse(try_from_str), - short = 'a', - long, - help = "Specify a remote broker", - name = "REMOTE" - )] - remote_broker_addr: Option, - - #[clap( - parse(try_from_str), - short, - long, - help = "Set an initial corpus directory", - name = "INPUT" - )] - input: Vec, - - #[clap( - short, - long, - parse(try_from_str), - help = "Set the output directory, default is ./out", - name = "OUTPUT", - default_value = "./out" - )] - output: PathBuf, - - #[clap( - long, - help = "The configuration this fuzzer runs with, for multiprocessing", - name = "CONF", - default_value = "default launcher" - )] - configuration: String, - - #[clap( - long, - help = "The file to redirect stdout input to (/dev/null if unset)" - )] - stdout_file: Option, - - #[clap(help = "The harness")] - harness: String, - - #[clap(help = "The symbol name to look up and hook")] - symbol: String, - - #[clap(help = "The modules to instrument, separated by colons")] - modules_to_instrument: String, -} +use libafl_frida::cmplog_rt::CmpLogRuntime; /// The main fn, usually parsing parameters, and starting the fuzzer pub fn main() { - // Registry the metadata types used in this fuzzer - // Needed only on no_std - //RegistryBuilder::register::(); - - let opt = Opt::parse(); color_backtrace::install(); - println!( - "Workdir: {:?}", - env::current_dir().unwrap().to_string_lossy().to_string() - ); + let options = parse_args(); unsafe { - match fuzz( - &opt.harness, - &opt.symbol, - &opt.modules_to_instrument.split(':').collect::>(), - //modules_to_instrument, - &opt.input, - &opt.output, - opt.broker_port, - &opt.cores, - opt.stdout_file.as_deref(), - opt.remote_broker_addr, - opt.configuration, - ) { + match fuzz(options) { Ok(()) | Err(Error::ShuttingDown) => println!("\nFinished fuzzing. Good bye."), Err(e) => panic!("Error during fuzzing: {:?}", e), } @@ -171,34 +68,19 @@ pub fn main() { /// The actual fuzzer #[allow(clippy::too_many_lines, clippy::too_many_arguments)] -unsafe fn fuzz( - module_name: &str, - symbol_name: &str, - modules_to_instrument: &[&str], - corpus_dirs: &[PathBuf], - objective_dir: &Path, - broker_port: u16, - cores: &Cores, - stdout_file: Option<&str>, - broker_addr: Option, - configuration: String, -) -> Result<(), Error> { +unsafe fn fuzz(options: FuzzerOptions) -> Result<(), Error> { // 'While the stats are state, they are usually used in the broker - which is likely never restarted let monitor = MultiMonitor::new(|s| println!("{}", s)); let shmem_provider = StdShMemProvider::new()?; let mut run_client = |state: Option>, - mut mgr: LlmpRestartingEventManager<_, _, _, _>, - _core_id| { - // The restarting state will spawn the same process again as child, then restarted it each time it crashes. - - // println!("{:?}", mgr.mgr_id()); - - let lib = libloading::Library::new(module_name).unwrap(); + mgr: LlmpRestartingEventManager<_, _, _, _>, + core_id| { + let lib = libloading::Library::new(options.clone().harness.unwrap()).unwrap(); let target_func: libloading::Symbol< unsafe extern "C" fn(data: *const u8, size: usize) -> i32, - > = lib.get(symbol_name.as_bytes()).unwrap(); + > = lib.get(options.harness_function.as_bytes()).unwrap(); let mut frida_harness = |input: &BytesInput| { let target = input.target_bytes(); @@ -207,168 +89,403 @@ unsafe fn fuzz( ExitKind::Ok }; - let gum = Gum::obtain(); - let frida_options = FridaOptions::parse_env_options(); - let coverage = CoverageRuntime::new(); - // let asan = AsanRuntime::new(frida_options.clone()); - let mut frida_helper = FridaInstrumentationHelper::new( - &gum, - &frida_options, - module_name, - modules_to_instrument, - tuple_list!(coverage), - ); + if options.asan && options.asan_cores.contains(core_id) { + (|state: Option>, + mut mgr: LlmpRestartingEventManager<_, _, _, _>, + _core_id| { + let gum = unsafe { Gum::obtain() }; - // Create an observation channel using the coverage map - let edges_observer = HitcountsMapObserver::new(StdMapObserver::new_from_ptr( - "edges", - frida_helper.map_ptr_mut().unwrap(), - MAP_SIZE, - )); + let coverage = CoverageRuntime::new(); + #[cfg(unix)] + let asan = AsanRuntime::new(options.clone()); - // Create an observation channel to keep track of the execution time - let time_observer = TimeObserver::new("time"); + #[cfg(unix)] + let mut frida_helper = + FridaInstrumentationHelper::new(&gum, &options, tuple_list!(coverage, asan)); + #[cfg(windows)] + let mut frida_helper = + FridaInstrumentationHelper::new(&gum, &options, tuple_list!(coverage)); - let feedback_state = MapFeedbackState::with_observer(&edges_observer); - // Feedback to rate the interestingness of an input - // This one is composed by two Feedbacks in OR - let feedback = feedback_or!( - // New maximization map feedback linked to the edges observer and the feedback state - MaxMapFeedback::new_tracking(&feedback_state, &edges_observer, true, false), - // Time feedback, this one does not need a feedback state - TimeFeedback::new_with_observer(&time_observer) - ); + // Create an observation channel using the coverage map + let edges_observer = HitcountsMapObserver::new(unsafe { + StdMapObserver::new_from_ptr( + "edges", + frida_helper.map_ptr_mut().unwrap(), + MAP_SIZE, + ) + }); - // Feedbacks to recognize an input as solution + // Create an observation channel to keep track of the execution time + let time_observer = TimeObserver::new("time"); - #[cfg(unix)] - let objective = feedback_or_fast!( - CrashFeedback::new(), - TimeoutFeedback::new(), - AsanErrorsFeedback::new() - ); + let feedback_state = MapFeedbackState::with_observer(&edges_observer); - #[cfg(windows)] - let objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new()); + // Feedback to rate the interestingness of an input + // This one is composed by two Feedbacks in OR + let feedback = feedback_or!( + // New maximization map feedback linked to the edges observer and the feedback state + MaxMapFeedback::new_tracking(&feedback_state, &edges_observer, true, false), + // Time feedback, this one does not need a feedback state + TimeFeedback::new_with_observer(&time_observer) + ); - // If not restarting, create a State from scratch - let mut state = state.unwrap_or_else(|| { - StdState::new( - // RNG - StdRand::with_seed(current_nanos()), - // Corpus that will be evolved, we keep it in memory for performance - CachedOnDiskCorpus::new(PathBuf::from("./corpus_discovered"), 64).unwrap(), - // Corpus in which we store solutions (crashes in this example), - // on disk so the user can get them after stopping the fuzzer - OnDiskCorpus::new_save_meta( - objective_dir.to_path_buf(), - Some(OnDiskMetadataFormat::JsonPretty), - ) - .unwrap(), - // States of the feedbacks. - // They are the data related to the feedbacks that you want to persist in the State. - tuple_list!(feedback_state), - ) - }); + // Feedbacks to recognize an input as solution + #[cfg(unix)] + let objective = feedback_or_fast!( + CrashFeedback::new(), + TimeoutFeedback::new(), + AsanErrorsFeedback::new() + ); + #[cfg(windows)] + let objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new()); - println!("We're a client, let's fuzz :)"); + // If not restarting, create a State from scratch + let mut state = state.unwrap_or_else(|| { + StdState::new( + // RNG + StdRand::with_seed(current_nanos()), + // Corpus that will be evolved, we keep it in memory for performance + CachedOnDiskCorpus::new(PathBuf::from("./corpus_discovered"), 64).unwrap(), + // Corpus in which we store solutions (crashes in this example), + // on disk so the user can get them after stopping the fuzzer + OnDiskCorpus::new_save_meta( + options.output.to_path_buf(), + Some(OnDiskMetadataFormat::JsonPretty), + ) + .unwrap(), + // States of the feedbacks. + // They are the data related to the feedbacks that you want to persist in the State. + tuple_list!(feedback_state), + ) + }); - // Create a PNG dictionary if not existing - if state.metadata().get::().is_none() { - state.add_metadata(Tokens::from([ - vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG header - b"IHDR".to_vec(), - b"IDAT".to_vec(), - b"PLTE".to_vec(), - b"IEND".to_vec(), - ])); - } + println!("We're a client, let's fuzz :)"); - // Setup a basic mutator with a mutational stage - let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations())); + // Create a PNG dictionary if not existing + if state.metadata().get::().is_none() { + state.add_metadata(Tokens::from([ + vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG header + b"IHDR".to_vec(), + b"IDAT".to_vec(), + b"PLTE".to_vec(), + b"IEND".to_vec(), + ])); + } - // A minimization+queue policy to get testcasess from the corpus - let scheduler = IndexesLenTimeMinimizerCorpusScheduler::new(QueueCorpusScheduler::new()); + // Setup a basic mutator with a mutational stage + let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations())); - // A fuzzer with feedbacks and a corpus scheduler - let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + // A minimization+queue policy to get testcasess from the corpus + let scheduler = + IndexesLenTimeMinimizerCorpusScheduler::new(QueueCorpusScheduler::new()); - // Create the executor for an in-process function with just one observer for edge coverage - #[cfg(unix)] - let mut executor = FridaInProcessExecutor::new( - &gum, - InProcessExecutor::new( - &mut frida_harness, - tuple_list!( + // A fuzzer with feedbacks and a corpus scheduler + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + + #[cfg(unix)] + let observers = tuple_list!( edges_observer, time_observer, - AsanErrorsObserver::new(&ASAN_ERRORS) - ), - &mut fuzzer, - &mut state, - &mut mgr, - )?, - &mut frida_helper, - ); + AsanErrorsObserver::new(unsafe { &ASAN_ERRORS }) + ); + #[cfg(windows)] + let observers = tuple_list!(edges_observer, time_observer); - #[cfg(windows)] - let mut executor = FridaInProcessExecutor::new( - &gum, - InProcessExecutor::new( - &mut frida_harness, - tuple_list!(edges_observer, time_observer,), - &mut fuzzer, - &mut state, - &mut mgr, - )?, - &mut frida_helper, - ); + // Create the executor for an in-process function with just one observer for edge coverage + let mut executor = FridaInProcessExecutor::new( + &gum, + InProcessExecutor::new( + &mut frida_harness, + observers, + &mut fuzzer, + &mut state, + &mut mgr, + )?, + &mut frida_helper, + ); - // In case the corpus is empty (on first run), reset - if state.corpus().count() < 1 { - state - .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, corpus_dirs) - .unwrap_or_else(|_| panic!("Failed to load initial corpus at {:?}", &corpus_dirs)); - println!("We imported {} inputs from disk.", state.corpus().count()); - } + // In case the corpus is empty (on first run), reset + if state.corpus().count() < 1 { + state + .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &options.input) + .unwrap_or_else(|_| { + panic!("Failed to load initial corpus at {:?}", &options.input) + }); + println!("We imported {} inputs from disk.", state.corpus().count()); + } - if frida_options.cmplog_enabled() { - // Create an observation channel using cmplog map - let cmplog_observer = CmpLogObserver::new("cmplog", &mut CMPLOG_MAP, true); + let mut stages = tuple_list!(StdMutationalStage::new(mutator)); - let mut executor = ShadowExecutor::new(executor, tuple_list!(cmplog_observer)); + fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?; - let tracing = ShadowTracingStage::new(&mut executor); + Ok(()) + })(state, mgr, core_id) + } else if options.cmplog && options.cmplog_cores.contains(core_id) { + (|state: Option>, + mut mgr: LlmpRestartingEventManager<_, _, _, _>, + _core_id| { + let gum = unsafe { Gum::obtain() }; - // Setup a randomic Input2State stage - let i2s = StdMutationalStage::new(StdScheduledMutator::new(tuple_list!( - I2SRandReplace::new() - ))); + let coverage = CoverageRuntime::new(); + let cmplog = CmpLogRuntime::new(); - // Setup a basic mutator - let mutational = StdMutationalStage::new(mutator); + let mut frida_helper = + FridaInstrumentationHelper::new(&gum, &options, tuple_list!(coverage, cmplog)); - // The order of the stages matter! - let mut stages = tuple_list!(tracing, i2s, mutational); + // Create an observation channel using the coverage map + let edges_observer = HitcountsMapObserver::new(unsafe { + StdMapObserver::new_from_ptr( + "edges", + frida_helper.map_ptr_mut().unwrap(), + MAP_SIZE, + ) + }); - fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?; + // Create an observation channel to keep track of the execution time + let time_observer = TimeObserver::new("time"); + + let feedback_state = MapFeedbackState::with_observer(&edges_observer); + + // Feedback to rate the interestingness of an input + // This one is composed by two Feedbacks in OR + let feedback = feedback_or!( + // New maximization map feedback linked to the edges observer and the feedback state + MaxMapFeedback::new_tracking(&feedback_state, &edges_observer, true, false), + // Time feedback, this one does not need a feedback state + TimeFeedback::new_with_observer(&time_observer) + ); + + // Feedbacks to recognize an input as solution + let objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new()); + + // If not restarting, create a State from scratch + let mut state = state.unwrap_or_else(|| { + StdState::new( + // RNG + StdRand::with_seed(current_nanos()), + // Corpus that will be evolved, we keep it in memory for performance + CachedOnDiskCorpus::new(PathBuf::from("./corpus_discovered"), 64).unwrap(), + // Corpus in which we store solutions (crashes in this example), + // on disk so the user can get them after stopping the fuzzer + OnDiskCorpus::new_save_meta( + options.output.to_path_buf(), + Some(OnDiskMetadataFormat::JsonPretty), + ) + .unwrap(), + // States of the feedbacks. + // They are the data related to the feedbacks that you want to persist in the State. + tuple_list!(feedback_state), + ) + }); + + println!("We're a client, let's fuzz :)"); + + // Create a PNG dictionary if not existing + if state.metadata().get::().is_none() { + state.add_metadata(Tokens::from([ + vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG header + b"IHDR".to_vec(), + b"IDAT".to_vec(), + b"PLTE".to_vec(), + b"IEND".to_vec(), + ])); + } + + // Setup a basic mutator with a mutational stage + let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations())); + + // A minimization+queue policy to get testcasess from the corpus + let scheduler = + IndexesLenTimeMinimizerCorpusScheduler::new(QueueCorpusScheduler::new()); + + // A fuzzer with feedbacks and a corpus scheduler + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + + #[cfg(unix)] + let observers = tuple_list!( + edges_observer, + time_observer, + AsanErrorsObserver::new(unsafe { &ASAN_ERRORS }) + ); + #[cfg(windows)] + let observers = tuple_list!(edges_observer, time_observer,); + + // Create the executor for an in-process function with just one observer for edge coverage + let mut executor = FridaInProcessExecutor::new( + &gum, + InProcessExecutor::new( + &mut frida_harness, + observers, + &mut fuzzer, + &mut state, + &mut mgr, + )?, + &mut frida_helper, + ); + + // In case the corpus is empty (on first run), reset + if state.corpus().count() < 1 { + state + .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &options.input) + .unwrap_or_else(|_| { + panic!("Failed to load initial corpus at {:?}", &options.input) + }); + println!("We imported {} inputs from disk.", state.corpus().count()); + } + + // Create an observation channel using cmplog map + let cmplog_observer = + CmpLogObserver::new("cmplog", unsafe { &mut CMPLOG_MAP }, true); + + let mut executor = ShadowExecutor::new(executor, tuple_list!(cmplog_observer)); + + let tracing = ShadowTracingStage::new(&mut executor); + + // Setup a randomic Input2State stage + let i2s = StdMutationalStage::new(StdScheduledMutator::new(tuple_list!( + I2SRandReplace::new() + ))); + + // Setup a basic mutator + let mutational = StdMutationalStage::new(mutator); + + // The order of the stages matter! + let mut stages = tuple_list!(tracing, i2s, mutational); + + fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?; + + Ok(()) + })(state, mgr, core_id) } else { - let mut stages = tuple_list!(StdMutationalStage::new(mutator)); + (|state: Option>, + mut mgr: LlmpRestartingEventManager<_, _, _, _>, + _core_id| { + let gum = unsafe { Gum::obtain() }; - fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?; - }; - Ok(()) + let coverage = CoverageRuntime::new(); + + let mut frida_helper = + FridaInstrumentationHelper::new(&gum, &options, tuple_list!(coverage)); + + // Create an observation channel using the coverage map + let edges_observer = HitcountsMapObserver::new(unsafe { + StdMapObserver::new_from_ptr( + "edges", + frida_helper.map_ptr_mut().unwrap(), + MAP_SIZE, + ) + }); + + // Create an observation channel to keep track of the execution time + let time_observer = TimeObserver::new("time"); + + let feedback_state = MapFeedbackState::with_observer(&edges_observer); + + // Feedback to rate the interestingness of an input + // This one is composed by two Feedbacks in OR + let feedback = feedback_or!( + // New maximization map feedback linked to the edges observer and the feedback state + MaxMapFeedback::new_tracking(&feedback_state, &edges_observer, true, false), + // Time feedback, this one does not need a feedback state + TimeFeedback::new_with_observer(&time_observer) + ); + + // Feedbacks to recognize an input as solution + let objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new()); + + // If not restarting, create a State from scratch + let mut state = state.unwrap_or_else(|| { + StdState::new( + // RNG + StdRand::with_seed(current_nanos()), + // Corpus that will be evolved, we keep it in memory for performance + CachedOnDiskCorpus::new(PathBuf::from("./corpus_discovered"), 64).unwrap(), + // Corpus in which we store solutions (crashes in this example), + // on disk so the user can get them after stopping the fuzzer + OnDiskCorpus::new_save_meta( + options.output.to_path_buf(), + Some(OnDiskMetadataFormat::JsonPretty), + ) + .unwrap(), + // States of the feedbacks. + // They are the data related to the feedbacks that you want to persist in the State. + tuple_list!(feedback_state), + ) + }); + + println!("We're a client, let's fuzz :)"); + + // Create a PNG dictionary if not existing + if state.metadata().get::().is_none() { + state.add_metadata(Tokens::from([ + vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG header + b"IHDR".to_vec(), + b"IDAT".to_vec(), + b"PLTE".to_vec(), + b"IEND".to_vec(), + ])); + } + + // Setup a basic mutator with a mutational stage + let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations())); + + // A minimization+queue policy to get testcasess from the corpus + let scheduler = + IndexesLenTimeMinimizerCorpusScheduler::new(QueueCorpusScheduler::new()); + + // A fuzzer with feedbacks and a corpus scheduler + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + + #[cfg(unix)] + let observers = tuple_list!( + edges_observer, + time_observer, + AsanErrorsObserver::new(unsafe { &ASAN_ERRORS }) + ); + #[cfg(windows)] + let observers = tuple_list!(edges_observer, time_observer,); + + // Create the executor for an in-process function with just one observer for edge coverage + let mut executor = FridaInProcessExecutor::new( + &gum, + InProcessExecutor::new( + &mut frida_harness, + observers, + &mut fuzzer, + &mut state, + &mut mgr, + )?, + &mut frida_helper, + ); + + // In case the corpus is empty (on first run), reset + if state.corpus().count() < 1 { + state + .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &options.input) + .unwrap_or_else(|_| { + panic!("Failed to load initial corpus at {:?}", &options.input) + }); + println!("We imported {} inputs from disk.", state.corpus().count()); + } + + let mut stages = tuple_list!(StdMutationalStage::new(mutator)); + + fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?; + + Ok(()) + })(state, mgr, core_id) + } }; Launcher::builder() - .configuration(EventConfig::from_name(&configuration)) + .configuration(EventConfig::AlwaysUnique) .shmem_provider(shmem_provider) .monitor(monitor) .run_client(&mut run_client) - .cores(cores) - .broker_port(broker_port) - .stdout_file(stdout_file) - .remote_broker_addr(broker_addr) + .cores(&options.cores) + .broker_port(options.broker_port) + .stdout_file(Some(&options.stdout)) + .remote_broker_addr(options.remote_broker_addr) .build() .launch() } diff --git a/libafl/src/bolts/cli.rs b/libafl/src/bolts/cli.rs index 32dd05af67..e5f89f43d2 100644 --- a/libafl/src/bolts/cli.rs +++ b/libafl/src/bolts/cli.rs @@ -15,7 +15,7 @@ //! fn main() { //! // make sure to add `features = ["cli"]` to the `libafl` crate in `Cargo.toml` //! let parsed = parse_args(); -//! +//! //! // call appropriate logic, passing in parsed options //! if parsed.replay.is_some() { //! replay(parsed); @@ -64,6 +64,7 @@ //!``` use clap::{App, AppSettings, IntoApp, Parser}; +use serde::{Deserialize, Serialize}; #[cfg(feature = "frida_cli")] use std::error; use std::net::SocketAddr; @@ -102,7 +103,7 @@ fn parse_instrumentation_location( } /// Top-level container for cli options/arguments/subcommands -#[derive(Parser, Debug)] +#[derive(Parser, Clone, Debug, Serialize, Deserialize)] #[clap( setting(AppSettings::ArgRequiredElseHelp), setting(AppSettings::SubcommandPrecedenceOverArg), @@ -122,10 +123,21 @@ pub struct FuzzerOptions { #[clap(short, long, default_value = "/dev/null")] pub stdout: String, + /// the name of the configuration to use + #[clap(short, long, default_value = "default configuration")] + pub configuration: String, + /// enable Address Sanitizer (ASAN) #[clap(short = 'A', long, help_heading = "Fuzz Options")] pub asan: bool, + /// Enable ASAN on each of the provided cores. Use 'all' to select all available + /// cores. 'none' to run a client without binding to any core. + /// ex: '1,2-4,6' selects the cores 1, 2, 3, 4, and 6. + #[cfg(feature = "frida_cli")] + #[clap(short, long, default_value = "0", parse(try_from_str = Cores::from_cmdline), help_heading = "ASAN Options")] + pub asan_cores: Cores, + /// number of fuzz iterations to perform #[clap(short = 'I', long, help_heading = "Fuzz Options", default_value = "0")] pub iterations: usize, @@ -139,6 +151,21 @@ pub struct FuzzerOptions { #[clap(last = true, name = "HARNESS_ARGS")] pub harness_args: Vec, + /// harness function to call + #[cfg(feature = "frida_cli")] + #[clap( + short = 'F', + long, + default_value = "LLVMFuzzerTestOneInput", + help_heading = "Frida Options" + )] + pub harness_function: String, + + /// additional libraries to instrument + #[cfg(feature = "frida_cli")] + #[clap(short, long, help_heading = "Frida Options")] + pub libs_to_instrument: Vec, + /// enable CmpLog instrumentation #[cfg_attr( feature = "frida_cli", @@ -150,6 +177,13 @@ pub struct FuzzerOptions { )] pub cmplog: bool, + /// Enable CmpLog on each of the provided cores. Use 'all' to select all available + /// cores. 'none' to run a client without binding to any core. + /// ex: '1,2-4,6' selects the cores 1, 2, 3, 4, and 6. + #[cfg(feature = "frida_cli")] + #[clap(short, long, default_value = "0", parse(try_from_str = Cores::from_cmdline), help_heading = "Frida Options")] + pub cmplog_cores: Cores, + /// enable ASAN leak detection #[cfg(feature = "frida_cli")] #[clap(short, long, help_heading = "ASAN Options")] @@ -293,7 +327,7 @@ impl FuzzerOptions { /// fn main() { /// // example command line invocation: /// // ./path-to-bin custom --bar stuff - /// + /// /// // clap's builder syntax to define the parser would be fine as well, but here we /// // show the derive option /// let cmd: App = CustomFooParser::into_app(); diff --git a/libafl/src/bolts/os/mod.rs b/libafl/src/bolts/os/mod.rs index 80169911cd..c763fee0b0 100644 --- a/libafl/src/bolts/os/mod.rs +++ b/libafl/src/bolts/os/mod.rs @@ -5,6 +5,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; +use serde::{Deserialize, Serialize}; #[cfg(any(unix, all(windows, feature = "std")))] use crate::Error; @@ -106,7 +107,7 @@ pub fn dup2(fd: i32, device: i32) -> Result<(), Error> { } /// Core ID -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct CoreId { /// The id of this core pub id: usize, @@ -155,7 +156,7 @@ impl From for CoreId { } /// A list of [`CoreId`] to use for fuzzing -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Cores { /// The original commandline used during parsing pub cmdline: String, @@ -218,6 +219,13 @@ impl Cores { ids: cores, }) } + + /// Checks if this [`Cores`] instance contains a given ``core_id`` + #[must_use] + pub fn contains(&self, core_id: usize) -> bool { + let core_id = CoreId::from(core_id); + self.ids.contains(&core_id) + } } impl From<&[usize]> for Cores { diff --git a/libafl_frida/Cargo.toml b/libafl_frida/Cargo.toml index 805e1d3cc8..bcb115fae4 100644 --- a/libafl_frida/Cargo.toml +++ b/libafl_frida/Cargo.toml @@ -19,7 +19,7 @@ cmplog = [] cc = { version = "1.0", features = ["parallel"] } [dependencies] -libafl = { path = "../libafl", version = "0.7.1", features = ["std", "libafl_derive"] } +libafl = { path = "../libafl", version = "0.7.1", features = ["std", "libafl_derive", "frida_cli"] } libafl_targets = { path = "../libafl_targets", version = "0.7.1", features = ["std", "sancov_cmplog"] } nix = "0.23" diff --git a/libafl_frida/src/alloc.rs b/libafl_frida/src/alloc.rs index 549436355b..6fc6232d4d 100644 --- a/libafl_frida/src/alloc.rs +++ b/libafl_frida/src/alloc.rs @@ -1,5 +1,6 @@ use frida_gum::{PageProtection, RangeDetails}; use hashbrown::HashMap; +use libafl::bolts::cli::FuzzerOptions; use nix::{ libc::memset, sys::mman::{mmap, MapFlags, ProtFlags}, @@ -22,17 +23,14 @@ use serde::{Deserialize, Serialize}; use std::io; use std::{collections::BTreeMap, ffi::c_void}; -use crate::{ - asan::errors::{AsanError, AsanErrors}, - FridaOptions, -}; +use crate::asan::errors::{AsanError, AsanErrors}; /// An allocator wrapper with binary-only address sanitization #[derive(Debug)] #[allow(missing_docs)] pub struct Allocator { #[allow(dead_code)] - options: FridaOptions, + options: FuzzerOptions, page_size: usize, shadow_offset: usize, shadow_bit: usize, @@ -78,7 +76,7 @@ impl Allocator { all(target_arch = "aarch64", target_os = "android") )))] #[must_use] - pub fn new(_: FridaOptions) -> Self { + pub fn new(_: FuzzerOptions) -> Self { todo!("Shadow region not yet supported for this platform!"); } @@ -90,7 +88,7 @@ impl Allocator { ))] #[must_use] #[allow(clippy::too_many_lines)] - pub fn new(options: FridaOptions) -> Self { + pub fn new(options: FuzzerOptions) -> Self { let ret = unsafe { sysconf(_SC_PAGESIZE) }; assert!( ret >= 0, @@ -259,9 +257,9 @@ impl Allocator { } else { size }; - if size > self.options.asan_max_allocation() { + if size > self.options.max_allocation { #[allow(clippy::manual_assert)] - if self.options.asan_max_allocation_panics() { + if self.options.max_allocation_panics { panic!("ASAN: Allocation is too large: 0x{:x}", size); } @@ -269,7 +267,7 @@ impl Allocator { } let rounded_up_size = self.round_up_to_page(size) + 2 * self.page_size; - if self.total_allocation_size + rounded_up_size > self.options.asan_max_total_allocation() { + if self.total_allocation_size + rounded_up_size > self.options.max_total_allocation { return std::ptr::null_mut(); } self.total_allocation_size += rounded_up_size; @@ -278,7 +276,7 @@ impl Allocator { //println!("reusing allocation at {:x}, (actual mapping starts at {:x}) size {:x}", metadata.address, metadata.address - self.page_size, size); metadata.is_malloc_zero = is_malloc_zero; metadata.size = size; - if self.options.enable_asan_allocation_backtraces { + if self.options.allocation_backtraces { metadata.allocation_site_backtrace = Some(Backtrace::new_unresolved()); } metadata @@ -311,7 +309,7 @@ impl Allocator { actual_size: rounded_up_size, ..AllocationMetadata::default() }; - if self.options.enable_asan_allocation_backtraces { + if self.options.allocation_backtraces { metadata.allocation_site_backtrace = Some(Backtrace::new_unresolved()); } @@ -356,7 +354,7 @@ impl Allocator { let shadow_mapping_start = map_to_shadow!(self, ptr as usize); metadata.freed = true; - if self.options.enable_asan_allocation_backtraces { + if self.options.allocation_backtraces { metadata.release_site_backtrace = Some(Backtrace::new_unresolved()); } diff --git a/libafl_frida/src/asan/asan_rt.rs b/libafl_frida/src/asan/asan_rt.rs index 11fdedb3cf..fcd54f2425 100644 --- a/libafl_frida/src/asan/asan_rt.rs +++ b/libafl_frida/src/asan/asan_rt.rs @@ -13,7 +13,7 @@ use core::{ }; use frida_gum::{ModuleDetails, NativePointer, RangeDetails}; use hashbrown::HashMap; -use libafl::bolts::AsSlice; +use libafl::bolts::{cli::FuzzerOptions, AsSlice}; use nix::sys::mman::{mmap, MapFlags, ProtFlags}; use rangemap::RangeMap; @@ -58,7 +58,6 @@ use crate::{ asan::errors::{AsanError, AsanErrors, AsanReadWriteError, ASAN_ERRORS}, helper::FridaRuntime, utils::writer_register, - FridaOptions, }; #[cfg(target_arch = "aarch64")] @@ -135,7 +134,7 @@ pub struct AsanRuntime { blob_check_mem_48bytes: Option>, blob_check_mem_64bytes: Option>, stalked_addresses: HashMap, - options: FridaOptions, + options: FuzzerOptions, module_map: Option, suppressed_addresses: Vec, shadow_check_func: Option bool>, @@ -172,8 +171,8 @@ impl FridaRuntime for AsanRuntime { self.unpoison_all_existing_memory(); self.module_map = Some(ModuleMap::new_from_names(modules_to_instrument)); - if let Some(suppressed_specifiers) = self.options.dont_instrument_locations() { - for (module_name, offset) in suppressed_specifiers { + if !self.options.dont_instrument.is_empty() { + for (module_name, offset) in self.options.dont_instrument.clone() { let module_details = ModuleDetails::with_name(module_name).unwrap(); let lib_start = module_details.range().base_address().0 as usize; self.suppressed_addresses.push(lib_start + offset); @@ -288,9 +287,9 @@ impl FridaRuntime for AsanRuntime { impl AsanRuntime { /// Create a new `AsanRuntime` #[must_use] - pub fn new(options: FridaOptions) -> AsanRuntime { + pub fn new(options: FuzzerOptions) -> AsanRuntime { Self { - check_for_leaks_enabled: options.asan_detect_leaks(), + check_for_leaks_enabled: options.detect_leaks, current_report_impl: 0, allocator: Allocator::new(options.clone()), regs: [0; ASAN_SAVE_REGISTER_COUNT], diff --git a/libafl_frida/src/asan/errors.rs b/libafl_frida/src/asan/errors.rs index c32e42af1b..efc349c77c 100644 --- a/libafl_frida/src/asan/errors.rs +++ b/libafl_frida/src/asan/errors.rs @@ -8,7 +8,7 @@ use color_backtrace::{default_output_stream, BacktracePrinter, Verbosity}; use frida_gum::interceptor::Interceptor; use frida_gum::ModuleDetails; use libafl::{ - bolts::{ownedref::OwnedPtr, tuples::Named}, + bolts::{cli::FuzzerOptions, ownedref::OwnedPtr, tuples::Named}, corpus::Testcase, events::EventFirer, executors::ExitKind, @@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize}; use std::io::Write; use termcolor::{Color, ColorSpec, WriteColor}; -use crate::{alloc::AllocationMetadata, asan::asan_rt::ASAN_SAVE_REGISTER_COUNT, FridaOptions}; +use crate::{alloc::AllocationMetadata, asan::asan_rt::ASAN_SAVE_REGISTER_COUNT}; #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct AsanReadWriteError { @@ -94,14 +94,14 @@ impl AsanError { #[allow(clippy::unsafe_derive_deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, SerdeAny)] pub struct AsanErrors { - options: FridaOptions, + options: FuzzerOptions, errors: Vec, } impl AsanErrors { /// Creates a new `AsanErrors` struct #[must_use] - pub fn new(options: FridaOptions) -> Self { + pub fn new(options: FuzzerOptions) -> Self { Self { options, errors: Vec::new(), @@ -534,7 +534,7 @@ impl AsanErrors { }; #[allow(clippy::manual_assert)] - if !self.options.asan_continue_after_error() { + if !self.options.continue_on_error { panic!("ASAN: Crashing target!"); } } diff --git a/libafl_frida/src/helper.rs b/libafl_frida/src/helper.rs index 2e2438ba39..1f56ccaaf7 100644 --- a/libafl_frida/src/helper.rs +++ b/libafl_frida/src/helper.rs @@ -1,5 +1,5 @@ use libafl::{ - bolts::tuples::MatchFirstType, + bolts::{cli::FuzzerOptions, tuples::MatchFirstType}, inputs::{HasTargetBytes, Input}, Error, }; @@ -10,10 +10,8 @@ use libafl_targets::drcov::DrCovBasicBlock; #[cfg(all(feature = "cmplog", target_arch = "aarch64"))] use crate::cmplog_rt::CmpLogRuntime; use crate::coverage_rt::CoverageRuntime; -#[cfg(windows)] -use crate::FridaOptions; #[cfg(unix)] -use crate::{asan::asan_rt::AsanRuntime, drcov_rt::DrCovRuntime, FridaOptions}; +use crate::{asan::asan_rt::AsanRuntime, drcov_rt::DrCovRuntime}; #[cfg(target_arch = "aarch64")] use capstone::{ arch::{self, BuildsCapstone}, @@ -124,7 +122,7 @@ pub struct FridaInstrumentationHelper<'a, RT> { capstone: Capstone, ranges: RangeMap, module_map: ModuleMap, - options: &'a FridaOptions, + options: &'a FuzzerOptions, runtimes: RT, } @@ -170,13 +168,7 @@ where /// Constructor function to create a new [`FridaInstrumentationHelper`], given a `module_name`. #[allow(clippy::too_many_lines)] #[must_use] - pub fn new( - gum: &'a Gum, - options: &'a FridaOptions, - _harness_module_name: &str, - modules_to_instrument: &'a [&str], - runtimes: RT, - ) -> Self { + pub fn new(gum: &'a Gum, options: &'a FuzzerOptions, runtimes: RT) -> Self { // workaround frida's frida-gum-allocate-near bug: #[cfg(unix)] unsafe { @@ -202,6 +194,16 @@ where } } + let mut modules_to_instrument = vec![options + .harness + .as_ref() + .unwrap() + .to_string_lossy() + .to_string()]; + modules_to_instrument.append(&mut options.libs_to_instrument.clone()); + let modules_to_instrument: Vec<&str> = + modules_to_instrument.iter().map(AsRef::as_ref).collect(); + let mut helper = Self { transformer: None, #[cfg(target_arch = "aarch64")] @@ -219,12 +221,12 @@ where .build() .expect("Failed to create Capstone object"), ranges: RangeMap::new(), - module_map: ModuleMap::new_from_names(modules_to_instrument), + module_map: ModuleMap::new_from_names(&modules_to_instrument), options, runtimes, }; - if helper.options().stalker_enabled() { + if options.cmplog || options.asan || !options.disable_coverage { for (i, module) in helper.module_map.values().iter().enumerate() { let range = module.range(); let start = range.base_address().0 as usize; @@ -233,8 +235,8 @@ where .ranges .insert(start..(start + range.size()), (i as u16, module.path())); } - if let Some(suppressed_specifiers) = helper.options().dont_instrument_locations() { - for (module_name, offset) in suppressed_specifiers { + if !options.dont_instrument.is_empty() { + for (module_name, offset) in options.dont_instrument.clone() { let module_details = ModuleDetails::with_name(module_name).unwrap(); let lib_start = module_details.range().base_address().0 as usize; // println!("removing address: {:#x}", lib_start + offset); @@ -354,7 +356,7 @@ where helper.transformer = Some(transformer); helper .runtimes - .init_all(gum, &helper.ranges, modules_to_instrument); + .init_all(gum, &helper.ranges, &modules_to_instrument); } helper } @@ -402,7 +404,7 @@ where /// If stalker is enabled pub fn stalker_enabled(&self) -> bool { - self.options.stalker_enabled() + self.options.cmplog || self.options.asan || !self.options.disable_coverage } /// Pointer to coverage map @@ -423,7 +425,7 @@ where /// Return the ref to options #[inline] - pub fn options(&self) -> &FridaOptions { + pub fn options(&self) -> &FuzzerOptions { self.options } }