From bce3474824cd590ceadf873fa35d0580144e4b3b Mon Sep 17 00:00:00 2001 From: d0ntrash Date: Fri, 10 Jan 2025 18:35:49 +0100 Subject: [PATCH] Add nyx_launcher example fuzzer --- fuzzers/full_system/nyx_launcher/Cargo.toml | 36 +++ fuzzers/full_system/nyx_launcher/README.md | 11 + .../full_system/nyx_launcher/src/client.rs | 42 +++ .../full_system/nyx_launcher/src/fuzzer.rs | 152 +++++++++++ .../full_system/nyx_launcher/src/instance.rs | 257 ++++++++++++++++++ fuzzers/full_system/nyx_launcher/src/main.rs | 22 ++ .../full_system/nyx_launcher/src/options.rs | 112 ++++++++ 7 files changed, 632 insertions(+) create mode 100644 fuzzers/full_system/nyx_launcher/Cargo.toml create mode 100644 fuzzers/full_system/nyx_launcher/README.md create mode 100644 fuzzers/full_system/nyx_launcher/src/client.rs create mode 100644 fuzzers/full_system/nyx_launcher/src/fuzzer.rs create mode 100644 fuzzers/full_system/nyx_launcher/src/instance.rs create mode 100644 fuzzers/full_system/nyx_launcher/src/main.rs create mode 100644 fuzzers/full_system/nyx_launcher/src/options.rs diff --git a/fuzzers/full_system/nyx_launcher/Cargo.toml b/fuzzers/full_system/nyx_launcher/Cargo.toml new file mode 100644 index 0000000000..0cc1daad41 --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "nyx_launcher" +version = "0.14.1" +authors = ["Konstantin Bücheler "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["std"] +std = [] + +## Build with a simple event manager instead of Launcher - don't fork, and crash after the first bug. +simplemgr = [] + +[profile.release] +lto = true +codegen-units = 1 +opt-level = 3 +debug = true + +[build-dependencies] +vergen = { version = "8.2.1", features = ["build", "cargo", "git", "gitcl", "rustc", "si"] } + +[dependencies] +clap = { version = "4.5.18", features = ["derive", "string"] } +libafl = { path = "../../../libafl", features = ["tui_monitor"] } +libafl_bolts = { path = "../../../libafl_bolts", features = [ + "errors_backtrace", +] } +libafl_nyx = { path = "../../../libafl_nyx/"} +log = {version = "0.4.20" } +nix = { version = "0.29.0", features = ["fs"] } +rangemap = { version = "1.5.1" } +readonly = { version = "0.2.12" } +typed-builder = { version = "0.20.0" } diff --git a/fuzzers/full_system/nyx_launcher/README.md b/fuzzers/full_system/nyx_launcher/README.md new file mode 100644 index 0000000000..4bbf669326 --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/README.md @@ -0,0 +1,11 @@ +# nyx_launcher + +Example fuzzer based on `qemu_launcher` but for Nyx. + +## Run the fuzzer + +Run with an existing nyx shared dir: + +``` +cargo run -- --input input/ --output output/ --share /tmp/shareddir/ --buffer-size 4096 --cores 0-1 -v --cmplog-cores 1 +``` diff --git a/fuzzers/full_system/nyx_launcher/src/client.rs b/fuzzers/full_system/nyx_launcher/src/client.rs new file mode 100644 index 0000000000..605481d3f5 --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/src/client.rs @@ -0,0 +1,42 @@ +use libafl::{ + corpus::{InMemoryOnDiskCorpus, OnDiskCorpus}, + events::ClientDescription, + inputs::BytesInput, + monitors::Monitor, + state::StdState, + Error, +}; +use libafl_bolts::rands::StdRand; + +use crate::{ + instance::{ClientMgr, Instance}, + options::FuzzerOptions, +}; + +#[allow(clippy::module_name_repetitions)] +pub type ClientState = + StdState, StdRand, OnDiskCorpus>; + +pub struct Client<'a> { + options: &'a FuzzerOptions, +} + +impl<'a> Client<'a> { + pub fn new(options: &FuzzerOptions) -> Client { + Client { options } + } + + pub fn run( + &self, + state: Option, + mgr: ClientMgr, + client_description: ClientDescription, + ) -> Result<(), Error> { + let instance = Instance::builder() + .options(self.options) + .mgr(mgr) + .client_description(client_description); + + instance.build().run(state) + } +} diff --git a/fuzzers/full_system/nyx_launcher/src/fuzzer.rs b/fuzzers/full_system/nyx_launcher/src/fuzzer.rs new file mode 100644 index 0000000000..a0d5fc2afe --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/src/fuzzer.rs @@ -0,0 +1,152 @@ +use std::{ + cell::RefCell, + fs::{File, OpenOptions}, + io::{self, Write}, +}; + +use clap::Parser; + +use libafl::events::{ + ClientDescription, EventConfig, Launcher, LlmpEventManager, LlmpRestartingEventManager, + MonitorTypedEventManager, +}; +use libafl::{ + monitors::{tui::TuiMonitor, Monitor, MultiMonitor}, + Error, +}; + +use libafl_bolts::shmem::{ShMemProvider, StdShMemProvider}; +use libafl_bolts::{ + core_affinity::CoreId, current_time, llmp::LlmpBroker, staterestore::StateRestorer, + tuples::tuple_list, +}; +#[cfg(unix)] +use { + nix::unistd::dup, + std::os::unix::io::{AsRawFd, FromRawFd}, +}; + +use crate::{client::Client, options::FuzzerOptions}; + +pub struct Fuzzer { + options: FuzzerOptions, +} + +impl Fuzzer { + pub fn new() -> Fuzzer { + let options = FuzzerOptions::parse(); + options.validate(); + Fuzzer { options } + } + + pub fn fuzz(&self) -> Result<(), Error> { + if self.options.tui { + let monitor = TuiMonitor::builder() + .title("Nyx Launcher") + .version("0.14.1") + .enhanced_graphics(true) + .build(); + self.launch(monitor) + } else { + let log = self.options.log.as_ref().and_then(|l| { + OpenOptions::new() + .append(true) + .create(true) + .open(l) + .ok() + .map(RefCell::new) + }); + + #[cfg(unix)] + let stdout_cpy = RefCell::new(unsafe { + let new_fd = dup(io::stdout().as_raw_fd()).unwrap(); + File::from_raw_fd(new_fd) + }); + + // The stats reporter for the broker + let monitor = MultiMonitor::new(|s| { + #[cfg(unix)] + writeln!(stdout_cpy.borrow_mut(), "{s}").unwrap(); + #[cfg(windows)] + println!("{s}"); + + if let Some(log) = &log { + writeln!(log.borrow_mut(), "{:?} {}", current_time(), s).unwrap(); + } + }); + self.launch(monitor) + } + } + + fn launch(&self, monitor: M) -> Result<(), Error> + where + M: Monitor + Clone, + { + // The shared memory allocator + let mut shmem_provider = StdShMemProvider::new()?; + + /* If we are running in verbose, don't provide a replacement stdout, otherwise, use /dev/null */ + let stdout = if self.options.verbose { + None + } else { + Some("/dev/null") + }; + + let client = Client::new(&self.options); + + if self.options.rerun_input.is_some() { + // If we want to rerun a single input but we use a restarting mgr, we'll have to create a fake restarting mgr that doesn't actually restart. + // It's not pretty but better than recompiling with simplemgr. + + // Just a random number, let's hope it's free :) + let broker_port = 13120; + let _fake_broker = LlmpBroker::create_attach_to_tcp( + shmem_provider.clone(), + tuple_list!(), + broker_port, + ) + .unwrap(); + + // To rerun an input, instead of using a launcher, we create dummy parameters and run the client directly. + return client.run( + None, + MonitorTypedEventManager::<_, M>::new(LlmpRestartingEventManager::new( + LlmpEventManager::builder() + .build_on_port( + shmem_provider.clone(), + broker_port, + EventConfig::AlwaysUnique, + None, + ) + .unwrap(), + StateRestorer::new(shmem_provider.new_shmem(0x1000).unwrap()), + )), + ClientDescription::new(0, 0, CoreId(0)), + ); + } + + #[cfg(feature = "simplemgr")] + return client.run(None, SimpleEventManager::new(monitor), CoreId(0)); + + // Build and run a Launcher + match Launcher::builder() + .shmem_provider(shmem_provider) + .broker_port(self.options.port) + .configuration(EventConfig::from_build_id()) + .monitor(monitor) + .run_client(|s, m, c| client.run(s, MonitorTypedEventManager::<_, M>::new(m), c)) + .cores(&self.options.cores) + .stdout_file(stdout) + .stderr_file(stdout) + .build() + .launch() + { + Ok(()) => Ok(()), + Err(Error::ShuttingDown) => { + println!("Fuzzing stopped by user. Good bye."); + Ok(()) + } + Err(err) => Err(err), + } + } +} diff --git a/fuzzers/full_system/nyx_launcher/src/instance.rs b/fuzzers/full_system/nyx_launcher/src/instance.rs new file mode 100644 index 0000000000..69c88cec3e --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/src/instance.rs @@ -0,0 +1,257 @@ +use std::{marker::PhantomData, process}; + +use libafl::events::{ + ClientDescription, LlmpRestartingEventManager, MonitorTypedEventManager, NopEventManager, +}; +use libafl::executors::ShadowExecutor; +use libafl::mutators::I2SRandReplace; +use libafl::stages::CalibrationStage; +use libafl::{ + corpus::{Corpus, InMemoryOnDiskCorpus, OnDiskCorpus}, + events::EventRestarter, + executors::Executor, + feedback_and_fast, feedback_or, feedback_or_fast, + feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback}, + fuzzer::{Evaluator, Fuzzer, StdFuzzer}, + inputs::BytesInput, + monitors::Monitor, + mutators::{havoc_mutations, StdMOptMutator, StdScheduledMutator}, + mutators::{tokens_mutations, Tokens}, + observers::{CanTrack, HitcountsMapObserver, StdMapObserver, TimeObserver}, + schedulers::{ + powersched::PowerSchedule, IndexesLenTimeMinimizerScheduler, PowerQueueScheduler, + }, + stages::{power::StdPowerMutationalStage, ShadowTracingStage, StagesTuple, StdMutationalStage}, + state::{HasCorpus, HasMaxSize, StdState, UsesState}, + Error, HasMetadata, NopFuzzer, +}; +use libafl_bolts::shmem::StdShMemProvider; +use libafl_bolts::{ + current_nanos, + rands::StdRand, + tuples::{tuple_list, Merge}, +}; +use libafl_nyx::{ + cmplog::NyxCmpObserver, executor::NyxExecutor, helper::NyxHelper, settings::NyxSettings, +}; +use typed_builder::TypedBuilder; + +use crate::options::FuzzerOptions; + +pub type ClientState = + StdState, StdRand, OnDiskCorpus>; + +pub type ClientMgr = + MonitorTypedEventManager, M>; + +#[derive(TypedBuilder)] +pub struct Instance<'a, M: Monitor> { + options: &'a FuzzerOptions, + /// The harness. We create it before forking, then `take()` it inside the client. + mgr: ClientMgr, + client_description: ClientDescription, + #[builder(default=PhantomData)] + phantom: PhantomData, +} + +impl<'a, M: Monitor> Instance<'a, M> { + pub fn run(mut self, state: Option) -> Result<(), Error> { + let parent_cpu_id = self + .options + .cores + .ids + .first() + .expect("unable to get first core id"); + + let settings = NyxSettings::builder() + .cpu_id(self.client_description.core_id().0) + .parent_cpu_id(Some(parent_cpu_id.0 as usize)) + .input_buffer_size(self.options.buffer_size) + .timeout_secs(0) + .timeout_micro_secs(self.options.timeout) + .build(); + + let helper = NyxHelper::new(self.options.shared_dir(), settings)?; + + let trace_observer = HitcountsMapObserver::new(unsafe { + StdMapObserver::from_mut_ptr("trace", helper.bitmap_buffer, helper.bitmap_size) + }) + .track_indices(); + + // Create an observation channel to keep track of the execution time + let time_observer = TimeObserver::new("time"); + + let map_feedback = MaxMapFeedback::new(&trace_observer); + + // let stdout_observer = StdOutObserver::new("hprintf_output"); + + let calibration = CalibrationStage::new(&map_feedback); + + // Feedback to rate the interestingness of an input + // This one is composed by two Feedbacks in OR + let mut feedback = feedback_or!( + // New maximization map feedback linked to the edges observer and the feedback state + map_feedback, + // Time feedback, this one does not need a feedback state + TimeFeedback::new(&time_observer), + // Append stdout to metadata + // StdOutToMetadataFeedback::new(&stdout_observer) + ); + + // A feedback to choose if an input is a solution or not + let mut objective = feedback_and_fast!( + // CrashFeedback::new(), + feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new()), + // Take it only if trigger new coverage over crashes + // For deduplication + MaxMapFeedback::with_name("mapfeedback_metadata_objective", &trace_observer) + ); + + // If not restarting, create a State from scratch + let mut state = match state { + Some(x) => x, + None => { + StdState::new( + // RNG + StdRand::with_seed(current_nanos()), + // Corpus that will be evolved, we keep it in memory for performance + InMemoryOnDiskCorpus::no_meta( + self.options.queue_dir(self.client_description.core_id()), + )?, + // Corpus in which we store solutions (crashes in this example), + // on disk so the user can get them after stopping the fuzzer + OnDiskCorpus::new(self.options.crashes_dir(self.client_description.core_id()))?, + // States of the feedbacks. + // The feedbacks can report the data that should persist in the State. + &mut feedback, + // Same for objective feedbacks + &mut objective, + )? + } + }; + + // A minimization+queue policy to get testcasess from the corpus + let scheduler = IndexesLenTimeMinimizerScheduler::new( + &trace_observer, + PowerQueueScheduler::new(&mut state, &trace_observer, PowerSchedule::fast()), + ); + + let observers = tuple_list!(trace_observer, time_observer); // stdout_observer); + + let mut tokens = Tokens::new(); + + if let Some(tokenfile) = &self.options.tokens { + tokens.add_from_file(tokenfile)?; + } + + state.add_metadata(tokens); + + state.set_max_size(self.options.buffer_size); + + // A fuzzer with feedbacks and a corpus scheduler + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + + if let Some(rerun_input) = &self.options.rerun_input { + // TODO: We might want to support non-bytes inputs at some point? + let bytes = std::fs::read(rerun_input) + .unwrap_or_else(|_| panic!("Could not load file {rerun_input:?}")); + let input = BytesInput::new(bytes); + + let mut executor = NyxExecutor::builder().build(helper, observers); + + let exit_kind = executor + .run_target( + &mut NopFuzzer::new(), + &mut state, + &mut NopEventManager::new(), + &input, + ) + .expect("Error running target"); + println!("Rerun finished with ExitKind {:?}", exit_kind); + // We're done :) + process::exit(0); + } + + if self + .options + .is_cmplog_core(self.client_description.core_id()) + { + let cmplog_observer = NyxCmpObserver::new("cmplog", helper.redqueen_path.clone(), true); + + let executor = NyxExecutor::builder().build(helper, observers); + + // Show the cmplog observer + let mut executor = ShadowExecutor::new(executor, tuple_list!(cmplog_observer)); + + // Setup a randomic Input2State stage + let i2s = StdMutationalStage::new(StdScheduledMutator::new(tuple_list!( + I2SRandReplace::new() + ))); + + let tracing = ShadowTracingStage::new(&mut executor); + + // Setup a MOPT mutator + let mutator = StdMOptMutator::new( + &mut state, + havoc_mutations().merge(tokens_mutations()), + 7, + 5, + )?; + + let power: StdPowerMutationalStage<_, _, BytesInput, _, _, _> = + StdPowerMutationalStage::new(mutator); + + // The order of the stages matter! + let mut stages = tuple_list!(calibration, tracing, i2s, power); + + return self.fuzz(&mut state, &mut fuzzer, &mut executor, &mut stages); + } + + let mut executor = NyxExecutor::builder().build(helper, observers); + + // Setup an havoc mutator with a mutational stage + let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations())); + + let mut stages = tuple_list!(StdMutationalStage::new(mutator)); + + self.fuzz(&mut state, &mut fuzzer, &mut executor, &mut stages) + } + + fn fuzz( + &mut self, + state: &mut ClientState, + fuzzer: &mut Z, + executor: &mut E, + stages: &mut ST, + ) -> Result<(), Error> + where + Z: Fuzzer, ClientState, ST> + + Evaluator, BytesInput, ClientState>, + E: UsesState + Executor, Z>, + ST: StagesTuple, ClientState, Z>, + { + let corpus_dirs = [self.options.input_dir()]; + + if state.must_load_initial_inputs() { + state + .load_initial_inputs(fuzzer, executor, &mut self.mgr, &corpus_dirs) + .unwrap_or_else(|_| { + println!("Failed to load initial corpus at {corpus_dirs:?}"); + process::exit(0); + }); + println!("We imported {} inputs from disk.", state.corpus().count()); + } + + if let Some(iters) = self.options.iterations { + fuzzer.fuzz_loop_for(stages, executor, state, &mut self.mgr, iters)?; + + // It's important, that we store the state before restarting! + // Else, the parent will not respawn a new child and quit. + self.mgr.on_restart(state)?; + } else { + fuzzer.fuzz_loop(stages, executor, state, &mut self.mgr)?; + } + + Ok(()) + } +} diff --git a/fuzzers/full_system/nyx_launcher/src/main.rs b/fuzzers/full_system/nyx_launcher/src/main.rs new file mode 100644 index 0000000000..7e55f426b7 --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/src/main.rs @@ -0,0 +1,22 @@ +//! A libfuzzer-like fuzzer using qemu for binary-only coverage +#[cfg(target_os = "linux")] +mod client; +#[cfg(target_os = "linux")] +mod fuzzer; +#[cfg(target_os = "linux")] +mod instance; +#[cfg(target_os = "linux")] +mod options; + +#[cfg(target_os = "linux")] +use crate::fuzzer::Fuzzer; + +#[cfg(target_os = "linux")] +pub fn main() { + Fuzzer::new().fuzz().unwrap(); +} + +#[cfg(not(target_os = "linux"))] +pub fn main() { + panic!("libafl_nyx is only supported on linux!"); +} diff --git a/fuzzers/full_system/nyx_launcher/src/options.rs b/fuzzers/full_system/nyx_launcher/src/options.rs new file mode 100644 index 0000000000..7ee5ceb838 --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/src/options.rs @@ -0,0 +1,112 @@ +use std::path::PathBuf; + +use clap::{error::ErrorKind, CommandFactory, Parser}; +use libafl_bolts::core_affinity::{CoreId, Cores}; + +#[readonly::make] +#[derive(Parser, Debug)] +#[clap(author, about, long_about = None)] +#[allow(clippy::module_name_repetitions)] +#[command( + name = format!("nyx_launcher"), + about, + long_about = "Binary fuzzer using NYX" +)] +pub struct FuzzerOptions { + #[arg(short, long, help = "Input directory")] + pub input: String, + + #[arg(short, long, help = "Output directory")] + pub output: String, + + #[arg(short, long, help = "Shared directory")] + pub share: String, + + #[arg(short, long, help = "Input buffer size")] + pub buffer_size: usize, + + #[arg(short = 'x', long, help = "Tokens file")] + pub tokens: Option, + + #[arg(long, help = "Log file")] + pub log: Option, + + #[arg(long, help = "Timeout in milli-seconds", default_value = "1000")] + pub timeout: u32, + + #[arg(long = "port", help = "Broker port", default_value_t = 1337_u16)] + pub port: u16, + + #[arg(long, help = "Cpu cores to use", default_value = "all", value_parser = Cores::from_cmdline)] + pub cores: Cores, + + #[arg(long, help = "Cpu cores to use for CmpLog", value_parser = Cores::from_cmdline)] + pub cmplog_cores: Option, + + #[clap(short, long, help = "Enable output from the fuzzer clients")] + pub verbose: bool, + + #[clap(long, help = "Enable AFL++ style output", conflicts_with = "verbose")] + pub tui: bool, + + #[arg(long = "iterations", help = "Maximum numer of iterations")] + pub iterations: Option, + + #[arg( + short = 'r', + help = "An input to rerun, instead of starting to fuzz. Will ignore all other settings apart from -d." + )] + pub rerun_input: Option, +} + +impl FuzzerOptions { + pub fn input_dir(&self) -> PathBuf { + PathBuf::from(&self.input) + } + + pub fn shared_dir(&self) -> PathBuf { + PathBuf::from(&self.share) + } + + pub fn output_dir(&self, core_id: CoreId) -> PathBuf { + let mut dir = PathBuf::from(&self.output); + dir.push(format!("cpu_{:03}", core_id.0)); + dir + } + + pub fn queue_dir(&self, core_id: CoreId) -> PathBuf { + let mut dir = self.output_dir(core_id).clone(); + dir.push("queue"); + dir + } + + pub fn crashes_dir(&self, core_id: CoreId) -> PathBuf { + let mut dir = self.output_dir(core_id).clone(); + dir.push("crashes"); + dir + } + + pub fn is_cmplog_core(&self, core_id: CoreId) -> bool { + self.cmplog_cores + .as_ref() + .is_some_and(|c| c.contains(core_id)) + } + + pub fn validate(&self) { + if let Some(cmplog_cores) = &self.cmplog_cores { + for id in &cmplog_cores.ids { + if !self.cores.contains(*id) { + let mut cmd = FuzzerOptions::command(); + cmd.error( + ErrorKind::ValueValidation, + format!( + "Cmplog cores ({}) must be a subset of total cores ({})", + cmplog_cores.cmdline, self.cores.cmdline + ), + ) + .exit(); + } + } + } + } +}