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
[unix]
run: harness build
run: build harness
{{ FUZZER }} \
--output ./output \
--input ./corpus \

View File

@ -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()

View File

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

View File

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

View File

@ -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()

View File

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

View File

@ -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::*;

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)
}