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 <domenukk@gmail.com>
This commit is contained in:
Konstantin Bücheler 2025-01-18 13:21:04 +01:00 committed by GitHub
parent 2e26af90db
commit faeed19c43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 902 additions and 1 deletions

View File

@ -276,6 +276,7 @@ jobs:
- ./fuzzers/forkserver/baby_fuzzer_with_forkexecutor - ./fuzzers/forkserver/baby_fuzzer_with_forkexecutor
# Full-system # Full-system
- ./fuzzers/full_system/nyx_launcher
- ./fuzzers/full_system/nyx_libxml2_standalone - ./fuzzers/full_system/nyx_libxml2_standalone
- ./fuzzers/full_system/nyx_libxml2_parallel - ./fuzzers/full_system/nyx_libxml2_parallel

View File

@ -0,0 +1,41 @@
[package]
name = "nyx_launcher"
version = "0.14.1"
authors = ["Konstantin Bücheler <buecheko@protonmail.com>"]
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" }

View File

@ -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
```

View File

@ -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<BytesInput, InMemoryOnDiskCorpus<BytesInput>, StdRand, OnDiskCorpus<BytesInput>>;
pub struct Client<'a> {
options: &'a FuzzerOptions,
}
impl Client<'_> {
pub fn new(options: &FuzzerOptions) -> Client {
Client { options }
}
pub fn run<M: Monitor>(
&self,
state: Option<ClientState>,
mgr: ClientMgr<M>,
client_description: ClientDescription,
) -> Result<(), Error> {
let instance = Instance::builder()
.options(self.options)
.mgr(mgr)
.client_description(client_description);
instance.build().run(state)
}
}

View File

@ -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<M>(&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),
}
}
}

View File

@ -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<BytesInput, InMemoryOnDiskCorpus<BytesInput>, StdRand, OnDiskCorpus<BytesInput>>;
pub type ClientMgr<M> =
MonitorTypedEventManager<LlmpRestartingEventManager<(), ClientState, StdShMemProvider>, 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<M>,
client_description: ClientDescription,
#[builder(default=PhantomData)]
phantom: PhantomData<M>,
}
impl<M: Monitor> Instance<'_, M> {
pub fn run(mut self, state: Option<ClientState>) -> 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<Z, E, ST>(
&mut self,
state: &mut ClientState,
fuzzer: &mut Z,
executor: &mut E,
stages: &mut ST,
) -> Result<(), Error>
where
Z: Fuzzer<E, ClientMgr<M>, ClientState, ST>
+ Evaluator<E, ClientMgr<M>, BytesInput, ClientState>,
ST: StagesTuple<E, ClientMgr<M>, 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(())
}
}

View File

@ -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!");
}

View File

@ -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<String>,
#[arg(long, help = "Log file")]
pub log: Option<String>,
#[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<Cores>,
#[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<u64>,
#[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<PathBuf>,
}
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();
}
}
}
}
}

View File

@ -38,6 +38,11 @@ libafl_targets = { workspace = true, default-features = true, features = [
nix = { workspace = true, default-features = true, features = ["fs"] } nix = { workspace = true, default-features = true, features = ["fs"] }
typed-builder = { workspace = true } 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] [lints]
workspace = true workspace = true

232
libafl_nyx/src/cmplog.rs Normal file
View File

@ -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<I, S> Observer<I, S> 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<RedqueenBpType, String> {
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<u8>,
pub rhs: Vec<u8>,
pub imm: bool,
}
impl RedqueenEvent {
fn new(line: &str) -> Result<Self, String> {
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::<usize>()
.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<Vec<u8>> {
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<RedqueenEvent>,
}
fn parse_redqueen_data(data: &str) -> RedqueenInfo {
let bps = data
.lines()
.filter_map(|line| RedqueenEvent::new(line).ok())
.collect::<Vec<_>>();
RedqueenInfo { bps }
}
impl TryInto<CmpValues> for RedqueenEvent {
type Error = String;
fn try_into(self) -> Result<CmpValues, Self::Error> {
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()),
}
}
}

View File

@ -14,7 +14,7 @@ use libafl::{
use libafl_bolts::{tuples::RefIndexable, AsSlice}; use libafl_bolts::{tuples::RefIndexable, AsSlice};
use libnyx::NyxReturnValue; use libnyx::NyxReturnValue;
use crate::helper::NyxHelper; use crate::{cmplog::CMPLOG_ENABLED, helper::NyxHelper};
/// executor for nyx standalone mode /// executor for nyx standalone mode
pub struct NyxExecutor<S, OT> { pub struct NyxExecutor<S, OT> {
@ -80,6 +80,13 @@ where
self.helper.nyx_process.set_input(buffer, size); self.helper.nyx_process.set_input(buffer, size);
self.helper.nyx_process.set_hprintf_fd(hprintf_fd); 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 // exec will take care of trace_bits, so no need to reset
let exit_kind = match self.helper.nyx_process.exec() { let exit_kind = match self.helper.nyx_process.exec() {
NyxReturnValue::Normal => ExitKind::Ok, NyxReturnValue::Normal => ExitKind::Ok,
@ -116,6 +123,13 @@ where
ob.observe_stdout(&stdout); 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) Ok(exit_kind)
} }
} }

View File

@ -9,6 +9,7 @@ use crate::settings::NyxSettings;
pub struct NyxHelper { pub struct NyxHelper {
pub nyx_process: NyxProcess, pub nyx_process: NyxProcess,
pub nyx_stdout: File, pub nyx_stdout: File,
pub redqueen_path: String,
pub timeout: Duration, pub timeout: Duration,
@ -71,9 +72,16 @@ impl NyxHelper {
let mut timeout = Duration::from_secs(u64::from(settings.timeout_secs)); let mut timeout = Duration::from_secs(u64::from(settings.timeout_secs));
timeout += Duration::from_micros(u64::from(settings.timeout_micro_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 { Ok(Self {
nyx_process, nyx_process,
nyx_stdout, nyx_stdout,
redqueen_path,
timeout, timeout,
bitmap_size, bitmap_size,
bitmap_buffer, bitmap_buffer,

View File

@ -1,4 +1,6 @@
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub mod cmplog;
#[cfg(target_os = "linux")]
pub mod executor; pub mod executor;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub mod helper; pub mod helper;