From 99c011bdcbc9c86e61ea51e9041abf028c1f4efd Mon Sep 17 00:00:00 2001 From: Evian-Zhang Date: Tue, 6 May 2025 20:05:54 +0800 Subject: [PATCH 1/2] Migrate to newest libafl --- Cargo.toml | 6 +- examples/sample.rs | 6 +- src/executor.rs | 337 +++++++++++++++++++++++++++++---------------- src/forkserver.rs | 278 +++++++++++++++++++++++++++++++++++++ src/harness.rs | 154 ++++++--------------- src/hash.rs | 2 +- src/lib.rs | 55 ++++---- src/target.rs | 99 +++++-------- 8 files changed, 605 insertions(+), 332 deletions(-) create mode 100644 src/forkserver.rs diff --git a/Cargo.toml b/Cargo.toml index df3e0848..f23e2b45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libafl_targets = {git = "https://github.com/wtdcode/LibAFL", branch = "ucafl", features = ["pointer_maps"]} -libafl = {git = "https://github.com/wtdcode/LibAFL", branch = "ucafl"} -libafl_bolts = {git = "https://github.com/wtdcode/LibAFL", branch = "ucafl"} +libafl_targets = { git = "https://github.com/AFLplusplus/LibAFL", features = ["pointer_maps", "forkserver"] } +libafl = { git = "https://github.com/AFLplusplus/LibAFL" } +libafl_bolts = { git = "https://github.com/AFLplusplus/LibAFL" } serde ={ version = "1.0", features = ["derive"] } unicorn-engine = { git = "https://github.com/unicorn-engine/unicorn", branch = "dev"} diff --git a/examples/sample.rs b/examples/sample.rs index c251640c..3ea1e1b9 100644 --- a/examples/sample.rs +++ b/examples/sample.rs @@ -14,7 +14,7 @@ fn place_input_cb<'a, D: 'a>( return false; } let cp_len = input.len().min(8); - buf[0..cp_len].copy_from_slice(input); + buf[0..cp_len].copy_from_slice(&input[0..cp_len]); let rdx = u64::from_le_bytes(buf); uc.reg_write(RegisterX86::RDX, rdx) .expect("Fail to write reg"); @@ -23,7 +23,7 @@ fn place_input_cb<'a, D: 'a>( } fn main() { - let input_file = std::env::args().into_iter().skip(1).nth(0); + let input_file = std::env::args().nth(1); let mut uc = Unicorn::new_with_data(Arch::X86, Mode::MODE_64, UnicornFuzzData::default()) .expect("fail to open uc"); // ks.asm("mov rax, rdx; cmp rax, 0x114514; je die; xor rax, rax; die: mov rax, [rax]; xor rax, rax") @@ -33,7 +33,7 @@ fn main() { let pc = 0x1000; uc.reg_write(RegisterX86::RIP, pc) .expect("fail to write pc"); - let input_file = input_file.map(|t| PathBuf::from(t)); + let input_file = input_file.map(PathBuf::from); afl_fuzz( uc, input_file, diff --git a/src/executor.rs b/src/executor.rs index c070d571..8d989fbd 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,68 +1,21 @@ -use std::{ - hash::{Hash, Hasher}, - ops::Deref, -}; +//! Executor to conduct unicorn afl fuzzing in one execution round. + +use std::{marker::PhantomData, os::fd::OwnedFd}; use libafl::{ executors::{Executor, ExitKind, HasObservers}, - inputs::{BytesInput, Input}, - observers::ValueObserver, + inputs::HasTargetBytes, + observers::ObserversTuple, state::HasExecutions, }; -use libafl_bolts::{ - ownedref::{OwnedRef, OwnedSlice}, - tuples::{tuple_list, tuple_list_type, RefIndexable}, -}; +use libafl_bolts::tuples::RefIndexable; use libafl_targets::EDGES_MAP_PTR; -use log::{trace, warn}; -use serde::{Deserialize, Serialize}; +use log::{error, trace, warn}; use unicorn_engine::{uc_error, TcgOpCode, TcgOpFlag, UcHookId, Unicorn}; use crate::hash::afl_hash_ip; -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct UnsafeSliceInput<'a> { - pub input: OwnedSlice<'a, u8>, -} - -impl<'a> Deref for UnsafeSliceInput<'a> { - type Target = OwnedSlice<'a, u8>; - fn deref(&self) -> &Self::Target { - &self.input - } -} - -impl<'a> Hash for UnsafeSliceInput<'a> { - fn hash(&self, state: &mut H) { - self.input.to_vec().hash(state); - } -} - -impl<'a> UnsafeSliceInput<'a> { - pub fn to_bytes_input(&self) -> BytesInput { - BytesInput::new(self.input.to_vec()) - } -} - -impl<'a> Input for UnsafeSliceInput<'a> { - fn to_file

(&self, path: P) -> Result<(), libafl::Error> - where - P: AsRef, - { - self.to_bytes_input().to_file(path) - } - - fn from_file

(path: P) -> Result - where - P: AsRef, - { - let input = BytesInput::from_file(path)?; - Ok(Self { - input: OwnedSlice::from(input.into_inner()), - }) - } -} - +/// State for hook edge #[derive(Debug)] struct HookState { prev_loc: u32, @@ -72,8 +25,7 @@ struct HookState { fn get_afl_map_size() -> u32 { std::env::var("AFL_MAP_SIZE") .ok() - .map(|sz| u32::from_str_radix(&sz, 10).ok()) - .flatten() + .and_then(|sz| sz.parse::().ok()) .unwrap_or(1 << 16) // MAP_SIZE } @@ -86,6 +38,10 @@ fn get_afl_map_size() -> u32 { #[derive(Debug)] pub struct UnicornFuzzData { hook_state: HookState, + /// Store write side to child pipe. Closed when dropping + pub(crate) child_pipe_w: Option, + /// Store read side to parent pipe. Closed when dropping + pub(crate) parent_pipe_r: Option, /// User-defined data. pub user_data: D, } @@ -103,6 +59,10 @@ impl Default for UnicornFuzzData<()> { } impl UnicornFuzzData { + /// Create a new unicorn fuzz data. + /// + /// This will try to retrieve env `AFL_MAP_SIZE` to determine map size, and + /// fill the default value if no such env. pub fn new(user_data: D) -> Self { Self { hook_state: HookState { @@ -110,8 +70,15 @@ impl UnicornFuzzData { map_size: get_afl_map_size(), }, user_data, + child_pipe_w: None, + parent_pipe_r: None, } } + + /// Clear hook state. Always call this method before each execution + pub(crate) fn clear_prev_loc(&mut self) { + self.hook_state.prev_loc = 0; + } } unsafe fn update_coverage(idx: usize) { @@ -209,23 +176,105 @@ fn hook_opcode_cmpcov<'a, D: 'a>( } } -/// Executor for unicorn -pub struct UnicornAflExecutor<'a, D, FI, FV, FC> -where - D: 'a, - FI: FnMut(&mut Unicorn<'a, UnicornFuzzData>, &[u8], u64) -> bool + 'a, - FV: FnMut(&mut Unicorn<'a, UnicornFuzzData>, Result<(), uc_error>, &[u8], u64) -> bool + 'a, - FC: FnMut(&mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> + 'a, -{ - uc: Unicorn<'a, UnicornFuzzData>, +/// Callbacks for each execution round +pub trait UnicornAflExecutorHook<'a, D> { /// Place the generated input into unicorn's memory. /// /// Return false if the generated input is not acceptable - place_input_cb: FI, - /// Return true if the crash is valid after validation - validate_crash_cb: FV, + fn place_input( + &mut self, + uc: &mut Unicorn<'a, UnicornFuzzData>, + input: &[u8], + persistent_round: u64, + ) -> bool; + + /// Return true if the crash is valid after validation. + /// + /// The default implementation is [`dummy_uc_validate_crash_callback`][crate::target::dummy_uc_validate_crash_callback] + fn validate_crash( + &mut self, + uc: &mut Unicorn<'a, UnicornFuzzData>, + unicorn_result: Result<(), uc_error>, + input: &[u8], + persistent_round: u64, + ) -> bool { + crate::target::dummy_uc_validate_crash_callback(uc, unicorn_result, input, persistent_round) + } + /// The real procedure to kick unicorn engine start + /// + /// The default implementation is [`dummy_uc_fuzz_callback`][crate::target::dummy_uc_fuzz_callback] + fn fuzz(&mut self, uc: &mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> { + crate::target::dummy_uc_fuzz_callback(uc) + } +} + +/// Convenient struct to create a [`UnicornAflExecutorHook`] from closures +pub struct UnicornAflExecutorCustomHook<'a, D, FI, FV, FC> { + place_input_callback: FI, + validate_crash_callback: FV, fuzz_callback: FC, + phantom: PhantomData<&'a D>, +} + +impl<'a, D, FI, FV, FC> UnicornAflExecutorCustomHook<'a, D, FI, FV, FC> +where + FI: FnMut(&mut Unicorn<'a, UnicornFuzzData>, &[u8], u64) -> bool + 'a, + FV: FnMut(&mut Unicorn<'a, UnicornFuzzData>, Result<(), uc_error>, &[u8], u64) -> bool + 'a, + FC: FnMut(&mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> + 'a, +{ + /// Create a new custom hook from closures + pub fn new(place_input_callback: FI, validate_crash_callback: FV, fuzz_callback: FC) -> Self { + Self { + place_input_callback, + validate_crash_callback, + fuzz_callback, + phantom: PhantomData, + } + } +} + +impl<'a, D, FI, FV, FC> UnicornAflExecutorHook<'a, D> + for UnicornAflExecutorCustomHook<'a, D, FI, FV, FC> +where + FI: FnMut(&mut Unicorn<'a, UnicornFuzzData>, &[u8], u64) -> bool + 'a, + FV: FnMut(&mut Unicorn<'a, UnicornFuzzData>, Result<(), uc_error>, &[u8], u64) -> bool + 'a, + FC: FnMut(&mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> + 'a, +{ + fn place_input( + &mut self, + uc: &mut Unicorn<'a, UnicornFuzzData>, + input: &[u8], + persistent_round: u64, + ) -> bool { + (self.place_input_callback)(uc, input, persistent_round) + } + + fn validate_crash( + &mut self, + uc: &mut Unicorn<'a, UnicornFuzzData>, + unicorn_result: Result<(), uc_error>, + input: &[u8], + persistent_round: u64, + ) -> bool { + (self.validate_crash_callback)(uc, unicorn_result, input, persistent_round) + } + + fn fuzz(&mut self, uc: &mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> { + (self.fuzz_callback)(uc) + } +} + +/// Executor for unicorn afl fuzzing. Can be used in both forkserver mode +/// and LibAFL. +pub struct UnicornAflExecutor<'a, D, OT, H> +where + D: 'a, +{ + /// The real unicorn engine + pub uc: Unicorn<'a, UnicornFuzzData>, + /// The observers, observing each run + observers: OT, /// Whether the `validate_crash_cb` is invoked everytime regardless of /// the execution result. /// @@ -237,22 +286,22 @@ where sub_hook: UcHookId, /// Stored for deleting hook when dropping cmp_hook: UcHookId, - dumb_ob: tuple_list_type!(ValueObserver<'static, bool>), + /// Stored for deleting hook when dropping + new_tb_hook: UcHookId, + /// Callback hooks + callbacks: H, } -impl<'a, D, FI, FV, FC> UnicornAflExecutor<'a, D, FI, FV, FC> +impl<'a, D, OT, H> UnicornAflExecutor<'a, D, OT, H> where D: 'a, - FI: FnMut(&mut Unicorn<'a, UnicornFuzzData>, &[u8], u64) -> bool + 'a, - FV: FnMut(&mut Unicorn<'a, UnicornFuzzData>, Result<(), uc_error>, &[u8], u64) -> bool + 'a, - FC: FnMut(&mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> + 'a, + H: UnicornAflExecutorHook<'a, D>, { /// Create a new executor pub fn new( mut uc: Unicorn<'a, UnicornFuzzData>, - place_input_cb: FI, - validate_crash_cb: FV, - fuzz_callback: FC, + observers: OT, + callbacks: H, always_validate: bool, exits: Vec, ) -> Result { @@ -299,46 +348,100 @@ where .inspect_err(|ret| { warn!("Fail to add cmp hooks due to {ret:?}"); })?; + let new_tb_hook = uc + .add_edge_gen_hook(1, 0, |uc, cur_tb, _| { + if let Some(child_pipe_w) = &uc.get_data_mut().child_pipe_w { + if crate::forkserver::write_u32_to_fd( + child_pipe_w, + crate::forkserver::afl_child_ret::TSL_REQUEST, + ) + .is_err() + { + error!("Error writing TSL REQUEST"); + return; + } + #[expect(clippy::needless_return)] + if crate::forkserver::write_u64_to_fd(child_pipe_w, cur_tb.pc).is_err() { + error!("Error writing TSL REQUEST pc"); + return; + } + } + }) + .inspect_err(|ret| { + warn!("Fail to add edge gen hooks due to {ret:?}"); + })?; Ok(Self { uc, - place_input_cb, - validate_crash_cb, - fuzz_callback, + observers, + callbacks, always_validate, block_hook, sub_hook, cmp_hook, - dumb_ob: tuple_list!(ValueObserver::new("dumb_ob", OwnedRef::Owned(false.into()))), + new_tb_hook, }) } + + /// Bare execution without any state modification. Always call wrappers + /// like [`run_target`][Executor::run_target] or [`forkserver_run_harness`][crate::harness::forkserver_run_harness] + pub(crate) fn execute_internal( + &mut self, + input: &[u8], + persistent_round: u64, + ) -> Result { + self.uc.get_data_mut().clear_prev_loc(); + + let accepted = self + .callbacks + .place_input(&mut self.uc, input, persistent_round); + + if !accepted { + trace!("Input not accepted"); + return Ok(ExitKind::Ok); + } + + let err = self.callbacks.fuzz(&mut self.uc); + + if let Err(err) = &err { + trace!("Child returns: {err}"); + } else { + trace!("Child returns: OK"); + } + + let mut crash_found = false; + + if (err.is_err() || self.always_validate) + && self + .callbacks + .validate_crash(&mut self.uc, err, input, persistent_round) + { + crash_found = true; + } + + if crash_found { + Ok(ExitKind::Crash) + } else { + Ok(ExitKind::Ok) + } + } } -impl<'a, D, FI, FV, FC> HasObservers for UnicornAflExecutor<'a, D, FI, FV, FC> -where - D: 'a, - FI: FnMut(&mut Unicorn<'a, UnicornFuzzData>, &[u8], u64) -> bool + 'a, - FV: FnMut(&mut Unicorn<'a, UnicornFuzzData>, Result<(), uc_error>, &[u8], u64) -> bool + 'a, - FC: FnMut(&mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> + 'a, -{ - type Observers = tuple_list_type!(ValueObserver<'static, bool>); - fn observers(&self) -> libafl_bolts::tuples::RefIndexable<&Self::Observers, Self::Observers> { - RefIndexable::from(&self.dumb_ob) +impl HasObservers for UnicornAflExecutor<'_, D, OT, H> { + type Observers = OT; + + fn observers(&self) -> RefIndexable<&Self::Observers, Self::Observers> { + RefIndexable::from(&self.observers) } - fn observers_mut( - &mut self, - ) -> libafl_bolts::tuples::RefIndexable<&mut Self::Observers, Self::Observers> { - RefIndexable::from(&mut self.dumb_ob) + fn observers_mut(&mut self) -> RefIndexable<&mut Self::Observers, Self::Observers> { + RefIndexable::from(&mut self.observers) } } -impl<'a, D, FI, FV, FC> Drop for UnicornAflExecutor<'a, D, FI, FV, FC> +impl<'a, D, OT, H> Drop for UnicornAflExecutor<'a, D, OT, H> where D: 'a, - FI: FnMut(&mut Unicorn<'a, UnicornFuzzData>, &[u8], u64) -> bool + 'a, - FV: FnMut(&mut Unicorn<'a, UnicornFuzzData>, Result<(), uc_error>, &[u8], u64) -> bool + 'a, - FC: FnMut(&mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> + 'a, { fn drop(&mut self) { if let Err(ret) = self.uc.remove_hook(self.block_hook) { @@ -350,42 +453,34 @@ where if let Err(ret) = self.uc.remove_hook(self.cmp_hook) { warn!("Fail to uninstall cmp tcg opcode hook due to {ret:?}"); } + if let Err(ret) = self.uc.remove_hook(self.new_tb_hook) { + warn!("Fail to uninstall edge gen hook due to {ret:?}"); + } } } -impl<'a, 'b, EM, S, Z, D, FI, FV, FC> Executor, S, Z> - for UnicornAflExecutor<'b, D, FI, FV, FC> +impl<'a, EM, I, S, Z, D, OT, H> Executor for UnicornAflExecutor<'a, D, OT, H> where S: HasExecutions, - D: 'b, - FI: FnMut(&mut Unicorn<'b, UnicornFuzzData>, &[u8], u64) -> bool + 'b, - FV: FnMut(&mut Unicorn<'b, UnicornFuzzData>, Result<(), uc_error>, &[u8], u64) -> bool + 'b, - FC: FnMut(&mut Unicorn<'b, UnicornFuzzData>) -> Result<(), uc_error> + 'b, + I: HasTargetBytes, + OT: ObserversTuple, + D: 'a, + H: UnicornAflExecutorHook<'a, D>, { fn run_target( &mut self, _fuzzer: &mut Z, state: &mut S, _mgr: &mut EM, - input: &UnsafeSliceInput<'a>, + input: &I, ) -> Result { - let accepted = (self.place_input_cb)(&mut self.uc, input.as_ref(), *state.executions()); + *state.executions_mut() += 1; + self.observers.pre_exec_all(state, input)?; - if !accepted { - trace!("Input not accepted"); - return Ok(ExitKind::Ok); - } - - let err = (self.fuzz_callback)(&mut self.uc); + let exit_kind = self.execute_internal(&input.target_bytes(), *state.executions())?; - trace!("Child returns: {err:?}"); - - if err.is_err() || self.always_validate { - if (self.validate_crash_cb)(&mut self.uc, err, input.as_ref(), *state.executions()) { - return Ok(ExitKind::Crash); - } - } + self.observers.post_exec_all(state, input, &exit_kind)?; - Ok(ExitKind::Ok) + Ok(exit_kind) } } diff --git a/src/forkserver.rs b/src/forkserver.rs new file mode 100644 index 00000000..d35faee2 --- /dev/null +++ b/src/forkserver.rs @@ -0,0 +1,278 @@ +use std::os::fd::{AsFd, AsRawFd, OwnedFd}; + +use libafl_bolts::os::{ChildHandle, ForkResult}; +use libafl_targets::ForkserverParent; +use log::{error, trace}; +use nix::{ + sys::signal::{SigHandler, Signal}, + unistd::Pid, +}; + +use crate::{executor::UnicornAflExecutor, uc_afl_ret}; + +fn write_to_fd(fd: impl AsFd, message: &[u8]) -> Result<(), uc_afl_ret> { + let bytes_written = + nix::unistd::write(fd, message).map_err(|_| uc_afl_ret::UC_AFL_RET_ERROR)?; + if bytes_written != message.len() { + return Err(uc_afl_ret::UC_AFL_RET_ERROR); + } + Ok(()) +} +pub(crate) fn write_u32_to_fd(fd: impl AsFd, message: u32) -> Result<(), uc_afl_ret> { + write_to_fd(fd, &message.to_ne_bytes()) +} +pub(crate) fn write_u64_to_fd(fd: impl AsFd, message: u64) -> Result<(), uc_afl_ret> { + write_to_fd(fd, &message.to_ne_bytes()) +} + +fn read_from_fd(fd: impl AsFd, message: &mut [u8]) -> Result<(), uc_afl_ret> { + let bytes_read = nix::unistd::read(fd.as_fd().as_raw_fd(), message) + .map_err(|_| uc_afl_ret::UC_AFL_RET_ERROR)?; + if bytes_read != message.len() { + return Err(uc_afl_ret::UC_AFL_RET_ERROR); + } + Ok(()) +} +pub(crate) fn read_u32_from_fd(fd: impl AsFd) -> Result { + let mut buf = [0u8; 4]; + read_from_fd(fd, &mut buf)?; + Ok(u32::from_ne_bytes(buf)) +} +pub(crate) fn read_u64_from_fd(fd: impl AsFd) -> Result { + let mut buf = [0u8; 8]; + read_from_fd(fd, &mut buf)?; + Ok(u64::from_ne_bytes(buf)) +} + +/// Messages from unicornafl child to parent +pub(crate) mod afl_child_ret { + pub(crate) type ChildRet = u32; + /// Current execution done without any interestring findings. + /// Wait for parent to fire next execution + pub(crate) const NEXT: ChildRet = 0; + /// Current execution done with a crash found + pub(crate) const FOUND_CRASH: ChildRet = 1; + /// Edge generation event. This is always followed by generated edge PC. + /// + /// This will never be sent when child finished its execution. + pub(crate) const TSL_REQUEST: ChildRet = 2; + /// The child process has exited. + /// + /// This will never be sent from child to parent. Instead, this is a phantom + /// state used for forkserver parent state management. + pub(crate) const EXITED: ChildRet = 3; +} + +type AflChildRet = afl_child_ret::ChildRet; + +/// Forkserver parent for UnicornAFL +pub struct UnicornAflForkserverParent<'a, D, OT, H> +where + D: 'a, +{ + /// Executor. + /// + /// You could drop the parent and take ownership back when parent + /// returns from [`start_forkserver`][libafl_targets::start_forkserver], which + /// indicates that it is the child process, and parent is useless anymore (the + /// owned resources have been transferred to the executor itself). + pub(crate) executor: UnicornAflExecutor<'a, D, OT, H>, + child_pipe_r: Option, + child_pipe_w: Option, + parent_pipe_r: Option, + parent_pipe_w: Option, + last_child_pid: Option, + last_child_ret: AflChildRet, + old_sigchld_handler: Option, + wifsignaled: i32, +} + +impl<'a, D, OT, H> UnicornAflForkserverParent<'a, D, OT, H> +where + D: 'a, +{ + /// Create a new forkserver parent + pub fn new(executor: UnicornAflExecutor<'a, D, OT, H>) -> Self { + Self { + executor, + child_pipe_r: None, + child_pipe_w: None, + parent_pipe_r: None, + parent_pipe_w: None, + last_child_pid: None, + last_child_ret: afl_child_ret::EXITED, + old_sigchld_handler: None, + wifsignaled: get_valid_wifsignaled(), + } + } +} + +impl<'a, D, OT, H> ForkserverParent for UnicornAflForkserverParent<'a, D, OT, H> +where + D: 'a, +{ + fn pre_fuzzing(&mut self) -> Result<(), libafl::Error> { + let old_sigchld_handler = + (unsafe { nix::sys::signal::signal(Signal::SIGCHLD, SigHandler::SigDfl) }) + .inspect_err(|_| { + error!("Fail to swap signal handler for SIGCHLD."); + })?; + self.old_sigchld_handler = Some(old_sigchld_handler); + Ok(()) + } + + fn handle_child_requests(&mut self) -> Result { + let child_pipe_r = self.child_pipe_r.as_ref().unwrap().as_fd(); + self.last_child_ret = loop { + let Ok(child_msg) = read_u32_from_fd(child_pipe_r) else { + break afl_child_ret::EXITED; + }; + + trace!("Get a child_msg={child_msg}"); + + if child_msg == afl_child_ret::NEXT || child_msg == afl_child_ret::FOUND_CRASH { + break child_msg; + } else if child_msg == afl_child_ret::TSL_REQUEST { + let Ok(pc) = read_u64_from_fd(child_pipe_r) else { + error!("Fail to read child tsl request."); + break afl_child_ret::EXITED; + }; + + if self.executor.uc.ctl_request_cache(pc, None).is_ok() { + trace!("TB is cached at 0x{pc:x}"); + } else { + error!("Failed to cache the TB at 0x{pc:x}"); + } + } else { + error!("Unexpected response by child! {child_msg}. Please report this as bug for unicornafl. + Expected one of {{AFL_CHILD_NEXT: {}, AFL_CHILD_FOUND_CRASH: {}, AFL_CHILD_TSL_REQUEST: {}}}.", afl_child_ret::NEXT, afl_child_ret::FOUND_CRASH, afl_child_ret::TSL_REQUEST); + } + }; + + match self.last_child_ret { + afl_child_ret::NEXT => { + // Child asks for next in persistent mode + // This status tells AFL we are not crashed. + Ok(0) + } + afl_child_ret::FOUND_CRASH => { + // WIFSIGNALED(wifsignaled) == 1 -> tells AFL the child crashed + // (even though it's still alive for persistent mode) + Ok(self.wifsignaled) + } + afl_child_ret::EXITED => { + // If child exited, get and relay exit status to parent through waitpid + let mut status = 0i32; + if unsafe { + nix::libc::waitpid(*self.last_child_pid.as_ref().unwrap(), &mut status, 0) + } < 0 + { + // Zombie Child could not be collected. Scary! + error!("[!] The child's exit code could not be determined."); + return Err(libafl::Error::illegal_state("waitpid")); + } + + Ok(status) + } + _ => unreachable!(), + } + } + + fn spawn_child(&mut self, was_killed: bool) -> Result { + // If we stopped the child in persistent mode, but there was a race + // condition and afl-fuzz already issued SIGKILL, write off the old + // process. + + if self.last_child_ret != afl_child_ret::EXITED && was_killed { + error!("Child was killed by AFL in the meantime."); + + self.last_child_ret = afl_child_ret::EXITED; + if let Some(child_pid) = self.last_child_pid.take() { + nix::sys::wait::waitpid(Pid::from_raw(child_pid), None).inspect_err(|_| { + error!("Error waiting for child"); + })?; + } + } + + if self.last_child_ret == afl_child_ret::EXITED { + // Child dead. Establish new a channel with child to grab + // translation commands. We'll read from child_pipe_r, + // child will write to child_pipe_w. + let (child_pipe_r, child_pipe_w) = nix::unistd::pipe().inspect_err(|_| { + error!("[!] Error creating pipe to child"); + })?; + // The re-assignment will close the previously-unclosed pipe ends + self.child_pipe_r = Some(child_pipe_r); + self.child_pipe_w = Some(child_pipe_w); + let (parent_pipe_r, parent_pipe_w) = nix::unistd::pipe().inspect_err(|_| { + error!("[!] Error creating pipe to parent"); + })?; + self.parent_pipe_r = Some(parent_pipe_r); + self.parent_pipe_w = Some(parent_pipe_w); + + // Create a clone of our process. + let fork_result = (unsafe { libafl_bolts::os::fork() }).inspect_err(|_| { + error!("[!] Could not fork"); + })?; + + // In child process: close fds, resume execution. + match &fork_result { + ForkResult::Child => { + // New Child + (unsafe { + nix::sys::signal::signal( + Signal::SIGCHLD, + self.old_sigchld_handler.take().unwrap(), + ) + }) + .inspect_err(|_| { + error!("Fail to restore signal handler for SIGCHLD."); + })?; + self.child_pipe_r = None; + self.parent_pipe_w = None; + // Forward owned fd to executor to make it alive + self.executor.uc.get_data_mut().child_pipe_w = self.child_pipe_w.take(); + self.executor.uc.get_data_mut().parent_pipe_r = self.parent_pipe_r.take(); + } + ForkResult::Parent(child_pid) => { + // parent for new child + + // If we don't close this in parent, we don't get notified + // on afl_child_pipe once child is gone + self.child_pipe_w = None; + self.parent_pipe_r = None; + self.last_child_pid = Some(child_pid.pid); + } + } + Ok(fork_result) + } else { + // parent, in persistent mode + let child_pid = ChildHandle { + pid: *self.last_child_pid.as_ref().unwrap(), + }; + + // Special handling for persistent mode: if the child is alive + // but currently stopped, simply restart it with a write to + // afl_parent_pipe. In case we fuzz using shared map, use this + // method to forward the size of the current testcase to the + // child without cost. + if write_u32_to_fd(self.parent_pipe_w.as_ref().unwrap().as_fd(), 0).is_err() { + self.last_child_ret = afl_child_ret::EXITED; + return self.spawn_child(was_killed); + } + + Ok(ForkResult::Parent(child_pid)) + } + } +} + +/// Try to get a valid status which could make `WIFSIGNALED` return `true`. +fn get_valid_wifsignaled() -> i32 { + let mut status = 0; + + while !nix::libc::WIFSIGNALED(status) { + status += 1; + } + + status +} diff --git a/src/harness.rs b/src/harness.rs index cc06914a..c338282c 100644 --- a/src/harness.rs +++ b/src/harness.rs @@ -1,124 +1,56 @@ -use std::{ffi::CString, path::PathBuf}; +use std::path::PathBuf; -use libafl::{ - executors::Executor, - stages::{Restartable, Stage}, - Evaluator, -}; -use libafl_bolts::ownedref::OwnedSlice; +use libafl::executors::ExitKind; use libafl_targets::{EDGES_MAP_PTR, INPUT_LENGTH_PTR, INPUT_PTR}; -use nix::{ - libc::{mmap64, open, MAP_PRIVATE, O_RDONLY, PROT_READ, PROT_WRITE}, - sys::stat::fstat, -}; +use log::error; -use crate::executor::UnsafeSliceInput; +use crate::executor::{UnicornAflExecutor, UnicornAflExecutorHook}; -#[derive(Debug)] -pub struct LegacyHarnessStage { +/// Harness loop for forkserver mode +pub fn forkserver_run_harness<'a, D, OT, H>( + executor: &mut UnicornAflExecutor<'a, D, OT, H>, + input_path: Option, iters: usize, - map_size: usize, - input_str: Option, -} - -impl LegacyHarnessStage { - pub fn new(iters: usize, map_size: usize, input_str: Option) -> Self { - Self { - iters, - map_size, - input_str, - } - } -} - -impl Restartable for LegacyHarnessStage { - fn clear_progress(&mut self, _state: &mut S) -> Result<(), libafl::Error> { - Ok(()) - } - - fn should_restart(&mut self, _state: &mut S) -> Result { - Ok(true) - } -} - -impl<'a, E, EM, S, Z> Stage for LegacyHarnessStage +) -> Result<(), libafl::Error> where - E: Executor, S, Z>, - Z: Evaluator, S>, + D: 'a, + H: UnicornAflExecutorHook<'a, D>, { - fn perform( - &mut self, - fuzzer: &mut Z, - executor: &mut E, - state: &mut S, - manager: &mut EM, - ) -> Result<(), libafl::Error> { - let mut first_pass = true; - for _ in 0..self.iters { - // taken from __afl_persistent_loop - if first_pass { - first_pass = false; - unsafe { - std::ptr::write_bytes(EDGES_MAP_PTR, 0, self.map_size); - std::ptr::write(EDGES_MAP_PTR, 1); - } - } else { - // Waiting for next input - if self.input_str.is_none() { - nix::sys::signal::raise(nix::sys::signal::SIGSTOP).unwrap(); - unsafe { - std::ptr::write(EDGES_MAP_PTR, 1); - } - } + let mut first_pass = true; + for execution_round in 0..iters { + if first_pass { + first_pass = false; + } else if let Some(parent_pipe_r) = &executor.uc.get_data().parent_pipe_r { + if crate::forkserver::read_u32_from_fd(parent_pipe_r).is_err() { + error!("[!] Error reading from parent pipe. Parent dead?"); } + } + unsafe { + std::ptr::write_bytes(EDGES_MAP_PTR, 0, executor.uc.get_data().map_size() as usize); + std::ptr::write_volatile(EDGES_MAP_PTR, 1); + } - // Wrap inputs - let input = if let Some(input) = self.input_str.as_ref() { - unsafe { - let fpath = - CString::new(input.to_str().ok_or(libafl::Error::invalid_corpus( - format!("invalid path {:?}", input.as_os_str()), - ))?) - .unwrap(); // to_str has checked so - let fd = open(fpath.as_ptr(), O_RDONLY); - if fd == -1 { - return Err(libafl::Error::invalid_corpus(format!( - "invalid path {:?}", - input.as_os_str() - ))); - } - - let stat = fstat(fd).map_err(|e| libafl::Error::unknown(e.to_string()))?; - let ptr = mmap64( - std::ptr::null_mut(), - stat.st_size as usize, - PROT_READ | PROT_WRITE, - MAP_PRIVATE, - fd, - 0, - ); - if ptr.is_null() { - return Err(libafl::Error::illegal_state("mmap")); - } - - UnsafeSliceInput { - input: OwnedSlice::from_raw_parts(ptr as _, stat.st_size as usize), - } - } - } else { - UnsafeSliceInput { - input: unsafe { - OwnedSlice::from_raw_parts(INPUT_PTR, (*INPUT_LENGTH_PTR) as usize) - }, - } - }; - - let (ret, _) = fuzzer.evaluate_filtered(state, executor, manager, &input)?; - if ret.is_solution() { - std::process::abort(); + let input_str; + let input = if let Some(input) = input_path.as_ref() { + input_str = std::fs::read(input)?; + input_str.as_slice() + } else { + unsafe { std::slice::from_raw_parts(INPUT_PTR, (*INPUT_LENGTH_PTR) as usize) } + }; + + let exit_kind = executor.execute_internal(input, execution_round as u64)?; + + let msg = if matches!(exit_kind, ExitKind::Ok) { + crate::forkserver::afl_child_ret::NEXT + } else { + crate::forkserver::afl_child_ret::FOUND_CRASH + }; + if let Some(child_pipe_w) = &executor.uc.get_data().child_pipe_w { + if crate::forkserver::write_u32_to_fd(child_pipe_w, msg).is_err() { + error!("[!] Error writing to parent pipe. Parent dead?"); } } - - Ok(()) } + + Ok(()) } diff --git a/src/hash.rs b/src/hash.rs index fd2c1019..bad34e64 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -1,4 +1,4 @@ -///! Adapted from original unicornafl implementation +//! Adapted from original unicornafl implementation const SECRET: [u8; 192] = [ 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c, diff --git a/src/lib.rs b/src/lib.rs index c140f0d0..814afea6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,10 +4,11 @@ use std::{ path::PathBuf, }; -use executor::UnicornFuzzData; +use executor::{UnicornAflExecutorCustomHook, UnicornAflExecutorHook, UnicornFuzzData}; use unicorn_engine::{uc_error, unicorn_const::uc_engine, Unicorn}; pub mod executor; +mod forkserver; pub mod harness; pub mod hash; pub mod target; @@ -64,12 +65,6 @@ pub type uc_afl_fuzz_cb_t = extern "C" fn(uc: *mut uc_engine, data: *mut c_void) /// Customized afl fuzz routine entrypoint for Rust user. /// -/// If you want to use default crash validation callback, pass -/// [`dummy_uc_validate_crash_callback`][target::dummy_uc_validate_crash_callback]. -/// -/// If you want to use default fuzz callback, pass -/// [`dummy_uc_fuzz_callback`][target::dummy_uc_fuzz_callback]. -/// /// `exits` means instruction addresses that stop the execution. You can pass /// an empty vec here if there is not explicit exit. /// @@ -81,10 +76,7 @@ pub type uc_afl_fuzz_cb_t = extern "C" fn(uc: *mut uc_engine, data: *mut c_void) pub fn afl_fuzz_custom<'a, D: 'a>( uc: Unicorn<'a, UnicornFuzzData>, input_file: Option, - place_input_cb: impl FnMut(&mut Unicorn<'a, UnicornFuzzData>, &[u8], u64) -> bool + 'a, - validate_crash_cb: impl FnMut(&mut Unicorn<'a, UnicornFuzzData>, Result<(), uc_error>, &[u8], u64) -> bool - + 'a, - fuzz_callback: impl FnMut(&mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> + 'a, + callbacks: impl UnicornAflExecutorHook<'a, D>, exits: Vec, always_validate: bool, persistent_iters: u32, @@ -93,9 +85,7 @@ pub fn afl_fuzz_custom<'a, D: 'a>( uc, input_file, persistent_iters, - place_input_cb, - validate_crash_cb, - fuzz_callback, + callbacks, exits, always_validate, true, @@ -116,9 +106,11 @@ pub fn afl_fuzz<'a, D: 'a>( afl_fuzz_custom( uc, input_file, - place_input_cb, - target::dummy_uc_validate_crash_callback, - target::dummy_uc_fuzz_callback, + UnicornAflExecutorCustomHook::new( + place_input_cb, + target::dummy_uc_validate_crash_callback, + target::dummy_uc_fuzz_callback, + ), exits, always_validate, persistent_iters, @@ -184,6 +176,7 @@ pub extern "C" fn uc_afl_fuzz_custom( // This is due to the fact that two closure have different types even if // their signature is the same. As a result, we must split the invocation // to avoid checking the emptyness inside every round. +#[expect(clippy::too_many_arguments)] fn uc_afl_fuzz_internal( uc_handle: *mut uc_engine, input_file: *const c_char, @@ -266,9 +259,7 @@ fn uc_afl_fuzz_internal( (Some(validate_crash_cb), Some(fuzz_cb)) => afl_fuzz_custom( uc, input_file, - place_input_cb, - validate_crash_cb, - fuzz_cb, + UnicornAflExecutorCustomHook::new(place_input_cb, validate_crash_cb, fuzz_cb), exits, always_validate, persistent_iters, @@ -276,9 +267,11 @@ fn uc_afl_fuzz_internal( (Some(validate_crash_cb), None) => afl_fuzz_custom( uc, input_file, - place_input_cb, - validate_crash_cb, - target::dummy_uc_fuzz_callback, + UnicornAflExecutorCustomHook::new( + place_input_cb, + validate_crash_cb, + target::dummy_uc_fuzz_callback, + ), exits, always_validate, persistent_iters, @@ -286,9 +279,11 @@ fn uc_afl_fuzz_internal( (None, Some(fuzz_cb)) => afl_fuzz_custom( uc, input_file, - place_input_cb, - target::dummy_uc_validate_crash_callback, - fuzz_cb, + UnicornAflExecutorCustomHook::new( + place_input_cb, + target::dummy_uc_validate_crash_callback, + fuzz_cb, + ), exits, always_validate, persistent_iters, @@ -296,9 +291,11 @@ fn uc_afl_fuzz_internal( (None, None) => afl_fuzz_custom( uc, input_file, - place_input_cb, - target::dummy_uc_validate_crash_callback, - target::dummy_uc_fuzz_callback, + UnicornAflExecutorCustomHook::new( + place_input_cb, + target::dummy_uc_validate_crash_callback, + target::dummy_uc_fuzz_callback, + ), exits, always_validate, persistent_iters, diff --git a/src/target.rs b/src/target.rs index 18607e3e..63e391ce 100644 --- a/src/target.rs +++ b/src/target.rs @@ -1,26 +1,11 @@ use std::path::PathBuf; -use libafl::{ - corpus::{Corpus, InMemoryCorpus, Testcase}, - events::SimpleEventManager, - feedbacks::{BoolValueFeedback, CrashFeedback}, - monitors::SimpleMonitor, - schedulers::QueueScheduler, - state::StdState, - Fuzzer, StdFuzzer, -}; -use libafl_bolts::{ - ownedref::OwnedSlice, - rands::StdRand, - tuples::{tuple_list, Handle}, -}; -use libafl_targets::{EDGES_MAP_SIZE, SHM_FUZZING}; -use log::{debug, trace, warn}; +use libafl_targets::{__afl_map_size, EDGES_MAP_PTR, SHM_FUZZING}; +use log::{debug, error, trace, warn}; use unicorn_engine::{uc_error, Arch, RegisterARM, Unicorn}; use crate::{ - executor::{UnicornAflExecutor, UnicornFuzzData, UnsafeSliceInput}, - harness::LegacyHarnessStage, + executor::{UnicornAflExecutor, UnicornAflExecutorHook, UnicornFuzzData}, uc_afl_ret, }; @@ -33,7 +18,7 @@ pub fn dummy_uc_fuzz_callback<'a, D: 'a>( let mut pc = uc.pc_read()?; if arch == Arch::ARM { let cpsr = uc.reg_read(RegisterARM::CPSR)?; - if cpsr & 0x20 == 1 { + if cpsr & 0x20 != 0 { pc |= 1; } } @@ -51,15 +36,14 @@ pub fn dummy_uc_validate_crash_callback<'a, D: 'a>( unicorn_result.is_err() } -/// Internal entrypoint for fuzzing +/// Internal entrypoint for fuzzing. +/// +/// This is only expected to be runned in forkserver mode. pub fn child_fuzz<'a, D: 'a>( uc: Unicorn<'a, UnicornFuzzData>, input_file: Option, iters: u32, - place_input_cb: impl FnMut(&mut Unicorn<'a, UnicornFuzzData>, &[u8], u64) -> bool + 'a, - validate_crash_cb: impl FnMut(&mut Unicorn<'a, UnicornFuzzData>, Result<(), uc_error>, &[u8], u64) -> bool - + 'a, - fuzz_callback: impl FnMut(&mut Unicorn<'a, UnicornFuzzData>) -> Result<(), uc_error> + 'a, + callbacks: impl UnicornAflExecutorHook<'a, D>, exits: Vec, always_validate: bool, run_once_if_no_afl_present: bool, @@ -68,67 +52,54 @@ pub fn child_fuzz<'a, D: 'a>( #[cfg(feature = "env_logger")] env_logger::init(); - let has_afl = libafl_targets::map_input_shared_memory() && libafl_targets::map_shared_memory(); + let has_afl = libafl_targets::map_input_shared_memory().is_ok() + && libafl_targets::map_shared_memory().is_ok(); - trace!("AFL detected: {}", has_afl); - if !input_file.is_none() && has_afl { + trace!("AFL detected: {has_afl}"); + if input_file.is_some() && has_afl { warn!("Shared memory fuzzing is enabled and the input file is ignored!"); } if input_file.is_none() && !has_afl { warn!("No input file is provided. We will run harness with zero inputs."); } + let mut local_map; + if !has_afl && run_once_if_no_afl_present { + let map_size = uc.get_data().map_size(); + // This local variable will never be freed until current function is end, which + // is after the forkserver loop. + local_map = vec![0u8; map_size as usize]; + unsafe { EDGES_MAP_PTR = local_map.as_mut_ptr() } + // If no afl, input will be read from input_file in forkserver_run_harness, + // thus no need to setup INPUT_PTR and INPUT_LENGTH_PTR + } if has_afl || run_once_if_no_afl_present { let map_size = uc.get_data().map_size(); unsafe { - EDGES_MAP_SIZE = map_size as usize; + __afl_map_size = map_size as usize; SHM_FUZZING = 1; } - libafl_targets::start_forkserver(); - // Only child returns here - let map_size = unsafe { EDGES_MAP_SIZE }; - debug!("Map size is: {}", map_size); - let mut executor = UnicornAflExecutor::new( - uc, - place_input_cb, - validate_crash_cb, - fuzz_callback, - always_validate, - exits, - )?; - - let mut fb = BoolValueFeedback::new(&Handle::new("dumb_ob".into())); - let mut sol = CrashFeedback::new(); - let mut corpus = InMemoryCorpus::new(); - corpus.add(Testcase::new(UnsafeSliceInput { - input: OwnedSlice::from(Vec::::new()), - }))?; - let mut state = StdState::new( - StdRand::new(), - corpus, - InMemoryCorpus::new(), - &mut fb, - &mut sol, - )?; + debug!("Map size is: {map_size}"); + let executor = UnicornAflExecutor::new(uc, (), callbacks, always_validate, exits)?; + let mut forkserver_parent = crate::forkserver::UnicornAflForkserverParent::new(executor); + libafl_targets::start_forkserver(&mut forkserver_parent)?; + let mut executor = forkserver_parent.executor; - let mut mgr = SimpleEventManager::new(SimpleMonitor::new(|s| { - debug!("{}", s); - })); - let sched = QueueScheduler::new(); let iters = if !has_afl && run_once_if_no_afl_present { 1 } else { iters }; let input_file = if has_afl { None } else { input_file }; - let stage = LegacyHarnessStage::new(iters as usize, map_size, input_file); - let mut stages = tuple_list!(stage); - let mut fuzzer = StdFuzzer::new(sched, fb, sol); - - if let Err(e) = fuzzer.fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr) { - warn!("Fuzzing fails with error from libafl: {}", e); + if let Err(e) = + crate::harness::forkserver_run_harness(&mut executor, input_file, iters as usize) + { + // The error cannot be propagated since we are in child process now. + // So just log. + error!("Fuzzing fails with error from libafl: {e}"); } } else { // Run with libafl directly + unimplemented!("You should use UnicornAflExecutor directly to compose your fuzzer") } Ok(()) } From 986356e59cdfebcaf660c8b2bb4a3db3c8672e1d Mon Sep 17 00:00:00 2001 From: Evian-Zhang Date: Thu, 8 May 2025 13:53:53 +0800 Subject: [PATCH 2/2] Remove unimplemented macro --- src/target.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/target.rs b/src/target.rs index 63e391ce..a8c3a322 100644 --- a/src/target.rs +++ b/src/target.rs @@ -98,8 +98,7 @@ pub fn child_fuzz<'a, D: 'a>( error!("Fuzzing fails with error from libafl: {e}"); } } else { - // Run with libafl directly - unimplemented!("You should use UnicornAflExecutor directly to compose your fuzzer") + // TODO: Run with libafl directly } Ok(()) }