Hook using frida gum interceptor instead of gothook (#112)

* Move from gothook to frida-based hooks

* Force link against libc++

* Clippy + cleanup prints

* exclude ranges

* Add back guard pages; Implement libc hooks

* Bump frida-rust version

* Add hooks for mmap/munmap, as per issue #105

* Refactor to get rid of global allocator singleton

* Cleanup imports; Fix free out-of-range; Move to fixed addresses for asan allocatoins

* use frida-rust from crates.io now that it has caught up

* cargo fmt

* Clippy fixes

* Better clippy fix

* More clippy fix

* Formatting

* Review changes
This commit is contained in:
s1341 2021-05-25 14:45:06 +03:00 committed by GitHub
parent d4410c072a
commit 3a21ad59a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2664 additions and 1249 deletions

View File

@ -21,11 +21,10 @@ num_cpus = "1.0"
which = "4.1"
[target.'cfg(unix)'.dependencies]
libafl = { path = "../../libafl/", features = [ "std", "llmp_compression", "llmp_bind_public" ] } #, "llmp_small_maps", "llmp_debug"]}
libafl = { path = "../../libafl/", features = [ "std", "llmp_bind_public" ] } #, "llmp_small_maps", "llmp_debug"]}
libafl_frida = { path = "../../libafl_frida" }
capstone = "0.8.0"
frida-gum = { version = "0.5.0", git = "https://github.com/frida/frida-rust", features = [ "auto-download", "backtrace", "event-sink", "invocation-listener"], rev = "69f5b8236ab4b66296507803b4b7bfec79700e84" }
#frida-gum = { version = "0.5.0", path = "../../../frida-rust/frida-gum", features = [ "auto-download", "event-sink", "invocation-listener"] }
frida-gum = { version = "0.5.1", features = [ "auto-download", "backtrace", "event-sink", "invocation-listener"] }
lazy_static = "1.4.0"
libc = "0.2"
libloading = "0.7.0"

View File

@ -40,7 +40,7 @@ use libafl::{
use frida_gum::{
stalker::{NoneEventSink, Stalker},
Gum, NativePointer,
Gum, MemoryRange, NativePointer,
};
use std::{
@ -53,7 +53,7 @@ use std::{
};
use libafl_frida::{
asan_rt::{AsanErrorsFeedback, AsanErrorsObserver, ASAN_ERRORS},
asan_errors::{AsanErrorsFeedback, AsanErrorsObserver, ASAN_ERRORS},
helper::{FridaHelper, FridaInstrumentationHelper, MAP_SIZE},
FridaOptions,
};
@ -190,17 +190,15 @@ where
helper: &'c mut FH,
timeout: Duration,
) -> Self {
let stalker = Stalker::new(gum);
let mut stalker = Stalker::new(gum);
// Let's exclude the main module and libc.so at least:
//stalker.exclude(&MemoryRange::new(
//Module::find_base_address(&env::args().next().unwrap()),
//get_module_size(&env::args().next().unwrap()),
//));
//stalker.exclude(&MemoryRange::new(
//Module::find_base_address("libc.so"),
//get_module_size("libc.so"),
//));
for range in helper.ranges().gaps(&(0..usize::MAX)) {
println!("excluding range: {:x}-{:x}", range.start, range.end);
stalker.exclude(&MemoryRange::new(
NativePointer(range.start as *mut c_void),
range.end - range.start,
));
}
Self {
base: TimeoutExecutor::new(base, timeout),
@ -385,7 +383,8 @@ unsafe fn fuzz(
// RNG
StdRand::with_seed(current_nanos()),
// Corpus that will be evolved, we keep it in memory for performance
InMemoryCorpus::new(),
OnDiskCorpus::new(PathBuf::from("./corpus_discovered")).unwrap(),
//InMemoryCorpus::new(),
// Corpus in which we store solutions (crashes in this example),
// on disk so the user can get them after stopping the fuzzer
OnDiskCorpus::new_save_meta(
@ -440,15 +439,6 @@ unsafe fn fuzz(
&mut frida_helper,
Duration::new(10, 0),
);
// Let's exclude the main module and libc.so at least:
//executor.stalker.exclude(&MemoryRange::new(
//Module::find_base_address(&env::args().next().unwrap()),
//get_module_size(&env::args().next().unwrap()),
//));
//executor.stalker.exclude(&MemoryRange::new(
//Module::find_base_address("libc.so"),
//get_module_size("libc.so"),
//));
// In case the corpus is empty (on first run), reset
if state.corpus().count() < 1 {

View File

@ -851,10 +851,6 @@ where
println!("We're a client, let's fuzz :)");
for (var, val) in std::env::vars() {
println!("ENV VARS: {:?}: {:?}", var, val);
}
// If we're restarting, deserialize the old state.
let (state, mut mgr) = match receiver.recv_buf()? {
None => {

View File

@ -430,8 +430,7 @@ mod unix_signal_handler {
{
println!("Double crash\n");
#[cfg(target_os = "android")]
let si_addr =
{ ((_info._pad[0] as usize) | ((_info._pad[1] as usize) << 32)) as usize };
let si_addr = (_info._pad[0] as i64) | ((_info._pad[1] as i64) << 32);
#[cfg(not(target_os = "android"))]
let si_addr = { _info.si_addr() as usize };
@ -466,6 +465,7 @@ mod unix_signal_handler {
#[cfg(feature = "std")]
println!("Child crashed!");
#[allow(clippy::non_ascii_literal)]
#[cfg(all(
feature = "std",
any(target_os = "linux", target_os = "android"),

View File

@ -21,10 +21,8 @@ libc = "0.2.92"
hashbrown = "0.11"
libloading = "0.7.0"
rangemap = "0.1.10"
frida-gum = { version = "0.5.0", git = "https://github.com/frida/frida-rust", features = [ "auto-download", "backtrace", "event-sink", "invocation-listener"], rev = "69f5b8236ab4b66296507803b4b7bfec79700e84" }
frida-gum-sys = { version = "0.3.0", git = "https://github.com/frida/frida-rust", features = [ "auto-download", "event-sink", "invocation-listener"], rev = "69f5b8236ab4b66296507803b4b7bfec79700e84" }
#frida-gum = { version = "0.5.0", path = "../../frida-rust/frida-gum", features = [ "auto-download", "backtrace", "event-sink", "invocation-listener"] }
#frida-gum-sys = { version = "0.3.0", path = "../../frida-rust/frida-gum-sys", features = [ "auto-download", "event-sink", "invocation-listener"] }
frida-gum-sys = { version = "0.3", features = [ "auto-download", "event-sink", "invocation-listener"] }
frida-gum = { version = "0.5.1", features = [ "auto-download", "backtrace", "event-sink", "invocation-listener"] }
core_affinity = { version = "0.5", git = "https://github.com/s1341/core_affinity_rs" }
regex = "1.4"
dynasmrt = "1.0.1"
@ -35,6 +33,4 @@ serde = "1.0"
backtrace = { version = "0.3.58", default-features = false, features = ["std", "serde"] }
num-traits = "0.2.14"
ahash = "0.7"
[target.'cfg(unix)'.dependencies]
gothook = { version = "0.1" }
paste = "1.0"

View File

@ -2,4 +2,7 @@
fn main() {
cc::Build::new().file("src/gettls.c").compile("libgettls.a");
// Force linking against libc++
println!("cargo:rustc-link-lib=dylib=c++");
}

408
libafl_frida/src/alloc.rs Normal file
View File

@ -0,0 +1,408 @@
use hashbrown::HashMap;
use libafl::bolts::os::walk_self_maps;
use nix::{
libc::memset,
sys::mman::{mmap, MapFlags, ProtFlags},
};
use backtrace::Backtrace;
#[cfg(unix)]
use libc::{sysconf, _SC_PAGESIZE};
use rangemap::RangeSet;
use serde::{Deserialize, Serialize};
use std::{ffi::c_void, io};
use crate::{
asan_errors::{AsanError, AsanErrors},
FridaOptions,
};
pub(crate) struct Allocator {
options: FridaOptions,
page_size: usize,
shadow_offset: usize,
shadow_bit: usize,
pre_allocated_shadow: bool,
allocations: HashMap<usize, AllocationMetadata>,
shadow_pages: RangeSet<usize>,
allocation_queue: HashMap<usize, Vec<AllocationMetadata>>,
largest_allocation: usize,
base_mapping_addr: usize,
current_mapping_addr: usize,
}
macro_rules! map_to_shadow {
($self:expr, $address:expr) => {
(($address >> 3) + $self.shadow_offset) & ((1 << ($self.shadow_bit + 1)) - 1)
};
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub(crate) struct AllocationMetadata {
pub address: usize,
pub size: usize,
pub actual_size: usize,
pub allocation_site_backtrace: Option<Backtrace>,
pub release_site_backtrace: Option<Backtrace>,
pub freed: bool,
pub is_malloc_zero: bool,
}
impl Allocator {
pub fn new(options: FridaOptions) -> Self {
let ret = unsafe { sysconf(_SC_PAGESIZE) };
if ret < 0 {
panic!("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: usize = 0;
for try_shadow_bit in &[46usize, 36usize] {
let addr: usize = 1 << try_shadow_bit;
if unsafe {
mmap(
addr as *mut c_void,
page_size,
ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
MapFlags::MAP_PRIVATE
| MapFlags::MAP_ANONYMOUS
| MapFlags::MAP_FIXED
| MapFlags::MAP_NORESERVE,
-1,
0,
)
}
.is_ok()
{
shadow_bit = *try_shadow_bit;
break;
}
}
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(
addr as *mut c_void,
addr + addr,
ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
MapFlags::MAP_ANONYMOUS
| MapFlags::MAP_FIXED
| MapFlags::MAP_PRIVATE
| MapFlags::MAP_NORESERVE,
-1,
0,
)
}
.is_ok();
Self {
options,
page_size,
pre_allocated_shadow,
shadow_offset: 1 << shadow_bit,
shadow_bit,
allocations: HashMap::new(),
shadow_pages: RangeSet::new(),
allocation_queue: HashMap::new(),
largest_allocation: 0,
base_mapping_addr: addr + addr + addr,
current_mapping_addr: addr + addr + addr,
}
}
/// Retreive the shadow bit used by this allocator.
pub fn shadow_bit(&self) -> u32 {
self.shadow_bit as u32
}
#[inline]
fn round_up_to_page(&self, size: usize) -> usize {
((size + self.page_size) / self.page_size) * self.page_size
}
#[inline]
fn round_down_to_page(&self, value: usize) -> usize {
(value / self.page_size) * self.page_size
}
fn find_smallest_fit(&mut self, size: usize) -> Option<AllocationMetadata> {
let mut current_size = size;
while current_size <= self.largest_allocation {
if self.allocation_queue.contains_key(&current_size) {
if let Some(metadata) = self.allocation_queue.entry(current_size).or_default().pop()
{
return Some(metadata);
}
}
current_size *= 2;
}
None
}
pub unsafe fn alloc(&mut self, size: usize, _alignment: usize) -> *mut c_void {
let mut is_malloc_zero = false;
let size = if size == 0 {
println!("zero-sized allocation!");
is_malloc_zero = true;
16
} else {
size
};
if size > (1 << 30) {
panic!("Allocation is too large: 0x{:x}", size);
}
let rounded_up_size = self.round_up_to_page(size) + 2 * self.page_size;
let metadata = if let Some(mut metadata) = self.find_smallest_fit(rounded_up_size) {
//println!("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.enable_asan_allocation_backtraces {
metadata.allocation_site_backtrace = Some(Backtrace::new_unresolved());
}
metadata
} else {
let mapping = match mmap(
self.current_mapping_addr as *mut c_void,
rounded_up_size,
ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
MapFlags::MAP_ANONYMOUS
| MapFlags::MAP_PRIVATE
| MapFlags::MAP_FIXED
| MapFlags::MAP_NORESERVE,
-1,
0,
) {
Ok(mapping) => mapping as usize,
Err(err) => {
println!("An error occurred while mapping memory: {:?}", err);
return std::ptr::null_mut();
}
};
self.current_mapping_addr += rounded_up_size;
self.map_shadow_for_region(mapping, mapping + rounded_up_size, false);
let mut metadata = AllocationMetadata {
address: mapping,
size,
actual_size: rounded_up_size,
..AllocationMetadata::default()
};
if self.options.enable_asan_allocation_backtraces {
metadata.allocation_site_backtrace = Some(Backtrace::new_unresolved());
}
metadata
};
self.largest_allocation = std::cmp::max(self.largest_allocation, metadata.actual_size);
// unpoison the shadow memory for the allocation itself
Self::unpoison(
map_to_shadow!(self, metadata.address + self.page_size),
size,
);
let address = (metadata.address + self.page_size) as *mut c_void;
self.allocations
.insert(metadata.address + self.page_size, metadata);
//println!("serving address: {:?}, size: {:x}", address, size);
address
}
pub unsafe fn release(&mut self, ptr: *mut c_void) {
let mut metadata = if let Some(metadata) = self.allocations.get_mut(&(ptr as usize)) {
metadata
} else {
if !ptr.is_null() {
AsanErrors::get_mut().report_error(
AsanError::UnallocatedFree((ptr as usize, Backtrace::new())),
None,
);
}
return;
};
if metadata.freed {
AsanErrors::get_mut().report_error(
AsanError::DoubleFree((ptr as usize, metadata.clone(), Backtrace::new())),
None,
);
}
let shadow_mapping_start = map_to_shadow!(self, ptr as usize);
metadata.freed = true;
if self.options.enable_asan_allocation_backtraces {
metadata.release_site_backtrace = Some(Backtrace::new_unresolved());
}
// poison the shadow memory for the allocation
Self::poison(shadow_mapping_start, metadata.size);
}
pub fn find_metadata(
&mut self,
ptr: usize,
hint_base: usize,
) -> Option<&mut AllocationMetadata> {
let mut metadatas: Vec<&mut AllocationMetadata> = self.allocations.values_mut().collect();
metadatas.sort_by(|a, b| a.address.cmp(&b.address));
let mut offset_to_closest = i64::max_value();
let mut closest = None;
for metadata in metadatas {
let new_offset = if hint_base == metadata.address {
(ptr as i64 - metadata.address as i64).abs()
} else {
std::cmp::min(
offset_to_closest,
(ptr as i64 - metadata.address as i64).abs(),
)
};
if new_offset < offset_to_closest {
offset_to_closest = new_offset;
closest = Some(metadata);
}
}
closest
}
pub fn reset(&mut self) {
for (address, mut allocation) in self.allocations.drain() {
// First poison the memory.
Self::poison(map_to_shadow!(self, address), allocation.size);
// Reset the allocaiton metadata object
allocation.size = 0;
allocation.freed = false;
allocation.allocation_site_backtrace = None;
allocation.release_site_backtrace = None;
// Move the allocation from the allocations to the to-be-allocated queues
self.allocation_queue
.entry(allocation.actual_size)
.or_default()
.push(allocation);
}
}
pub fn get_usable_size(&self, ptr: *mut c_void) -> usize {
match self.allocations.get(&(ptr as usize)) {
Some(metadata) => metadata.size,
None => {
panic!(
"Attempted to get_usable_size on a pointer ({:?}) which was not allocated!",
ptr
);
}
}
}
fn unpoison(start: usize, size: usize) {
//println!("unpoisoning {:x} for {:x}", start, size / 8 + 1);
unsafe {
//println!("memset: {:?}", start as *mut c_void);
memset(start as *mut c_void, 0xff, size / 8);
let remainder = size % 8;
if remainder > 0 {
//println!("remainder: {:x}, offset: {:x}", remainder, start + size / 8);
memset(
(start + size / 8) as *mut c_void,
(0xff << (8 - remainder)) & 0xff,
1,
);
}
}
}
pub fn poison(start: usize, size: usize) {
//println!("poisoning {:x} for {:x}", start, size / 8 + 1);
unsafe {
//println!("memset: {:?}", start as *mut c_void);
memset(start as *mut c_void, 0x00, size / 8);
let remainder = size % 8;
if remainder > 0 {
//println!("remainder: {:x}, offset: {:x}", remainder, start + size / 8);
memset((start + size / 8) as *mut c_void, 0x00, 1);
}
}
}
/// Map shadow memory for a region, and optionally unpoison it
pub fn map_shadow_for_region(
&mut self,
start: usize,
end: usize,
unpoison: bool,
) -> (usize, usize) {
//println!("start: {:x}, end {:x}, size {:x}", start, end, end - start);
let shadow_mapping_start = map_to_shadow!(self, start);
if !self.pre_allocated_shadow {
let shadow_start = self.round_down_to_page(shadow_mapping_start);
let shadow_end =
self.round_up_to_page((end - start) / 8) + self.page_size + shadow_start;
for range in self.shadow_pages.gaps(&(shadow_start..shadow_end)) {
//println!("range: {:x}-{:x}, pagesize: {}", range.start, range.end, self.page_size);
unsafe {
mmap(
range.start as *mut c_void,
range.end - range.start,
ProtFlags::PROT_READ | ProtFlags::PROT_WRITE,
MapFlags::MAP_ANONYMOUS | MapFlags::MAP_FIXED | MapFlags::MAP_PRIVATE,
-1,
0,
)
.expect("An error occurred while mapping shadow memory");
}
}
self.shadow_pages.insert(shadow_start..shadow_end);
}
//println!("shadow_mapping_start: {:x}, shadow_size: {:x}", shadow_mapping_start, (end - start) / 8);
if unpoison {
Self::unpoison(shadow_mapping_start, end - start);
}
(shadow_mapping_start, (end - start) / 8)
}
pub fn map_to_shadow(&self, start: usize) -> usize {
map_to_shadow!(self, start)
}
#[inline]
pub fn is_managed(&self, ptr: *mut c_void) -> bool {
//self.allocations.contains_key(&(ptr as usize))
self.base_mapping_addr <= ptr as usize && (ptr as usize) < self.current_mapping_addr
}
pub fn check_for_leaks(&self) {
for metadata in self.allocations.values() {
if !metadata.freed {
AsanErrors::get_mut()
.report_error(AsanError::Leak((metadata.address, metadata.clone())), None);
}
}
}
/// Unpoison all the memory that is currently mapped with read/write permissions.
pub fn unpoison_all_existing_memory(&mut self) {
walk_self_maps(&mut |start, end, permissions, _path| {
if permissions.as_bytes()[0] == b'r' || permissions.as_bytes()[1] == b'w' {
if self.pre_allocated_shadow && start == 1 << self.shadow_bit {
return false;
}
self.map_shadow_for_region(start, end, true);
}
false
});
}
}

View File

@ -0,0 +1,569 @@
use backtrace::Backtrace;
use capstone::{arch::BuildsCapstone, Capstone};
use color_backtrace::{default_output_stream, BacktracePrinter, Verbosity};
use frida_gum::interceptor::Interceptor;
use libafl::{
bolts::{os::find_mapping_for_address, ownedref::OwnedPtr, tuples::Named},
corpus::Testcase,
events::EventFirer,
executors::{ExitKind, HasExecHooks},
feedbacks::Feedback,
inputs::{HasTargetBytes, Input},
observers::{Observer, ObserversTuple},
state::HasMetadata,
Error, SerdeAny,
};
use rangemap::RangeMap;
use serde::{Deserialize, Serialize};
use std::io::Write;
use termcolor::{Color, ColorSpec, WriteColor};
use crate::{alloc::AllocationMetadata, FridaOptions};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct AsanReadWriteError {
pub registers: [usize; 32],
pub pc: usize,
pub fault: (u16, u16, usize, usize),
pub metadata: AllocationMetadata,
pub backtrace: Backtrace,
}
#[derive(Debug, Clone, Serialize, Deserialize, SerdeAny)]
pub(crate) enum AsanError {
OobRead(AsanReadWriteError),
OobWrite(AsanReadWriteError),
ReadAfterFree(AsanReadWriteError),
WriteAfterFree(AsanReadWriteError),
DoubleFree((usize, AllocationMetadata, Backtrace)),
UnallocatedFree((usize, Backtrace)),
Unknown(([usize; 32], usize, (u16, u16, usize, usize), Backtrace)),
Leak((usize, AllocationMetadata)),
StackOobRead(([usize; 32], usize, (u16, u16, usize, usize), Backtrace)),
StackOobWrite(([usize; 32], usize, (u16, u16, usize, usize), Backtrace)),
BadFuncArgRead((String, usize, usize, Backtrace)),
BadFuncArgWrite((String, usize, usize, Backtrace)),
}
impl AsanError {
fn description(&self) -> &str {
match self {
AsanError::OobRead(_) => "heap out-of-bounds read",
AsanError::OobWrite(_) => "heap out-of-bounds write",
AsanError::DoubleFree(_) => "double-free",
AsanError::UnallocatedFree(_) => "unallocated-free",
AsanError::WriteAfterFree(_) => "heap use-after-free write",
AsanError::ReadAfterFree(_) => "heap use-after-free read",
AsanError::Unknown(_) => "heap unknown",
AsanError::Leak(_) => "memory-leak",
AsanError::StackOobRead(_) => "stack out-of-bounds read",
AsanError::StackOobWrite(_) => "stack out-of-bounds write",
AsanError::BadFuncArgRead(_) => "function arg resulting in bad read",
AsanError::BadFuncArgWrite(_) => "function arg resulting in bad write",
}
}
}
/// A struct holding errors that occurred during frida address sanitizer runs
#[allow(clippy::unsafe_derive_deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, SerdeAny)]
pub struct AsanErrors {
options: FridaOptions,
errors: Vec<AsanError>,
}
impl AsanErrors {
/// Creates a new `AsanErrors` struct
#[must_use]
pub fn new(options: FridaOptions) -> Self {
Self {
options,
errors: Vec::new(),
}
}
/// Clears this `AsanErrors` struct
pub fn clear(&mut self) {
self.errors.clear()
}
/// Gets the amount of `AsanErrors` in this struct
#[must_use]
pub fn len(&self) -> usize {
self.errors.len()
}
/// Returns `true` if no errors occurred
#[must_use]
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
/// Get a mutable reference to the global [`AsanErrors`] object
pub fn get_mut<'a>() -> &'a mut Self {
unsafe { ASAN_ERRORS.as_mut().unwrap() }
}
/// Report an error
#[allow(clippy::too_many_lines)]
pub(crate) fn report_error(
&mut self,
error: AsanError,
instrumented_ranges: Option<&RangeMap<usize, String>>,
) {
self.errors.push(error.clone());
let mut out_stream = default_output_stream();
let output = out_stream.as_mut();
let backtrace_printer = BacktracePrinter::new()
.clear_frame_filters()
.print_addresses(true)
.verbosity(Verbosity::Full)
.add_frame_filter(Box::new(|frames| {
frames.retain(
|x| matches!(&x.name, Some(n) if !n.starts_with("libafl_frida::asan_rt::")),
)
}));
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " Memory error detected! ").unwrap();
output
.set_color(ColorSpec::new().set_fg(Some(Color::Red)))
.unwrap();
write!(output, "{}", error.description()).unwrap();
match error {
AsanError::OobRead(mut error)
| AsanError::OobWrite(mut error)
| AsanError::ReadAfterFree(mut error)
| AsanError::WriteAfterFree(mut error) => {
let (basereg, indexreg, _displacement, fault_address) = error.fault;
if let Some((range, path)) = instrumented_ranges.unwrap().get_key_value(&error.pc) {
writeln!(
output,
" at 0x{:x} ({}@0x{:04x}), faulting address 0x{:x}",
error.pc,
path,
error.pc - range.start,
fault_address
)
.unwrap();
} else {
writeln!(
output,
" at 0x{:x}, faulting address 0x{:x}",
error.pc, fault_address
)
.unwrap();
}
output.reset().unwrap();
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " REGISTERS ").unwrap();
for reg in 0..=30 {
if reg == basereg {
output
.set_color(ColorSpec::new().set_fg(Some(Color::Red)))
.unwrap();
} else if reg == indexreg {
output
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))
.unwrap();
}
write!(
output,
"x{:02}: 0x{:016x} ",
reg, error.registers[reg as usize]
)
.unwrap();
output.reset().unwrap();
if reg % 4 == 3 {
writeln!(output).unwrap();
}
}
writeln!(output, "pc : 0x{:016x} ", error.pc).unwrap();
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " CODE ").unwrap();
let mut cs = Capstone::new()
.arm64()
.mode(capstone::arch::arm64::ArchMode::Arm)
.build()
.unwrap();
cs.set_skipdata(true).expect("failed to set skipdata");
let start_pc = error.pc - 4 * 5;
for insn in cs
.disasm_count(
unsafe { std::slice::from_raw_parts(start_pc as *mut u8, 4 * 11) },
start_pc as u64,
11,
)
.expect("failed to disassemble instructions")
.iter()
{
if insn.address() as usize == error.pc {
output
.set_color(ColorSpec::new().set_fg(Some(Color::Red)))
.unwrap();
writeln!(output, "\t => {}", insn).unwrap();
output.reset().unwrap();
} else {
writeln!(output, "\t {}", insn).unwrap();
}
}
backtrace_printer
.print_trace(&error.backtrace, output)
.unwrap();
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " ALLOCATION INFO ").unwrap();
let offset: i64 = fault_address as i64 - error.metadata.address as i64;
let direction = if offset > 0 { "right" } else { "left" };
writeln!(
output,
"access is {} to the {} of the 0x{:x} byte allocation at 0x{:x}",
offset, direction, error.metadata.size, error.metadata.address
)
.unwrap();
if error.metadata.is_malloc_zero {
writeln!(output, "allocation was zero-sized").unwrap();
}
if let Some(backtrace) = error.metadata.allocation_site_backtrace.as_mut() {
writeln!(output, "allocation site backtrace:").unwrap();
backtrace.resolve();
backtrace_printer.print_trace(backtrace, output).unwrap();
}
if error.metadata.freed {
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " FREE INFO ").unwrap();
if let Some(backtrace) = error.metadata.release_site_backtrace.as_mut() {
writeln!(output, "free site backtrace:").unwrap();
backtrace.resolve();
backtrace_printer.print_trace(backtrace, output).unwrap();
}
}
}
AsanError::BadFuncArgRead((name, address, size, backtrace))
| AsanError::BadFuncArgWrite((name, address, size, backtrace)) => {
writeln!(
output,
" in call to {}, argument {:#016x}, size: {:#x}",
name, address, size
)
.unwrap();
let invocation = Interceptor::current_invocation();
let cpu_context = invocation.cpu_context();
#[cfg(target_arch = "aarch64")]
{
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " REGISTERS ").unwrap();
for reg in 0..29 {
let val = cpu_context.reg(reg);
if val as usize == address {
output
.set_color(ColorSpec::new().set_fg(Some(Color::Red)))
.unwrap();
}
write!(output, "x{:02}: 0x{:016x} ", reg, val).unwrap();
output.reset().unwrap();
if reg % 4 == 3 {
writeln!(output).unwrap();
}
}
write!(output, "sp : 0x{:016x} ", cpu_context.sp()).unwrap();
write!(output, "lr : 0x{:016x} ", cpu_context.lr()).unwrap();
writeln!(output, "pc : 0x{:016x} ", cpu_context.pc()).unwrap();
}
backtrace_printer.print_trace(&backtrace, output).unwrap();
}
AsanError::DoubleFree((ptr, mut metadata, backtrace)) => {
writeln!(output, " of {:?}", ptr).unwrap();
output.reset().unwrap();
backtrace_printer.print_trace(&backtrace, output).unwrap();
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " ALLOCATION INFO ").unwrap();
writeln!(
output,
"allocation at 0x{:x}, with size 0x{:x}",
metadata.address, metadata.size
)
.unwrap();
if metadata.is_malloc_zero {
writeln!(output, "allocation was zero-sized").unwrap();
}
if let Some(backtrace) = metadata.allocation_site_backtrace.as_mut() {
writeln!(output, "allocation site backtrace:").unwrap();
backtrace.resolve();
backtrace_printer.print_trace(backtrace, output).unwrap();
}
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " FREE INFO ").unwrap();
if let Some(backtrace) = metadata.release_site_backtrace.as_mut() {
writeln!(output, "previous free site backtrace:").unwrap();
backtrace.resolve();
backtrace_printer.print_trace(backtrace, output).unwrap();
}
}
AsanError::UnallocatedFree((ptr, backtrace)) => {
writeln!(output, " of {:#016x}", ptr).unwrap();
output.reset().unwrap();
backtrace_printer.print_trace(&backtrace, output).unwrap();
}
AsanError::Leak((ptr, mut metadata)) => {
writeln!(output, " of {:#016x}", ptr).unwrap();
output.reset().unwrap();
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " ALLOCATION INFO ").unwrap();
writeln!(
output,
"allocation at 0x{:x}, with size 0x{:x}",
metadata.address, metadata.size
)
.unwrap();
if metadata.is_malloc_zero {
writeln!(output, "allocation was zero-sized").unwrap();
}
if let Some(backtrace) = metadata.allocation_site_backtrace.as_mut() {
writeln!(output, "allocation site backtrace:").unwrap();
backtrace.resolve();
backtrace_printer.print_trace(backtrace, output).unwrap();
}
}
AsanError::Unknown((registers, pc, fault, backtrace))
| AsanError::StackOobRead((registers, pc, fault, backtrace))
| AsanError::StackOobWrite((registers, pc, fault, backtrace)) => {
let (basereg, indexreg, _displacement, fault_address) = fault;
if let Ok((start, _, _, path)) = find_mapping_for_address(pc) {
writeln!(
output,
" at 0x{:x} ({}:0x{:04x}), faulting address 0x{:x}",
pc,
path,
pc - start,
fault_address
)
.unwrap();
} else {
writeln!(
output,
" at 0x{:x}, faulting address 0x{:x}",
pc, fault_address
)
.unwrap();
}
output.reset().unwrap();
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " REGISTERS ").unwrap();
for reg in 0..=30 {
if reg == basereg {
output
.set_color(ColorSpec::new().set_fg(Some(Color::Red)))
.unwrap();
} else if reg == indexreg {
output
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))
.unwrap();
}
write!(output, "x{:02}: 0x{:016x} ", reg, registers[reg as usize]).unwrap();
output.reset().unwrap();
if reg % 4 == 3 {
writeln!(output).unwrap();
}
}
writeln!(output, "pc : 0x{:016x} ", pc).unwrap();
#[allow(clippy::non_ascii_literal)]
writeln!(output, "{:━^100}", " CODE ").unwrap();
let mut cs = Capstone::new()
.arm64()
.mode(capstone::arch::arm64::ArchMode::Arm)
.build()
.unwrap();
cs.set_skipdata(true).expect("failed to set skipdata");
let start_pc = pc - 4 * 5;
for insn in cs
.disasm_count(
unsafe { std::slice::from_raw_parts(start_pc as *mut u8, 4 * 11) },
start_pc as u64,
11,
)
.expect("failed to disassemble instructions")
.iter()
{
if insn.address() as usize == pc {
output
.set_color(ColorSpec::new().set_fg(Some(Color::Red)))
.unwrap();
writeln!(output, "\t => {}", insn).unwrap();
output.reset().unwrap();
} else {
writeln!(output, "\t {}", insn).unwrap();
}
}
backtrace_printer.print_trace(&backtrace, output).unwrap();
}
};
if !self.options.asan_continue_after_error() {
panic!("Crashing target!");
}
}
}
/// static field for `AsanErrors` for a run
pub static mut ASAN_ERRORS: Option<AsanErrors> = None;
/// An observer for frida address sanitizer `AsanError`s for a frida executor run
#[derive(Serialize, Deserialize)]
#[allow(clippy::unsafe_derive_deserialize)]
pub struct AsanErrorsObserver {
errors: OwnedPtr<Option<AsanErrors>>,
}
impl Observer for AsanErrorsObserver {}
impl<EM, I, S, Z> HasExecHooks<EM, I, S, Z> for AsanErrorsObserver {
fn pre_exec(
&mut self,
_fuzzer: &mut Z,
_state: &mut S,
_mgr: &mut EM,
_input: &I,
) -> Result<(), Error> {
unsafe {
if ASAN_ERRORS.is_some() {
ASAN_ERRORS.as_mut().unwrap().clear();
}
}
Ok(())
}
}
impl Named for AsanErrorsObserver {
#[inline]
fn name(&self) -> &str {
"AsanErrors"
}
}
impl AsanErrorsObserver {
/// Creates a new `AsanErrorsObserver`, pointing to a constant `AsanErrors` field
#[must_use]
pub fn new(errors: &'static Option<AsanErrors>) -> Self {
Self {
errors: OwnedPtr::Ptr(errors as *const Option<AsanErrors>),
}
}
/// Creates a new `AsanErrorsObserver`, owning the `AsanErrors`
#[must_use]
pub fn new_owned(errors: Option<AsanErrors>) -> Self {
Self {
errors: OwnedPtr::Owned(Box::new(errors)),
}
}
/// Creates a new `AsanErrorsObserver` from a raw ptr
#[must_use]
pub fn new_from_ptr(errors: *const Option<AsanErrors>) -> Self {
Self {
errors: OwnedPtr::Ptr(errors),
}
}
/// gets the [`AsanErrors`] from the previous run
#[must_use]
pub fn errors(&self) -> Option<&AsanErrors> {
match &self.errors {
OwnedPtr::Ptr(p) => unsafe { p.as_ref().unwrap().as_ref() },
OwnedPtr::Owned(b) => b.as_ref().as_ref(),
}
}
}
/// A feedback reporting potential [`AsanErrors`] from an `AsanErrorsObserver`
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AsanErrorsFeedback {
errors: Option<AsanErrors>,
}
impl<I, S> Feedback<I, S> for AsanErrorsFeedback
where
I: Input + HasTargetBytes,
{
fn is_interesting<EM, OT>(
&mut self,
_state: &mut S,
_manager: &mut EM,
_input: &I,
observers: &OT,
_exit_kind: &ExitKind,
) -> Result<bool, Error>
where
EM: EventFirer<I, S>,
OT: ObserversTuple,
{
let observer = observers
.match_name::<AsanErrorsObserver>("AsanErrors")
.expect("An AsanErrorsFeedback needs an AsanErrorsObserver");
match observer.errors() {
None => Ok(false),
Some(errors) => {
if errors.errors.is_empty() {
Ok(false)
} else {
self.errors = Some(errors.clone());
Ok(true)
}
}
}
}
fn append_metadata(&mut self, _state: &mut S, testcase: &mut Testcase<I>) -> Result<(), Error> {
if let Some(errors) = &self.errors {
testcase.add_metadata(errors.clone());
}
Ok(())
}
fn discard_metadata(&mut self, _state: &mut S, _input: &I) -> Result<(), Error> {
self.errors = None;
Ok(())
}
}
impl Named for AsanErrorsFeedback {
#[inline]
fn name(&self) -> &str {
"AsanErrors"
}
}
impl AsanErrorsFeedback {
/// Create a new `AsanErrorsFeedback`
#[must_use]
pub fn new() -> Self {
Self { errors: None }
}
}
impl Default for AsanErrorsFeedback {
fn default() -> Self {
Self::new()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,6 @@ use capstone::{
Capstone, Insn,
};
use core::cell::RefCell;
#[cfg(target_arch = "x86_64")]
use frida_gum::instruction_writer::X86Register;
#[cfg(target_arch = "aarch64")]
@ -34,7 +33,7 @@ use frida_gum::{Gum, Module, PageProtection};
use num_traits::cast::FromPrimitive;
use rangemap::RangeMap;
use std::{path::PathBuf, rc::Rc};
use std::path::PathBuf;
use nix::sys::mman::{mmap, MapFlags, ProtFlags};
@ -46,7 +45,7 @@ pub trait FridaHelper<'a> {
fn transformer(&self) -> &Transformer<'a>;
/// Register a new thread with this `FridaHelper`
fn register_thread(&self);
fn register_thread(&mut self);
/// Called prior to execution of an input
fn pre_exec<I: Input + HasTargetBytes>(&mut self, input: &I);
@ -59,6 +58,8 @@ pub trait FridaHelper<'a> {
/// pointer to the frida coverage map
fn map_ptr(&mut self) -> *mut u8;
fn ranges(&self) -> &RangeMap<usize, (u16, &str)>;
}
/// (Default) map size for frida coverage reporting
@ -75,7 +76,7 @@ pub struct FridaInstrumentationHelper<'a> {
transformer: Option<Transformer<'a>>,
#[cfg(target_arch = "aarch64")]
capstone: Capstone,
asan_runtime: Rc<RefCell<AsanRuntime>>,
asan_runtime: AsanRuntime,
ranges: RangeMap<usize, (u16, &'a str)>,
options: &'a FridaOptions,
drcov_basic_blocks: Vec<DrCovBasicBlock>,
@ -87,8 +88,8 @@ impl<'a> FridaHelper<'a> for FridaInstrumentationHelper<'a> {
}
/// Register the current thread with the [`FridaInstrumentationHelper`]
fn register_thread(&self) {
self.asan_runtime.borrow().register_thread();
fn register_thread(&mut self) {
self.asan_runtime.register_thread();
}
fn pre_exec<I: Input + HasTargetBytes>(&mut self, input: &I) {
@ -96,7 +97,6 @@ impl<'a> FridaHelper<'a> for FridaInstrumentationHelper<'a> {
let slice = target_bytes.as_slice();
//println!("target_bytes: {:02x?}", slice);
self.asan_runtime
.borrow()
.unpoison(slice.as_ptr() as usize, slice.len());
}
@ -111,9 +111,9 @@ impl<'a> FridaHelper<'a> for FridaInstrumentationHelper<'a> {
if self.options.asan_enabled() {
if self.options.asan_detect_leaks() {
self.asan_runtime.borrow_mut().check_for_leaks();
self.asan_runtime.check_for_leaks();
}
self.asan_runtime.borrow_mut().reset_allocations();
self.asan_runtime.reset_allocations();
}
}
@ -124,6 +124,10 @@ impl<'a> FridaHelper<'a> for FridaInstrumentationHelper<'a> {
fn map_ptr(&mut self) -> *mut u8 {
self.map.as_mut_ptr()
}
fn ranges(&self) -> &RangeMap<usize, (u16, &str)> {
&self.ranges
}
}
/// Helper function to get the size of a module's CODE section from frida
@ -312,7 +316,6 @@ impl<'a> FridaInstrumentationHelper<'a> {
instruction.put_callout(|context| {
let real_address = match helper
.asan_runtime
.borrow()
.real_address_for_stalked(pc(&context))
{
Some(address) => *address,
@ -347,7 +350,7 @@ impl<'a> FridaInstrumentationHelper<'a> {
}
}
if helper.options().asan_enabled() || helper.options().drcov_enabled() {
helper.asan_runtime.borrow_mut().add_stalked_address(
helper.asan_runtime.add_stalked_address(
output.writer().pc() as usize - 4,
address as usize,
);
@ -358,7 +361,7 @@ impl<'a> FridaInstrumentationHelper<'a> {
});
helper.transformer = Some(transformer);
if helper.options().asan_enabled() || helper.options().drcov_enabled() {
helper.asan_runtime.borrow_mut().init(modules_to_instrument);
helper.asan_runtime.init(gum, modules_to_instrument);
}
}
helper
@ -410,7 +413,7 @@ impl<'a> FridaInstrumentationHelper<'a> {
writer.put_b_label(after_report_impl);
self.current_report_impl = writer.pc();
writer.put_bytes(self.asan_runtime.borrow().blob_report());
writer.put_bytes(self.asan_runtime.blob_report());
writer.put_label(after_report_impl);
}
@ -547,18 +550,18 @@ impl<'a> FridaInstrumentationHelper<'a> {
}
// Insert the check_shadow_mem code blob
match width {
1 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_byte()),
2 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_halfword()),
3 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_3bytes()),
4 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_dword()),
6 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_6bytes()),
8 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_qword()),
12 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_12bytes()),
16 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_16bytes()),
24 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_24bytes()),
32 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_32bytes()),
48 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_48bytes()),
64 => writer.put_bytes(&self.asan_runtime.borrow().blob_check_mem_64bytes()),
1 => writer.put_bytes(&self.asan_runtime.blob_check_mem_byte()),
2 => writer.put_bytes(&self.asan_runtime.blob_check_mem_halfword()),
3 => writer.put_bytes(&self.asan_runtime.blob_check_mem_3bytes()),
4 => writer.put_bytes(&self.asan_runtime.blob_check_mem_dword()),
6 => writer.put_bytes(&self.asan_runtime.blob_check_mem_6bytes()),
8 => writer.put_bytes(&self.asan_runtime.blob_check_mem_qword()),
12 => writer.put_bytes(&self.asan_runtime.blob_check_mem_12bytes()),
16 => writer.put_bytes(&self.asan_runtime.blob_check_mem_16bytes()),
24 => writer.put_bytes(&self.asan_runtime.blob_check_mem_24bytes()),
32 => writer.put_bytes(&self.asan_runtime.blob_check_mem_32bytes()),
48 => writer.put_bytes(&self.asan_runtime.blob_check_mem_48bytes()),
64 => writer.put_bytes(&self.asan_runtime.blob_check_mem_64bytes()),
_ => false,
};

View File

@ -3,17 +3,22 @@ The frida executor is a binary-only mode for `LibAFL`.
It can report coverage and, on supported architecutres, even reports memory access errors.
*/
/// The frida-asan allocator
pub mod alloc;
/// Handling of ASAN errors
pub mod asan_errors;
/// The frida address sanitizer runtime
pub mod asan_rt;
/// The `LibAFL` frida helper
pub mod helper;
// for parsing asan cores
use libafl::bolts::os::parse_core_bind_arg;
// for getting current core_id
use core_affinity::get_core_ids;
/// A representation of the various Frida options
#[derive(Clone, Debug)]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct FridaOptions {
enable_asan: bool,