From faeed19c430b91373e64f0b01688d63da970ebce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20B=C3=BCcheler?= Date: Sat, 18 Jan 2025 13:21:04 +0100 Subject: [PATCH] Add NyxCmpObserver and nyx_launcher example fuzzer (#2826) * Add NyxCmpObserver to libafl_nyx * Add nyx_launcher example fuzzer * Cargo Format/Clippy * Adapt to naming scheme * Taplo fmt * Add hex decode function to remove hex dependency * Add nyx_launcher to CI * Remove UsesState --------- Co-authored-by: Dominik Maier --- .github/workflows/build_and_test.yml | 1 + fuzzers/full_system/nyx_launcher/Cargo.toml | 41 +++ fuzzers/full_system/nyx_launcher/README.md | 11 + .../full_system/nyx_launcher/src/client.rs | 42 +++ .../full_system/nyx_launcher/src/fuzzer.rs | 153 +++++++++++ .../full_system/nyx_launcher/src/instance.rs | 258 ++++++++++++++++++ fuzzers/full_system/nyx_launcher/src/main.rs | 22 ++ .../full_system/nyx_launcher/src/options.rs | 112 ++++++++ libafl_nyx/Cargo.toml | 5 + libafl_nyx/src/cmplog.rs | 232 ++++++++++++++++ libafl_nyx/src/executor.rs | 16 +- libafl_nyx/src/helper.rs | 8 + libafl_nyx/src/lib.rs | 2 + 13 files changed, 902 insertions(+), 1 deletion(-) 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 create mode 100644 libafl_nyx/src/cmplog.rs diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 2d37cd3d17..d0a7575946 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -276,6 +276,7 @@ jobs: - ./fuzzers/forkserver/baby_fuzzer_with_forkexecutor # Full-system + - ./fuzzers/full_system/nyx_launcher - ./fuzzers/full_system/nyx_libxml2_standalone - ./fuzzers/full_system/nyx_libxml2_parallel diff --git a/fuzzers/full_system/nyx_launcher/Cargo.toml b/fuzzers/full_system/nyx_launcher/Cargo.toml new file mode 100644 index 0000000000..573f7900ec --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "nyx_launcher" +version = "0.14.1" +authors = ["Konstantin Bücheler "] +edition = "2021" + +[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..ceda37df4b --- /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 Client<'_> { + 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..1b1e8cfbd3 --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/src/fuzzer.rs @@ -0,0 +1,153 @@ +use std::{ + cell::RefCell, + fs::{File, OpenOptions}, + io::{self, Write}, +}; + +use clap::Parser; +use libafl::{ + events::{ + ClientDescription, EventConfig, Launcher, LlmpEventManager, LlmpRestartingEventManager, + MonitorTypedEventManager, + }, + monitors::{tui::TuiMonitor, Monitor, MultiMonitor}, + Error, +}; +use libafl_bolts::{ + core_affinity::CoreId, + current_time, + llmp::LlmpBroker, + shmem::{ShMemProvider, StdShMemProvider}, + 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..39c34e0348 --- /dev/null +++ b/fuzzers/full_system/nyx_launcher/src/instance.rs @@ -0,0 +1,258 @@ +use std::{marker::PhantomData, process}; + +use libafl::{ + corpus::{Corpus, InMemoryOnDiskCorpus, OnDiskCorpus}, + events::{ + ClientDescription, EventRestarter, LlmpRestartingEventManager, MonitorTypedEventManager, + NopEventManager, + }, + executors::{Executor, ShadowExecutor}, + feedback_and_fast, feedback_or, feedback_or_fast, + feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback}, + fuzzer::{Evaluator, Fuzzer, StdFuzzer}, + inputs::BytesInput, + monitors::Monitor, + mutators::{ + havoc_mutations, tokens_mutations, I2SRandReplace, StdMOptMutator, StdScheduledMutator, + Tokens, + }, + observers::{CanTrack, HitcountsMapObserver, StdMapObserver, TimeObserver}, + schedulers::{ + powersched::PowerSchedule, IndexesLenTimeMinimizerScheduler, PowerQueueScheduler, + }, + stages::{ + power::StdPowerMutationalStage, CalibrationStage, ShadowTracingStage, StagesTuple, + StdMutationalStage, + }, + state::{HasCorpus, HasMaxSize, StdState}, + Error, HasMetadata, NopFuzzer, +}; +use libafl_bolts::{ + current_nanos, + rands::StdRand, + shmem::StdShMemProvider, + 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 Instance<'_, 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)) + .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>, + 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(); + } + } + } + } +} diff --git a/libafl_nyx/Cargo.toml b/libafl_nyx/Cargo.toml index 0ebe149d6d..4e9f89ea50 100644 --- a/libafl_nyx/Cargo.toml +++ b/libafl_nyx/Cargo.toml @@ -38,6 +38,11 @@ libafl_targets = { workspace = true, default-features = true, features = [ nix = { workspace = true, default-features = true, features = ["fs"] } typed-builder = { workspace = true } +lazy_static = "1.5.0" +regex = "1.11.1" +serde = { version = "1.0.210", default-features = false, features = [ + "alloc", +] } # serialization lib [lints] workspace = true diff --git a/libafl_nyx/src/cmplog.rs b/libafl_nyx/src/cmplog.rs new file mode 100644 index 0000000000..2542ed9b8a --- /dev/null +++ b/libafl_nyx/src/cmplog.rs @@ -0,0 +1,232 @@ +//! The Nyx `CmpLog` Observer +//! +//! Reads and parses the redqueen results written by QEMU-Nyx and adds them to the state as `CmpValuesMetadata`. +use std::borrow::Cow; + +use lazy_static::lazy_static; +use libafl::{ + executors::ExitKind, + observers::{CmpValues, CmpValuesMetadata, Observer}, + state::HasExecutions, + Error, HasMetadata, +}; +use libafl_bolts::Named; +pub use libafl_targets::{ + cmps::{ + __libafl_targets_cmplog_instructions, __libafl_targets_cmplog_routines, CMPLOG_ENABLED, + }, + CmpLogMap, CmpLogObserver, CMPLOG_MAP_H, CMPLOG_MAP_PTR, CMPLOG_MAP_SIZE, CMPLOG_MAP_W, +}; +use serde::{Deserialize, Serialize}; + +/// A [`CmpObserver`] observer for Nyx +#[derive(Serialize, Deserialize, Debug)] +pub struct NyxCmpObserver { + /// Observer name + name: Cow<'static, str>, + /// Path to redqueen results file + path: Cow<'static, str>, + add_meta: bool, +} + +impl NyxCmpObserver { + /// Creates a new [`struct@NyxCmpObserver`] with the given filepath. + #[must_use] + pub fn new(name: &'static str, path: String, add_meta: bool) -> Self { + Self { + name: Cow::from(name), + path: Cow::from(path), + add_meta, + } + } +} + +impl Observer for NyxCmpObserver +where + S: HasMetadata + HasExecutions, + I: std::fmt::Debug, +{ + fn pre_exec(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> { + unsafe { + CMPLOG_ENABLED = 1; + } + Ok(()) + } + + fn post_exec(&mut self, state: &mut S, _input: &I, _exit_kind: &ExitKind) -> Result<(), Error> { + unsafe { + CMPLOG_ENABLED = 0; + } + if self.add_meta { + let meta = state.metadata_or_insert_with(CmpValuesMetadata::new); + let rq_data = parse_redqueen_data(&std::fs::read_to_string(self.path.as_ref())?); + for event in rq_data.bps { + if let Ok(cmp_value) = event.try_into() { + meta.list.push(cmp_value); + } + } + } + Ok(()) + } +} + +impl Named for NyxCmpObserver { + fn name(&self) -> &Cow<'static, str> { + &self.name + } +} + +// Based on https://github.com/nyx-fuzz/spec-fuzzer/blob/main/rust_fuzzer/src/runner.rs +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum RedqueenBpType { + Str, + Cmp, + Sub, +} + +impl RedqueenBpType { + fn new(data: &str) -> Result { + match data { + "STR" => Ok(Self::Str), + "CMP" => Ok(Self::Cmp), + "SUB" => Ok(Self::Sub), + _ => Err("Unknown redqueen type".to_string()), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +struct RedqueenEvent { + pub addr: u64, + pub bp_type: RedqueenBpType, + pub size: usize, + pub lhs: Vec, + pub rhs: Vec, + pub imm: bool, +} + +impl RedqueenEvent { + fn new(line: &str) -> Result { + lazy_static! { + static ref RE: regex::Regex = regex::Regex::new( + r"([0-9a-fA-F]+)\s+(CMP|SUB|STR)\s+(\d+)\s+([0-9a-fA-F]+)-([0-9a-fA-F]+)(\sIMM)?" + ) + .expect("Invalid regex pattern"); + } + + let captures = RE + .captures(line) + .ok_or_else(|| format!("Failed to parse Redqueen line: '{line}'"))?; + + let addr_s = captures.get(1).ok_or("Missing address field")?.as_str(); + let type_s = captures.get(2).ok_or("Missing type field")?.as_str(); + let size_s = captures.get(3).ok_or("Missing size field")?.as_str(); + let lhs_s = captures.get(4).ok_or("Missing LHS field")?.as_str(); + let rhs_s = captures.get(5).ok_or("Missing RHS field")?.as_str(); + let imm = captures.get(6).is_some_and(|_x| true); + + let addr = + u64::from_str_radix(addr_s, 16).map_err(|_| format!("Invalid address: '{addr_s}'"))?; + let bp_type = RedqueenBpType::new(type_s) + .map_err(|e| format!("Invalid redqueen type: '{type_s}' - {e}"))?; + let size = size_s + .parse::() + .map_err(|_| format!("Invalid size: '{size_s}'"))?; + let lhs = hex_to_bytes(lhs_s).ok_or("Decoding LHS failed")?; + let rhs = hex_to_bytes(rhs_s).ok_or("Decoding RHS failed")?; + + Ok(Self { + addr, + bp_type, + size, + lhs, + rhs, + imm, + }) + } +} + +fn hex_to_bytes(s: &str) -> Option> { + if s.len() % 2 == 0 { + (0..s.len()) + .step_by(2) + .map(|i| { + s.get(i..i + 2) + .and_then(|sub| u8::from_str_radix(sub, 16).ok()) + }) + .collect() + } else { + None + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +struct RedqueenInfo { + bps: Vec, +} + +fn parse_redqueen_data(data: &str) -> RedqueenInfo { + let bps = data + .lines() + .filter_map(|line| RedqueenEvent::new(line).ok()) + .collect::>(); + RedqueenInfo { bps } +} + +impl TryInto for RedqueenEvent { + type Error = String; + + fn try_into(self) -> Result { + match self.bp_type { + RedqueenBpType::Cmp => match self.size { + 8 => Ok(CmpValues::U8(( + *self.rhs.first().ok_or("Invalid RHS length for U8")?, + *self.lhs.first().ok_or("Invalid LHS length for U8")?, + self.imm, + ))), + 16 => Ok(CmpValues::U16(( + u16::from_be_bytes( + self.rhs + .try_into() + .map_err(|_| "Invalid RHS length for U16")?, + ), + u16::from_be_bytes( + self.lhs + .try_into() + .map_err(|_| "Invalid LHS length for U16")?, + ), + self.imm, + ))), + 32 => Ok(CmpValues::U32(( + u32::from_be_bytes( + self.rhs + .try_into() + .map_err(|_| "Invalid RHS length for U32")?, + ), + u32::from_be_bytes( + self.lhs + .try_into() + .map_err(|_| "Invalid LHS length for U32")?, + ), + self.imm, + ))), + 64 => Ok(CmpValues::U64(( + u64::from_be_bytes( + self.rhs + .try_into() + .map_err(|_| "Invalid RHS length for U64")?, + ), + u64::from_be_bytes( + self.lhs + .try_into() + .map_err(|_| "Invalid LHS length for U64")?, + ), + self.imm, + ))), + _ => Err("Invalid size".to_string()), + }, + // TODO: Add encoding for `STR` and `SUB` + _ => Err("Redqueen type not implemented".to_string()), + } + } +} diff --git a/libafl_nyx/src/executor.rs b/libafl_nyx/src/executor.rs index f3330c638d..7b51f9b86b 100644 --- a/libafl_nyx/src/executor.rs +++ b/libafl_nyx/src/executor.rs @@ -14,7 +14,7 @@ use libafl::{ use libafl_bolts::{tuples::RefIndexable, AsSlice}; use libnyx::NyxReturnValue; -use crate::helper::NyxHelper; +use crate::{cmplog::CMPLOG_ENABLED, helper::NyxHelper}; /// executor for nyx standalone mode pub struct NyxExecutor { @@ -80,6 +80,13 @@ where self.helper.nyx_process.set_input(buffer, size); self.helper.nyx_process.set_hprintf_fd(hprintf_fd); + unsafe { + if CMPLOG_ENABLED == 1 { + self.helper.nyx_process.option_set_redqueen_mode(true); + self.helper.nyx_process.option_apply(); + } + } + // exec will take care of trace_bits, so no need to reset let exit_kind = match self.helper.nyx_process.exec() { NyxReturnValue::Normal => ExitKind::Ok, @@ -116,6 +123,13 @@ where ob.observe_stdout(&stdout); } + unsafe { + if CMPLOG_ENABLED == 1 { + self.helper.nyx_process.option_set_redqueen_mode(false); + self.helper.nyx_process.option_apply(); + } + } + Ok(exit_kind) } } diff --git a/libafl_nyx/src/helper.rs b/libafl_nyx/src/helper.rs index 46673725be..dc58fe79aa 100644 --- a/libafl_nyx/src/helper.rs +++ b/libafl_nyx/src/helper.rs @@ -9,6 +9,7 @@ use crate::settings::NyxSettings; pub struct NyxHelper { pub nyx_process: NyxProcess, pub nyx_stdout: File, + pub redqueen_path: String, pub timeout: Duration, @@ -71,9 +72,16 @@ impl NyxHelper { let mut timeout = Duration::from_secs(u64::from(settings.timeout_secs)); timeout += Duration::from_micros(u64::from(settings.timeout_micro_secs)); + let redqueen_path = format!( + "{}/redqueen_workdir_{}/redqueen_results.txt", + nyx_config.workdir_path(), + nyx_config.worker_id() + ); + Ok(Self { nyx_process, nyx_stdout, + redqueen_path, timeout, bitmap_size, bitmap_buffer, diff --git a/libafl_nyx/src/lib.rs b/libafl_nyx/src/lib.rs index a1e3ffb763..37d9daa418 100644 --- a/libafl_nyx/src/lib.rs +++ b/libafl_nyx/src/lib.rs @@ -1,4 +1,6 @@ #[cfg(target_os = "linux")] +pub mod cmplog; +#[cfg(target_os = "linux")] pub mod executor; #[cfg(target_os = "linux")] pub mod helper;