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