//! A libfuzzer-like fuzzer using qemu for binary-only coverage //! #[cfg(feature = "i386")] use core::mem::size_of; use core::time::Duration; use std::{env, fmt::Write, fs::DirEntry, io, path::PathBuf, process}; use clap::{builder::Str, Parser}; use libafl::{ corpus::{Corpus, InMemoryCorpus}, events::{ launcher::Launcher, ClientDescription, EventConfig, LlmpRestartingEventManager, SendExiting, }, executors::ExitKind, fuzzer::StdFuzzer, inputs::{BytesInput, HasTargetBytes}, monitors::MultiMonitor, schedulers::QueueScheduler, state::{HasCorpus, StdState}, Error, }; use libafl_bolts::{ core_affinity::Cores, os::unix_signals::Signal, rands::StdRand, shmem::{ShMemProvider, StdShMemProvider}, tuples::tuple_list, AsSlice, }; use libafl_qemu::{ elf::EasyElf, modules::{drcov::DrCovModule, SnapshotModule}, ArchExtras, CallingConvention, Emulator, GuestAddr, GuestReg, MmapPerms, Qemu, QemuExecutor, QemuExitReason, QemuRWError, QemuShutdownCause, Regs, }; #[derive(Default)] pub struct Version; /// Parse a millis string to a [`Duration`]. Used for arg parsing. fn timeout_from_millis_str(time: &str) -> Result { Ok(Duration::from_millis(time.parse()?)) } impl From for Str { fn from(_: Version) -> Str { let version = [ ("Architecture:", env!("CPU_TARGET")), ("Build Timestamp:", env!("VERGEN_BUILD_TIMESTAMP")), ("Describe:", env!("VERGEN_GIT_DESCRIBE")), ("Commit SHA:", env!("VERGEN_GIT_SHA")), ("Commit Date:", env!("VERGEN_RUSTC_COMMIT_DATE")), ("Commit Branch:", env!("VERGEN_GIT_BRANCH")), ("Rustc Version:", env!("VERGEN_RUSTC_SEMVER")), ("Rustc Channel:", env!("VERGEN_RUSTC_CHANNEL")), ("Rustc Host Triple:", env!("VERGEN_RUSTC_HOST_TRIPLE")), ("Rustc Commit SHA:", env!("VERGEN_RUSTC_COMMIT_HASH")), ("Cargo Target Triple", env!("VERGEN_CARGO_TARGET_TRIPLE")), ] .iter() .fold(String::new(), |mut output, (k, v)| { let _ = writeln!(output, "{k:25}: {v}"); output }); format!("\n{version:}").into() } } #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] #[command( name = format!("qemu_coverage-{}",env!("CPU_TARGET")), version = Version::default(), about, long_about = "Module for generating DrCov coverage data using QEMU instrumentation" )] pub struct FuzzerOptions { #[arg(long, help = "Coverage file")] coverage_path: PathBuf, #[arg(long, help = "Input directory")] input_dir: PathBuf, #[arg(long, help = "Timeout in seconds", default_value = "5000", value_parser = timeout_from_millis_str)] timeout: Duration, #[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, #[arg(last = true, help = "Arguments passed to the target")] args: Vec, } pub const MAX_INPUT_SIZE: usize = 1048576; // 1MB pub fn fuzz() { env_logger::init(); let mut options = FuzzerOptions::parse(); let corpus_files = options .input_dir .read_dir() .expect("Failed to read corpus dir") .collect::, io::Error>>() .expect("Failed to read dir entry"); let num_files = corpus_files.len(); let num_cores = options.cores.ids.len(); let files_per_core = (num_files as f64 / num_cores as f64).ceil() as usize; let program = env::args().next().unwrap(); log::info!("Program: {program:}"); options.args.insert(0, program); log::info!("ARGS: {:#?}", options.args); env::remove_var("LD_LIBRARY_PATH"); let mut run_client = |state: Option<_>, mut mgr: LlmpRestartingEventManager<_, _, _, _, _>, client_description: ClientDescription| { let mut cov_path = options.coverage_path.clone(); let core_id = client_description.core_id(); let coverage_name = cov_path.file_stem().unwrap().to_str().unwrap(); let coverage_extension = cov_path.extension().unwrap_or_default().to_str().unwrap(); let core = core_id.0; cov_path.set_file_name(format!("{coverage_name}-{core:03}.{coverage_extension}")); let emulator_modules = tuple_list!( DrCovModule::builder().filename(cov_path.clone()).build(), SnapshotModule::new() ); let emulator = Emulator::empty() .qemu_parameters(options.args.clone()) .modules(emulator_modules) .build() .expect("QEMU initialization failed"); let qemu = emulator.qemu(); let mut elf_buffer = Vec::new(); let elf = EasyElf::from_file(qemu.binary_path(), &mut elf_buffer).unwrap(); let test_one_input_ptr = elf .resolve_symbol("LLVMFuzzerTestOneInput", qemu.load_addr()) .expect("Symbol LLVMFuzzerTestOneInput not found"); log::info!("LLVMFuzzerTestOneInput @ {test_one_input_ptr:#x}"); qemu.entry_break(test_one_input_ptr); for m in qemu.mappings() { log::info!( "Mapping: 0x{:016x}-0x{:016x}, {}", m.start(), m.end(), m.path().unwrap_or(&"".to_string()) ); } let pc: GuestReg = qemu.read_reg(Regs::Pc).unwrap(); log::info!("Break at {pc:#x}"); let ret_addr: GuestAddr = qemu.read_return_address().unwrap(); log::info!("Return address = {ret_addr:#x}"); qemu.set_breakpoint(ret_addr); let input_addr = qemu .map_private(0, MAX_INPUT_SIZE, MmapPerms::ReadWrite) .unwrap(); log::info!("Placing input at {input_addr:#x}"); let stack_ptr: GuestAddr = qemu.read_reg(Regs::Sp).unwrap(); let reset = |qemu: Qemu, buf: &[u8], len: GuestReg| -> Result<(), QemuRWError> { unsafe { qemu.write_mem(input_addr, buf)?; qemu.write_reg(Regs::Pc, test_one_input_ptr)?; qemu.write_reg(Regs::Sp, stack_ptr)?; qemu.write_return_address(ret_addr)?; qemu.write_function_argument(CallingConvention::Cdecl, 0, input_addr)?; qemu.write_function_argument(CallingConvention::Cdecl, 1, len)?; match qemu.run() { Ok(QemuExitReason::Breakpoint(_)) => {} Ok(QemuExitReason::End(QemuShutdownCause::HostSignal( Signal::SigInterrupt, ))) => process::exit(0), _ => panic!("Unexpected QEMU exit."), } Ok(()) } }; let mut harness = |emulator: &mut Emulator<_, _, _, _, _, _, _>, _state: &mut _, input: &BytesInput| { let qemu = emulator.qemu(); let target = input.target_bytes(); let mut buf = target.as_slice(); let mut len = buf.len(); if len > MAX_INPUT_SIZE { buf = &buf[0..MAX_INPUT_SIZE]; len = MAX_INPUT_SIZE; } let len = len as GuestReg; reset(qemu, buf, len).unwrap(); ExitKind::Ok }; let core_id = client_description.core_id(); let core_idx = options .cores .position(core_id) .expect("Failed to get core index"); let files = corpus_files .iter() .skip(files_per_core * core_idx) .take(files_per_core) .map(|x| x.path()) .collect::>(); if files.is_empty() { mgr.send_exiting()?; Err(Error::ShuttingDown)? } let mut feedback = (); let mut objective = (); let mut state = state.unwrap_or_else(|| { StdState::new( StdRand::new(), InMemoryCorpus::new(), InMemoryCorpus::new(), &mut feedback, &mut objective, ) .unwrap() }); let scheduler = QueueScheduler::new(); let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); let mut executor = QemuExecutor::new( emulator, &mut harness, (), &mut fuzzer, &mut state, &mut mgr, options.timeout, ) .expect("Failed to create QemuExecutor"); if state.must_load_initial_inputs() { state .load_initial_inputs_by_filenames(&mut fuzzer, &mut executor, &mut mgr, &files) .unwrap_or_else(|_| { println!("Failed to load initial corpus at {:?}", &options.input_dir); process::exit(0); }); log::info!("We imported {} inputs from disk.", state.corpus().count()); } log::info!("Processed {} inputs from disk.", files.len()); mgr.send_exiting()?; Err(Error::ShuttingDown)? }; match Launcher::builder() .shmem_provider(StdShMemProvider::new().expect("Failed to init shared memory")) .broker_port(options.port) .configuration(EventConfig::from_build_id()) .monitor(MultiMonitor::new(|s| println!("{s}"))) .run_client(&mut run_client) .cores(&options.cores) .build() .launch() { Ok(()) => (), Err(Error::ShuttingDown) => println!("Run finished successfully."), Err(err) => panic!("Failed to run launcher: {err:?}"), } }