Windows frida support (#1607)

* WIP: windows frida

* frida-windows: fix hooks not present on windows

* windows: allow building using cargo xwin

* frida-windows: fmrt

* frida-windows: cleanup and allow asan/drcov on windows

* frida-windows: fmt

* frida-windows: fix clippy

* frida-windows: handle unknown exceptions gracefully

* frida-windows: rework shadow mapping algo

* frida-windows: add hook functions

* frida-windows: hook functions; fix stack register

* minibsod: enable for windows

* check_shadow: fix edge casees

* asan_rt: rework and add hooks for windows

* inprocess: add minibsod on windows

* Fix warnings

* minibsod: disable test on windows

* WIP: HookRuntime

* Cleanup after merge

* Bump frida-gum version

* Fix conflict marker; update frida

* Make winsafe windows-specific

* Fmt

* Format

* Better detection of clang++ (using cc)

* Make AsanErrors crate public so we can use it in tests

* Add helper to get immediate of operand

* Use HookRuntime to hook asan functions

Tests now passing

* fmt

* Implement recurisve jmp resolve

* Fix reversed logic

* windows_hooks: Don't die if functions are already replaced

* Allow utils to work on windows

* Enable allocator hooking on windows

* Warnings; add trace to free

* Make ASAN tests run windows (with cargo xwin compilation)

* Fmt

* clang-format

* clang-format

* Add more tests

* Fix partial range access bug in unpoisoning/shadow_check

* Merge main

* Fix check_shadow and implement unit tests

* Fix hooking and PC retrieval

* WIP: Working gdiplus fuzzing with frida-ASAN, no false positives

* LibAFL Frida asan_rt and hook_rt fixes for frida_windows (#2095)

* Introduce aarch64

* MacOS fix - MemoryAreas is broken on MacOS and just loops

* Introduce working aarch64 ASAN check

* Implement large blob

* Fix hook_rt for arm64

* Fix poison/unpoison

* Fix shadow check

* Update x86-64

* Fix aarch64 unused import

* Remove extraneous println statement

* merge main

* Fixes

* alloc: add tests, pass the tests

* HookRuntime before AsanRuntime, and don't Asan if Hooked

* hook_rt: Fixes

* Frida windows check shadow fix (#2159)

* Fix check_shadow and add additional tests

* add some additional documentation

* Revert to Interceptor based hooks

* fixes

* format

* Get rid of hook_rt; fixes

* clang-format

* clang-format

* Fix with_threshold

* fixes

* fix build.rs

* fmt

* Fix offset to RDI on stack

* Fix clippy

* Fix build.rs

* clippy

* hook MapViewOfFile

* fmt

* fix

* clippy

* clippy

* Missing brace

* fix

* Clippy

* fomrrat

* fix i64 cast

* clippy exclude

* too many lines

* Undo merge fails

* fmt

* move debug print

* Fix some frida things

* Remove unused frida_to_cs fn for aarch64

* name

* Don't touch libafl_qemu

---------

Co-authored-by: Dongjia "toka" Zhang <tokazerkje@outlook.com>
Co-authored-by: Sharad Khanna <sharad@mineo333.dev>
Co-authored-by: Dominik Maier <domenukk@gmail.com>
Co-authored-by: Dominik Maier <dmnk@google.com>
This commit is contained in:
s1341 2024-05-14 11:45:56 +03:00 committed by GitHub
parent dce0761b11
commit 19087f3dab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2286 additions and 1216 deletions

View File

@ -24,12 +24,15 @@ tar = "0.4.37"
reqwest = { version = "0.11.4", features = ["blocking"] }
[dependencies]
libafl = { path = "../../libafl/", features = [ "std", "llmp_compression", "llmp_bind_public", "frida_cli" ] } #, "llmp_small_maps", "llmp_debug"]}
libafl = { path = "../../libafl/", features = [ "std", "llmp_compression",
"llmp_bind_public", "frida_cli", "errors_backtrace" ] } #, "llmp_small_maps", "llmp_debug"]}
libafl_bolts = { path = "../../libafl_bolts/" }
frida-gum = { version = "0.13.6", features = [ "auto-download", "event-sink", "invocation-listener"] }
libafl_frida = { path = "../../libafl_frida", features = ["cmplog"] }
libafl_targets = { path = "../../libafl_targets", features = ["sancov_cmplog"] }
libloading = "0.7"
mimalloc = { version = "*", default-features = false }
dlmalloc ={version = "0.2.6", features = ["global"]}
color-backtrace = "0.5"
env_logger = "0.10.0"
iced-x86 = { version = "1.20.0", features = ["code_asm"] }

View File

@ -0,0 +1,2 @@
[build]
target = "x86_64-pc-windows-msvc"

View File

@ -21,8 +21,13 @@ ULONG_PTR gdiplusToken;
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
LoadLibraryA("ole32.dll");
LoadLibraryA("gdi32full.dll");
LoadLibraryA("WindowsCodecs.dll");
LoadLibraryA("shcore.dll");
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
LoadLibraryA("gdi32.dll");
// DebugBreak();
break;
}
return TRUE;
@ -31,16 +36,16 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
extern "C" __declspec(dllexport) int LLVMFuzzerTestOneInput(const uint8_t *data,
size_t size) {
static DWORD init = 0;
if (!init) {
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
init = 1;
}
// if (!init) {
// init = 1;
// }
HGLOBAL m_hBuffer = ::GlobalAlloc(GMEM_MOVEABLE, size);
if (m_hBuffer) {
void *pBuffer = ::GlobalLock(m_hBuffer);
if (pBuffer) {
CopyMemory(pBuffer, data, size);
memcpy(pBuffer, data, size);
// CopyMemory(pBuffer, data, size);
IStream *pStream = NULL;
if (::CreateStreamOnHGlobal(m_hBuffer, FALSE, &pStream) == S_OK) {

View File

@ -6,9 +6,16 @@
//! going to make it compilable only for Windows, don't forget to modify the
//! `scripts/test_fuzzer.sh` to opt-out this fuzzer from that test.
#[cfg(unix)]
use mimalloc::MiMalloc;
#[cfg(unix)]
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
#[cfg(windows)]
use dlmalloc::GlobalDlmalloc;
#[cfg(windows)]
#[global_allocator]
static GLOBAL: GlobalDlmalloc = GlobalDlmalloc;
use std::path::PathBuf;
@ -17,8 +24,8 @@ use libafl::{
corpus::{CachedOnDiskCorpus, Corpus, OnDiskCorpus},
events::{launcher::Launcher, llmp::LlmpRestartingEventManager, EventConfig},
executors::{inprocess::InProcessExecutor, ExitKind, ShadowExecutor},
feedback_or, feedback_or_fast,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
feedback_and_fast, feedback_or, feedback_or_fast,
feedbacks::{ConstFeedback, CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
fuzzer::{Fuzzer, StdFuzzer},
inputs::{BytesInput, HasTargetBytes},
monitors::MultiMonitor,
@ -32,8 +39,6 @@ use libafl::{
state::{HasCorpus, StdState},
Error, HasMetadata,
};
#[cfg(unix)]
use libafl::{feedback_and_fast, feedbacks::ConstFeedback};
use libafl_bolts::{
cli::{parse_args, FuzzerOptions},
rands::StdRand,
@ -41,11 +46,11 @@ use libafl_bolts::{
tuples::{tuple_list, Merge},
AsSlice,
};
#[cfg(unix)]
use libafl_frida::asan::asan_rt::AsanRuntime;
#[cfg(unix)]
use libafl_frida::asan::errors::{AsanErrorsFeedback, AsanErrorsObserver};
use libafl_frida::{
asan::{
asan_rt::AsanRuntime,
errors::{AsanErrorsFeedback, AsanErrorsObserver},
},
cmplog_rt::CmpLogRuntime,
coverage_rt::{CoverageRuntime, MAP_SIZE},
executor::FridaInProcessExecutor,
@ -55,6 +60,7 @@ use libafl_targets::cmplog::CmpLogObserver;
/// The main fn, usually parsing parameters, and starting the fuzzer
pub fn main() {
env_logger::init();
color_backtrace::install();
let options = parse_args();
@ -97,16 +103,11 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
let gum = Gum::obtain();
let coverage = CoverageRuntime::new();
#[cfg(unix)]
let asan = AsanRuntime::new(options);
let asan = AsanRuntime::new(&options);
#[cfg(unix)]
let mut frida_helper =
FridaInstrumentationHelper::new(&gum, options, tuple_list!(coverage, asan));
#[cfg(windows)]
let mut frida_helper =
FridaInstrumentationHelper::new(&gum, options, tuple_list!(coverage));
//
// Create an observation channel using the coverage map
let edges_observer = HitcountsMapObserver::new(StdMapObserver::from_mut_ptr(
"edges",
@ -118,7 +119,6 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
// Create an observation channel to keep track of the execution time
let time_observer = TimeObserver::new("time");
#[cfg(unix)]
let asan_observer = AsanErrorsObserver::from_static_asan_errors();
// Feedback to rate the interestingness of an input
@ -131,18 +131,15 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
);
// Feedbacks to recognize an input as solution
#[cfg(unix)]
let mut objective = feedback_or_fast!(
CrashFeedback::new(),
TimeoutFeedback::new(),
// TimeoutFeedback::new(),
// true enables the AsanErrorFeedback
feedback_and_fast!(
ConstFeedback::from(true),
AsanErrorsFeedback::new(&asan_observer)
)
);
#[cfg(windows)]
let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
// If not restarting, create a State from scratch
let mut state = state.unwrap_or_else(|| {
@ -183,20 +180,18 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
#[cfg(unix)]
let observers = tuple_list!(edges_observer, time_observer, asan_observer);
#[cfg(windows)]
let observers = tuple_list!(edges_observer, time_observer);
let observers = tuple_list!(edges_observer, time_observer, asan_observer,);
// Create the executor for an in-process function with just one observer for edge coverage
let mut executor = FridaInProcessExecutor::new(
&gum,
InProcessExecutor::new(
InProcessExecutor::with_timeout(
&mut frida_harness,
observers,
&mut fuzzer,
&mut state,
&mut mgr,
options.timeout,
)?,
&mut frida_helper,
);
@ -237,7 +232,6 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
// Create an observation channel to keep track of the execution time
let time_observer = TimeObserver::new("time");
#[cfg(unix)]
let asan_observer = AsanErrorsObserver::from_static_asan_errors();
// Feedback to rate the interestingness of an input
@ -249,7 +243,6 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
TimeFeedback::new(&time_observer)
);
#[cfg(unix)]
let mut objective = feedback_or_fast!(
CrashFeedback::new(),
TimeoutFeedback::new(),
@ -258,8 +251,6 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
AsanErrorsFeedback::new(&asan_observer)
)
);
#[cfg(windows)]
let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
// If not restarting, create a State from scratch
let mut state = state.unwrap_or_else(|| {
@ -301,10 +292,7 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
#[cfg(unix)]
let observers = tuple_list!(edges_observer, time_observer, asan_observer);
#[cfg(windows)]
let observers = tuple_list!(edges_observer, time_observer,);
// Create the executor for an in-process function with just one observer for edge coverage
let mut executor = FridaInProcessExecutor::new(
@ -372,7 +360,6 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
// Create an observation channel to keep track of the execution time
let time_observer = TimeObserver::new("time");
#[cfg(unix)]
let asan_observer = AsanErrorsObserver::from_static_asan_errors();
// Feedback to rate the interestingness of an input
@ -384,7 +371,6 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
TimeFeedback::new(&time_observer)
);
#[cfg(unix)]
let mut objective = feedback_or_fast!(
CrashFeedback::new(),
TimeoutFeedback::new(),
@ -393,8 +379,6 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
AsanErrorsFeedback::new(&asan_observer)
)
);
#[cfg(windows)]
let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
// If not restarting, create a State from scratch
let mut state = state.unwrap_or_else(|| {
@ -436,20 +420,18 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
#[cfg(unix)]
let observers = tuple_list!(edges_observer, time_observer, asan_observer);
#[cfg(windows)]
let observers = tuple_list!(edges_observer, time_observer);
// Create the executor for an in-process function with just one observer for edge coverage
let mut executor = FridaInProcessExecutor::new(
&gum,
InProcessExecutor::new(
InProcessExecutor::with_timeout(
&mut frida_harness,
observers,
&mut fuzzer,
&mut state,
&mut mgr,
options.timeout,
)?,
&mut frida_helper,
);
@ -466,7 +448,9 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
let mut stages = tuple_list!(StdMutationalStage::new(mutator));
fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?;
fuzzer
.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)
.unwrap();
Ok(())
})(state, mgr, core_id)

View File

@ -26,7 +26,8 @@ reqwest = { version = "0.11.4", features = ["blocking"] }
[dependencies]
libafl = { path = "../../libafl/", features = [ "std", "llmp_compression", "llmp_bind_public", "frida_cli", "errors_backtrace" ] } #, "llmp_small_maps", "llmp_debug"]}
libafl = { path = "../../libafl/", features = [ "std", "llmp_compression",
"llmp_bind_public", "frida_cli", "errors_backtrace" ] } #, "llmp_small_maps", "llmp_debug"]}
libafl_bolts = { path = "../../libafl_bolts/" }
frida-gum = { version = "0.13.6", features = [ "auto-download", "event-sink", "invocation-listener"] }
libafl_frida = { path = "../../libafl_frida", features = ["cmplog"] }
@ -34,3 +35,5 @@ libafl_targets = { path = "../../libafl_targets", features = ["sancov_cmplog"] }
libloading = "0.7"
mimalloc = { version = "*", default-features = false }
color-backtrace = "0.5"
log = "0.4.20"
env_logger = "0.10.0"

View File

@ -88,7 +88,7 @@ static char *allocation = NULL;
__attribute__((noinline)) void func3(char *alloc) {
// printf("func3\n");
#ifdef _WIN32
if (rand() == 0) {
if ((rand() % 2) == 0) {
alloc[0x1ff] = 0xde;
printf("alloc[0x200]: %d\n", alloc[0x200]);
}

View File

@ -50,8 +50,8 @@ static GLOBAL: MiMalloc = MiMalloc;
/// The main fn, usually parsing parameters, and starting the fuzzer
pub fn main() {
env_logger::init();
color_backtrace::install();
let options = parse_args();
unsafe {
@ -65,6 +65,8 @@ pub fn main() {
/// The actual fuzzer
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
log::info!("Frida fuzzer starting up.");
// 'While the stats are state, they are usually used in the broker - which is likely never restarted
let monitor = MultiMonitor::new(|s| println!("{s}"));
@ -97,7 +99,7 @@ unsafe fn fuzz(options: &FuzzerOptions) -> Result<(), Error> {
#[cfg(unix)]
let mut frida_helper =
FridaInstrumentationHelper::new(&gum, options, tuple_list!(coverage, asan));
FridaInstrumentationHelper::new(&gum, options, tuple_list!(asan, coverage));
#[cfg(windows)]
let mut frida_helper =
FridaInstrumentationHelper::new(&gum, &options, tuple_list!(coverage));

View File

@ -113,6 +113,8 @@ pub mod windows_exception_handler {
sync::atomic::{compiler_fence, Ordering},
};
#[cfg(feature = "std")]
use std::io::Write;
#[cfg(feature = "std")]
use std::panic;
use libafl_bolts::os::windows_exceptions::{
@ -131,7 +133,7 @@ pub mod windows_exception_handler {
},
feedbacks::Feedback,
fuzzer::HasObjective,
inputs::UsesInput,
inputs::{Input, UsesInput},
state::{HasCorpus, HasExecutions, HasSolutions, State},
};
@ -394,7 +396,17 @@ pub mod windows_exception_handler {
// Make sure we don't crash in the crash handler forever.
if is_crash {
let input = data.take_current_input::<<E::State as UsesInput>::Input>();
{
let mut bsod = Vec::new();
{
let mut writer = std::io::BufWriter::new(&mut bsod);
writeln!(writer, "input: {:?}", input.generate_name(0)).unwrap();
libafl_bolts::minibsod::generate_minibsod(&mut writer, exception_pointers)
.unwrap();
writer.flush().unwrap();
}
log::error!("{}", std::str::from_utf8(&bsod).unwrap());
}
run_observers_and_save_state::<E, EM, OF, Z>(
executor,
state,

View File

@ -122,7 +122,7 @@ pub mod fs;
#[cfg(feature = "alloc")]
pub mod llmp;
pub mod math;
#[cfg(all(feature = "std", unix))]
#[cfg(feature = "std")]
pub mod minibsod;
pub mod os;
#[cfg(feature = "alloc")]

View File

@ -1,7 +1,5 @@
//! Implements a mini-bsod generator.
//! It dumps all important registers and prints a stacktrace.
//! You may use the [`crate::os::unix_signals::ucontext`]
//! function to get a [`ucontext_t`].
#[cfg(target_vendor = "apple")]
use core::mem::size_of;
@ -9,6 +7,7 @@ use std::io::{BufWriter, Write};
#[cfg(any(target_os = "solaris", target_os = "illumos"))]
use std::process::Command;
#[cfg(unix)]
use libc::siginfo_t;
#[cfg(target_vendor = "apple")]
use mach::{
@ -19,7 +18,10 @@ use mach::{
vm_region::{vm_region_recurse_info_t, vm_region_submap_info_64},
vm_types::{mach_vm_address_t, mach_vm_size_t, natural_t},
};
#[cfg(windows)]
use windows::Win32::System::Diagnostics::Debug::{CONTEXT, EXCEPTION_POINTERS};
#[cfg(unix)]
use crate::os::unix_signals::{ucontext_t, Signal};
/// Write the content of all important registers
@ -391,7 +393,7 @@ pub fn dump_registers<W: Write>(
write!(writer, "cs : {:#016x}, ", ucontext.sc_cs)?;
Ok(())
}
///
/// Write the content of all important registers
#[cfg(all(target_os = "openbsd", target_arch = "aarch64"))]
#[allow(clippy::similar_names)]
@ -452,6 +454,34 @@ pub fn dump_registers<W: Write>(
}
/// Write the content of all important registers
#[cfg(windows)]
#[allow(clippy::similar_names)]
pub fn dump_registers<W: Write>(
writer: &mut BufWriter<W>,
context: &CONTEXT,
) -> Result<(), std::io::Error> {
write!(writer, "r8 : {:#016x}, ", context.R8)?;
write!(writer, "r9 : {:#016x}, ", context.R9)?;
write!(writer, "r10: {:#016x}, ", context.R10)?;
writeln!(writer, "r11: {:#016x}, ", context.R11)?;
write!(writer, "r12: {:#016x}, ", context.R12)?;
write!(writer, "r13: {:#016x}, ", context.R13)?;
write!(writer, "r14: {:#016x}, ", context.R14)?;
writeln!(writer, "r15: {:#016x}, ", context.R15)?;
write!(writer, "rdi: {:#016x}, ", context.Rdi)?;
write!(writer, "rsi: {:#016x}, ", context.Rsi)?;
write!(writer, "rbp: {:#016x}, ", context.Rbp)?;
writeln!(writer, "rbx: {:#016x}, ", context.Rbx)?;
write!(writer, "rdx: {:#016x}, ", context.Rdx)?;
write!(writer, "rax: {:#016x}, ", context.Rax)?;
write!(writer, "rcx: {:#016x}, ", context.Rcx)?;
writeln!(writer, "rsp: {:#016x}, ", context.Rsp)?;
write!(writer, "rip: {:#016x}, ", context.Rip)?;
writeln!(writer, "efl: {:#016x}, ", context.EFlags)?;
Ok(())
}
#[cfg(all(target_os = "haiku", target_arch = "x86_64"))]
#[allow(clippy::similar_names)]
pub fn dump_registers<W: Write>(
@ -489,6 +519,7 @@ pub fn dump_registers<W: Write>(
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd",
windows,
target_os = "haiku",
any(target_os = "solaris", target_os = "illumos"),
)))]
@ -751,6 +782,7 @@ fn write_crash<W: Write>(
target_os = "dragonfly",
target_os = "openbsd",
target_os = "netbsd",
windows,
target_os = "haiku",
any(target_os = "solaris", target_os = "illumos"),
)))]
@ -765,6 +797,33 @@ fn write_crash<W: Write>(
Ok(())
}
#[cfg(windows)]
fn write_crash<W: Write>(
writer: &mut BufWriter<W>,
exception_pointers: *mut EXCEPTION_POINTERS,
) -> Result<(), std::io::Error> {
// TODO add fault addr for other platforms.
unsafe {
writeln!(
writer,
"Received exception {:0x} at address {:x}",
(*exception_pointers)
.ExceptionRecord
.as_mut()
.unwrap()
.ExceptionCode
.0,
(*exception_pointers)
.ExceptionRecord
.as_mut()
.unwrap()
.ExceptionAddress as usize
)
}?;
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn write_minibsod<W: Write>(writer: &mut BufWriter<W>) -> Result<(), std::io::Error> {
match std::fs::read_to_string("/proc/self/maps") {
@ -1023,6 +1082,30 @@ pub fn generate_minibsod<W: Write>(
write_minibsod(writer)
}
/// Generates a mini-BSOD given an `EXCEPTION_POINTERS` structure.
#[cfg(windows)]
#[allow(
clippy::non_ascii_literal,
clippy::too_many_lines,
clippy::not_unsafe_ptr_arg_deref
)]
pub fn generate_minibsod<W: Write>(
writer: &mut BufWriter<W>,
exception_pointers: *mut EXCEPTION_POINTERS,
) -> Result<(), std::io::Error> {
writeln!(writer, "{:━^100}", " CRASH ")?;
write_crash(writer, exception_pointers)?;
writeln!(writer, "{:━^100}", " REGISTERS ")?;
dump_registers(writer, unsafe {
(*exception_pointers).ContextRecord.as_mut().unwrap()
})?;
writeln!(writer, "{:━^100}", " BACKTRACE ")?;
writeln!(writer, "{:?}", backtrace::Backtrace::new())?;
writeln!(writer, "{:━^100}", " MAPS ")?;
write_minibsod(writer)
}
#[cfg(unix)]
#[cfg(test)]
mod tests {

View File

@ -35,7 +35,7 @@ const EXCEPTION_CONTINUE_EXECUTION: c_long = -1;
const EXCEPTION_CONTINUE_SEARCH: c_long = 0;
// For SEH
//const EXCEPTION_EXECUTE_HANDLER: c_long = 1;
// const EXCEPTION_EXECUTE_HANDLER: c_long = 1;
// From https://github.com/Alexpux/mingw-w64/blob/master/mingw-w64-headers/crt/signal.h
pub const SIGINT: i32 = 2;

View File

@ -80,9 +80,13 @@ ahash = "0.8"
paste = "1.0"
log = "0.4.20"
mmap-rs = "0.6.0"
bit_reverse = "0.1.8"
yaxpeax-arch = "0.2.7"
[target.'cfg(windows)'.dependencies]
winsafe = {version = "0.0.18", features = ["kernel"]}
[dev-dependencies]
serial_test = { version = "3", default-features = false, features = ["logging"] }
clap = {version = "4.5", features = ["derive"]}

View File

@ -7,34 +7,59 @@ fn main() {
cc::Build::new().file("src/gettls.c").compile("libgettls.a");
}
let target_family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap();
// Force linking against libc++
#[cfg(unix)]
println!("cargo:rustc-link-lib=dylib=c++");
if target_family == "unix" {
println!("cargo:rustc-link-lib=dylib=c++");
}
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=test_harness.cpp");
println!("cargo:rerun-if-changed=src/gettls.c");
// Build the test harness
// clang++ -shared -fPIC -O0 -o test_harness.so test_harness.cpp
#[cfg(unix)]
{
// Check if we have clang++ installed
let clangpp = std::process::Command::new("clang++")
.arg("--version")
.output();
// Check if we have clang++ installed
match clangpp {
Ok(_) => {
std::process::Command::new("clang++")
.arg("-shared")
.arg("-fPIC")
.arg("-O0")
.arg("-o")
.arg("test_harness.so")
.arg("test_harness.cpp")
.status()
.expect("Failed to build test harness");
}
Err(_) => {
println!("cargo:warning=clang++ not found, skipping test harness build");
}
}
if target_family == "windows" {
let compiler = cc::Build::new()
.cpp(true)
.file("test_harness.a")
.get_compiler();
let mut cmd = std::process::Command::new(compiler.path());
let cmd = cmd
.args(compiler.args())
.arg("test_harness.cpp")
.arg("/link");
#[cfg(unix)]
let cmd = cmd
.arg(format!(
"/libpath:{}/.cache/cargo-xwin/xwin/crt/lib/x86_64/",
std::env::var("HOME").unwrap()
))
.arg(format!(
"/libpath:{}/.cache/cargo-xwin/xwin/sdk/lib/ucrt/x86_64/",
std::env::var("HOME").unwrap()
))
.arg(format!(
"/libpath:{}/.cache/cargo-xwin/xwin/sdk/lib/um/x86_64/",
std::env::var("HOME").unwrap()
));
cmd.arg("/dll").arg("/OUT:test_harness.dll");
cmd.status().expect("Failed to link test_harness.dll");
} else {
let compiler = cc::Build::new()
.cpp(true)
.opt_level(0)
.shared_flag(true)
.get_compiler();
let clangpp = compiler.path();
let mut cmd = std::process::Command::new(clangpp);
cmd.args(compiler.args())
.arg("test_harness.cpp")
.arg("-o")
.arg("test_harness.so")
.status()
.expect("Failed to link test_harness");
}
}

View File

@ -1,4 +1,5 @@
#[cfg(any(
windows,
target_os = "linux",
target_vendor = "apple",
all(
@ -13,6 +14,7 @@ use frida_gum::{PageProtection, RangeDetails};
use hashbrown::HashMap;
use libafl_bolts::cli::FuzzerOptions;
#[cfg(any(
windows,
target_os = "linux",
target_vendor = "apple",
all(
@ -20,8 +22,7 @@ use libafl_bolts::cli::FuzzerOptions;
target_os = "android"
)
))]
use mmap_rs::{MemoryAreas, MmapFlags, MmapMut, MmapOptions, ReservedMut};
use nix::libc::memset;
use mmap_rs::{MmapFlags, MmapMut, MmapOptions, ReservedMut};
use rangemap::RangeSet;
use serde::{Deserialize, Serialize};
@ -41,7 +42,9 @@ pub struct Allocator {
/// The shadow bit
shadow_bit: usize,
/// The reserved (pre-allocated) shadow mapping
pre_allocated_shadow_mappings: HashMap<(usize, usize), ReservedMut>,
pre_allocated_shadow_mappings: Vec<ReservedMut>,
/// Whether we've pre allocated a shadow mapping:
using_pre_allocated_shadow_mapping: bool,
/// All tracked allocations
allocations: HashMap<usize, AllocationMetadata>,
/// All mappings
@ -157,7 +160,6 @@ impl Allocator {
pub unsafe fn alloc(&mut self, size: usize, _alignment: usize) -> *mut c_void {
let mut is_malloc_zero = false;
let size = if size == 0 {
// log::warn!("zero-sized allocation!");
is_malloc_zero = true;
16
} else {
@ -179,7 +181,6 @@ impl Allocator {
self.total_allocation_size += rounded_up_size;
let metadata = if let Some(mut metadata) = self.find_smallest_fit(rounded_up_size) {
//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.allocation_backtraces {
@ -234,14 +235,13 @@ impl Allocator {
let address = (metadata.address + self.page_size) as *mut c_void;
self.allocations.insert(address as usize, metadata);
// log::trace!("serving address: {:?}, size: {:x}", address, size);
log::trace!("serving address: {:?}, size: {:x}", address, size);
address
}
/// Releases the allocation at the given address.
#[allow(clippy::missing_safety_doc)]
pub unsafe fn release(&mut self, ptr: *mut c_void) {
//log::trace!("freeing address: {:?}", ptr);
let Some(metadata) = self.allocations.get_mut(&(ptr as usize)) else {
if !ptr.is_null() {
AsanErrors::get_mut_blocking()
@ -339,34 +339,30 @@ impl Allocator {
}
fn unpoison(start: usize, size: usize) {
// log::trace!("unpoisoning {:x} for {:x}", start, size / 8 + 1);
unsafe {
// log::trace!("memset: {:?}", start as *mut c_void);
memset(start as *mut c_void, 0xff, size / 8);
std::slice::from_raw_parts_mut(start as *mut u8, size / 8).fill(0xff);
let remainder = size % 8;
if remainder > 0 {
// log::trace!("remainder: {:x}, offset: {:x}", remainder, start + size / 8);
memset(
(start + size / 8) as *mut c_void,
(0xff << (8 - remainder)) & 0xff,
1,
);
let mut current_value = ((start + size / 8) as *const u8).read();
current_value |= 0xff << (8 - remainder);
((start + size / 8) as *mut u8).write(current_value);
}
}
}
/// Poisonn an area in memory
pub fn poison(start: usize, size: usize) {
// log::trace!("poisoning {:x} for {:x}", start, size / 8 + 1);
unsafe {
// log::trace!("memset: {:?}", start as *mut c_void);
memset(start as *mut c_void, 0x00, size / 8);
std::slice::from_raw_parts_mut(start as *mut u8, size / 8).fill(0x0);
let remainder = size % 8;
if remainder > 0 {
// log::trace!("remainder: {:x}, offset: {:x}", remainder, start + size / 8);
memset((start + size / 8) as *mut c_void, 0x00, 1);
let mask = !(0xff << (8 - remainder));
let mut current_value = ((start + size / 8) as *const u8).read();
current_value &= mask;
((start + size / 8) as *mut u8).write(current_value);
}
}
}
@ -381,87 +377,154 @@ impl Allocator {
let shadow_mapping_start = map_to_shadow!(self, start);
let shadow_start = self.round_down_to_page(shadow_mapping_start);
// I'm not sure this works as planned. The same address appearing as start and end is mapped to
// different addresses.
let shadow_end = self.round_up_to_page((end - start) / 8) + self.page_size + shadow_start;
log::trace!(
"map_shadow_for_region start: {:x}, end {:x}, size {:x}, shadow {:x}-{:x}",
start,
end,
end - start,
shadow_start,
shadow_end
);
if self.pre_allocated_shadow_mappings.is_empty() {
for range in self.shadow_pages.gaps(&(shadow_start..shadow_end)) {
/*
log::trace!(
"range: {:x}-{:x}, pagesize: {}",
range.start, range.end, self.page_size
);
*/
let mapping = MmapOptions::new(range.end - range.start - 1)
.unwrap()
.with_address(range.start)
.map_mut()
.expect("An error occurred while mapping shadow memory");
self.mappings.insert(range.start, mapping);
}
log::trace!("adding shadow pages {:x} - {:x}", shadow_start, shadow_end);
self.shadow_pages.insert(shadow_start..shadow_end);
} else {
let mut new_shadow_mappings = Vec::new();
let shadow_end = self.round_up_to_page((end - start) / 8 + self.page_size + shadow_start);
if self.using_pre_allocated_shadow_mapping {
let mut newly_committed_regions = Vec::new();
for gap in self.shadow_pages.gaps(&(shadow_start..shadow_end)) {
for ((pa_start, pa_end), shadow_mapping) in &mut self.pre_allocated_shadow_mappings
{
if *pa_start <= gap.start && gap.start < *pa_start + shadow_mapping.len() {
log::trace!("pa_start: {:x}, pa_end {:x}, gap.start {:x}, shadow_mapping.ptr {:x}, shadow_mapping.len {:x}",
*pa_start, *pa_end, gap.start, shadow_mapping.as_ptr() as usize, shadow_mapping.len());
// Split the preallocated mapping into two parts, keeping the
// part before the gap and returning the part starting with the gap as a new mapping
let mut start_mapping =
shadow_mapping.split_off(gap.start - *pa_start).unwrap();
// Split the new mapping into two parts,
// keeping the part holding the gap and returning the part starting after the gap as a new mapping
let end_mapping = start_mapping.split_off(gap.end - gap.start).unwrap();
//Push the new after-the-gap mapping to the list of mappings to be added
new_shadow_mappings.push(((gap.end, *pa_end), end_mapping));
// Insert the new gap mapping into the list of mappings
self.mappings
.insert(gap.start, start_mapping.try_into().unwrap());
let mut new_reserved_region = None;
for reserved in &mut self.pre_allocated_shadow_mappings {
if gap.start >= reserved.start() && gap.end <= reserved.end() {
let mut to_be_commited =
reserved.split_off(gap.start - reserved.start()).unwrap();
if to_be_commited.end() > gap.end {
let upper = to_be_commited
.split_off(gap.end - to_be_commited.start())
.unwrap();
new_reserved_region = Some(upper);
}
let commited: MmapMut = to_be_commited
.try_into()
.expect("Failed to commit reserved shadow memory");
newly_committed_regions.push(commited);
break;
}
}
}
for new_shadow_mapping in new_shadow_mappings {
log::trace!(
"adding pre_allocated_shadow_mappings and shadow pages {:x} - {:x}",
new_shadow_mapping.0 .0,
new_shadow_mapping.0 .1
);
self.pre_allocated_shadow_mappings
.insert(new_shadow_mapping.0, new_shadow_mapping.1);
if let Some(new_reserved_region) = new_reserved_region {
self.pre_allocated_shadow_mappings.push(new_reserved_region);
}
}
for newly_committed_region in newly_committed_regions {
self.shadow_pages
.insert(new_shadow_mapping.0 .0..new_shadow_mapping.0 .1);
.insert(newly_committed_region.start()..newly_committed_region.end());
self.mappings
.insert(newly_committed_region.start(), newly_committed_region);
}
}
// log::trace!("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)
(shadow_mapping_start, (end - start) / 8 + 1)
}
#[inline]
#[must_use]
fn check_shadow_aligned(&mut self, address: *const c_void, size: usize) -> bool {
assert_eq!(
(address as usize) & 7,
0,
"check_shadow_aligned used when address is not aligned. Use check_shadow"
);
assert_eq!(
size & 7,
0,
"check_shadow_aligned used when size is not aligned. Use check_shadow"
);
if size == 0 {
return true;
}
let shadow_addr = map_to_shadow!(self, (address as usize));
let shadow_size = size >> 3;
let buf = unsafe { std::slice::from_raw_parts_mut(shadow_addr as *mut u8, shadow_size) };
let (prefix, aligned, suffix) = unsafe { buf.align_to::<u128>() };
if !prefix.iter().all(|&x| x == 0xff)
|| !suffix.iter().all(|&x| x == 0xff)
|| !aligned
.iter()
.all(|&x| x == 0xffffffffffffffffffffffffffffffffu128)
{
return false;
}
true
}
/// Checks whether the given address up till size is valid unpoisoned shadow memory.
/// TODO: check edge cases
#[inline]
#[must_use]
pub fn check_shadow(&mut self, address: *const c_void, size: usize) -> bool {
//the algorithm for check_shadow is as follows:
//1. we first check if its managed. if is not then exit
//2. we check if it is aligned. this should be 99% of accesses. If it is do an aligned check and leave
//3. if it is not split the check into 3 parts: the pre-aligment bytes, the aligned portion, and the post alignment posts
//3. The prealignment bytes are the unaligned bytes (if any) located in the qword preceding the aligned portion. Perform a specialied check to ensure that the bytes from [start, align(start, 8)) are valid. In this case align(start,8) aligns start to the next 8 byte boundary.
//4. The aligned check is where the address and the size is 8 byte aligned. Use check_shadow_aligned to check it
//5. The post-alignment is the same as pre-alignment except it is the qword following the aligned portion. Use a specialized check to ensure that [end & ~7, end) is valid.
if size == 0
/*|| !self.is_managed(address as *mut c_void)*/
{
return true;
}
if !self.is_managed(address as *mut c_void) {
log::trace!("unmanaged address to check_shadow: {:?}, {size:x}", address);
return true;
}
//fast path. most buffers are likely 8 byte aligned in size and address
if (address as usize).trailing_zeros() >= 3 && size.trailing_zeros() >= 3 {
return self.check_shadow_aligned(address, size);
}
//slow path. check everything
let start_address = address as usize;
let end_address = start_address + size;
//8 byte align the start/end so we can use check_shadow_aligned for the majority of it
//in the case of subqword accesses (i.e,, the entire access is located within 1 qword), aligned_start > aligned_end naturally
let aligned_start = (start_address + 7) & !7;
let aligned_end = end_address & !7;
let start_offset = start_address & 7;
let end_offset = end_address & 7;
//if the start is unaligned
if start_address != aligned_start {
let start_shadow = map_to_shadow!(self, start_address);
let start_mask: u8 = 0xff << (8 - start_offset);
if unsafe { (start_shadow as *const u8).read() } & start_mask != start_mask {
return false;
}
}
//if this is not true then it must be a subqword access as the start will be larger than the end
if aligned_start <= aligned_end {
if !self
.check_shadow_aligned(aligned_start as *const c_void, aligned_end - aligned_start)
{
return false;
}
if end_address != aligned_end {
let end_shadow = map_to_shadow!(self, end_address);
let end_mask = 0xff << (8 - end_offset); //we want to check from the beginning of the qword to the offset
if unsafe { (end_shadow as *const u8).read() } & end_mask != end_mask {
return false;
}
}
}
// self.map_shadow_for_region(address, address + size, false);
true
}
/// Maps the address to a shadow address
#[inline]
#[must_use]
@ -473,7 +536,7 @@ impl Allocator {
#[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
self.shadow_offset <= ptr as usize && (ptr as usize) < self.current_mapping_addr
}
/// Checks if any of the allocations has not been freed
@ -488,17 +551,19 @@ impl Allocator {
/// Unpoison all the memory that is currently mapped with read/write permissions.
pub fn unpoison_all_existing_memory(&mut self) {
RangeDetails::enumerate_with_prot(PageProtection::NoAccess, &mut |range: &RangeDetails| {
if range.protection() as u32 & PageProtection::ReadWrite as u32 != 0 {
RangeDetails::enumerate_with_prot(
PageProtection::Read,
&mut |range: &RangeDetails| -> bool {
let start = range.memory_range().base_address().0 as usize;
let end = start + range.memory_range().size();
if !self.pre_allocated_shadow_mappings.is_empty() && start == 1 << self.shadow_bit {
return true;
if !self.is_managed(start as *mut c_void) {
self.map_shadow_for_region(start, end, true);
}
self.map_shadow_for_region(start, end, true);
}
true
});
true
},
);
}
/// Initialize the allocator, making sure a valid shadow bit is selected.
@ -512,57 +577,61 @@ impl Allocator {
let mut occupied_ranges: Vec<(usize, usize)> = vec![];
// max(userspace address) this is usually 0x8_0000_0000_0000 - 1 on x64 linux.
#[cfg(unix)]
let mut userspace_max: usize = 0;
// Enumerate memory ranges that are already occupied.
for area in MemoryAreas::open(None).unwrap() {
let start = area.as_ref().unwrap().start();
let end = area.unwrap().end();
occupied_ranges.push((start, end));
// log::trace!("Occupied {:x} {:x}", start, end);
let base: usize = 2;
// On x64, if end > 2**48, then that's in vsyscall or something.
#[cfg(all(unix, target_arch = "x86_64"))]
if end <= base.pow(48) && end > userspace_max {
userspace_max = end;
}
#[cfg(all(not(unix), target_arch = "x86_64"))]
if (end >> 3) <= base.pow(44) && (end >> 3) > userspace_max {
userspace_max = end >> 3;
}
RangeDetails::enumerate_with_prot(
PageProtection::Read,
&mut |range: &RangeDetails| -> bool {
let start = range.memory_range().base_address().0 as usize;
let end = start + range.memory_range().size();
occupied_ranges.push((start, end));
// On x64, if end > 2**48, then that's in vsyscall or something.
#[cfg(all(unix, target_arch = "x86_64"))]
if end <= 2_usize.pow(48) && end > userspace_max {
userspace_max = end;
}
//
// #[cfg(all(not(unix), target_arch = "x86_64"))]
// if end <= 2_usize.pow(64) && end > userspace_max {
// userspace_max = end;
// }
// On aarch64, 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;
}
}
// On aarch64, if end > 2**52, then range is not in userspace
#[cfg(target_arch = "aarch64")]
if end <= 2_usize.pow(52) && end > userspace_max {
userspace_max = end;
}
let mut maxbit = 0;
true
},
);
#[cfg(unix)]
let mut maxbit = 63;
#[cfg(windows)]
let maxbit = 63;
#[cfg(unix)]
for power in 1..64 {
let base: usize = 2;
if base.pow(power) > userspace_max {
if 2_usize.pow(power) > userspace_max {
maxbit = power;
break;
}
}
{
for try_shadow_bit in &[maxbit, maxbit - 4, maxbit - 3, maxbit - 2] {
for try_shadow_bit in 44..maxbit {
let addr: usize = 1 << try_shadow_bit;
let shadow_start = addr;
let shadow_end = addr + addr + addr;
let mut good_candidate = true;
// check if the proposed shadow bit overlaps with occupied ranges.
for (start, end) in &occupied_ranges {
// log::trace!("{:x} {:x}, {:x} {:x} -> {:x} - {:x}", shadow_start, shadow_end, start, end,
// shadow_start + ((start >> 3) & ((1 << (try_shadow_bit + 1)) - 1)),
// shadow_start + ((end >> 3) & ((1 << (try_shadow_bit + 1)) - 1))
// );
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");
log::warn!("shadow_bit {try_shadow_bit:} is not suitable");
good_candidate = false;
break;
}
@ -573,7 +642,7 @@ impl Allocator {
> shadow_end)
{
log::warn!(
"shadow_bit {try_shadow_bit:x} is not suitable (shadow out of range)"
"shadow_bit {try_shadow_bit:} is not suitable (shadow out of range)"
);
good_candidate = false;
break;
@ -582,33 +651,26 @@ impl Allocator {
if good_candidate {
// We reserve the shadow memory space of size addr*2, but don't commit it.
if let Ok(mapping) = MmapOptions::new(1 << (*try_shadow_bit + 1))
if let Ok(mapping) = MmapOptions::new(1 << (try_shadow_bit + 1))
.unwrap()
.with_flags(MmapFlags::NO_RESERVE)
.with_address(addr)
.reserve_mut()
{
shadow_bit = (*try_shadow_bit).try_into().unwrap();
shadow_bit = (try_shadow_bit).try_into().unwrap();
log::warn!("shadow_bit {shadow_bit:x} is suitable");
log::trace!(
"adding pre_allocated_shadow_mappings {:x} - {:x} with size {:}",
addr,
(addr + (1 << (shadow_bit + 1))),
mapping.len()
);
self.pre_allocated_shadow_mappings
.insert((addr, (addr + (1 << (shadow_bit + 1)))), mapping);
log::warn!("shadow_bit {shadow_bit:} is suitable");
self.pre_allocated_shadow_mappings.push(mapping);
self.using_pre_allocated_shadow_mapping = true;
break;
}
log::warn!("shadow_bit {try_shadow_bit:x} is not suitable - failed to allocate shadow memory");
log::warn!("shadow_bit {try_shadow_bit:} is not suitable - failed to allocate shadow memory");
}
}
}
// assert!(shadow_bit != 0);
// attempt to pre-map the entire shadow-memory space
log::warn!("shadow_bit: {shadow_bit}");
assert!(shadow_bit != 0);
let addr: usize = 1 << shadow_bit;
@ -643,7 +705,8 @@ impl Default for Allocator {
max_total_allocation: 1 << 32,
allocation_backtraces: false,
page_size,
pre_allocated_shadow_mappings: HashMap::new(),
pre_allocated_shadow_mappings: Vec::new(),
using_pre_allocated_shadow_mapping: false,
mappings: HashMap::new(),
shadow_offset: 0,
shadow_bit: 0,
@ -657,3 +720,61 @@ impl Default for Allocator {
}
}
}
#[test]
fn check_shadow() {
let mut allocator = Allocator::default();
allocator.init();
let allocation = unsafe { allocator.alloc(8, 8) };
assert!(!allocation.is_null());
assert!(allocator.check_shadow(allocation, 1));
assert!(allocator.check_shadow(allocation, 2));
assert!(allocator.check_shadow(allocation, 3));
assert!(allocator.check_shadow(allocation, 4));
assert!(allocator.check_shadow(allocation, 5));
assert!(allocator.check_shadow(allocation, 6));
assert!(allocator.check_shadow(allocation, 7));
assert!(allocator.check_shadow(allocation, 8));
assert!(!allocator.check_shadow(allocation, 9));
assert!(!allocator.check_shadow(allocation, 10));
assert!(allocator.check_shadow(unsafe { allocation.offset(1) }, 7));
assert!(allocator.check_shadow(unsafe { allocation.offset(2) }, 6));
assert!(allocator.check_shadow(unsafe { allocation.offset(3) }, 5));
assert!(allocator.check_shadow(unsafe { allocation.offset(4) }, 4));
assert!(allocator.check_shadow(unsafe { allocation.offset(5) }, 3));
assert!(allocator.check_shadow(unsafe { allocation.offset(6) }, 2));
assert!(allocator.check_shadow(unsafe { allocation.offset(7) }, 1));
assert!(allocator.check_shadow(unsafe { allocation.offset(8) }, 0));
assert!(!allocator.check_shadow(unsafe { allocation.offset(9) }, 1));
assert!(!allocator.check_shadow(unsafe { allocation.offset(9) }, 8));
assert!(!allocator.check_shadow(unsafe { allocation.offset(1) }, 9));
assert!(!allocator.check_shadow(unsafe { allocation.offset(1) }, 8));
assert!(!allocator.check_shadow(unsafe { allocation.offset(2) }, 8));
assert!(!allocator.check_shadow(unsafe { allocation.offset(3) }, 8));
let allocation = unsafe { allocator.alloc(0xc, 0) };
assert!(allocator.check_shadow(unsafe { allocation.offset(4) }, 8));
//subqword access
assert!(allocator.check_shadow(unsafe { allocation.offset(3) }, 2));
//unaligned access
assert!(allocator.check_shadow(unsafe { allocation.offset(3) }, 8));
let allocation = unsafe { allocator.alloc(0x20, 0) };
//access with unaligned parts at the beginning and end
assert!(allocator.check_shadow(unsafe { allocation.offset(10) }, 21));
//invalid, unaligned access
assert!(!allocator.check_shadow(unsafe { allocation.offset(10) }, 29));
let allocation = unsafe { allocator.alloc(4, 0) };
assert!(!allocation.is_null());
assert!(allocator.check_shadow(allocation, 1));
assert!(allocator.check_shadow(allocation, 2));
assert!(allocator.check_shadow(allocation, 3));
assert!(allocator.check_shadow(allocation, 4));
assert!(!allocator.check_shadow(allocation, 5));
assert!(!allocator.check_shadow(allocation, 6));
assert!(!allocator.check_shadow(allocation, 7));
assert!(!allocator.check_shadow(allocation, 8));
let allocation = unsafe { allocator.alloc(0xc, 0) };
assert!(allocator.check_shadow(unsafe { allocation.offset(4) }, 8));
let allocation = unsafe { allocator.alloc(0x3c, 0) };
assert!(allocator.check_shadow(unsafe { allocation.offset(0x3a) }, 2));
}

File diff suppressed because it is too large Load Diff

View File

@ -91,7 +91,7 @@ pub(crate) enum AsanError {
}
impl AsanError {
fn description(&self) -> &str {
pub fn description(&self) -> &str {
match self {
AsanError::OobRead(_) => "heap out-of-bounds read",
AsanError::OobWrite(_) => "heap out-of-bounds write",
@ -114,7 +114,7 @@ impl AsanError {
#[derive(Debug, Clone, Serialize, Deserialize, SerdeAny)]
pub struct AsanErrors {
continue_on_error: bool,
errors: Vec<AsanError>,
pub(crate) errors: Vec<AsanError>,
}
impl AsanErrors {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
#[cfg(all(unix, not(test)))]
use core::borrow::Borrow;
use core::fmt::{self, Debug, Formatter};
#[cfg(windows)]
use std::process::abort;
use std::{ffi::c_void, marker::PhantomData};
use frida_gum::{
@ -21,7 +21,7 @@ use libafl::{
};
use libafl_bolts::tuples::RefIndexable;
#[cfg(all(unix, not(test)))]
#[cfg(not(test))]
use crate::asan::errors::AsanErrors;
use crate::helper::{FridaInstrumentationHelper, FridaRuntimeTuple};
#[cfg(windows)]
@ -106,11 +106,13 @@ where
self.stalker.deactivate();
}
#[cfg(all(unix, not(test)))]
#[cfg(not(test))]
unsafe {
if !AsanErrors::get_mut_blocking().borrow().is_empty() {
if !AsanErrors::get_mut_blocking().is_empty() {
log::error!("Crashing target as it had ASan errors");
libc::raise(libc::SIGABRT);
#[cfg(windows)]
abort();
}
}
self.helper.post_exec(input)?;
@ -206,6 +208,7 @@ where
}
}
log::info!("disable_excludes: {:}", helper.disable_excludes);
if !helper.disable_excludes {
for range in ranges.gaps(&(0..usize::MAX)) {
log::info!("excluding range: {:x}-{:x}", range.start, range.end);

View File

@ -6,9 +6,8 @@ use std::{
rc::Rc,
};
#[cfg(unix)]
use frida_gum::instruction_writer::InstructionWriter;
use frida_gum::{
instruction_writer::InstructionWriter,
stalker::{StalkerIterator, StalkerOutput, Transformer},
Gum, Module, ModuleDetails, ModuleMap, PageProtection,
};
@ -28,11 +27,9 @@ use yaxpeax_arm::armv8::a64::{ARMv8, InstDecoder};
#[cfg(target_arch = "x86_64")]
use yaxpeax_x86::amd64::InstDecoder;
#[cfg(unix)]
use crate::asan::asan_rt::AsanRuntime;
#[cfg(feature = "cmplog")]
use crate::cmplog_rt::CmpLogRuntime;
use crate::{coverage_rt::CoverageRuntime, drcov_rt::DrCovRuntime};
use crate::{asan::asan_rt::AsanRuntime, coverage_rt::CoverageRuntime, drcov_rt::DrCovRuntime};
#[cfg(target_vendor = "apple")]
const ANONYMOUS_FLAG: MapFlags = MapFlags::MAP_ANON;
@ -275,6 +272,11 @@ impl FridaInstrumentationHelperBuilder {
if stalker_enabled {
for (i, module) in module_map.values().iter().enumerate() {
log::trace!(
"module: {:?} {:x}",
module.name(),
module.range().base_address().0 as usize
);
let range = module.range();
let start = range.base_address().0 as usize;
ranges
@ -330,7 +332,7 @@ impl Default for FridaInstrumentationHelperBuilder {
fn default() -> Self {
Self {
stalker_enabled: true,
disable_excludes: true,
disable_excludes: false,
instrument_module_predicate: None,
skip_module_predicate: Box::new(|module| {
// Skip the instrumentation module to avoid recursion.
@ -445,7 +447,7 @@ where
let runtimes = Rc::clone(runtimes);
#[cfg(target_arch = "x86_64")]
let decoder = InstDecoder::minimal();
let decoder = InstDecoder::default();
#[cfg(target_arch = "aarch64")]
let decoder = <ARMv8 as Arch>::Decoder::default();
@ -459,7 +461,7 @@ where
basic_block: StalkerIterator,
output: &StalkerOutput,
ranges: &Rc<RefCell<RangeMap<usize, (u16, String)>>>,
runtimes: &Rc<RefCell<RT>>,
runtimes_unborrowed: &Rc<RefCell<RT>>,
decoder: InstDecoder,
) {
let mut first = true;
@ -469,10 +471,10 @@ where
let instr = instruction.instr();
let instr_size = instr.bytes().len();
let address = instr.address();
// log::trace!("block @ {:x} transformed to {:x}", address, output.writer().pc());
// log::trace!("x - block @ {:x} transformed to {:x}", address, output.writer().pc());
//the ASAN check needs to be done before the hook_rt check due to x86 insns such as call [mem]
if ranges.borrow().contains_key(&(address as usize)) {
let mut runtimes = (*runtimes).borrow_mut();
let mut runtimes = (*runtimes_unborrowed).borrow_mut();
if first {
first = false;
// log::info!(
@ -483,24 +485,29 @@ where
if let Some(rt) = runtimes.match_first_type_mut::<CoverageRuntime>() {
rt.emit_coverage_mapping(address, output);
}
if let Some(_rt) = runtimes.match_first_type_mut::<DrCovRuntime>() {
basic_block_start = address;
}
}
#[cfg(unix)]
let res = if let Some(_rt) = runtimes.match_first_type_mut::<AsanRuntime>() {
AsanRuntime::asan_is_interesting_instruction(decoder, address, instr)
} else {
None
};
#[cfg(all(target_arch = "x86_64", unix))]
#[cfg(target_arch = "x86_64")]
if let Some(details) = res {
if let Some(rt) = runtimes.match_first_type_mut::<AsanRuntime>() {
rt.emit_shadow_check(
address, output, details.0, details.1, details.2, details.3, details.4,
address,
output,
instr.bytes().len(),
details.0,
details.1,
details.2,
details.3,
details.4,
);
}
}
@ -541,7 +548,6 @@ where
}
}
#[cfg(unix)]
if let Some(rt) = runtimes.match_first_type_mut::<AsanRuntime>() {
rt.add_stalked_address(
output.writer().pc() as usize - instr_size,
@ -556,7 +562,10 @@ where
instruction.keep();
}
if basic_block_size != 0 {
if let Some(rt) = runtimes.borrow_mut().match_first_type_mut::<DrCovRuntime>() {
if let Some(rt) = runtimes_unborrowed
.borrow_mut()
.match_first_type_mut::<DrCovRuntime>()
{
log::trace!("{basic_block_start:#016X}:{basic_block_size:X}");
rt.drcov_basic_blocks.push(DrCovBasicBlock::new(
basic_block_start as usize,

View File

@ -64,10 +64,8 @@ Additional documentation is available in [the `LibAFL` book](https://aflplus.plu
)]
/// The frida-asan allocator
#[cfg(unix)]
pub mod alloc;
#[cfg(unix)]
pub mod asan;
#[cfg(windows)]
@ -369,7 +367,7 @@ mod tests {
use crate::{
asan::{
asan_rt::AsanRuntime,
errors::{AsanErrorsFeedback, AsanErrorsObserver},
errors::{AsanErrors, AsanErrorsFeedback, AsanErrorsObserver},
},
coverage_rt::CoverageRuntime,
executor::FridaInProcessExecutor,
@ -378,20 +376,56 @@ mod tests {
static GUM: OnceLock<Gum> = OnceLock::new();
#[allow(clippy::too_many_lines)]
unsafe fn test_asan(options: &FuzzerOptions) {
// The names of the functions to run
let tests = vec![
("LLVMFuzzerTestOneInput", 0),
("heap_oob_read", 1),
("heap_oob_write", 1),
("heap_uaf_write", 1),
("heap_uaf_read", 1),
("malloc_heap_oob_read", 1),
("malloc_heap_oob_write", 1),
("malloc_heap_uaf_write", 1),
("malloc_heap_uaf_read", 1),
("LLVMFuzzerTestOneInput", None),
("heap_oob_read", Some("heap out-of-bounds read")),
("heap_oob_write", Some("heap out-of-bounds write")),
("heap_uaf_write", Some("heap use-after-free write")),
("heap_uaf_read", Some("heap use-after-free read")),
("malloc_heap_oob_read", Some("heap out-of-bounds read")),
("malloc_heap_oob_write", Some("heap out-of-bounds write")),
(
"malloc_heap_oob_write_0x12",
Some("heap out-of-bounds write"),
),
(
"malloc_heap_oob_write_0x14",
Some("heap out-of-bounds write"),
),
(
"malloc_heap_oob_write_0x17",
Some("heap out-of-bounds write"),
),
(
"malloc_heap_oob_write_0x17_int_at_0x16",
Some("heap out-of-bounds write"),
),
(
"malloc_heap_oob_write_0x17_int_at_0x15",
Some("heap out-of-bounds write"),
),
("malloc_heap_oob_write_0x17_int_at_0x13", None),
(
"malloc_heap_oob_write_0x17_int_at_0x14",
Some("heap out-of-bounds write"),
),
("malloc_heap_uaf_write", Some("heap use-after-free write")),
("malloc_heap_uaf_read", Some("heap use-after-free read")),
];
//NOTE: RTLD_NOW is required on linux as otherwise the hooks will NOT work
#[cfg(target_os = "linux")]
let lib = libloading::os::unix::Library::open(
Some(options.clone().harness.unwrap()),
libloading::os::unix::RTLD_NOW,
)
.unwrap();
#[cfg(not(target_os = "linux"))]
let lib = libloading::Library::new(options.clone().harness.unwrap()).unwrap();
let coverage = CoverageRuntime::new();
@ -404,7 +438,7 @@ mod tests {
// Run the tests for each function
for test in tests {
let (function_name, err_cnt) = test;
let (function_name, expected_error) = test;
log::info!("Testing with harness function {}", function_name);
let mut corpus = InMemoryCorpus::<BytesInput>::new();
@ -415,7 +449,7 @@ mod tests {
let rand = StdRand::with_seed(0);
let mut feedback = ConstFeedback::new(false);
let mut feedback = ConstFeedback::new(true);
let asan_obs = AsanErrorsObserver::from_static_asan_errors();
@ -446,6 +480,12 @@ mod tests {
);
{
#[cfg(target_os = "linux")]
let target_func: libloading::os::unix::Symbol<
unsafe extern "C" fn(data: *const u8, size: usize) -> i32,
> = lib.get(function_name.as_bytes()).unwrap();
#[cfg(not(target_os = "linux"))]
let target_func: libloading::Symbol<
unsafe extern "C" fn(data: *const u8, size: usize) -> i32,
> = lib.get(function_name.as_bytes()).unwrap();
@ -473,19 +513,25 @@ mod tests {
let mutator = StdScheduledMutator::new(tuple_list!(BitFlipMutator::new()));
let mut stages = tuple_list!(StdMutationalStage::with_max_iterations(mutator, 1));
// log::info!("Starting fuzzing!");
log::info!("Starting fuzzing!");
fuzzer
.fuzz_one(&mut stages, &mut executor, &mut state, &mut event_manager)
.unwrap_or_else(|_| panic!("Error in fuzz_one"));
log::info!("Done fuzzing! Got {} solutions", state.solutions().count());
if let Some(expected_error) = expected_error {
assert_eq!(state.solutions().count(), 1);
if let Some(error) = AsanErrors::get_mut_blocking().errors.first() {
assert_eq!(error.description(), expected_error);
}
} else {
assert_eq!(state.solutions().count(), 0);
}
}
assert_eq!(state.solutions().count(), err_cnt);
}
}
#[test]
#[cfg(unix)]
fn run_test_asan() {
// Read RUST_LOG from the environment and set the log level accordingly (not using env_logger)
// Note that in cargo test, the output of successfull tests is suppressed by default,
@ -505,7 +551,10 @@ mod tests {
SimpleStdoutLogger::set_logger().unwrap();
// Check if the harness dynamic library is present, if not - skip the test
let test_harness = "test_harness.so";
#[cfg(unix)]
let test_harness = "./test_harness.so";
#[cfg(windows)]
let test_harness = ".\\test_harness.dll";
assert!(
std::path::Path::new(test_harness).exists(),
"Skipping test, {test_harness} not found"

View File

@ -1,7 +1,8 @@
#[cfg(target_arch = "aarch64")]
use frida_gum::instruction_writer::Aarch64Register;
#[cfg(target_arch = "x86_64")]
use frida_gum::instruction_writer::X86Register;
use frida_gum::{instruction_writer::X86Register, CpuContext};
use libafl::Error;
#[cfg(target_arch = "aarch64")]
use num_traits::cast::FromPrimitive;
#[cfg(target_arch = "x86_64")]
@ -158,6 +159,30 @@ const X86_64_REGS: [(RegSpec, X86Register); 34] = [
(RegSpec::rip(), X86Register::Rip),
];
/// Get the value of a register given a context
#[cfg(target_arch = "x86_64")]
pub fn get_register(context: &CpuContext, reg: X86Register) -> u64 {
match reg {
X86Register::Rax => context.rax(),
X86Register::Rbx => context.rbx(),
X86Register::Rcx => context.rcx(),
X86Register::Rdx => context.rdx(),
X86Register::Rdi => context.rdi(),
X86Register::Rsi => context.rsi(),
X86Register::Rsp => context.rsp(),
X86Register::Rbp => context.rbp(),
X86Register::R8 => context.r8(),
X86Register::R9 => context.r9(),
X86Register::R10 => context.r10(),
X86Register::R11 => context.r11(),
X86Register::R12 => context.r12(),
X86Register::R13 => context.r13(),
X86Register::R14 => context.r14(),
X86Register::R15 => context.r15(),
_ => 0,
}
}
/// The writer registers
/// frida registers: <https://docs.rs/frida-gum/latest/frida_gum/instruction_writer/enum.X86Register.html>
/// capstone registers: <https://docs.rs/capstone-sys/latest/capstone_sys/x86_reg/index.html>
@ -176,9 +201,25 @@ pub fn writer_register(reg: RegSpec) -> X86Register {
}
/// Translates a frida instruction to a disassembled instruction.
#[cfg(all(target_arch = "x86_64", unix))]
pub(crate) fn frida_to_cs(decoder: InstDecoder, frida_insn: &frida_gum_sys::Insn) -> Instruction {
decoder.decode_slice(frida_insn.bytes()).unwrap()
#[cfg(target_arch = "x86_64")]
pub(crate) fn frida_to_cs(
decoder: InstDecoder,
frida_insn: &frida_gum_sys::Insn,
) -> Result<Instruction, Error> {
match decoder.decode_slice(frida_insn.bytes()) {
Ok(result) => Ok(result),
Err(error) => {
log::error!(
"{:?}: {:x}: {:?}",
error,
frida_insn.address(),
frida_insn.bytes()
);
Err(Error::illegal_state(
"Instruction did not disassemble properly",
))
}
}
}
#[cfg(target_arch = "x86_64")]
@ -225,6 +266,23 @@ pub fn operand_details(operand: &Operand) -> Option<(X86Register, X86Register, u
}
}
#[cfg(target_arch = "x86_64")]
/// Get the immediate value of the operand
pub fn immediate_value(operand: &Operand) -> Option<i64> {
match operand {
Operand::ImmediateI8(v) => Some(i64::from(*v)),
Operand::ImmediateU8(v) => Some(i64::from(*v)),
Operand::ImmediateI16(v) => Some(i64::from(*v)),
Operand::ImmediateI32(v) => Some(i64::from(*v)),
Operand::ImmediateU16(v) => Some(i64::from(*v)),
Operand::ImmediateU32(v) => Some(i64::from(*v)),
Operand::ImmediateI64(v) => Some(*v),
#[allow(clippy::cast_possible_wrap)]
Operand::ImmediateU64(v) => Some(*v as i64),
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
#[cfg(target_arch = "x86_64")]
/// What kind of memory access this instruction has

View File

@ -29,7 +29,7 @@ pub fn initialize(gum: &Gum) {
NativePointer(is_processor_feature_present_detour as *mut c_void),
NativePointer(std::ptr::null_mut()),
)
.unwrap();
.unwrap_or_else(|_| NativePointer(std::ptr::null_mut()));
interceptor
.replace(
@ -37,7 +37,7 @@ pub fn initialize(gum: &Gum) {
NativePointer(unhandled_exception_filter_detour as *mut c_void),
NativePointer(std::ptr::null_mut()),
)
.unwrap();
.unwrap_or_else(|_| NativePointer(std::ptr::null_mut()));
unsafe extern "C" fn is_processor_feature_present_detour(feature: u32) -> bool {
let result = match feature {

View File

@ -2,62 +2,132 @@
#include <stdlib.h>
#include <string>
extern "C" int heap_uaf_read(const uint8_t *_data, size_t _size) {
#ifdef _MSC_VER
#include <windows.h>
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call,
LPVOID lpReserved) {
return TRUE;
}
#define EXTERN __declspec(dllexport) extern "C"
#else
#define EXTERN
extern "C" {
#endif
EXTERN int heap_uaf_read(const uint8_t *_data, size_t _size) {
int *array = new int[100];
delete[] array;
fprintf(stdout, "%d\n", array[5]);
return 0;
}
extern "C" int heap_uaf_write(const uint8_t *_data, size_t _size) {
EXTERN int heap_uaf_write(const uint8_t *_data, size_t _size) {
int *array = new int[100];
delete[] array;
array[5] = 1;
return 0;
}
extern "C" int heap_oob_read(const uint8_t *_data, size_t _size) {
EXTERN int heap_oob_read(const uint8_t *_data, size_t _size) {
int *array = new int[100];
fprintf(stdout, "%d\n", array[100]);
delete[] array;
return 0;
}
extern "C" int heap_oob_write(const uint8_t *_data, size_t _size) {
EXTERN int heap_oob_write(const uint8_t *_data, size_t _size) {
int *array = new int[100];
array[100] = 1;
delete[] array;
return 0;
}
extern "C" int malloc_heap_uaf_read(const uint8_t *_data, size_t _size) {
EXTERN int malloc_heap_uaf_read(const uint8_t *_data, size_t _size) {
int *array = static_cast<int *>(malloc(100 * sizeof(int)));
free(array);
fprintf(stdout, "%d\n", array[5]);
return 0;
}
extern "C" int malloc_heap_uaf_write(const uint8_t *_data, size_t _size) {
EXTERN int malloc_heap_uaf_write(const uint8_t *_data, size_t _size) {
int *array = static_cast<int *>(malloc(100 * sizeof(int)));
free(array);
array[5] = 1;
return 0;
}
extern "C" int malloc_heap_oob_read(const uint8_t *_data, size_t _size) {
EXTERN int malloc_heap_oob_read(const uint8_t *_data, size_t _size) {
int *array = static_cast<int *>(malloc(100 * sizeof(int)));
fprintf(stdout, "%d\n", array[100]);
free(array);
return 0;
}
extern "C" int malloc_heap_oob_write(const uint8_t *_data, size_t _size) {
EXTERN int malloc_heap_oob_write(const uint8_t *_data, size_t _size) {
int *array = static_cast<int *>(malloc(100 * sizeof(int)));
array[100] = 1;
free(array);
return 0;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
EXTERN int malloc_heap_oob_write_0x12(const uint8_t *_data, size_t _size) {
char *array = static_cast<char *>(malloc(0x12));
array[0x12] = 1;
free(array);
return 0;
}
EXTERN int malloc_heap_oob_write_0x14(const uint8_t *_data, size_t _size) {
char *array = static_cast<char *>(malloc(0x14));
array[0x14] = 1;
free(array);
return 0;
}
EXTERN int malloc_heap_oob_write_0x17(const uint8_t *_data, size_t _size) {
char *array = static_cast<char *>(malloc(0x17));
array[0x17] = 1;
free(array);
return 0;
}
EXTERN int malloc_heap_oob_write_0x17_int_at_0x16(const uint8_t *_data,
size_t _size) {
char *array = static_cast<char *>(malloc(0x17));
*(int *)(&array[0x16]) = 1;
free(array);
return 0;
}
EXTERN int malloc_heap_oob_write_0x17_int_at_0x15(const uint8_t *_data,
size_t _size) {
char *array = static_cast<char *>(malloc(0x17));
*(int *)(&array[0x15]) = 1;
free(array);
return 0;
}
EXTERN int malloc_heap_oob_write_0x17_int_at_0x14(const uint8_t *_data,
size_t _size) {
char *array = static_cast<char *>(malloc(0x17));
*(int *)(&array[0x14]) = 1;
free(array);
return 0;
}
EXTERN int malloc_heap_oob_write_0x17_int_at_0x13(const uint8_t *_data,
size_t _size) {
char *array = static_cast<char *>(malloc(0x17));
*(int *)(&array[0x13]) = 1;
free(array);
return 0;
}
EXTERN int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// abort();
return 0;
}
#ifndef _MSC_VER
}
#endif

View File

@ -200,10 +200,11 @@ fn main() {
}
}
let target_family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap();
#[cfg(feature = "forkserver")]
{
#[cfg(unix)]
{
if target_family == "unix" {
println!("cargo:rerun-if-changed=src/forkserver.c");
cc::Build::new()
@ -212,8 +213,8 @@ fn main() {
}
}
#[cfg(all(feature = "windows_asan", windows))]
{
#[cfg(feature = "windows_asan")]
if target_family == "windows" {
println!("cargo:rerun-if-changed=src/windows_asan.c");
cc::Build::new()

View File

@ -15,9 +15,17 @@ void *__libafl_asan_region_is_poisoned(void *beg, size_t size) {
return NULL;
}
#pragma comment( \
linker, \
"/alternatename:__asan_region_is_poisoned=__libafl_asan_region_is_poisoned")
#if defined(__clang__) && defined(_MSC_VER)
void *__asan_region_is_poisoned(void *beg, size_t size) {
(void)beg;
(void)size;
return NULL;
}
#else
#pragma comment( \
linker, \
"/alternatename:__asan_region_is_poisoned=__libafl_asan_region_is_poisoned")
#endif
#elif defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))