From 3a62013c857b9733bc01e3ad55ed9441b95d467d Mon Sep 17 00:00:00 2001 From: WorksButNotTested <62701594+WorksButNotTested@users.noreply.github.com> Date: Wed, 21 May 2025 12:26:02 +0100 Subject: [PATCH] LibAFL_QEMU: Add redirect stdout module (#3256) * Add redirect stdout * Review changes --- fuzzers/binary_only/qemu_cmin/Justfile | 2 +- fuzzers/binary_only/qemu_cmin/src/fuzzer.rs | 55 ++++--- .../binary_only/qemu_coverage/src/fuzzer.rs | 2 +- fuzzers/binary_only/qemu_tmin/Justfile | 6 +- .../qemu_tmin/src/tmin_multi_core.rs | 29 +++- .../qemu_tmin/src/tmin_single_core.rs | 31 +++- libafl_qemu/src/modules/usermode/mod.rs | 3 + .../src/modules/usermode/redirect_stdout.rs | 155 ++++++++++++++++++ 8 files changed, 247 insertions(+), 36 deletions(-) create mode 100644 libafl_qemu/src/modules/usermode/redirect_stdout.rs diff --git a/fuzzers/binary_only/qemu_cmin/Justfile b/fuzzers/binary_only/qemu_cmin/Justfile index 3526aac249..96744f68e4 100644 --- a/fuzzers/binary_only/qemu_cmin/Justfile +++ b/fuzzers/binary_only/qemu_cmin/Justfile @@ -28,7 +28,7 @@ harness: libpng -lm -static [unix] -run: harness build +run: build harness {{ FUZZER }} \ --output ./output \ --input ./corpus \ diff --git a/fuzzers/binary_only/qemu_cmin/src/fuzzer.rs b/fuzzers/binary_only/qemu_cmin/src/fuzzer.rs index 9df44305a4..0b42c23225 100644 --- a/fuzzers/binary_only/qemu_cmin/src/fuzzer.rs +++ b/fuzzers/binary_only/qemu_cmin/src/fuzzer.rs @@ -1,6 +1,7 @@ //! A binary-only corpus minimizer using qemu, similar to AFL++ afl-cmin #[cfg(feature = "i386")] use core::mem::size_of; +use core::str::from_utf8; #[cfg(feature = "snapshot")] use core::time::Duration; use std::{env, fmt::Write, io, path::PathBuf, process, ptr::NonNull}; @@ -20,7 +21,6 @@ use libafl::{ Error, }; use libafl_bolts::{ - core_affinity::Cores, os::unix_signals::Signal, rands::StdRand, shmem::{ShMemProvider, StdShMemProvider}, @@ -30,8 +30,10 @@ use libafl_bolts::{ #[cfg(feature = "fork")] use libafl_qemu::QemuForkExecutor; use libafl_qemu::{ - elf::EasyElf, modules::edges::StdEdgeCoverageChildModule, ArchExtras, Emulator, GuestAddr, - GuestReg, MmapPerms, QemuExitError, QemuExitReason, QemuShutdownCause, Regs, + elf::EasyElf, + modules::{edges::StdEdgeCoverageChildModule, RedirectStdoutModule}, + ArchExtras, Emulator, GuestAddr, GuestReg, MmapPerms, QemuExitError, QemuExitReason, + QemuShutdownCause, Regs, }; #[cfg(feature = "snapshot")] use libafl_qemu::{modules::SnapshotModule, QemuExecutor}; @@ -86,12 +88,6 @@ pub struct FuzzerOptions { #[arg(long, help = "Timeout in seconds", default_value_t = 1_u64)] timeout: u64, - #[arg(long = "port", help = "Broker port", default_value_t = 1337_u16)] - port: u16, - - #[arg(long, help = "Cpu cores to use", default_value = "all", value_parser = Cores::from_cmdline)] - cores: Cores, - #[clap(short, long, help = "Enable output from the fuzzer clients")] verbose: bool, @@ -137,17 +133,38 @@ pub fn fuzz() -> Result<(), Error> { )) }; + let stdout_callback = |buf: &[u8]| { + if let Ok(s) = from_utf8(buf) { + let msg = s.trim_end(); + if msg.len() != 0 { + log::info!("{msg}"); + } + } + }; + + let redirect_stdout_module = if options.verbose { + RedirectStdoutModule::new() + .with_stderr(stdout_callback) + .with_stdout(stdout_callback) + } else { + RedirectStdoutModule::new() + }; + #[cfg(feature = "fork")] - let modules = tuple_list!(StdEdgeCoverageChildModule::builder() - .const_map_observer(edges_observer.as_mut()) - .build()?); + let modules = tuple_list!( + StdEdgeCoverageChildModule::builder() + .const_map_observer(edges_observer.as_mut()) + .build()?, + redirect_stdout_module + ); #[cfg(feature = "snapshot")] let modules = tuple_list!( StdEdgeCoverageChildModule::builder() .const_map_observer(edges_observer.as_mut()) .build()?, - SnapshotModule::new() + SnapshotModule::new(), + redirect_stdout_module, ); let emulator = Emulator::empty() @@ -180,9 +197,7 @@ pub fn fuzz() -> Result<(), Error> { let stack_ptr: GuestAddr = qemu.read_reg(Regs::Sp).unwrap(); - let monitor = SimpleMonitor::new(|s| { - println!("{s}"); - }); + let monitor = SimpleMonitor::new(|s| log::info!("{s}")); let (state, mut mgr) = match SimpleRestartingEventManager::launch(monitor, &mut shmem_provider) { Ok(res) => res, @@ -304,20 +319,20 @@ pub fn fuzz() -> Result<(), Error> { Duration::from_millis(5000), )?; - println!("Importing {} seeds...", files.len()); + log::info!("Importing {} seeds...", files.len()); if state.must_load_initial_inputs() { state .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &files) .unwrap_or_else(|_| { - println!("Failed to load initial corpus"); + log::error!("Failed to load initial corpus"); process::exit(0); }); - println!("Imported {} seeds from disk.", state.corpus().count()); + log::info!("Imported {} seeds from disk.", state.corpus().count()); } let size = state.corpus().count(); - println!( + log::info!( "Removed {} duplicates from {} seeds", files.len() - size, files.len() diff --git a/fuzzers/binary_only/qemu_coverage/src/fuzzer.rs b/fuzzers/binary_only/qemu_coverage/src/fuzzer.rs index 23b36f0531..5bd7cd2092 100644 --- a/fuzzers/binary_only/qemu_coverage/src/fuzzer.rs +++ b/fuzzers/binary_only/qemu_coverage/src/fuzzer.rs @@ -156,7 +156,7 @@ pub fn fuzz() { qemu.entry_break(test_one_input_ptr); let mappings = QemuMappingsViewer::new(&qemu); - println!("{:#?}", mappings); + log::info!("{:#?}", mappings); let pc: GuestReg = qemu.read_reg(Regs::Pc).unwrap(); log::info!("Break at {pc:#x}"); diff --git a/fuzzers/binary_only/qemu_tmin/Justfile b/fuzzers/binary_only/qemu_tmin/Justfile index b07aee7228..d6403f758c 100644 --- a/fuzzers/binary_only/qemu_tmin/Justfile +++ b/fuzzers/binary_only/qemu_tmin/Justfile @@ -30,20 +30,18 @@ harness: libpng -lm -static [unix] -run_single: harness build +run_single: build harness {{ FUZZER_SINGLE }} \ --output ./output \ --input ./corpus \ - --verbose \ -- {{ HARNESS }} [unix] -run_multi: harness build +run_multi: build harness {{ FUZZER_MULTI }} \ --output ./output \ --input ./corpus \ --cores 0 \ - --verbose \ -- {{ HARNESS }} [unix] diff --git a/fuzzers/binary_only/qemu_tmin/src/tmin_multi_core.rs b/fuzzers/binary_only/qemu_tmin/src/tmin_multi_core.rs index d4ae52a030..015abcf395 100644 --- a/fuzzers/binary_only/qemu_tmin/src/tmin_multi_core.rs +++ b/fuzzers/binary_only/qemu_tmin/src/tmin_multi_core.rs @@ -1,6 +1,7 @@ //! A binary-only testcase minimizer using qemu, similar to AFL++ afl-tmin #[cfg(feature = "i386")] use core::mem::size_of; +use core::str::from_utf8; #[cfg(feature = "snapshot")] use core::time::Duration; use std::{env, fmt::Write, io, path::PathBuf, process, ptr::NonNull}; @@ -32,8 +33,10 @@ use libafl_bolts::{ AsSlice, AsSliceMut, }; use libafl_qemu::{ - elf::EasyElf, modules::edges::StdEdgeCoverageChildModule, ArchExtras, Emulator, GuestAddr, - GuestReg, MmapPerms, QemuExitError, QemuExitReason, QemuShutdownCause, Regs, + elf::EasyElf, + modules::{edges::StdEdgeCoverageChildModule, RedirectStdoutModule}, + ArchExtras, Emulator, GuestAddr, GuestReg, MmapPerms, QemuExitError, QemuExitReason, + QemuShutdownCause, Regs, }; #[cfg(feature = "snapshot")] use libafl_qemu::{modules::SnapshotModule, QemuExecutor}; @@ -188,13 +191,31 @@ pub fn fuzz() { )) }; + let stdout_callback = |buf: &[u8]| { + if let Ok(s) = from_utf8(buf) { + let msg = s.trim_end(); + if msg.len() != 0 { + log::info!("{msg}"); + } + } + }; + + let redirect_stdout_module = if options.verbose { + RedirectStdoutModule::new() + .with_stderr(stdout_callback) + .with_stdout(stdout_callback) + } else { + RedirectStdoutModule::new() + }; + // In either fork/snapshot mode, we link the observer to QEMU #[cfg(feature = "snapshot")] let modules = tuple_list!( StdEdgeCoverageChildModule::builder() .const_map_observer(edges_observer.as_mut()) .build()?, - SnapshotModule::new() + SnapshotModule::new(), + redirect_stdout_module ); // Create our QEMU emulator @@ -342,7 +363,7 @@ pub fn fuzz() { .shmem_provider(shmem_provider.clone()) .broker_port(options.port) .configuration(EventConfig::from_build_id()) - .monitor(MultiMonitor::new(|s| println!("{s}"))) + .monitor(MultiMonitor::new(|s| log::info!("{s}"))) .run_client(&mut run_client) .cores(&options.cores) .build() diff --git a/fuzzers/binary_only/qemu_tmin/src/tmin_single_core.rs b/fuzzers/binary_only/qemu_tmin/src/tmin_single_core.rs index 4bf2015fdf..d5989d4cdd 100644 --- a/fuzzers/binary_only/qemu_tmin/src/tmin_single_core.rs +++ b/fuzzers/binary_only/qemu_tmin/src/tmin_single_core.rs @@ -1,6 +1,7 @@ //! A binary-only testcase minimizer using qemu, similar to AFL++ afl-tmin #[cfg(feature = "i386")] use core::mem::size_of; +use core::str::from_utf8; #[cfg(feature = "snapshot")] use core::time::Duration; use std::{env, fmt::Write, io, path::PathBuf, process, ptr::NonNull}; @@ -29,8 +30,10 @@ use libafl_bolts::{ AsSlice, AsSliceMut, }; use libafl_qemu::{ - elf::EasyElf, modules::edges::StdEdgeCoverageChildModule, ArchExtras, Emulator, GuestAddr, - GuestReg, MmapPerms, QemuExitError, QemuExitReason, QemuShutdownCause, Regs, + elf::EasyElf, + modules::{edges::StdEdgeCoverageChildModule, RedirectStdoutModule}, + ArchExtras, Emulator, GuestAddr, GuestReg, MmapPerms, QemuExitError, QemuExitReason, + QemuShutdownCause, Regs, }; #[cfg(feature = "snapshot")] use libafl_qemu::{modules::SnapshotModule, QemuExecutor}; @@ -146,12 +149,30 @@ pub fn fuzz() -> Result<(), Error> { )) }; + let stdout_callback = |buf: &[u8]| { + if let Ok(s) = from_utf8(buf) { + let msg = s.trim_end(); + if msg.len() != 0 { + log::info!("{msg}"); + } + } + }; + + let redirect_stdout_module = if options.verbose { + RedirectStdoutModule::new() + .with_stderr(stdout_callback) + .with_stdout(stdout_callback) + } else { + RedirectStdoutModule::new() + }; + #[cfg(feature = "snapshot")] let modules = tuple_list!( StdEdgeCoverageChildModule::builder() .const_map_observer(edges_observer.as_mut()) .build()?, - SnapshotModule::new() + SnapshotModule::new(), + redirect_stdout_module ); // Create our QEMU emulator @@ -223,9 +244,7 @@ pub fn fuzz() -> Result<(), Error> { }; // Set up the most basic monitor possible. - let monitor = SimpleMonitor::new(|s| { - println!("{s}"); - }); + let monitor = SimpleMonitor::new(|s| log::info!("{s}")); let (state, mut mgr) = match SimpleRestartingEventManager::launch(monitor, &mut shmem_provider) { Ok(res) => res, diff --git a/libafl_qemu/src/modules/usermode/mod.rs b/libafl_qemu/src/modules/usermode/mod.rs index f63ce25466..f5776251d3 100644 --- a/libafl_qemu/src/modules/usermode/mod.rs +++ b/libafl_qemu/src/modules/usermode/mod.rs @@ -19,3 +19,6 @@ pub mod asan_guest; pub use asan_guest::AsanGuestModule; pub mod redirect_stdin; pub use redirect_stdin::*; + +pub mod redirect_stdout; +pub use redirect_stdout::*; diff --git a/libafl_qemu/src/modules/usermode/redirect_stdout.rs b/libafl_qemu/src/modules/usermode/redirect_stdout.rs new file mode 100644 index 0000000000..07fdc6c18c --- /dev/null +++ b/libafl_qemu/src/modules/usermode/redirect_stdout.rs @@ -0,0 +1,155 @@ +use core::{ + fmt::{self, Debug}, + slice::from_raw_parts, +}; + +use libafl_bolts::HasLen; +use libafl_qemu_sys::GuestAddr; + +#[cfg(not(cpu_target = "hexagon"))] +use crate::SYS_write; +use crate::{ + Qemu, + emu::EmulatorModules, + modules::{EmulatorModule, EmulatorModuleTuple}, + qemu::{Hook, SyscallHookResult}, +}; + +#[cfg(cpu_target = "hexagon")] +/// Hexagon syscalls are not currently supported by the `syscalls` crate, so we just paste this here for now. +/// +#[expect(non_upper_case_globals)] +const SYS_write: u8 = 64; + +/// This module hijacks any read to buffer from stdin, and instead fill the buffer from the specified input address +/// This is useful when your binary target reads the input from the stdin. +/// With this you can just fuzz more like afl++ +/// You need to use this with snapshot module! +#[derive(Clone)] +pub struct RedirectStdoutModule +where + F: FnMut(&[u8]), +{ + stdout: Option, + stderr: Option, +} + +impl Debug for RedirectStdoutModule +where + F: FnMut(&[u8]), +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RedirectStdoutModule") + .finish_non_exhaustive() + } +} + +impl Default for RedirectStdoutModule +where + F: FnMut(&[u8]), +{ + fn default() -> Self { + Self::new() + } +} + +impl RedirectStdoutModule +where + F: FnMut(&[u8]), +{ + #[must_use] + /// constuctor + pub fn new() -> Self { + Self { + stdout: None, + stderr: None, + } + } +} + +impl RedirectStdoutModule +where + F: FnMut(&[u8]) + Clone, +{ + #[must_use] + /// Create with specified stdout callback + pub fn with_stdout(&self, stdout: F) -> Self { + Self { + stdout: Some(stdout), + stderr: self.stderr.clone(), + } + } + + #[must_use] + /// Create with specified stderr callback + pub fn with_stderr(&self, stderr: F) -> Self { + Self { + stdout: self.stdout.clone(), + stderr: Some(stderr), + } + } +} + +impl EmulatorModule for RedirectStdoutModule +where + I: Unpin + HasLen + Debug, + S: Unpin, + F: FnMut(&[u8]) + 'static, +{ + fn first_exec( + &mut self, + _qemu: Qemu, + emulator_modules: &mut EmulatorModules, + _state: &mut S, + ) where + ET: EmulatorModuleTuple, + { + emulator_modules.pre_syscalls(Hook::Function(syscall_write_hook::)); + } +} + +#[expect(clippy::too_many_arguments)] +fn syscall_write_hook( + _qemu: Qemu, + emulator_modules: &mut EmulatorModules, + _state: Option<&mut S>, + syscall: i32, + x0: GuestAddr, + x1: GuestAddr, + x2: GuestAddr, + _x3: GuestAddr, + _x4: GuestAddr, + _x5: GuestAddr, + _x6: GuestAddr, + _x7: GuestAddr, +) -> SyscallHookResult +where + ET: EmulatorModuleTuple, + I: Unpin + HasLen + Debug, + S: Unpin, + F: FnMut(&[u8]) + 'static, +{ + let h = emulator_modules + .get_mut::>() + .unwrap(); + if syscall != SYS_write as i32 { + return SyscallHookResult::Run; + } + + let fd = x0 as i32; + let buf = x1 as *const u8; + let len = x2; + + let callback = match fd { + libc::STDOUT_FILENO => h.stdout.as_mut(), + libc::STDERR_FILENO => h.stderr.as_mut(), + _ => return SyscallHookResult::Run, + }; + + if let Some(callback) = callback { + let buf = unsafe { from_raw_parts(buf, len as usize) }; + (callback)(buf); + } + + SyscallHookResult::Skip(len) +}