From 36734083f93d0cd58972fffda4e63133ca35fed7 Mon Sep 17 00:00:00 2001 From: "Marco C." <46560192+Marcondiro@users.noreply.github.com> Date: Tue, 3 Dec 2024 08:43:17 +0100 Subject: [PATCH] Intel PT minor fixes/improvements (#2724) * waitpid_filtered to ignore SIGWINCH * Fix warnings unused manifest key: *.version * Add export_raw feature to libafl_intelpt * derive Debug for IntelPTHook * Clippy * Update target program ELF offsets * Add comment to KVM pt_mode check * refactor * Add intel_pt_export_raw feature in libafl * map_error instead of unwrap * borrow checker friendly join_split_trace and copy trace before deocde to prevent decoding failures * Set ip_filters (also) with builder * Move trace to file * Fix Cargo.toml docs * Ignore blocks with no instruction most likely they are filtered out --- .../intel_pt_command_executor/src/main.rs | 15 ++-- libafl/Cargo.toml | 2 + libafl/src/executors/command.rs | 73 ++++++++++------ libafl/src/executors/hooks/intel_pt.rs | 25 +++--- libafl_concolic/symcc_runtime/Cargo.toml | 2 +- libafl_intelpt/Cargo.toml | 2 + libafl_intelpt/src/lib.rs | 85 ++++++++++++++----- libafl_qemu/Cargo.toml | 4 +- 8 files changed, 140 insertions(+), 68 deletions(-) diff --git a/fuzzers/binary_only/intel_pt_command_executor/src/main.rs b/fuzzers/binary_only/intel_pt_command_executor/src/main.rs index e8c977a775..f91ed0644d 100644 --- a/fuzzers/binary_only/intel_pt_command_executor/src/main.rs +++ b/fuzzers/binary_only/intel_pt_command_executor/src/main.rs @@ -84,24 +84,25 @@ pub fn main() { // A fuzzer with feedbacks and a corpus scheduler let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); - let mut intel_pt = IntelPT::builder().cpu(cpu.0).inherit(true).build().unwrap(); - // The target is a ET_DYN elf, it will be relocated by the loader with this offset. // see https://github.com/torvalds/linux/blob/c1e939a21eb111a6d6067b38e8e04b8809b64c4e/arch/x86/include/asm/elf.h#L234C1-L239C38 const DEFAULT_MAP_WINDOW: usize = (1 << 47) - PAGE_SIZE; - const ELF_ET_DYN_BASE: usize = DEFAULT_MAP_WINDOW / 3 * 2 & !(PAGE_SIZE - 1); + const ELF_ET_DYN_BASE: usize = (DEFAULT_MAP_WINDOW / 3 * 2) & !(PAGE_SIZE - 1); // Set the instruction pointer (IP) filter and memory image of our target. // These information can be retrieved from `readelf -l` (for example) - let code_memory_addresses = ELF_ET_DYN_BASE + 0x14000..=ELF_ET_DYN_BASE + 0x14000 + 0x40000; + let code_memory_addresses = ELF_ET_DYN_BASE + 0x15000..=ELF_ET_DYN_BASE + 0x14000 + 0x41000; - intel_pt - .set_ip_filters(&[code_memory_addresses.clone()]) + let intel_pt = IntelPT::builder() + .cpu(cpu.0) + .inherit(true) + .ip_filters(&[code_memory_addresses.clone()]) + .build() .unwrap(); let sections = [Section { file_path: target_path.to_string_lossy().to_string(), - file_offset: 0x13000, + file_offset: 0x14000, size: (*code_memory_addresses.end() - *code_memory_addresses.start() + 1) as u64, virtual_address: *code_memory_addresses.start() as u64, }]; diff --git a/libafl/Cargo.toml b/libafl/Cargo.toml index d7c1823a43..a7da17c57e 100644 --- a/libafl/Cargo.toml +++ b/libafl/Cargo.toml @@ -115,6 +115,8 @@ intel_pt = [ "dep:nix", "dep:num_enum", ] +## Save all the Intel PT raw traces to files, use only for debug +intel_pt_export_raw = ["intel_pt", "libafl_intelpt/export_raw"] ## Enables features for corpus minimization cmin = ["z3"] diff --git a/libafl/src/executors/command.rs b/libafl/src/executors/command.rs index 946c45a695..7bf6db343c 100644 --- a/libafl/src/executors/command.rs +++ b/libafl/src/executors/command.rs @@ -29,7 +29,19 @@ use libafl_bolts::{ #[cfg(target_os = "linux")] use libc::STDIN_FILENO; #[cfg(target_os = "linux")] -use nix::unistd::Pid; +use nix::{ + errno::Errno, + sys::{ + ptrace, + signal::Signal, + wait::WaitStatus, + wait::{ + waitpid, WaitPidFlag, + WaitStatus::{Exited, PtraceEvent, Signaled, Stopped}, + }, + }, + unistd::Pid, +}; #[cfg(target_os = "linux")] use typed_builder::TypedBuilder; @@ -204,8 +216,6 @@ where match unsafe { fork() } { Ok(ForkResult::Parent { child }) => Ok(child), Ok(ForkResult::Child) => { - ptrace::traceme().unwrap(); - if let Some(c) = self.cpu { c.set_affinity_forced().unwrap(); } @@ -241,6 +251,7 @@ where } } + ptrace::traceme().unwrap(); // After this STOP, the process is traced with PTrace (no hooks yet) raise(Signal::SIGSTOP).unwrap(); @@ -405,13 +416,13 @@ where T: CommandConfigurator<::Input>, { #[inline] - fn set_timeout(&mut self, timeout: Duration) { - *self.configurer.exec_timeout_mut() = timeout; + fn timeout(&self) -> Duration { + self.configurer.exec_timeout() } #[inline] - fn timeout(&self) -> Duration { - self.configurer.exec_timeout() + fn set_timeout(&mut self, timeout: Duration) { + *self.configurer.exec_timeout_mut() = timeout; } } @@ -437,32 +448,28 @@ where _mgr: &mut EM, input: &Self::Input, ) -> Result { - use nix::sys::{ - ptrace, - signal::Signal, - wait::{ - waitpid, WaitPidFlag, - WaitStatus::{Exited, PtraceEvent, Signaled, Stopped}, - }, - }; - *state.executions_mut() += 1; let child = self.configurer.spawn_child(input)?; - let wait_status = waitpid(child, Some(WaitPidFlag::WUNTRACED))?; + let wait_status = waitpid_filtered(child, Some(WaitPidFlag::WUNTRACED))?; if !matches!(wait_status, Stopped(c, Signal::SIGSTOP) if c == child) { - return Err(Error::unknown("Unexpected state of child process")); + return Err(Error::unknown(format!( + "Unexpected state of child process {wait_status:?} (while waiting for SIGSTOP)" + ))); } - ptrace::setoptions(child, ptrace::Options::PTRACE_O_TRACEEXEC)?; + let options = ptrace::Options::PTRACE_O_TRACEEXEC | ptrace::Options::PTRACE_O_EXITKILL; + ptrace::setoptions(child, options)?; ptrace::cont(child, None)?; - let wait_status = waitpid(child, None)?; + let wait_status = waitpid_filtered(child, None)?; if !matches!(wait_status, PtraceEvent(c, Signal::SIGTRAP, e) if c == child && e == (ptrace::Event::PTRACE_EVENT_EXEC as i32) ) { - return Err(Error::unknown("Unexpected state of child process")); + return Err(Error::unknown(format!( + "Unexpected state of child process {wait_status:?} (while waiting for SIGTRAP PTRACE_EVENT_EXEC)" + ))); } self.observers.pre_exec_child_all(state, input)?; @@ -471,6 +478,8 @@ where } self.hooks.pre_exec_all(state, input); + // todo: it might be better to keep the target ptraced in case the target handles sigalarm, + // breaking the libafl timeout ptrace::detach(child, None)?; let res = match waitpid(child, None)? { Exited(pid, 0) if pid == child => ExitKind::Ok, @@ -478,9 +487,9 @@ where Signaled(pid, Signal::SIGALRM, _has_coredump) if pid == child => ExitKind::Timeout, Signaled(pid, Signal::SIGABRT, _has_coredump) if pid == child => ExitKind::Crash, Signaled(pid, Signal::SIGKILL, _has_coredump) if pid == child => ExitKind::Oom, - Stopped(pid, Signal::SIGALRM) if pid == child => ExitKind::Timeout, - Stopped(pid, Signal::SIGABRT) if pid == child => ExitKind::Crash, - Stopped(pid, Signal::SIGKILL) if pid == child => ExitKind::Oom, + // Stopped(pid, Signal::SIGALRM) if pid == child => ExitKind::Timeout, + // Stopped(pid, Signal::SIGABRT) if pid == child => ExitKind::Crash, + // Stopped(pid, Signal::SIGKILL) if pid == child => ExitKind::Oom, s => { // TODO other cases? return Err(Error::unsupported( @@ -855,6 +864,22 @@ pub trait CommandConfigurator: Sized { } } +/// waitpid wrapper that ignores some signals sent by the ptraced child +#[cfg(all(feature = "std", target_os = "linux"))] +fn waitpid_filtered(pid: Pid, options: Option) -> Result { + loop { + let wait_status = waitpid(pid, options); + let sig = match &wait_status { + // IGNORED + Ok(Stopped(c, Signal::SIGWINCH)) if *c == pid => Signal::SIGWINCH, + // RETURNED + Ok(ws) => break Ok(*ws), + Err(e) => break Err(*e), + }; + ptrace::cont(pid, sig)?; + } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/libafl/src/executors/hooks/intel_pt.rs b/libafl/src/executors/hooks/intel_pt.rs index f4acce679e..a712972e24 100644 --- a/libafl/src/executors/hooks/intel_pt.rs +++ b/libafl/src/executors/hooks/intel_pt.rs @@ -30,7 +30,7 @@ pub struct Section { } /// Hook to enable Intel Processor Trace (PT) tracing -#[derive(TypedBuilder)] +#[derive(Debug, TypedBuilder)] pub struct IntelPTHook { #[builder(default = IntelPT::builder().build().unwrap())] intel_pt: IntelPT, @@ -40,17 +40,6 @@ pub struct IntelPTHook { map_len: usize, } -//fixme: just derive(Debug) once https://github.com/sum-catnip/libipt-rs/pull/4 will be on crates.io -impl Debug for IntelPTHook { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - f.debug_struct("IntelPTHook") - .field("intel_pt", &self.intel_pt) - .field("map_ptr", &self.map_ptr) - .field("map_len", &self.map_len) - .finish() - } -} - impl ExecutorHook for IntelPTHook where S: UsesInput + Serialize, @@ -63,13 +52,19 @@ where } fn post_exec(&mut self, _state: &mut S, _input: &S::Input) { - self.intel_pt.disable_tracing().unwrap(); + let pt = &mut self.intel_pt; + pt.disable_tracing().unwrap(); let slice = unsafe { &mut *slice_from_raw_parts_mut(self.map_ptr, self.map_len) }; - let _ = self - .intel_pt + let _ = pt .decode_traces_into_map(&mut self.image.0, slice) .inspect_err(|e| log::warn!("Intel PT trace decoding failed: {e}")); + #[cfg(feature = "intel_pt_export_raw")] + { + let _ = pt + .dump_last_trace_to_file() + .inspect_err(|e| log::warn!("Intel PT trace save to file failed: {e}")); + } } } diff --git a/libafl_concolic/symcc_runtime/Cargo.toml b/libafl_concolic/symcc_runtime/Cargo.toml index 8cccd60633..a0f91e025e 100644 --- a/libafl_concolic/symcc_runtime/Cargo.toml +++ b/libafl_concolic/symcc_runtime/Cargo.toml @@ -44,7 +44,7 @@ cmake = { workspace = true } bindgen = { workspace = true } regex = { workspace = true } which = { workspace = true } -symcc_libafl = { workspace = true, default-features = true, version = "0.14.1" } +symcc_libafl = { workspace = true, default-features = true } [lints] workspace = true diff --git a/libafl_intelpt/Cargo.toml b/libafl_intelpt/Cargo.toml index 200882507f..10d29ea704 100644 --- a/libafl_intelpt/Cargo.toml +++ b/libafl_intelpt/Cargo.toml @@ -15,6 +15,8 @@ default = ["std", "libipt"] std = ["libafl_bolts/std"] libipt = ["std", "dep:libipt"] +## Export raw Intel PT traces on decode, useful for debug, disabled by default for best performance +export_raw = [] [dev-dependencies] static_assertions = { workspace = true } diff --git a/libafl_intelpt/src/lib.rs b/libafl_intelpt/src/lib.rs index c6a0a140c9..920e7aeb5c 100644 --- a/libafl_intelpt/src/lib.rs +++ b/libafl_intelpt/src/lib.rs @@ -19,6 +19,7 @@ use std::{ }; #[cfg(target_os = "linux")] use std::{ + boxed::Box, ffi::{CStr, CString}, fmt::Debug, format, fs, @@ -129,6 +130,8 @@ pub struct IntelPT { aux_tail: *mut u64, previous_decode_head: u64, ip_filters: Vec>, + #[cfg(feature = "export_raw")] + last_decode_trace: Vec, } #[cfg(target_os = "linux")] @@ -302,8 +305,12 @@ impl IntelPT { } } else { // Head pointer wrapped, the trace is split - unsafe { self.join_split_trace(head_wrap, tail_wrap) } + OwnedRefMut::Owned(self.join_split_trace(head_wrap, tail_wrap)) }; + #[cfg(feature = "export_raw")] + { + self.last_decode_trace = data.as_ref().to_vec(); + } let mut config = ConfigBuilder::new(data.as_mut()).map_err(error_from_pt_error)?; config.filter(self.ip_filters_to_addr_filter()); @@ -351,19 +358,17 @@ impl IntelPT { #[inline] #[must_use] - unsafe fn join_split_trace(&self, head_wrap: u64, tail_wrap: u64) -> OwnedRefMut<[u8]> { - let first_ptr = self.perf_aux_buffer.add(tail_wrap as usize) as *mut u8; + fn join_split_trace(&self, head_wrap: u64, tail_wrap: u64) -> Box<[u8]> { + let first_ptr = unsafe { self.perf_aux_buffer.add(tail_wrap as usize) as *mut u8 }; let first_len = self.perf_aux_buffer_size - tail_wrap as usize; + let second_ptr = self.perf_aux_buffer as *mut u8; let second_len = head_wrap as usize; - OwnedRefMut::Owned( - [ - slice::from_raw_parts(first_ptr, first_len), - slice::from_raw_parts(second_ptr, second_len), - ] - .concat() - .into_boxed_slice(), - ) + + let mut vec = Vec::with_capacity(first_len + second_len); + vec.extend_from_slice(unsafe { slice::from_raw_parts(first_ptr, first_len) }); + vec.extend_from_slice(unsafe { slice::from_raw_parts(second_ptr, second_len) }); + vec.into_boxed_slice() } #[inline] @@ -395,7 +400,7 @@ impl IntelPT { *status = s; let offset = decoder.offset().map_err(error_from_pt_error)?; - if !b.speculative() && skip < offset { + if b.ninsn() > 0 && !b.speculative() && skip < offset { let id = hash_me(*previous_block_end_ip) ^ hash_me(b.ip()); // SAFETY: the index is < map.len() since the modulo operation is applied let map_loc = unsafe { map.get_unchecked_mut(id as usize % map.len()) }; @@ -416,6 +421,29 @@ impl IntelPT { } Ok(()) } + + /// Get the raw trace used in the last decoding + #[cfg(feature = "export_raw")] + pub fn last_decode_trace(&self) -> Vec { + self.last_decode_trace.clone() + } + + /// Dump the raw trace used in the last decoding to the file + /// /// `./traces/trace_` + #[cfg(feature = "export_raw")] + pub fn dump_last_trace_to_file(&self) -> Result<(), Error> { + use std::{fs, io::Write, path::Path, time}; + + let traces_dir = Path::new("traces"); + fs::create_dir_all(traces_dir)?; + let timestamp = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .map_err(|e| Error::unknown(e.to_string()))? + .as_micros(); + let mut file = fs::File::create(traces_dir.join(format!("trace_{timestamp}")))?; + file.write_all(&self.last_decode_trace())?; + Ok(()) + } } #[cfg(target_os = "linux")] @@ -441,6 +469,7 @@ pub struct IntelPTBuilder { inherit: bool, perf_buffer_size: usize, perf_aux_buffer_size: usize, + ip_filters: Vec>, } #[cfg(target_os = "linux")] @@ -450,14 +479,15 @@ impl Default for IntelPTBuilder { /// The default configuration corresponds to: /// ```rust /// use libafl_intelpt::{IntelPTBuilder, PAGE_SIZE}; - /// let builder = unsafe { std::mem::zeroed::() } + /// let builder = IntelPTBuilder::default() /// .pid(None) /// .all_cpus() /// .exclude_kernel(true) /// .exclude_hv(true) /// .inherit(false) /// .perf_buffer_size(128 * PAGE_SIZE + PAGE_SIZE).unwrap() - /// .perf_aux_buffer_size(2 * 1024 * 1024).unwrap(); + /// .perf_aux_buffer_size(2 * 1024 * 1024).unwrap() + /// .ip_filters(&[]); /// assert_eq!(builder, IntelPTBuilder::default()); /// ``` fn default() -> Self { @@ -469,6 +499,7 @@ impl Default for IntelPTBuilder { inherit: false, perf_buffer_size: 128 * PAGE_SIZE + PAGE_SIZE, perf_aux_buffer_size: 2 * 1024 * 1024, + ip_filters: Vec::new(), } } } @@ -532,9 +563,7 @@ impl IntelPTBuilder { let aux_head = unsafe { &raw mut (*buff_metadata).aux_head }; let aux_tail = unsafe { &raw mut (*buff_metadata).aux_tail }; - let ip_filters = Vec::with_capacity(*NR_ADDR_FILTERS.as_ref().unwrap_or(&0) as usize); - - Ok(IntelPT { + let mut intel_pt = IntelPT { fd, perf_buffer, perf_aux_buffer, @@ -543,8 +572,14 @@ impl IntelPTBuilder { aux_head, aux_tail, previous_decode_head: 0, - ip_filters, - }) + ip_filters: Vec::with_capacity(*NR_ADDR_FILTERS.as_ref().unwrap_or(&0) as usize), + #[cfg(feature = "export_raw")] + last_decode_trace: Vec::new(), + }; + if !self.ip_filters.is_empty() { + intel_pt.set_ip_filters(&self.ip_filters)?; + } + Ok(intel_pt) } /// Warn if the configuration is not recommended @@ -638,6 +673,15 @@ impl IntelPTBuilder { self.perf_aux_buffer_size = perf_aux_buffer_size; Ok(self) } + + #[must_use] + /// Set filters based on Instruction Pointer (IP) + /// + /// Only instructions in `filters` ranges will be traced. + pub fn ip_filters(mut self, filters: &[RangeInclusive]) -> Self { + self.ip_filters = filters.to_vec(); + self + } } /// Perf event config for `IntelPT` @@ -717,6 +761,9 @@ pub fn availability_in_qemu_kvm() -> Result<(), String> { #[cfg(target_os = "linux")] { let kvm_pt_mode_path = "/sys/module/kvm_intel/parameters/pt_mode"; + // Ignore the case when the file does not exist since it has been removed. + // KVM default is `System` mode + // https://lore.kernel.org/all/20241101185031.1799556-1-seanjc@google.com/t/#u if let Ok(s) = fs::read_to_string(kvm_pt_mode_path) { match s.trim().parse::().map(TryInto::try_into) { Ok(Ok(KvmPTMode::System)) => (), diff --git a/libafl_qemu/Cargo.toml b/libafl_qemu/Cargo.toml index eab48f1a79..d107dc4d0b 100644 --- a/libafl_qemu/Cargo.toml +++ b/libafl_qemu/Cargo.toml @@ -90,7 +90,7 @@ clippy = ["libafl_qemu_sys/clippy"] [dependencies] libafl = { workspace = true, features = ["std", "derive", "regex"] } libafl_bolts = { workspace = true, features = ["std", "derive"] } -libafl_targets = { workspace = true, default-features = true, version = "0.14.1" } +libafl_targets = { workspace = true, default-features = true } libafl_qemu_sys = { workspace = true } libafl_derive = { workspace = true, default-features = true } @@ -129,7 +129,7 @@ getset = "0.1.3" document-features = { workspace = true, optional = true } [build-dependencies] -libafl_qemu_build = { workspace = true, default-features = true, version = "0.14.1" } +libafl_qemu_build = { workspace = true, default-features = true } pyo3-build-config = { workspace = true, optional = true } rustversion = { workspace = true } bindgen = { workspace = true }