diff --git a/libafl_nyx/Cargo.toml b/libafl_nyx/Cargo.toml index 0ebe149d6d..66340a330e 100644 --- a/libafl_nyx/Cargo.toml +++ b/libafl_nyx/Cargo.toml @@ -38,6 +38,12 @@ 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" +hex = "0.4.3" +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..183cffedb0 --- /dev/null +++ b/libafl_nyx/src/cmplog.rs @@ -0,0 +1,222 @@ +//! 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" => return Ok(Self::Str), + "CMP" => return Ok(Self::Cmp), + "SUB" => return 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).map(|_x| true).unwrap_or(false); + + 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::decode(lhs_s).map_err(|e| format!("Failed to decode LHS: '{}' - {}", lhs_s, e))?; + let rhs = + hex::decode(rhs_s).map_err(|e| format!("Failed to decode RHS: '{}' - {}", rhs_s, e))?; + + Ok(Self { + addr, + bp_type, + size, + lhs, + rhs, + imm, + }) + } +} + +#[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::>(); + return RedqueenInfo { bps }; +} + +impl TryInto for RedqueenEvent { + type Error = String; + + fn try_into(self) -> Result { + match self.bp_type { + RedqueenBPType::Cmp => { + return 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 c83e35ade2..16f705b5e0 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 { @@ -88,6 +88,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, @@ -124,6 +131,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..9ea442284d 100644 --- a/libafl_nyx/src/lib.rs +++ b/libafl_nyx/src/lib.rs @@ -4,3 +4,5 @@ pub mod executor; pub mod helper; #[cfg(target_os = "linux")] pub mod settings; +#[cfg(target_os = "linux")] +pub mod cmplog;