From 2bfed2d48800323e80a91c707d3c425a889cf5d5 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Fri, 11 Oct 2024 12:41:42 +0200 Subject: [PATCH] Move all target-specific code to harness.rs (#2605) --- fuzzers/binary_only/qemu_launcher/README.md | 15 ++++--- .../binary_only/qemu_launcher/src/client.rs | 29 +++--------- .../binary_only/qemu_launcher/src/harness.rs | 44 ++++++++++++++++++- .../binary_only/qemu_launcher/src/instance.rs | 10 ++++- 4 files changed, 68 insertions(+), 30 deletions(-) diff --git a/fuzzers/binary_only/qemu_launcher/README.md b/fuzzers/binary_only/qemu_launcher/README.md index bc9c9b767d..3b7d3dfea7 100644 --- a/fuzzers/binary_only/qemu_launcher/README.md +++ b/fuzzers/binary_only/qemu_launcher/README.md @@ -1,9 +1,13 @@ # qemu_launcher_ -This folder contains an example fuzzer for libpng, using LLMP for fast multi-process fuzzing and crash detection. It has been tested on Linux. -This automatically spawns n child processes, and binds them to a free core. +This folder contains an example fuzzer that will fuzz binary-only targets, cross-architecture, on Linux. +It's using LLMP for fast multi-process fuzzing and crash detection. +This automatically spawns `n` child processes, and binds them to a free core. + +To adapt the fuzzer to your custom target, change [`harness.rs`](./src/harness.rs). The following architectures are supported: + * arm * aarch64 * i386 @@ -11,10 +15,10 @@ The following architectures are supported: * mips * ppc -Note that the injection feature `-j` is currently only supported on x86_64 -and aarch64. +For usermode, this fuzzer supports injection fuzzing with `-j`. ## Prerequisites + ```bash sudo apt install \ gcc-arm-linux-gnueabi \ @@ -32,7 +36,8 @@ sudo apt install \ ## Run -Defaults to `x86_64` architecture +Defaults to `x86_64` architecture. Change the architecture by + ```bash cargo make run ``` diff --git a/fuzzers/binary_only/qemu_launcher/src/client.rs b/fuzzers/binary_only/qemu_launcher/src/client.rs index 044f216e5a..56c71bbc0d 100644 --- a/fuzzers/binary_only/qemu_launcher/src/client.rs +++ b/fuzzers/binary_only/qemu_launcher/src/client.rs @@ -11,16 +11,16 @@ use libafl_bolts::{core_affinity::CoreId, rands::StdRand, tuples::tuple_list}; #[cfg(feature = "injections")] use libafl_qemu::modules::injections::InjectionModule; use libafl_qemu::{ - elf::EasyElf, modules::{ asan::{init_qemu_with_asan, AsanModule}, asan_guest::{init_qemu_with_asan_guest, AsanGuestModule}, cmplog::CmpLogModule, }, - ArchExtras, GuestAddr, Qemu, + Qemu, }; use crate::{ + harness::Harness, instance::{ClientMgr, Instance}, options::FuzzerOptions, }; @@ -55,16 +55,7 @@ impl<'a> Client<'a> { .collect::>() } - fn start_pc(qemu: Qemu) -> Result { - let mut elf_buffer = Vec::new(); - let elf = EasyElf::from_file(qemu.binary_path(), &mut elf_buffer)?; - - let start_pc = elf - .resolve_symbol("LLVMFuzzerTestOneInput", qemu.load_addr()) - .ok_or_else(|| Error::empty_optional("Symbol LLVMFuzzerTestOneInput not found"))?; - Ok(start_pc) - } - + #[allow(clippy::too_many_lines)] pub fn run( &self, state: Option, @@ -72,9 +63,11 @@ impl<'a> Client<'a> { core_id: CoreId, ) -> Result<(), Error> { let mut args = self.args()?; + Harness::edit_args(&mut args); log::debug!("ARGS: {:#?}", args); let mut env = self.env(); + Harness::edit_env(&mut env); log::debug!("ENV: {:#?}", env); let is_asan = self.options.is_asan_core(core_id); @@ -96,9 +89,6 @@ impl<'a> Client<'a> { } }; - let start_pc = Self::start_pc(qemu)?; - log::debug!("start_pc @ {start_pc:#x}"); - #[cfg(not(feature = "injections"))] let injection_module = None; @@ -118,13 +108,7 @@ impl<'a> Client<'a> { } }); - qemu.entry_break(start_pc); - - let ret_addr: GuestAddr = qemu - .read_return_address() - .map_err(|e| Error::unknown(format!("Failed to read return address: {e:?}")))?; - log::debug!("ret_addr = {ret_addr:#x}"); - qemu.set_breakpoint(ret_addr); + let harness = Harness::init(qemu).expect("Error setting up harness."); let is_cmplog = self.options.is_cmplog_core(core_id); @@ -136,6 +120,7 @@ impl<'a> Client<'a> { let instance_builder = Instance::builder() .options(self.options) .qemu(qemu) + .harness(harness) .mgr(mgr) .core_id(core_id) .extra_tokens(extra_tokens); diff --git a/fuzzers/binary_only/qemu_launcher/src/harness.rs b/fuzzers/binary_only/qemu_launcher/src/harness.rs index 8a959ef6ea..ac7fc58ac8 100644 --- a/fuzzers/binary_only/qemu_launcher/src/harness.rs +++ b/fuzzers/binary_only/qemu_launcher/src/harness.rs @@ -4,7 +4,9 @@ use libafl::{ Error, }; use libafl_bolts::AsSlice; -use libafl_qemu::{ArchExtras, CallingConvention, GuestAddr, GuestReg, MmapPerms, Qemu, Regs}; +use libafl_qemu::{ + elf::EasyElf, ArchExtras, CallingConvention, GuestAddr, GuestReg, MmapPerms, Qemu, Regs, +}; pub struct Harness { qemu: Qemu, @@ -17,7 +19,40 @@ pub struct Harness { pub const MAX_INPUT_SIZE: usize = 1_048_576; // 1MB impl Harness { - pub fn new(qemu: Qemu) -> Result { + /// Change environment + #[inline] + #[allow(clippy::ptr_arg)] + pub fn edit_env(_env: &mut Vec<(String, String)>) {} + + /// Change arguments + #[inline] + #[allow(clippy::ptr_arg)] + pub fn edit_args(_args: &mut Vec) {} + + /// Helper function to find the function we want to fuzz. + fn start_pc(qemu: Qemu) -> Result { + let mut elf_buffer = Vec::new(); + let elf = EasyElf::from_file(qemu.binary_path(), &mut elf_buffer)?; + + let start_pc = elf + .resolve_symbol("LLVMFuzzerTestOneInput", qemu.load_addr()) + .ok_or_else(|| Error::empty_optional("Symbol LLVMFuzzerTestOneInput not found"))?; + Ok(start_pc) + } + + /// Initialize the emulator, run to the entrypoint (or jump there) and return the [`Harness`] struct + pub fn init(qemu: Qemu) -> Result { + let start_pc = Self::start_pc(qemu)?; + log::debug!("start_pc @ {start_pc:#x}"); + + qemu.entry_break(start_pc); + + let ret_addr: GuestAddr = qemu + .read_return_address() + .map_err(|e| Error::unknown(format!("Failed to read return address: {e:?}")))?; + log::debug!("ret_addr = {ret_addr:#x}"); + qemu.set_breakpoint(ret_addr); + let input_addr = qemu .map_private(0, MAX_INPUT_SIZE, MmapPerms::ReadWrite) .map_err(|e| Error::unknown(format!("Failed to map input buffer: {e:}")))?; @@ -43,6 +78,11 @@ impl Harness { }) } + /// If we need to do extra work after forking, we can do that here. + #[inline] + #[allow(clippy::unused_self)] + pub fn post_fork(&self) {} + pub fn run(&self, input: &BytesInput) -> ExitKind { self.reset(input).unwrap(); ExitKind::Ok diff --git a/fuzzers/binary_only/qemu_launcher/src/instance.rs b/fuzzers/binary_only/qemu_launcher/src/instance.rs index 7cd2bd2068..f6b7734272 100644 --- a/fuzzers/binary_only/qemu_launcher/src/instance.rs +++ b/fuzzers/binary_only/qemu_launcher/src/instance.rs @@ -61,6 +61,9 @@ pub type ClientMgr = #[derive(TypedBuilder)] pub struct Instance<'a, M: Monitor> { options: &'a FuzzerOptions, + /// The harness. We create it before forking, then `take()` it inside the client. + #[builder(setter(strip_option))] + harness: Option, qemu: Qemu, mgr: ClientMgr, core_id: CoreId, @@ -186,7 +189,12 @@ impl<'a, M: Monitor> Instance<'a, M> { state.add_metadata(tokens); - let harness = Harness::new(self.qemu)?; + let harness = self + .harness + .take() + .expect("The harness can never be None here!"); + harness.post_fork(); + let mut harness = |_emulator: &mut Emulator<_, _, _, _, _>, _state: &mut _, input: &BytesInput| harness.run(input);