LibAFL_QEMU: Add redirect stdout module (#3256)

* Add redirect stdout

* Review changes
This commit is contained in:
WorksButNotTested 2025-05-21 12:26:02 +01:00 committed by GitHub
parent 0b25d723c0
commit 3a62013c85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 247 additions and 36 deletions

View File

@ -28,7 +28,7 @@ harness: libpng
-lm -static -lm -static
[unix] [unix]
run: harness build run: build harness
{{ FUZZER }} \ {{ FUZZER }} \
--output ./output \ --output ./output \
--input ./corpus \ --input ./corpus \

View File

@ -1,6 +1,7 @@
//! A binary-only corpus minimizer using qemu, similar to AFL++ afl-cmin //! A binary-only corpus minimizer using qemu, similar to AFL++ afl-cmin
#[cfg(feature = "i386")] #[cfg(feature = "i386")]
use core::mem::size_of; use core::mem::size_of;
use core::str::from_utf8;
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
use core::time::Duration; use core::time::Duration;
use std::{env, fmt::Write, io, path::PathBuf, process, ptr::NonNull}; use std::{env, fmt::Write, io, path::PathBuf, process, ptr::NonNull};
@ -20,7 +21,6 @@ use libafl::{
Error, Error,
}; };
use libafl_bolts::{ use libafl_bolts::{
core_affinity::Cores,
os::unix_signals::Signal, os::unix_signals::Signal,
rands::StdRand, rands::StdRand,
shmem::{ShMemProvider, StdShMemProvider}, shmem::{ShMemProvider, StdShMemProvider},
@ -30,8 +30,10 @@ use libafl_bolts::{
#[cfg(feature = "fork")] #[cfg(feature = "fork")]
use libafl_qemu::QemuForkExecutor; use libafl_qemu::QemuForkExecutor;
use libafl_qemu::{ use libafl_qemu::{
elf::EasyElf, modules::edges::StdEdgeCoverageChildModule, ArchExtras, Emulator, GuestAddr, elf::EasyElf,
GuestReg, MmapPerms, QemuExitError, QemuExitReason, QemuShutdownCause, Regs, modules::{edges::StdEdgeCoverageChildModule, RedirectStdoutModule},
ArchExtras, Emulator, GuestAddr, GuestReg, MmapPerms, QemuExitError, QemuExitReason,
QemuShutdownCause, Regs,
}; };
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
use libafl_qemu::{modules::SnapshotModule, QemuExecutor}; use libafl_qemu::{modules::SnapshotModule, QemuExecutor};
@ -86,12 +88,6 @@ pub struct FuzzerOptions {
#[arg(long, help = "Timeout in seconds", default_value_t = 1_u64)] #[arg(long, help = "Timeout in seconds", default_value_t = 1_u64)]
timeout: 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")] #[clap(short, long, help = "Enable output from the fuzzer clients")]
verbose: bool, 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")] #[cfg(feature = "fork")]
let modules = tuple_list!(StdEdgeCoverageChildModule::builder() let modules = tuple_list!(
StdEdgeCoverageChildModule::builder()
.const_map_observer(edges_observer.as_mut()) .const_map_observer(edges_observer.as_mut())
.build()?); .build()?,
redirect_stdout_module
);
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
let modules = tuple_list!( let modules = tuple_list!(
StdEdgeCoverageChildModule::builder() StdEdgeCoverageChildModule::builder()
.const_map_observer(edges_observer.as_mut()) .const_map_observer(edges_observer.as_mut())
.build()?, .build()?,
SnapshotModule::new() SnapshotModule::new(),
redirect_stdout_module,
); );
let emulator = Emulator::empty() let emulator = Emulator::empty()
@ -180,9 +197,7 @@ pub fn fuzz() -> Result<(), Error> {
let stack_ptr: GuestAddr = qemu.read_reg(Regs::Sp).unwrap(); let stack_ptr: GuestAddr = qemu.read_reg(Regs::Sp).unwrap();
let monitor = SimpleMonitor::new(|s| { let monitor = SimpleMonitor::new(|s| log::info!("{s}"));
println!("{s}");
});
let (state, mut mgr) = match SimpleRestartingEventManager::launch(monitor, &mut shmem_provider) let (state, mut mgr) = match SimpleRestartingEventManager::launch(monitor, &mut shmem_provider)
{ {
Ok(res) => res, Ok(res) => res,
@ -304,20 +319,20 @@ pub fn fuzz() -> Result<(), Error> {
Duration::from_millis(5000), Duration::from_millis(5000),
)?; )?;
println!("Importing {} seeds...", files.len()); log::info!("Importing {} seeds...", files.len());
if state.must_load_initial_inputs() { if state.must_load_initial_inputs() {
state state
.load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &files) .load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &files)
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
println!("Failed to load initial corpus"); log::error!("Failed to load initial corpus");
process::exit(0); 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(); let size = state.corpus().count();
println!( log::info!(
"Removed {} duplicates from {} seeds", "Removed {} duplicates from {} seeds",
files.len() - size, files.len() - size,
files.len() files.len()

View File

@ -156,7 +156,7 @@ pub fn fuzz() {
qemu.entry_break(test_one_input_ptr); qemu.entry_break(test_one_input_ptr);
let mappings = QemuMappingsViewer::new(&qemu); let mappings = QemuMappingsViewer::new(&qemu);
println!("{:#?}", mappings); log::info!("{:#?}", mappings);
let pc: GuestReg = qemu.read_reg(Regs::Pc).unwrap(); let pc: GuestReg = qemu.read_reg(Regs::Pc).unwrap();
log::info!("Break at {pc:#x}"); log::info!("Break at {pc:#x}");

View File

@ -30,20 +30,18 @@ harness: libpng
-lm -static -lm -static
[unix] [unix]
run_single: harness build run_single: build harness
{{ FUZZER_SINGLE }} \ {{ FUZZER_SINGLE }} \
--output ./output \ --output ./output \
--input ./corpus \ --input ./corpus \
--verbose \
-- {{ HARNESS }} -- {{ HARNESS }}
[unix] [unix]
run_multi: harness build run_multi: build harness
{{ FUZZER_MULTI }} \ {{ FUZZER_MULTI }} \
--output ./output \ --output ./output \
--input ./corpus \ --input ./corpus \
--cores 0 \ --cores 0 \
--verbose \
-- {{ HARNESS }} -- {{ HARNESS }}
[unix] [unix]

View File

@ -1,6 +1,7 @@
//! A binary-only testcase minimizer using qemu, similar to AFL++ afl-tmin //! A binary-only testcase minimizer using qemu, similar to AFL++ afl-tmin
#[cfg(feature = "i386")] #[cfg(feature = "i386")]
use core::mem::size_of; use core::mem::size_of;
use core::str::from_utf8;
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
use core::time::Duration; use core::time::Duration;
use std::{env, fmt::Write, io, path::PathBuf, process, ptr::NonNull}; use std::{env, fmt::Write, io, path::PathBuf, process, ptr::NonNull};
@ -32,8 +33,10 @@ use libafl_bolts::{
AsSlice, AsSliceMut, AsSlice, AsSliceMut,
}; };
use libafl_qemu::{ use libafl_qemu::{
elf::EasyElf, modules::edges::StdEdgeCoverageChildModule, ArchExtras, Emulator, GuestAddr, elf::EasyElf,
GuestReg, MmapPerms, QemuExitError, QemuExitReason, QemuShutdownCause, Regs, modules::{edges::StdEdgeCoverageChildModule, RedirectStdoutModule},
ArchExtras, Emulator, GuestAddr, GuestReg, MmapPerms, QemuExitError, QemuExitReason,
QemuShutdownCause, Regs,
}; };
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
use libafl_qemu::{modules::SnapshotModule, QemuExecutor}; 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 // In either fork/snapshot mode, we link the observer to QEMU
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
let modules = tuple_list!( let modules = tuple_list!(
StdEdgeCoverageChildModule::builder() StdEdgeCoverageChildModule::builder()
.const_map_observer(edges_observer.as_mut()) .const_map_observer(edges_observer.as_mut())
.build()?, .build()?,
SnapshotModule::new() SnapshotModule::new(),
redirect_stdout_module
); );
// Create our QEMU emulator // Create our QEMU emulator
@ -342,7 +363,7 @@ pub fn fuzz() {
.shmem_provider(shmem_provider.clone()) .shmem_provider(shmem_provider.clone())
.broker_port(options.port) .broker_port(options.port)
.configuration(EventConfig::from_build_id()) .configuration(EventConfig::from_build_id())
.monitor(MultiMonitor::new(|s| println!("{s}"))) .monitor(MultiMonitor::new(|s| log::info!("{s}")))
.run_client(&mut run_client) .run_client(&mut run_client)
.cores(&options.cores) .cores(&options.cores)
.build() .build()

View File

@ -1,6 +1,7 @@
//! A binary-only testcase minimizer using qemu, similar to AFL++ afl-tmin //! A binary-only testcase minimizer using qemu, similar to AFL++ afl-tmin
#[cfg(feature = "i386")] #[cfg(feature = "i386")]
use core::mem::size_of; use core::mem::size_of;
use core::str::from_utf8;
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
use core::time::Duration; use core::time::Duration;
use std::{env, fmt::Write, io, path::PathBuf, process, ptr::NonNull}; use std::{env, fmt::Write, io, path::PathBuf, process, ptr::NonNull};
@ -29,8 +30,10 @@ use libafl_bolts::{
AsSlice, AsSliceMut, AsSlice, AsSliceMut,
}; };
use libafl_qemu::{ use libafl_qemu::{
elf::EasyElf, modules::edges::StdEdgeCoverageChildModule, ArchExtras, Emulator, GuestAddr, elf::EasyElf,
GuestReg, MmapPerms, QemuExitError, QemuExitReason, QemuShutdownCause, Regs, modules::{edges::StdEdgeCoverageChildModule, RedirectStdoutModule},
ArchExtras, Emulator, GuestAddr, GuestReg, MmapPerms, QemuExitError, QemuExitReason,
QemuShutdownCause, Regs,
}; };
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
use libafl_qemu::{modules::SnapshotModule, QemuExecutor}; 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")] #[cfg(feature = "snapshot")]
let modules = tuple_list!( let modules = tuple_list!(
StdEdgeCoverageChildModule::builder() StdEdgeCoverageChildModule::builder()
.const_map_observer(edges_observer.as_mut()) .const_map_observer(edges_observer.as_mut())
.build()?, .build()?,
SnapshotModule::new() SnapshotModule::new(),
redirect_stdout_module
); );
// Create our QEMU emulator // Create our QEMU emulator
@ -223,9 +244,7 @@ pub fn fuzz() -> Result<(), Error> {
}; };
// Set up the most basic monitor possible. // Set up the most basic monitor possible.
let monitor = SimpleMonitor::new(|s| { let monitor = SimpleMonitor::new(|s| log::info!("{s}"));
println!("{s}");
});
let (state, mut mgr) = match SimpleRestartingEventManager::launch(monitor, &mut shmem_provider) let (state, mut mgr) = match SimpleRestartingEventManager::launch(monitor, &mut shmem_provider)
{ {
Ok(res) => res, Ok(res) => res,

View File

@ -19,3 +19,6 @@ pub mod asan_guest;
pub use asan_guest::AsanGuestModule; pub use asan_guest::AsanGuestModule;
pub mod redirect_stdin; pub mod redirect_stdin;
pub use redirect_stdin::*; pub use redirect_stdin::*;
pub mod redirect_stdout;
pub use redirect_stdout::*;

View File

@ -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.
/// <https://github.com/qemu/qemu/blob/11be70677c70fdccd452a3233653949b79e97908/linux-user/hexagon/syscall_nr.h#L230>
#[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<F>
where
F: FnMut(&[u8]),
{
stdout: Option<F>,
stderr: Option<F>,
}
impl<F> Debug for RedirectStdoutModule<F>
where
F: FnMut(&[u8]),
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RedirectStdoutModule")
.finish_non_exhaustive()
}
}
impl<F> Default for RedirectStdoutModule<F>
where
F: FnMut(&[u8]),
{
fn default() -> Self {
Self::new()
}
}
impl<F> RedirectStdoutModule<F>
where
F: FnMut(&[u8]),
{
#[must_use]
/// constuctor
pub fn new() -> Self {
Self {
stdout: None,
stderr: None,
}
}
}
impl<F> RedirectStdoutModule<F>
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<F, I, S> EmulatorModule<I, S> for RedirectStdoutModule<F>
where
I: Unpin + HasLen + Debug,
S: Unpin,
F: FnMut(&[u8]) + 'static,
{
fn first_exec<ET>(
&mut self,
_qemu: Qemu,
emulator_modules: &mut EmulatorModules<ET, I, S>,
_state: &mut S,
) where
ET: EmulatorModuleTuple<I, S>,
{
emulator_modules.pre_syscalls(Hook::Function(syscall_write_hook::<F, ET, I, S>));
}
}
#[expect(clippy::too_many_arguments)]
fn syscall_write_hook<F, ET, I, S>(
_qemu: Qemu,
emulator_modules: &mut EmulatorModules<ET, I, S>,
_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, S>,
I: Unpin + HasLen + Debug,
S: Unpin,
F: FnMut(&[u8]) + 'static,
{
let h = emulator_modules
.get_mut::<RedirectStdoutModule<F>>()
.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)
}