From 7f0a4f1d7eb382c8e06ed14a5123a6eaab24bfa0 Mon Sep 17 00:00:00 2001 From: Fabian Freyer Date: Wed, 20 Sep 2023 11:08:59 +0200 Subject: [PATCH] libafl_frida: Add FridaInstrumentationHelperBuilder, don't rely on Clap options (#1523) * impr(frida): Don't keep FuzzerOptions in Helper Instead, keep the actual values that are needed. This allows us to make a builder for FridaInstrumentationBuilder in a subsequent commit. * refactor(frida): Move workaround to separate method This is just code movement. * refactor(frida): move transformer initialization Mostly code movement here, sets up replacing `new` with a builder. The one exception is the introduction of a lifetime bound on RT, which needs to outlive the transformer. This could be generic, but there's probably no reason to introduce an additional lifetime. However, because of this lifetime introduction, this is _technically_ a breaking change. * impr(frida): Pass module map to runtimes Instead of passing a slice of modules to instrument, and re-building the modulemap, pass a Ref-counted module map directly to the initialization. * feat(frida): Builder for InstrumentationHelper Co-authored-by: Dominik Maier * impr(frida/alloc): optional options in allocator Move all the initialization into Default::default with sensible defaults and override parameters set from options in new. * impr(frida): remove options from AsanError The only option AsanError uses is whether to continue on error. Instead of keeping a whole clone of the options around, just store that single boolean value. * impr(frida/asan): Use less FuzzerOptions * Implement Default::default to get a good default AsanRuntime --------- Co-authored-by: Dominik Maier --- fuzzers/frida_executable_libpng/src/fuzzer.rs | 2 +- fuzzers/frida_gdiplus/src/fuzzer.rs | 2 +- fuzzers/frida_libpng/src/fuzzer.rs | 2 +- libafl_frida/src/alloc.rs | 290 ++++---- libafl_frida/src/asan/asan_rt.rs | 105 ++- libafl_frida/src/asan/errors.rs | 10 +- libafl_frida/src/cmplog_rt.rs | 5 +- libafl_frida/src/coverage_rt.rs | 4 +- libafl_frida/src/drcov_rt.rs | 4 +- libafl_frida/src/executor.rs | 2 +- libafl_frida/src/helper.rs | 686 ++++++++++++------ 11 files changed, 700 insertions(+), 412 deletions(-) diff --git a/fuzzers/frida_executable_libpng/src/fuzzer.rs b/fuzzers/frida_executable_libpng/src/fuzzer.rs index df61717007..21a40c08da 100644 --- a/fuzzers/frida_executable_libpng/src/fuzzer.rs +++ b/fuzzers/frida_executable_libpng/src/fuzzer.rs @@ -104,7 +104,7 @@ unsafe fn fuzz( let coverage = CoverageRuntime::new(); #[cfg(unix)] - let asan = AsanRuntime::new(options.clone()); + let asan = AsanRuntime::new(&options); #[cfg(unix)] let mut frida_helper = diff --git a/fuzzers/frida_gdiplus/src/fuzzer.rs b/fuzzers/frida_gdiplus/src/fuzzer.rs index a226311782..0af353a2fd 100644 --- a/fuzzers/frida_gdiplus/src/fuzzer.rs +++ b/fuzzers/frida_gdiplus/src/fuzzer.rs @@ -99,7 +99,7 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> { let coverage = CoverageRuntime::new(); #[cfg(unix)] - let asan = AsanRuntime::new(options.clone()); + let asan = AsanRuntime::new(&options); #[cfg(unix)] let mut frida_helper = diff --git a/fuzzers/frida_libpng/src/fuzzer.rs b/fuzzers/frida_libpng/src/fuzzer.rs index 23ce1fcfd8..846eea1e1f 100644 --- a/fuzzers/frida_libpng/src/fuzzer.rs +++ b/fuzzers/frida_libpng/src/fuzzer.rs @@ -94,7 +94,7 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> { let coverage = CoverageRuntime::new(); #[cfg(unix)] - let asan = AsanRuntime::new(options.clone()); + let asan = AsanRuntime::new(&options); #[cfg(unix)] let mut frida_helper = diff --git a/libafl_frida/src/alloc.rs b/libafl_frida/src/alloc.rs index b7ec8ef909..ef0ff0e058 100644 --- a/libafl_frida/src/alloc.rs +++ b/libafl_frida/src/alloc.rs @@ -28,9 +28,10 @@ use crate::asan::errors::{AsanError, AsanErrors}; /// An allocator wrapper with binary-only address sanitization #[derive(Debug)] pub struct Allocator { - /// The fuzzer options - #[allow(dead_code)] - options: FuzzerOptions, + max_allocation: usize, + max_total_allocation: usize, + max_allocation_panics: bool, + allocation_backtraces: bool, /// The page size page_size: usize, /// The shadow offsets @@ -104,130 +105,13 @@ impl Allocator { all(target_arch = "aarch64", target_os = "android") ))] #[must_use] - #[allow(clippy::too_many_lines)] - pub fn new(options: FuzzerOptions) -> Self { - let ret = unsafe { sysconf(_SC_PAGESIZE) }; - assert!( - ret >= 0, - "Failed to read pagesize {:?}", - io::Error::last_os_error() - ); - - #[allow(clippy::cast_sign_loss)] - let page_size = ret as usize; - // probe to find a usable shadow bit: - let mut shadow_bit = 0; - - let mut occupied_ranges: Vec<(usize, usize)> = vec![]; - // max(userspace address) this is usually 0x8_0000_0000_0000 - 1 on x64 linux. - let mut userspace_max: usize = 0; - - // Enumerate memory ranges that are already occupied. - for prot in [ - PageProtection::Read, - PageProtection::Write, - PageProtection::Execute, - ] { - RangeDetails::enumerate_with_prot(prot, &mut |details| { - let start = details.memory_range().base_address().0 as usize; - let end = start + details.memory_range().size(); - occupied_ranges.push((start, end)); - // log::trace!("{:x} {:x}", start, end); - let base: usize = 2; - // On x64, if end > 2**48, then that's in vsyscall or something. - #[cfg(target_arch = "x86_64")] - if end <= base.pow(48) && end > userspace_max { - userspace_max = end; - } - - // On x64, if end > 2**52, then range is not in userspace - #[cfg(target_arch = "aarch64")] - if end <= base.pow(52) && end > userspace_max { - userspace_max = end; - } - - true - }); - } - - let mut maxbit = 0; - for power in 1..64 { - let base: usize = 2; - if base.pow(power) > userspace_max { - maxbit = power; - break; - } - } - - { - for try_shadow_bit in &[maxbit - 4, maxbit - 3, maxbit - 2] { - let addr: usize = 1 << try_shadow_bit; - let shadow_start = addr; - let shadow_end = addr + addr + addr; - - // check if the proposed shadow bit overlaps with occupied ranges. - for (start, end) in &occupied_ranges { - if (shadow_start <= *end) && (*start <= shadow_end) { - // log::trace!("{:x} {:x}, {:x} {:x}",shadow_start,shadow_end,start,end); - log::warn!("shadow_bit {try_shadow_bit:x} is not suitable"); - break; - } - } - - if unsafe { - mmap( - NonZeroUsize::new(addr), - NonZeroUsize::new_unchecked(page_size), - ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, - MapFlags::MAP_PRIVATE - | ANONYMOUS_FLAG - | MapFlags::MAP_FIXED - | MapFlags::MAP_NORESERVE, - -1, - 0, - ) - } - .is_ok() - { - shadow_bit = (*try_shadow_bit).try_into().unwrap(); - break; - } - } - } - - log::warn!("shadow_bit {shadow_bit:x} is suitable"); - assert!(shadow_bit != 0); - // attempt to pre-map the entire shadow-memory space - - let addr: usize = 1 << shadow_bit; - let pre_allocated_shadow = unsafe { - mmap( - NonZeroUsize::new(addr), - NonZeroUsize::new_unchecked(addr + addr), - ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, - ANONYMOUS_FLAG - | MapFlags::MAP_FIXED - | MapFlags::MAP_PRIVATE - | MapFlags::MAP_NORESERVE, - -1, - 0, - ) - } - .is_ok(); - + pub fn new(options: &FuzzerOptions) -> Self { Self { - options, - page_size, - pre_allocated_shadow, - shadow_offset: 1 << shadow_bit, - shadow_bit, - allocations: HashMap::new(), - shadow_pages: RangeSet::new(), - allocation_queue: BTreeMap::new(), - largest_allocation: 0, - total_allocation_size: 0, - base_mapping_addr: addr + addr + addr, - current_mapping_addr: addr + addr + addr, + max_allocation: options.max_allocation, + max_allocation_panics: options.max_allocation_panics, + max_total_allocation: options.max_total_allocation, + allocation_backtraces: options.allocation_backtraces, + ..Self::default() } } @@ -272,9 +156,9 @@ impl Allocator { } else { size }; - if size > self.options.max_allocation { + if size > self.max_allocation { #[allow(clippy::manual_assert)] - if self.options.max_allocation_panics { + if self.max_allocation_panics { panic!("ASAN: Allocation is too large: 0x{size:x}"); } @@ -282,7 +166,7 @@ impl Allocator { } let rounded_up_size = self.round_up_to_page(size) + 2 * self.page_size; - if self.total_allocation_size + rounded_up_size > self.options.max_total_allocation { + if self.total_allocation_size + rounded_up_size > self.max_total_allocation { return std::ptr::null_mut(); } self.total_allocation_size += rounded_up_size; @@ -291,7 +175,7 @@ impl Allocator { //log::trace!("reusing allocation at {:x}, (actual mapping starts at {:x}) size {:x}", metadata.address, metadata.address - self.page_size, size); metadata.is_malloc_zero = is_malloc_zero; metadata.size = size; - if self.options.allocation_backtraces { + if self.allocation_backtraces { metadata.allocation_site_backtrace = Some(Backtrace::new_unresolved()); } metadata @@ -324,7 +208,7 @@ impl Allocator { actual_size: rounded_up_size, ..AllocationMetadata::default() }; - if self.options.allocation_backtraces { + if self.allocation_backtraces { metadata.allocation_site_backtrace = Some(Backtrace::new_unresolved()); } @@ -367,7 +251,7 @@ impl Allocator { let shadow_mapping_start = map_to_shadow!(self, ptr as usize); metadata.freed = true; - if self.options.allocation_backtraces { + if self.allocation_backtraces { metadata.release_site_backtrace = Some(Backtrace::new_unresolved()); } @@ -563,3 +447,145 @@ impl Allocator { }); } } + +impl Default for Allocator { + /// Creates a new [`Allocator`] (not supported on this platform!) + #[cfg(not(any( + target_os = "linux", + target_vendor = "apple", + all(target_arch = "aarch64", target_os = "android") + )))] + fn default() -> Self { + todo!("Shadow region not yet supported for this platform!"); + } + + #[allow(clippy::too_many_lines)] + fn default() -> Self { + let ret = unsafe { sysconf(_SC_PAGESIZE) }; + assert!( + ret >= 0, + "Failed to read pagesize {:?}", + io::Error::last_os_error() + ); + + #[allow(clippy::cast_sign_loss)] + let page_size = ret as usize; + // probe to find a usable shadow bit: + let mut shadow_bit = 0; + + let mut occupied_ranges: Vec<(usize, usize)> = vec![]; + // max(userspace address) this is usually 0x8_0000_0000_0000 - 1 on x64 linux. + let mut userspace_max: usize = 0; + + // Enumerate memory ranges that are already occupied. + for prot in [ + PageProtection::Read, + PageProtection::Write, + PageProtection::Execute, + ] { + RangeDetails::enumerate_with_prot(prot, &mut |details| { + let start = details.memory_range().base_address().0 as usize; + let end = start + details.memory_range().size(); + occupied_ranges.push((start, end)); + // log::trace!("{:x} {:x}", start, end); + let base: usize = 2; + // On x64, if end > 2**48, then that's in vsyscall or something. + #[cfg(target_arch = "x86_64")] + if end <= base.pow(48) && end > userspace_max { + userspace_max = end; + } + + // On x64, if end > 2**52, then range is not in userspace + #[cfg(target_arch = "aarch64")] + if end <= base.pow(52) && end > userspace_max { + userspace_max = end; + } + + true + }); + } + + let mut maxbit = 0; + for power in 1..64 { + let base: usize = 2; + if base.pow(power) > userspace_max { + maxbit = power; + break; + } + } + + { + for try_shadow_bit in &[maxbit - 4, maxbit - 3, maxbit - 2] { + let addr: usize = 1 << try_shadow_bit; + let shadow_start = addr; + let shadow_end = addr + addr + addr; + + // check if the proposed shadow bit overlaps with occupied ranges. + for (start, end) in &occupied_ranges { + if (shadow_start <= *end) && (*start <= shadow_end) { + // log::trace!("{:x} {:x}, {:x} {:x}",shadow_start,shadow_end,start,end); + log::warn!("shadow_bit {try_shadow_bit:x} is not suitable"); + break; + } + } + + if unsafe { + mmap( + NonZeroUsize::new(addr), + NonZeroUsize::new_unchecked(page_size), + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_PRIVATE + | ANONYMOUS_FLAG + | MapFlags::MAP_FIXED + | MapFlags::MAP_NORESERVE, + -1, + 0, + ) + } + .is_ok() + { + shadow_bit = (*try_shadow_bit).try_into().unwrap(); + break; + } + } + } + + log::warn!("shadow_bit {shadow_bit:x} is suitable"); + assert!(shadow_bit != 0); + // attempt to pre-map the entire shadow-memory space + + let addr: usize = 1 << shadow_bit; + let pre_allocated_shadow = unsafe { + mmap( + NonZeroUsize::new(addr), + NonZeroUsize::new_unchecked(addr + addr), + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + ANONYMOUS_FLAG + | MapFlags::MAP_FIXED + | MapFlags::MAP_PRIVATE + | MapFlags::MAP_NORESERVE, + -1, + 0, + ) + } + .is_ok(); + + Self { + max_allocation: 1 << 30, + max_allocation_panics: false, + max_total_allocation: 1 << 32, + allocation_backtraces: false, + page_size, + pre_allocated_shadow, + shadow_offset: 1 << shadow_bit, + shadow_bit, + allocations: HashMap::new(), + shadow_pages: RangeSet::new(), + allocation_queue: BTreeMap::new(), + largest_allocation: 0, + total_allocation_size: 0, + base_mapping_addr: addr + addr + addr, + current_mapping_addr: addr + addr + addr, + } + } +} diff --git a/libafl_frida/src/asan/asan_rt.rs b/libafl_frida/src/asan/asan_rt.rs index 17aa84e921..d9c731c30f 100644 --- a/libafl_frida/src/asan/asan_rt.rs +++ b/libafl_frida/src/asan/asan_rt.rs @@ -10,7 +10,7 @@ use core::{ fmt::{self, Debug, Formatter}, ptr::addr_of_mut, }; -use std::{ffi::c_void, num::NonZeroUsize, ptr::write_volatile}; +use std::{ffi::c_void, num::NonZeroUsize, ptr::write_volatile, rc::Rc}; use backtrace::Backtrace; #[cfg(target_arch = "x86_64")] @@ -57,7 +57,7 @@ use crate::utils::instruction_width; use crate::{ alloc::Allocator, asan::errors::{AsanError, AsanErrors, AsanReadWriteError, ASAN_ERRORS}, - helper::FridaRuntime, + helper::{FridaRuntime, SkipRange}, utils::writer_register, }; @@ -139,9 +139,10 @@ pub struct AsanRuntime { blob_check_mem_48bytes: Option>, blob_check_mem_64bytes: Option>, stalked_addresses: HashMap, - options: FuzzerOptions, - module_map: Option, + module_map: Option>, suppressed_addresses: Vec, + skip_ranges: Vec, + continue_on_error: bool, shadow_check_func: Option bool>, #[cfg(target_arch = "aarch64")] @@ -152,8 +153,9 @@ impl Debug for AsanRuntime { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("AsanRuntime") .field("stalked_addresses", &self.stalked_addresses) - .field("options", &self.options) + .field("continue_on_error", &self.continue_on_error) .field("module_map", &"") + .field("skip_ranges", &self.skip_ranges) .field("suppressed_addresses", &self.suppressed_addresses) .finish_non_exhaustive() } @@ -167,10 +169,10 @@ impl FridaRuntime for AsanRuntime { &mut self, gum: &Gum, _ranges: &RangeMap, - modules_to_instrument: &[&str], + module_map: &Rc, ) { unsafe { - ASAN_ERRORS = Some(AsanErrors::new(self.options.clone())); + ASAN_ERRORS = Some(AsanErrors::new(self.continue_on_error)); } self.generate_instrumentation_blobs(); @@ -178,14 +180,16 @@ impl FridaRuntime for AsanRuntime { self.generate_shadow_check_function(); self.unpoison_all_existing_memory(); - self.module_map = Some(ModuleMap::new_from_names(gum, modules_to_instrument)); - if !self.options.dont_instrument.is_empty() { - for (module_name, offset) in self.options.dont_instrument.clone() { - let module_details = ModuleDetails::with_name(module_name).unwrap(); - let lib_start = module_details.range().base_address().0 as usize; - self.suppressed_addresses.push(lib_start + offset); - } - } + self.module_map = Some(module_map.clone()); + self.suppressed_addresses + .extend(self.skip_ranges.iter().map(|skip| match skip { + SkipRange::Absolute(range) => range.start, + SkipRange::ModuleRelative { name, range } => { + let module_details = ModuleDetails::with_name(name.clone()).unwrap(); + let lib_start = module_details.range().base_address().0 as usize; + lib_start + range.start + } + })); self.hook_functions(gum); /* @@ -295,33 +299,22 @@ impl FridaRuntime for AsanRuntime { impl AsanRuntime { /// Create a new `AsanRuntime` #[must_use] - pub fn new(options: FuzzerOptions) -> AsanRuntime { + pub fn new(options: &FuzzerOptions) -> AsanRuntime { + let skip_ranges = options + .dont_instrument + .iter() + .map(|(name, offset)| SkipRange::ModuleRelative { + name: name.clone(), + range: *offset..*offset + 4, + }) + .collect(); + let continue_on_error = options.continue_on_error; Self { check_for_leaks_enabled: options.detect_leaks, - current_report_impl: 0, - allocator: Allocator::new(options.clone()), - regs: [0; ASAN_SAVE_REGISTER_COUNT], - blob_report: None, - blob_check_mem_byte: None, - blob_check_mem_halfword: None, - blob_check_mem_dword: None, - blob_check_mem_qword: None, - blob_check_mem_16bytes: None, - blob_check_mem_3bytes: None, - blob_check_mem_6bytes: None, - blob_check_mem_12bytes: None, - blob_check_mem_24bytes: None, - blob_check_mem_32bytes: None, - blob_check_mem_48bytes: None, - blob_check_mem_64bytes: None, - stalked_addresses: HashMap::new(), - options, - module_map: None, - suppressed_addresses: Vec::new(), - shadow_check_func: None, - - #[cfg(target_arch = "aarch64")] - eh_frame: [0; ASAN_EH_FRAME_DWORD_COUNT], + allocator: Allocator::new(options), + skip_ranges, + continue_on_error, + ..Self::default() } } @@ -2716,3 +2709,35 @@ impl AsanRuntime { )); } } + +impl Default for AsanRuntime { + fn default() -> Self { + Self { + check_for_leaks_enabled: false, + current_report_impl: 0, + allocator: Allocator::default(), + regs: [0; ASAN_SAVE_REGISTER_COUNT], + blob_report: None, + blob_check_mem_byte: None, + blob_check_mem_halfword: None, + blob_check_mem_dword: None, + blob_check_mem_qword: None, + blob_check_mem_16bytes: None, + blob_check_mem_3bytes: None, + blob_check_mem_6bytes: None, + blob_check_mem_12bytes: None, + blob_check_mem_24bytes: None, + blob_check_mem_32bytes: None, + blob_check_mem_48bytes: None, + blob_check_mem_64bytes: None, + stalked_addresses: HashMap::new(), + module_map: None, + suppressed_addresses: Vec::new(), + skip_ranges: Vec::new(), + continue_on_error: false, + shadow_check_func: None, + #[cfg(target_arch = "aarch64")] + eh_frame: [0; ASAN_EH_FRAME_DWORD_COUNT], + } + } +} diff --git a/libafl_frida/src/asan/errors.rs b/libafl_frida/src/asan/errors.rs index 6f85842f55..07ee2fc67b 100644 --- a/libafl_frida/src/asan/errors.rs +++ b/libafl_frida/src/asan/errors.rs @@ -17,7 +17,7 @@ use libafl::{ state::{HasClientPerfMonitor, HasMetadata}, Error, }; -use libafl_bolts::{cli::FuzzerOptions, ownedref::OwnedPtr, Named, SerdeAny}; +use libafl_bolts::{ownedref::OwnedPtr, Named, SerdeAny}; use serde::{Deserialize, Serialize}; use termcolor::{Color, ColorSpec, WriteColor}; @@ -95,17 +95,17 @@ impl AsanError { #[allow(clippy::unsafe_derive_deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, SerdeAny)] pub struct AsanErrors { - options: FuzzerOptions, + continue_on_error: bool, errors: Vec, } impl AsanErrors { /// Creates a new `AsanErrors` struct #[must_use] - pub fn new(options: FuzzerOptions) -> Self { + pub fn new(continue_on_error: bool) -> Self { Self { - options, errors: Vec::new(), + continue_on_error, } } @@ -529,7 +529,7 @@ impl AsanErrors { }; #[allow(clippy::manual_assert)] - if !self.options.continue_on_error { + if !self.continue_on_error { panic!("ASAN: Crashing target!"); } } diff --git a/libafl_frida/src/cmplog_rt.rs b/libafl_frida/src/cmplog_rt.rs index 49aa9459d7..dbf01ab939 100644 --- a/libafl_frida/src/cmplog_rt.rs +++ b/libafl_frida/src/cmplog_rt.rs @@ -19,6 +19,9 @@ extern "C" { pub fn __libafl_targets_cmplog_instructions(k: u64, shape: u8, arg1: u64, arg2: u64); } +use frida_gum::ModuleMap; +use std::rc::Rc; + #[cfg(target_arch = "aarch64")] use frida_gum::{ instruction_writer::{Aarch64Register, IndexMode, InstructionWriter}, @@ -90,7 +93,7 @@ impl FridaRuntime for CmpLogRuntime { &mut self, _gum: &frida_gum::Gum, _ranges: &RangeMap, - _modules_to_instrument: &[&str], + _module_map: &Rc, ) { self.generate_instrumentation_blobs(); } diff --git a/libafl_frida/src/coverage_rt.rs b/libafl_frida/src/coverage_rt.rs index b16b45b1a4..a5215f750d 100644 --- a/libafl_frida/src/coverage_rt.rs +++ b/libafl_frida/src/coverage_rt.rs @@ -5,7 +5,7 @@ use std::{cell::RefCell, marker::PhantomPinned, pin::Pin, rc::Rc}; #[cfg(target_arch = "aarch64")] use dynasmrt::DynasmLabelApi; use dynasmrt::{dynasm, DynasmApi}; -use frida_gum::{instruction_writer::InstructionWriter, stalker::StalkerOutput}; +use frida_gum::{instruction_writer::InstructionWriter, stalker::StalkerOutput, ModuleMap}; use libafl_bolts::math::xxh3_rrmxmx_mixer; use rangemap::RangeMap; @@ -38,7 +38,7 @@ impl FridaRuntime for CoverageRuntime { &mut self, _gum: &frida_gum::Gum, _ranges: &RangeMap, - _modules_to_instrument: &[&str], + _module_map: &Rc, ) { } diff --git a/libafl_frida/src/drcov_rt.rs b/libafl_frida/src/drcov_rt.rs index dc79b55ab3..d5d0d63593 100644 --- a/libafl_frida/src/drcov_rt.rs +++ b/libafl_frida/src/drcov_rt.rs @@ -2,9 +2,11 @@ use std::{ collections::HashMap, hash::{BuildHasher, Hasher}, + rc::Rc, }; use ahash::RandomState; +use frida_gum::ModuleMap; use libafl::{ inputs::{HasTargetBytes, Input}, Error, @@ -31,7 +33,7 @@ impl FridaRuntime for DrCovRuntime { &mut self, _gum: &frida_gum::Gum, ranges: &RangeMap, - _modules_to_instrument: &[&str], + _module_map: &Rc, ) { self.ranges = ranges.clone(); std::fs::create_dir_all("./coverage") diff --git a/libafl_frida/src/executor.rs b/libafl_frida/src/executor.rs index 8445322cf8..2b28931c9a 100644 --- a/libafl_frida/src/executor.rs +++ b/libafl_frida/src/executor.rs @@ -202,7 +202,7 @@ where } } - if !helper.options().disable_excludes { + if !helper.disable_excludes { for range in ranges.gaps(&(0..usize::MAX)) { log::info!("excluding range: {:x}-{:x}", range.start, range.end); stalker.exclude(&MemoryRange::new( diff --git a/libafl_frida/src/helper.rs b/libafl_frida/src/helper.rs index e16e9bd1b2..3eac1e9d2b 100644 --- a/libafl_frida/src/helper.rs +++ b/libafl_frida/src/helper.rs @@ -1,6 +1,8 @@ use core::fmt::{self, Debug, Formatter}; use std::{ cell::{Ref, RefCell, RefMut}, + fs, + path::{Path, PathBuf}, rc::Rc, }; @@ -13,7 +15,10 @@ use capstone::{ use frida_gum::instruction_writer::InstructionWriter; #[cfg(unix)] use frida_gum::CpuContext; -use frida_gum::{stalker::Transformer, Gum, Module, ModuleDetails, ModuleMap, PageProtection}; +use frida_gum::{ + stalker::{StalkerIterator, StalkerOutput, Transformer}, + Gum, Module, ModuleDetails, ModuleMap, PageProtection, +}; use libafl::{ inputs::{HasTargetBytes, Input}, Error, @@ -43,7 +48,7 @@ pub trait FridaRuntime: 'static + Debug { &mut self, gum: &Gum, ranges: &RangeMap, - modules_to_instrument: &[&str], + module_map: &Rc, ); /// Method called before execution @@ -60,7 +65,7 @@ pub trait FridaRuntimeTuple: MatchFirstType + Debug { &mut self, gum: &Gum, ranges: &RangeMap, - modules_to_instrument: &[&str], + module_map: &Rc, ); /// Method called before execution @@ -75,7 +80,7 @@ impl FridaRuntimeTuple for () { &mut self, _gum: &Gum, _ranges: &RangeMap, - _modules_to_instrument: &[&str], + _module_map: &Rc, ) { } fn pre_exec_all(&mut self, _input: &I) -> Result<(), Error> { @@ -95,10 +100,10 @@ where &mut self, gum: &Gum, ranges: &RangeMap, - modules_to_instrument: &[&str], + module_map: &Rc, ) { - self.0.init(gum, ranges, modules_to_instrument); - self.1.init_all(gum, ranges, modules_to_instrument); + self.0.init(gum, ranges, module_map); + self.1.init_all(gum, ranges, module_map); } fn pre_exec_all(&mut self, input: &I) -> Result<(), Error> { @@ -112,12 +117,234 @@ where } } +/// Represents a range to be skipped for instrumentation +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SkipRange { + /// An absolute range + Absolute(std::ops::Range), + + /// A range relative to the module with the given name + ModuleRelative { + /// The module name + name: String, + + /// The address range + range: std::ops::Range, + }, +} + +/// Builder for [`FridaInstrumentationHelper`](FridaInstrumentationHelper) +pub struct FridaInstrumentationHelperBuilder { + stalker_enabled: bool, + disable_excludes: bool, + #[allow(clippy::type_complexity)] + instrument_module_predicate: Option bool>>, + skip_module_predicate: Box bool>, + skip_ranges: Vec, +} + +impl FridaInstrumentationHelperBuilder { + /// Create a new `FridaInstrumentationHelperBuilder` + pub fn new() -> Self { + Self::default() + } + + /// Enable or disable the Stalker + /// + /// Required for coverage collection, ASAN, and `CmpLog`. + /// Enabled by default. + #[must_use] + pub fn enable_stalker(self, enabled: bool) -> Self { + Self { + stalker_enabled: enabled, + ..self + } + } + + /// Disable excludes + /// + /// Don't use `stalker.exclude()`. + /// See + #[must_use] + pub fn disable_excludes(self, disabled: bool) -> Self { + Self { + disable_excludes: disabled, + ..self + } + } + + /// Modules for which the given predicate returns `true` will be instrumented. + /// + /// Can be specified multiple times; a module will be instrumented if _any_ of the given predicates match. + /// [`skip_modules_if`](Self::skip_modules-if) will override these. + /// + /// # Example + /// Instrument all modules in `/usr/lib` as well as `libfoo.so`: + /// ``` + ///# use libafl_frida::helper::FridaInstrumentationHelperBuilder; + /// let builder = FridaInstrumentationHelperBuilder::new() + /// .instrument_module_if(|module| module.name() == "libfoo.so") + /// .instrument_module_if(|module| module.path().starts_with("/usr/lib")); + /// ``` + #[must_use] + pub fn instrument_module_if bool + 'static>( + mut self, + mut predicate: F, + ) -> Self { + let new = move |module: &_| match &mut self.instrument_module_predicate { + Some(existing) => existing(module) || predicate(module), + None => predicate(module), + }; + Self { + instrument_module_predicate: Some(Box::new(new)), + ..self + } + } + + /// Modules for which the given predicate returns `true` will not be instrumented. + /// + /// Can be specified multiple times; a module will be skipped if _any_ of the given predicates match. + /// Overrides modules included using [`instrument_module_if`](Self::instrument_module_if). + /// + /// # Example + /// Instrument all modules in `/usr/lib`, but exclude `libfoo.so`. + /// + /// ``` + ///# use libafl_frida::helper::FridaInstrumentationHelperBuilder; + /// let builder = FridaInstrumentationHelperBuilder::new() + /// .instrument_module_if(|module| module.path().starts_with("/usr/lib")) + /// .skip_module_if(|module| module.name() == "libfoo.so"); + /// ``` + #[must_use] + pub fn skip_module_if bool + 'static>( + mut self, + mut predicate: F, + ) -> Self { + let new = move |module: &_| (self.skip_module_predicate)(module) || predicate(module); + Self { + skip_module_predicate: Box::new(new), + ..self + } + } + + /// Skip a specific range + #[must_use] + pub fn skip_range(mut self, range: SkipRange) -> Self { + self.skip_ranges.push(range); + self + } + + /// Skip a set of ranges + #[must_use] + pub fn skip_ranges>(mut self, ranges: I) -> Self { + self.skip_ranges.extend(ranges); + self + } + + /// Build a `FridaInstrumentationHelper` + pub fn build( + self, + gum: &Gum, + mut runtimes: RT, + ) -> FridaInstrumentationHelper<'_, RT> { + let Self { + stalker_enabled, + disable_excludes, + mut instrument_module_predicate, + mut skip_module_predicate, + skip_ranges, + } = self; + + let mut module_filter = Box::new(move |module| { + if let Some(instrument_module_predicate) = &mut instrument_module_predicate { + let skip = skip_module_predicate(&module); + let should_instrument = instrument_module_predicate(&module); + should_instrument && !skip + } else { + !skip_module_predicate(&module) + } + }); + let module_map = Rc::new(ModuleMap::new_with_filter(gum, &mut module_filter)); + + let mut ranges = RangeMap::new(); + if stalker_enabled { + for (i, module) in module_map.values().iter().enumerate() { + let range = module.range(); + let start = range.base_address().0 as usize; + ranges.insert(start..(start + range.size()), (i as u16, module.path())); + } + for skip in skip_ranges { + match skip { + SkipRange::Absolute(range) => ranges.remove(range), + SkipRange::ModuleRelative { name, range } => { + let module_details = ModuleDetails::with_name(name).unwrap(); + let lib_start = module_details.range().base_address().0 as usize; + ranges.remove((lib_start + range.start)..(lib_start + range.end)); + } + } + } + runtimes.init_all(gum, &ranges, &module_map); + } + + // Wrap ranges and runtimes in reference-counted refcells in order to move + // these references both into the struct that we return and the transformer callback + // that we pass to frida-gum. + let ranges = Rc::new(RefCell::new(ranges)); + let runtimes = Rc::new(RefCell::new(runtimes)); + + let transformer = FridaInstrumentationHelper::build_transformer(gum, &ranges, &runtimes); + + #[cfg(unix)] + FridaInstrumentationHelper::<'_, RT>::workaround_gum_allocate_near(); + + FridaInstrumentationHelper { + transformer, + ranges, + runtimes, + stalker_enabled, + disable_excludes, + } + } +} + +impl Debug for FridaInstrumentationHelperBuilder { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut dbg_me = f.debug_struct("FridaInstrumentationHelper"); + dbg_me + .field("stalker_enabled", &self.stalker_enabled) + .field("instrument_module_predicate", &"") + .field("skip_module_predicate", &"") + .field("skip_ranges", &self.skip_ranges) + .field("disable_excludes", &self.disable_excludes); + dbg_me.finish() + } +} + +impl Default for FridaInstrumentationHelperBuilder { + fn default() -> Self { + Self { + stalker_enabled: true, + disable_excludes: true, + instrument_module_predicate: None, + skip_module_predicate: Box::new(|module| { + // Skip the instrumentation module to avoid recursion. + let range = module.range(); + let start = range.base_address().0 as usize; + let range = start..(start + range.size()); + range.contains(&(Self::new as usize)) + }), + skip_ranges: Vec::new(), + } + } +} + /// An helper that feeds `FridaInProcessExecutor` with edge-coverage instrumentation pub struct FridaInstrumentationHelper<'a, RT: 'a> { - options: &'a FuzzerOptions, transformer: Transformer<'a>, ranges: Rc>>, runtimes: Rc>, + stalker_enabled: bool, + pub(crate) disable_excludes: bool, } impl Debug for FridaInstrumentationHelper<'_, RT> { @@ -126,7 +353,7 @@ impl Debug for FridaInstrumentationHelper<'_, RT> { dbg_me .field("ranges", &self.ranges) .field("module_map", &"") - .field("options", &self.options); + .field("stalker_enabled", &self.stalker_enabled); dbg_me.finish() } } @@ -154,17 +381,231 @@ fn pc(context: &CpuContext) -> usize { context.rip() as usize } +fn pathlist_contains_module(list: I, module: &ModuleDetails) -> bool +where + I: IntoIterator, + P: AsRef, +{ + let module_name = module.name(); + let module_path = PathBuf::from(module.path()); + let canonicalized_module_path = fs::canonicalize(&module_path).ok(); + list.into_iter().any(|path| { + let path = path.as_ref(); + + path == Path::new(&module_name) + || path == module_path + || fs::canonicalize(path).ok() == canonicalized_module_path + }) +} + +impl<'a> FridaInstrumentationHelper<'a, ()> { + /// Create a builder to initialize a `FridaInstrumentationHelper`. + /// + /// See the documentation of [`FridaInstrumentationHelperBuilder`](FridaInstrumentationHelperBuilder) + /// for more details. + pub fn builder() -> FridaInstrumentationHelperBuilder { + FridaInstrumentationHelperBuilder::default() + } +} + /// The implementation of the [`FridaInstrumentationHelper`] impl<'a, RT> FridaInstrumentationHelper<'a, RT> where - RT: FridaRuntimeTuple, + RT: FridaRuntimeTuple + 'a, { - /// Constructor function to create a new [`FridaInstrumentationHelper`], given a `module_name`. - #[allow(clippy::too_many_lines)] + /// Constructor function to create a new [`FridaInstrumentationHelper`], given CLI Options. #[must_use] - pub fn new(gum: &'a Gum, options: &'a FuzzerOptions, mut runtimes: RT) -> Self { - // workaround frida's frida-gum-allocate-near bug: - #[cfg(unix)] + pub fn new<'b>(gum: &'a Gum, options: &'b FuzzerOptions, runtimes: RT) -> Self { + let harness = options.harness.clone(); + let libs_to_instrument = options + .libs_to_instrument + .iter() + .map(PathBuf::from) + .collect::>(); + return FridaInstrumentationHelper::builder() + .enable_stalker(options.cmplog || options.asan || !options.disable_coverage) + .disable_excludes(options.disable_excludes) + .instrument_module_if(move |module| pathlist_contains_module(&harness, module)) + .instrument_module_if(move |module| { + pathlist_contains_module(&libs_to_instrument, module) + }) + .skip_ranges(options.dont_instrument.iter().map(|(name, offset)| { + SkipRange::ModuleRelative { + name: name.clone(), + range: *offset..*offset + 4, + } + })) + .build(gum, runtimes); + } + + #[allow(clippy::too_many_lines)] + fn build_transformer( + gum: &'a Gum, + ranges: &Rc>>, + runtimes: &Rc>, + ) -> Transformer<'a> { + let ranges = Rc::clone(ranges); + let runtimes = Rc::clone(runtimes); + + #[cfg(target_arch = "aarch64")] + let capstone = Capstone::new() + .arm64() + .mode(arch::arm64::ArchMode::Arm) + .detail(true) + .build() + .expect("Failed to create Capstone object"); + #[cfg(all(target_arch = "x86_64", unix))] + let capstone = Capstone::new() + .x86() + .mode(arch::x86::ArchMode::Mode64) + .detail(true) + .build() + .expect("Failed to create Capstone object"); + + Transformer::from_callback(gum, move |basic_block, output| { + Self::transform( + basic_block, + &output, + &ranges, + &runtimes, + #[cfg(any(target_arch = "aarch64", all(target_arch = "x86_64", unix)))] + &capstone, + ); + }) + } + + fn transform( + basic_block: StalkerIterator, + output: &StalkerOutput, + ranges: &Rc>>, + runtimes: &Rc>, + #[cfg(any(target_arch = "aarch64", all(target_arch = "x86_64", unix)))] capstone: &Capstone, + ) { + let mut first = true; + for instruction in basic_block { + let instr = instruction.instr(); + #[cfg(unix)] + let instr_size = instr.bytes().len(); + let address = instr.address(); + //log::trace!("block @ {:x} transformed to {:x}", address, output.writer().pc()); + + if ranges.borrow().contains_key(&(address as usize)) { + let mut runtimes = (*runtimes).borrow_mut(); + if first { + first = false; + // log::info!( + // "block @ {:x} transformed to {:x}", + // address, + // output.writer().pc() + // ); + if let Some(rt) = runtimes.match_first_type_mut::() { + rt.emit_coverage_mapping(address, output); + } + + #[cfg(unix)] + if let Some(rt) = runtimes.match_first_type_mut::() { + instruction.put_callout(|context| { + let real_address = rt.real_address_for_stalked(pc(&context)); + //let (range, (id, name)) = helper.ranges.get_key_value(&real_address).unwrap(); + //log::trace!("{}:0x{:016x}", name, real_address - range.start); + rt.drcov_basic_blocks.push(DrCovBasicBlock::new( + real_address, + real_address + instr_size, + )); + }); + } + } + + #[cfg(unix)] + let res = if let Some(_rt) = runtimes.match_first_type_mut::() { + AsanRuntime::asan_is_interesting_instruction(capstone, address, instr) + } else { + None + }; + + #[cfg(all(target_arch = "x86_64", unix))] + if let Some((segment, width, basereg, indexreg, scale, disp)) = res { + if let Some(rt) = runtimes.match_first_type_mut::() { + rt.emit_shadow_check( + address, + output, + segment, + width, + basereg, + indexreg, + scale, + disp.try_into().unwrap(), + ); + } + } + + #[cfg(target_arch = "aarch64")] + if let Some((basereg, indexreg, displacement, width, shift, extender)) = res { + if let Some(rt) = runtimes.match_first_type_mut::() { + rt.emit_shadow_check( + address, + &output, + basereg, + indexreg, + displacement, + width, + shift, + extender, + ); + } + } + + #[cfg(all(feature = "cmplog", target_arch = "aarch64"))] + if let Some(rt) = runtimes.match_first_type_mut::() { + if let Some((op1, op2, special_case)) = + CmpLogRuntime::cmplog_is_interesting_instruction(&capstone, address, instr) + { + //emit code that saves the relevant data in runtime(passes it to x0, x1) + rt.emit_comparison_handling(address, &output, &op1, &op2, special_case); + } + } + + #[cfg(unix)] + if let Some(rt) = runtimes.match_first_type_mut::() { + rt.add_stalked_address( + output.writer().pc() as usize - instr_size, + address as usize, + ); + } + + #[cfg(unix)] + if let Some(rt) = runtimes.match_first_type_mut::() { + rt.add_stalked_address( + output.writer().pc() as usize - instr_size, + address as usize, + ); + } + } + instruction.keep(); + } + } + + /* + /// Return the runtime + pub fn runtime(&self) -> Option<&R> + where + R: FridaRuntime, + { + self.runtimes.borrow().match_first_type::() + } + + /// Return the mutable runtime + pub fn runtime_mut(&mut self) -> Option<&mut R> + where + R: FridaRuntime, + { + (*self.runtimes).borrow_mut().match_first_type_mut::() + } + */ + + // workaround frida's frida-gum-allocate-near bug: + #[cfg(unix)] + fn workaround_gum_allocate_near() { unsafe { for _ in 0..512 { mmap( @@ -187,211 +628,8 @@ where .expect("Failed to map dummy regions for frida workaround"); } } - - let mut modules_to_instrument = vec![options - .harness - .as_ref() - .unwrap() - .to_string_lossy() - .to_string()]; - modules_to_instrument.append(&mut options.libs_to_instrument.clone()); - let modules_to_instrument: Vec<&str> = - modules_to_instrument.iter().map(AsRef::as_ref).collect(); - - let module_map = ModuleMap::new_from_names(gum, &modules_to_instrument); - let mut ranges = RangeMap::new(); - - if options.cmplog || options.asan || !options.disable_coverage { - for (i, module) in module_map.values().iter().enumerate() { - let range = module.range(); - let start = range.base_address().0 as usize; - // log::trace!("start: {:x}", start); - ranges.insert(start..(start + range.size()), (i as u16, module.path())); - } - if !options.dont_instrument.is_empty() { - for (module_name, offset) in options.dont_instrument.clone() { - let module_details = ModuleDetails::with_name(module_name).unwrap(); - let lib_start = module_details.range().base_address().0 as usize; - // log::info!("removing address: {:#x}", lib_start + offset); - ranges.remove((lib_start + offset)..(lib_start + offset + 4)); - } - } - - // make sure we aren't in the instrumented list, as it would cause recursions - assert!( - !ranges.contains_key(&(Self::new as usize)), - "instrumented libraries must not include the fuzzer" - ); - - runtimes.init_all(gum, &ranges, &modules_to_instrument); - } - - #[cfg(target_arch = "aarch64")] - let capstone = Capstone::new() - .arm64() - .mode(arch::arm64::ArchMode::Arm) - .detail(true) - .build() - .expect("Failed to create Capstone object"); - #[cfg(all(target_arch = "x86_64", unix))] - let capstone = Capstone::new() - .x86() - .mode(arch::x86::ArchMode::Mode64) - .detail(true) - .build() - .expect("Failed to create Capstone object"); - - // Wrap ranges and runtimes in reference-counted refcells in order to move - // these references both into the struct that we return and the transformer callback - // that we pass to frida-gum. - let ranges = Rc::new(RefCell::new(ranges)); - let runtimes = Rc::new(RefCell::new(runtimes)); - - let transformer = { - let ranges = Rc::clone(&ranges); - let runtimes = Rc::clone(&runtimes); - Transformer::from_callback(gum, move |basic_block, output| { - let mut first = true; - for instruction in basic_block { - let instr = instruction.instr(); - #[cfg(unix)] - let instr_size = instr.bytes().len(); - let address = instr.address(); - //log::trace!("block @ {:x} transformed to {:x}", address, output.writer().pc()); - - if ranges.borrow().contains_key(&(address as usize)) { - let mut runtimes = (*runtimes).borrow_mut(); - if first { - first = false; - // log::info!( - // "block @ {:x} transformed to {:x}", - // address, - // output.writer().pc() - // ); - if let Some(rt) = runtimes.match_first_type_mut::() { - rt.emit_coverage_mapping(address, &output); - } - - #[cfg(unix)] - if let Some(rt) = runtimes.match_first_type_mut::() { - instruction.put_callout(|context| { - let real_address = rt.real_address_for_stalked(pc(&context)); - //let (range, (id, name)) = helper.ranges.get_key_value(&real_address).unwrap(); - //log::trace!("{}:0x{:016x}", name, real_address - range.start); - rt.drcov_basic_blocks.push(DrCovBasicBlock::new( - real_address, - real_address + instr_size, - )); - }); - } - } - - #[cfg(unix)] - let res = if let Some(_rt) = runtimes.match_first_type_mut::() - { - AsanRuntime::asan_is_interesting_instruction(&capstone, address, instr) - } else { - None - }; - - #[cfg(all(target_arch = "x86_64", unix))] - if let Some((segment, width, basereg, indexreg, scale, disp)) = res { - if let Some(rt) = runtimes.match_first_type_mut::() { - rt.emit_shadow_check( - address, - &output, - segment, - width, - basereg, - indexreg, - scale, - disp.try_into().unwrap(), - ); - } - } - - #[cfg(target_arch = "aarch64")] - if let Some((basereg, indexreg, displacement, width, shift, extender)) = res - { - if let Some(rt) = runtimes.match_first_type_mut::() { - rt.emit_shadow_check( - address, - &output, - basereg, - indexreg, - displacement, - width, - shift, - extender, - ); - } - } - - #[cfg(all(feature = "cmplog", target_arch = "aarch64"))] - if let Some(rt) = runtimes.match_first_type_mut::() { - if let Some((op1, op2, special_case)) = - CmpLogRuntime::cmplog_is_interesting_instruction( - &capstone, address, instr, - ) - { - //emit code that saves the relevant data in runtime(passes it to x0, x1) - rt.emit_comparison_handling( - address, - &output, - &op1, - &op2, - special_case, - ); - } - } - - #[cfg(unix)] - if let Some(rt) = runtimes.match_first_type_mut::() { - rt.add_stalked_address( - output.writer().pc() as usize - instr_size, - address as usize, - ); - } - - #[cfg(unix)] - if let Some(rt) = runtimes.match_first_type_mut::() { - rt.add_stalked_address( - output.writer().pc() as usize - instr_size, - address as usize, - ); - } - } - instruction.keep(); - } - }) - }; - - Self { - options, - transformer, - ranges, - runtimes, - } } - /* - /// Return the runtime - pub fn runtime(&self) -> Option<&R> - where - R: FridaRuntime, - { - self.runtimes.borrow().match_first_type::() - } - - /// Return the mutable runtime - pub fn runtime_mut(&mut self) -> Option<&mut R> - where - R: FridaRuntime, - { - (*self.runtimes).borrow_mut().match_first_type_mut::() - } - */ - /// Returns ref to the Transformer pub fn transformer(&self) -> &Transformer<'a> { &self.transformer @@ -402,11 +640,11 @@ where &mut self, gum: &'a Gum, ranges: &RangeMap, - modules_to_instrument: &'a [&str], + module_map: &Rc, ) { (*self.runtimes) .borrow_mut() - .init_all(gum, ranges, modules_to_instrument); + .init_all(gum, ranges, module_map); } /// Method called before execution @@ -421,7 +659,7 @@ where /// If stalker is enabled pub fn stalker_enabled(&self) -> bool { - self.options.cmplog || self.options.asan || !self.options.disable_coverage + self.stalker_enabled } /// Pointer to coverage map @@ -441,10 +679,4 @@ where pub fn ranges_mut(&mut self) -> RefMut> { (*self.ranges).borrow_mut() } - - /// Return the ref to options - #[inline] - pub fn options(&self) -> &FuzzerOptions { - self.options - } }