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:
parent
2e26af90db
commit
faeed19c43
1
.github/workflows/build_and_test.yml
vendored
1
.github/workflows/build_and_test.yml
vendored
@ -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
|
||||
|
||||
|
41
fuzzers/full_system/nyx_launcher/Cargo.toml
Normal file
41
fuzzers/full_system/nyx_launcher/Cargo.toml
Normal 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" }
|
11
fuzzers/full_system/nyx_launcher/README.md
Normal file
11
fuzzers/full_system/nyx_launcher/README.md
Normal 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
|
||||
```
|
42
fuzzers/full_system/nyx_launcher/src/client.rs
Normal file
42
fuzzers/full_system/nyx_launcher/src/client.rs
Normal 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)
|
||||
}
|
||||
}
|
153
fuzzers/full_system/nyx_launcher/src/fuzzer.rs
Normal file
153
fuzzers/full_system/nyx_launcher/src/fuzzer.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
258
fuzzers/full_system/nyx_launcher/src/instance.rs
Normal file
258
fuzzers/full_system/nyx_launcher/src/instance.rs
Normal 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(())
|
||||
}
|
||||
}
|
22
fuzzers/full_system/nyx_launcher/src/main.rs
Normal file
22
fuzzers/full_system/nyx_launcher/src/main.rs
Normal 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!");
|
||||
}
|
112
fuzzers/full_system/nyx_launcher/src/options.rs
Normal file
112
fuzzers/full_system/nyx_launcher/src/options.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
232
libafl_nyx/src/cmplog.rs
Normal file
232
libafl_nyx/src/cmplog.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
@ -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<S, OT> {
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user