diff --git a/crates/libafl/src/executors/hooks/mod.rs b/crates/libafl/src/executors/hooks/mod.rs index 8b4cd9cfd9..9430516f47 100644 --- a/crates/libafl/src/executors/hooks/mod.rs +++ b/crates/libafl/src/executors/hooks/mod.rs @@ -21,7 +21,11 @@ pub mod inprocess; pub mod timer; /// Intel Processor Trace (PT) -#[cfg(all(feature = "intel_pt", target_os = "linux", target_arch = "x86_64"))] +#[cfg(all( + feature = "intel_pt", + any(windows, target_os = "linux"), + target_arch = "x86_64" +))] pub mod intel_pt; /// The hook that runs before and after the executor runs the target diff --git a/crates/libafl_intelpt/Cargo.toml b/crates/libafl_intelpt/Cargo.toml index 2d415477ba..497dfa92f4 100644 --- a/crates/libafl_intelpt/Cargo.toml +++ b/crates/libafl_intelpt/Cargo.toml @@ -19,10 +19,11 @@ export_raw = [] [dev-dependencies] static_assertions = { workspace = true } +proc-maps = "0.4.0" +env_logger = "0.11.10" [target.'cfg(target_os = "linux" )'.dev-dependencies] nix = { workspace = true } -proc-maps = "0.4.0" [dependencies] arbitrary-int = { workspace = true } @@ -32,7 +33,7 @@ libc = { workspace = true } log = { workspace = true } num_enum = { workspace = true, default-features = false } num-traits = { workspace = true, default-features = false } -ptcov = { version = "0.1.0" } +ptcov = { git = "https://github.com/Marcondiro/ptcov", branch = "fix_pgd" } raw-cpuid = { version = "11.1.0" } [target.'cfg(target_os = "linux" )'.dependencies] @@ -40,7 +41,14 @@ caps = { version = "0.5.5" } perf-event-open-sys = { version = "5.0.0" } [target.'cfg(target_os = "windows" )'.dependencies] -ptcov = { version = "0.1.0", features = ["retc"] } +hashbrown = { workspace = true } +ptcov = { features = [ + "retc", +], git = "https://github.com/Marcondiro/ptcov", branch = "fix_pgd" } +windows = { workspace = true, features = [ + "Win32_System_IO", + "Win32_Storage_FileSystem", +] } [lints] workspace = true diff --git a/crates/libafl_intelpt/src/lib.rs b/crates/libafl_intelpt/src/lib.rs index f587aa2e95..c3034551f7 100644 --- a/crates/libafl_intelpt/src/lib.rs +++ b/crates/libafl_intelpt/src/lib.rs @@ -24,6 +24,14 @@ mod linux; #[cfg(target_os = "linux")] pub use linux::*; +#[cfg(target_os = "windows")] +mod windows; +#[cfg(target_os = "windows")] +pub use windows::*; + +#[cfg(any(target_os = "linux", target_os = "windows"))] +mod utils; + /// Size of a memory page pub const PAGE_SIZE: usize = 4096; @@ -41,6 +49,7 @@ pub fn availability() -> Result<(), String> { let mut reasons = Vec::new(); let cpuid = CpuId::new(); + if let Some(vendor) = cpuid.get_vendor_info() { if vendor.as_str() != "GenuineIntel" && vendor.as_str() != "GenuineIotel" { reasons.push("Only Intel CPUs are supported".to_owned()); @@ -57,11 +66,26 @@ pub fn availability() -> Result<(), String> { reasons.push("Failed to read CPU Extended Features".to_owned()); } + if let Some(pti) = cpuid.get_processor_trace_info() { + if pti.configurable_address_ranges() == 0 { + reasons.push( + "The current CPU does not support Intel PT filtering: no address filters available" + .to_owned(), + ); + } + } else { + reasons.push("Failed to read CPU Processor Trace Info".to_owned()); + } + #[cfg(target_os = "linux")] if let Err(r) = availability_in_linux() { reasons.push(r); } - #[cfg(not(target_os = "linux"))] + #[cfg(target_os = "windows")] + if let Err(r) = availability_in_windows() { + reasons.push(r); + } + #[cfg(not(any(target_os = "linux", target_os = "windows")))] reasons.push("Only linux hosts are supported at the moment".to_owned()); if reasons.is_empty() { diff --git a/crates/libafl_intelpt/src/linux.rs b/crates/libafl_intelpt/src/linux.rs index b71dd55414..cac8ecee0f 100644 --- a/crates/libafl_intelpt/src/linux.rs +++ b/crates/libafl_intelpt/src/linux.rs @@ -32,10 +32,9 @@ use perf_event_open_sys::{ perf_event_open, }; pub use ptcov::{CoverageEntry, PtCoverageDecoder, PtCoverageDecoderBuilder, PtImage}; -use ptcov::{PtCpu, PtCpuVendor}; -use raw_cpuid::CpuId; use super::{PAGE_SIZE, availability}; +use crate::utils::current_cpu; const PT_EVENT_PATH: &str = "/sys/bus/event_source/devices/intel_pt"; @@ -107,7 +106,6 @@ impl<'a> IntelPT<'a> { /// Set filters based on Instruction Pointer (IP) /// /// Only instructions in `filters` ranges will be traced. - /// NOTE: only filters of type `AddrFilterType::FILTER` are supported. fn set_ip_filters(&mut self, filters: &[RangeInclusive]) -> Result<(), Error> { let str_filter = filters .iter() @@ -296,7 +294,7 @@ impl<'a> IntelPT<'a> { } /// Dump the raw trace used in the last decoding to the file - /// /// `./traces/trace_` + /// `./traces/trace_` #[cfg(feature = "export_raw")] pub fn dump_last_trace_to_file(&self) -> Result<(), Error> { use std::{fs, io::Write, path::Path, time}; @@ -761,18 +759,6 @@ const fn wrap_aux_pointer(ptr: u64, perf_aux_buffer_size: usize) -> u64 { ptr & (perf_aux_buffer_size as u64 - 1) } -fn current_cpu() -> Option { - let cpuid = CpuId::new(); - cpuid.get_feature_info().map(|fi| { - PtCpu::new( - PtCpuVendor::Intel, - fi.family_id().into(), - fi.model_id(), - fi.stepping_id(), - ) - }) -} - #[cfg(test)] mod test { use std::path::PathBuf; diff --git a/crates/libafl_intelpt/src/utils.rs b/crates/libafl_intelpt/src/utils.rs new file mode 100644 index 0000000000..affccd546c --- /dev/null +++ b/crates/libafl_intelpt/src/utils.rs @@ -0,0 +1,14 @@ +use ptcov::{PtCpu, PtCpuVendor}; +use raw_cpuid::CpuId; + +pub fn current_cpu() -> Option { + let cpuid = CpuId::new(); + cpuid.get_feature_info().map(|fi| { + PtCpu::new( + PtCpuVendor::Intel, + fi.family_id().into(), + fi.model_id(), + fi.stepping_id(), + ) + }) +} diff --git a/crates/libafl_intelpt/src/windows/ipt.rs b/crates/libafl_intelpt/src/windows/ipt.rs new file mode 100644 index 0000000000..66d86f5d6e --- /dev/null +++ b/crates/libafl_intelpt/src/windows/ipt.rs @@ -0,0 +1,317 @@ +/// ipt.sys struct definitions, conversion fns and constants +use core::ops::RangeInclusive; + +use arbitrary_int::{u3, u4}; +use bitbybit::bitfield; +use windows::Win32::Foundation::HANDLE; + +pub const TRACE_VERSION: u16 = 1; + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct BufferVersion { + major: u32, + minor: u32, +} +const BUFFER_VERSION: BufferVersion = BufferVersion { major: 1, minor: 0 }; + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct ConfigureThreadAddressFilterRange { + thread_handle: HANDLE, + range_index: u32, + range_config: FilterRangeSettings, + start_address: u64, + end_address: u64, +} + +impl ConfigureThreadAddressFilterRange { + pub const fn new( + thread_handle: HANDLE, + range_index: u32, + filter: &RangeInclusive, + ) -> Self { + Self { + thread_handle, + range_index, + range_config: FilterRangeSettings::Ip, + start_address: *filter.start(), + end_address: *filter.end(), + } + } +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +#[expect(dead_code, reason = "Not all the config are yet used/supported")] +enum FilterRangeSettings { + Disable = 0, + Ip = 1, + TraceStop = 2, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct GetProcessTrace { + trace_version: u16, + _padding: [u8; 6], + process_handle: u64, +} + +impl GetProcessTrace { + fn new(process_handle: HANDLE) -> Self { + Self { + trace_version: TRACE_VERSION, + _padding: [0u8; 6], + process_handle: process_handle.0 as u64, + } + } +} + +#[repr(C)] +pub struct InputBuffer { + version: BufferVersion, + input_type: InputType, + _padding: u32, + payload: InputPayload, +} + +impl InputBuffer { + const fn new(input_type: InputType, payload: InputPayload) -> Self { + InputBuffer { + version: BUFFER_VERSION, + input_type, + _padding: 0, + payload, + } + } + + pub fn get_process_trace(process_handle: HANDLE) -> Self { + Self::new( + InputType::GetProcessTrace, + GetProcessTrace::new(process_handle).into(), + ) + } + + pub fn get_process_trace_size(process_handle: HANDLE) -> Self { + Self::new( + InputType::GetProcessTraceSize, + GetProcessTrace::new(process_handle).into(), + ) + } + + pub fn pause_thread_trace(thread_handle: HANDLE) -> Self { + Self::new( + InputType::PauseThreadTrace, + PauseResumeThreadTrace::new(thread_handle).into(), + ) + } + + pub fn resume_thread_trace(thread_handle: HANDLE) -> Self { + Self::new( + InputType::ResumeThreadTrace, + PauseResumeThreadTrace::new(thread_handle).into(), + ) + } + + pub fn set_thread_ip_filter( + thread_handle: HANDLE, + range_index: u32, + filter: &RangeInclusive, + ) -> Self { + Self::new( + InputType::ConfigureThreadAddressFilterRange, + ConfigureThreadAddressFilterRange::new(thread_handle, range_index, filter).into(), + ) + } + + pub fn start_process_trace(process_handle: HANDLE, options: Options) -> Self { + Self::new( + InputType::StartProcessTrace, + StartProcessTrace { + process_handle: process_handle.0 as u64, + options, + } + .into(), + ) + } +} + +#[repr(C)] +union InputPayload { + get_trace: GetProcessTrace, + start_trace: StartProcessTrace, + stop_trace: StopProcessTrace, + configure_thread_address_filter_range: ConfigureThreadAddressFilterRange, + // query_filter: QueryThreadFilter, + pause_resume_thread: PauseResumeThreadTrace, + _pad: [u8; 32], +} + +impl From for InputPayload { + fn from(value: ConfigureThreadAddressFilterRange) -> Self { + Self { + configure_thread_address_filter_range: value, + } + } +} + +impl From for InputPayload { + fn from(value: StartProcessTrace) -> Self { + Self { start_trace: value } + } +} + +impl From for InputPayload { + fn from(value: GetProcessTrace) -> Self { + Self { get_trace: value } + } +} + +impl From for InputPayload { + fn from(value: PauseResumeThreadTrace) -> Self { + Self { + pause_resume_thread: value, + } + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(u32)] +#[expect(dead_code, reason = "Not all the commands are used")] +enum InputType { + GetTraceVersion = 0, + GetProcessTraceSize, + GetProcessTrace, + StartCoreTracing, + RegisterExtendedImageForTracing, + StartProcessTrace, + StopProcessTrace, + PauseThreadTrace, + ResumeThreadTrace, + QueryProcessTrace, + QueryCoreTrace, + StopTraceOnEachCore = 12, + ConfigureThreadAddressFilterRange, + QueryThreadAddressFilterRange, + QueryThreadTraceStopRangeEntered, +} + +#[derive(Debug)] +#[repr(u32)] +pub enum Ioctl { + Request = 0x220004, + ReadTrace = 0x220006, +} + +#[bitfield(u64)] +#[derive(Debug)] +pub struct Options { + #[bits(0..=3, rw)] + option_version: u4, + #[bits(4..=7, rw)] + timing_settings: u4, + /// As in `IA32_RTIT_CTL` + #[bits(8..=11, rw)] + mtc_frequency: u4, + #[bits(12..=15, rw)] + cyc_threshold: u4, + #[bits(16..=19, rw)] + topa_pages_pow2: u4, + /// Not relevant when tracing by process handle + #[bits(20..=22, rw)] + match_settings: u3, + #[bits(23..=23, rw)] + inherit: bool, + #[bits(24..=27, rw)] + mode_settings: u4, + #[bits(28..=63)] + reserved: u36, +} + +impl Options { + const VERSION: u4 = u4::new(1); +} + +impl Default for Options { + fn default() -> Self { + Self::builder() + .with_option_version(Self::VERSION) + .with_topa_pages_pow2(u4::new(5)) // 128 kB + .with_inherit(false) + .with_mode_settings(u4::new(0)) // todo: better understand IPT_MODE_SETTINGS difference between Ctl and Reg + .value + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct OutGetTraceSize { + trace_version: u16, + _padding: [u8; 6], + pub trace_size: u64, +} + +#[repr(C)] +pub union OutputBuffer { + // get_trace_version: OutGetTraceVersion, + pub get_trace_size: OutGetTraceSize, + // query_filter: OutQueryThreadFilter, + // pause_trace: OutPauseResumeTrace, + // resume_trace: OutPauseResumeTrace, + _pad: [u8; 24], +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct PauseResumeThreadTrace { + thread_handle: HANDLE, +} + +impl PauseResumeThreadTrace { + const fn new(thread_handle: HANDLE) -> Self { + PauseResumeThreadTrace { thread_handle } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct StartProcessTrace { + process_handle: u64, + options: Options, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct StopProcessTrace { + process_handle: u64, +} + +#[repr(C)] +#[derive(Debug)] +pub struct TraceData { + pub trace_version: u16, + pub valid_trace: u16, + pub trace_size: u32, +} + +#[repr(C, packed(4))] +#[derive(Debug)] +pub struct TraceHeader { + pub thread_id: u64, + timing_settings: u32, + mtc_frequency: u32, + frequency_to_tsc_ratio: u32, + pub ring_buffer_offset: u32, + pub trace_size: u32, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ipt_input_buffer_size() { + assert_eq!(size_of::(), 0x30); + } +} diff --git a/crates/libafl_intelpt/src/windows/mod.rs b/crates/libafl_intelpt/src/windows/mod.rs new file mode 100644 index 0000000000..b3ed65baf7 --- /dev/null +++ b/crates/libafl_intelpt/src/windows/mod.rs @@ -0,0 +1,413 @@ +/// Intel Processor Trace code using the `ipt.sys` Windows driver. +/// +/// This code is heavily inspired by `winipt` by Alex Ionescu and its other authors. +/// Credits also go to Frederic Kah and Justin Avril, who drafted the initial implementation. +mod ipt; + +use alloc::{string::String, vec::Vec}; +use core::{fmt::Debug, mem::MaybeUninit, ops::RangeInclusive, ptr::slice_from_raw_parts_mut}; +#[cfg(feature = "export_raw")] +use std::string::ToString; + +use hashbrown::HashMap; +use libafl_bolts::Error; +use ptcov::PtCoverageDecoderBuilder; +pub use ptcov::{CoverageEntry, PtCoverageDecoder, PtImage}; +use raw_cpuid::CpuId; +use windows::{ + Win32::{ + Foundation::HANDLE, + Storage::FileSystem::{ + CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_FLAG_NO_BUFFERING, FILE_FLAG_SEQUENTIAL_SCAN, + FILE_GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING, + }, + System::{ + IO::DeviceIoControl, + Threading::{ + GetCurrentProcessId, OpenProcess, OpenThread, PROCESS_QUERY_INFORMATION, + PROCESS_VM_READ, THREAD_GET_CONTEXT, + }, + }, + }, + core::{Owned, w}, +}; + +use crate::utils::current_cpu; + +#[derive(Debug)] +struct ThreadCoverageDecoder<'a> { + decoder: PtCoverageDecoder<'a>, + previous_decode_head: u32, +} + +/// Intel Processor Trace (PT) +#[derive(Debug)] +pub struct IntelPT<'a> { + ipt_handle: Owned, + target_process_handle: Owned, + thread_id: Option, + ptcov_decoders: HashMap>, + images: &'a [PtImage<'a>], + last_decode_threads: Vec, + #[cfg(feature = "export_raw")] + last_decode_trace: Vec, +} + +impl<'a> IntelPT<'a> { + /// Create a default builder + /// + /// Checkout [`IntelPTBuilder::default()`] for more details + #[must_use] + pub fn builder() -> IntelPTBuilder<'a> { + IntelPTBuilder::default() + } + + pub fn set_thread_id(&mut self, thread_id: Option) { + self.thread_id = thread_id; + } + + /// Set filters based on Instruction Pointer (IP) + /// + /// Only instructions in `filters` ranges will be traced. + /// `thread_id` must be set in order to set the filters, otherwise this function will return an + /// error. + pub fn set_ip_filters(&mut self, filters: &[RangeInclusive]) -> Result<(), Error> { + let thread_handle = if let Some(thread_id) = self.thread_id { + unsafe { Owned::new(OpenThread(THREAD_GET_CONTEXT, false, thread_id)?) } + } else { + return Err(Error::unsupported( + "IP filtering requires the `thread_id` to be set!", + )); + }; + + for (i, filter) in filters.iter().enumerate() { + let input = + ipt::InputBuffer::set_thread_ip_filter(*thread_handle, i.try_into()?, filter); + let (_, out_size) = self.send_device_io_request(&input).map_err(|e| { + Error::unsupported(format!( + "Failed to set IP filters: {e}. Number of supported filters: {}", + nr_addr_filters().unwrap_or_default() + )) + })?; + debug_assert_eq!(out_size, 0); + } + Ok(()) + } + + pub fn enable_tracing(&mut self) -> Result<(), Error> { + self.toggle_tracing(true) + } + + pub fn disable_tracing(&mut self) -> Result<(), Error> { + self.toggle_tracing(false) + } + + // If the target thread_id is not set, this function will be a best effort based on the threads + // seen in the last decoding. Enumerating the threads for every iteration kills performances. + // If a new thread is spawn it is traced by default. If a thread ends, the reativation will fail + // with a log message but without returning the error. + fn toggle_tracing(&mut self, enable: bool) -> Result<(), Error> { + if let Some(thread_id) = self.thread_id { + let mut thread_handle = + unsafe { Owned::new(OpenThread(THREAD_GET_CONTEXT, false, thread_id)?) }; + self.toggle_thread_tracing(&mut thread_handle, enable)?; + } else { + for thread_id in &self.last_decode_threads { + let mut thread_handle = + match unsafe { OpenThread(THREAD_GET_CONTEXT, false, *thread_id) } { + Ok(handle) => unsafe { Owned::new(handle) }, + Err(e) => { + log::info!("Failed to toggle tracing for thread {thread_id}: {e}"); + continue; + } + }; + + let _ = self + .toggle_thread_tracing(&mut thread_handle, enable) + .inspect_err(|e| { + log::info!("Failed to toggle tracing for thread {thread_id}: {e}"); + }); + } + } + + Ok(()) + } + + fn toggle_thread_tracing(&self, thread_handle: &mut HANDLE, enable: bool) -> Result<(), Error> { + let input = if enable { + ipt::InputBuffer::resume_thread_trace(*thread_handle) + } else { + ipt::InputBuffer::pause_thread_trace(*thread_handle) + }; + let (_, out_size) = self.send_device_io_request(&input)?; + debug_assert_eq!(out_size as usize, size_of::()); + + Ok(()) + } + + /// Fill the coverage map by decoding the PT traces + /// + /// This function consumes the traces. + #[expect( + clippy::cast_ptr_alignment, + reason = "Given the structure of the ipt buffer, casts to headers are always aligned" + )] + pub fn decode_traces_into_map( + &mut self, + // images: &[PtImage], todo: introduce support for JIT/ self modifying code ecc + map_ptr: *mut T, + map_len: usize, + ) -> Result<(), Error> + where + T: CoverageEntry, + { + self.last_decode_threads.clear(); + #[cfg(feature = "export_raw")] + { + self.last_decode_trace.clear(); + } + + // get trace + let trace_size = self.get_trace_size()?; + let mut trace_buffer: Vec = Vec::with_capacity(trace_size as usize); + + let input = ipt::InputBuffer::get_process_trace(*self.target_process_handle); + + let mut out_size = 0; + unsafe { + DeviceIoControl( + *self.ipt_handle, + ipt::Ioctl::ReadTrace as u32, + Some(&raw const input as *const core::ffi::c_void), + size_of::() as u32, + Some(trace_buffer.as_mut_ptr() as *mut core::ffi::c_void), + trace_buffer.capacity() as u32, + Some(&raw mut out_size), + None, + ) + }?; + + debug_assert!((out_size as usize) <= trace_buffer.capacity()); + // SAFETY: Windows driver should not overflow user buffer + unsafe { + trace_buffer.set_len(out_size as usize); + }; + + debug_assert!(trace_buffer.as_ptr().cast::().is_aligned()); + let trace_data = unsafe { + trace_buffer + .as_ptr() + .cast::() + .as_ref_unchecked() + }; + debug_assert_eq!(trace_data.trace_version, ipt::TRACE_VERSION); + if trace_data.valid_trace == 0 { + return Err(Error::runtime( + "Intel PT: failed to get a valid trace from ipt.sys", + )); + } + + let mut slice = &trace_buffer[size_of::()..]; + while slice.len() >= size_of::() { + debug_assert!(slice.as_ptr().cast::().is_aligned()); + let inner_header = + unsafe { slice.as_ptr().cast::().as_ref_unchecked() }; + slice = &slice[size_of::()..]; + self.last_decode_threads.push(inner_header.thread_id as u32); + + if self + .thread_id + .is_none_or(|thread_id| u64::from(thread_id) == inner_header.thread_id) + { + let ptcov_decoder = self.ptcov_decoders.entry(inner_header.thread_id).or_insert( + ThreadCoverageDecoder { + decoder: PtCoverageDecoderBuilder::new() + .cpu(current_cpu()) + .images(self.images) + .build(), + previous_decode_head: 0, + }, + ); + + let mut split_buffer = Vec::new(); + let trace = if inner_header.ring_buffer_offset >= ptcov_decoder.previous_decode_head + { + &slice[ptcov_decoder.previous_decode_head as usize + ..inner_header.ring_buffer_offset as usize] + } else { + split_buffer.extend( + &slice[ptcov_decoder.previous_decode_head as usize + ..inner_header.trace_size as usize], + ); + split_buffer.extend(&slice[0..inner_header.ring_buffer_offset as usize]); + &split_buffer[..] + }; + ptcov_decoder.previous_decode_head = inner_header.ring_buffer_offset; + + #[cfg(feature = "export_raw")] + { + self.last_decode_trace.extend(trace); + } + + let coverage = unsafe { &mut *slice_from_raw_parts_mut(map_ptr, map_len) }; + + if let Err(e) = ptcov_decoder.decoder.coverage(trace, coverage) { + log::warn!("PT trace decoding to coverage failed: {e:x?}"); + coverage.fill(0.into()); + } + } + slice = &slice[inner_header.trace_size as usize..]; + } + + Ok(()) + } + + fn get_trace_size(&self) -> windows::core::Result { + let input = ipt::InputBuffer::get_process_trace_size(*self.target_process_handle); + let (out, out_size) = self.send_device_io_request(&input)?; + + debug_assert_eq!(out_size, size_of::() as u32); + + Ok(unsafe { out.get_trace_size.trace_size }) + } + + fn send_device_io_request( + &self, + input: &ipt::InputBuffer, + ) -> windows::core::Result<(ipt::OutputBuffer, u32)> { + let mut out = MaybeUninit::::uninit(); + let mut out_size = 0; + + unsafe { + DeviceIoControl( + *self.ipt_handle, + ipt::Ioctl::Request as u32, + Some((&raw const *input).cast()), + size_of::() as u32, + Some(out.as_mut_ptr().cast()), + size_of::() as u32, + Some(&raw mut out_size), + None, + ) + }?; + + Ok((unsafe { out.assume_init() }, out_size)) + } + + #[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(()) + } +} + +#[derive(Debug)] +pub struct IntelPTBuilder<'a> { + images: &'a [PtImage<'a>], + pid: u32, +} + +impl Default for IntelPTBuilder<'_> { + fn default() -> Self { + let pid = unsafe { GetCurrentProcessId() }; + Self { images: &[], pid } + } +} + +// todo Consider removing the builder entirely?? +impl<'a> IntelPTBuilder<'a> { + pub fn build(self) -> Result, Error> { + let ipt_handle = open_ipt_handle().map_err(Error::unsupported)?; + + let target_process_handle = unsafe { + OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, self.pid) + .map(|h| Owned::new(h)) + }?; + + let intel_pt = IntelPT { + ipt_handle, + target_process_handle, + thread_id: None, + ptcov_decoders: HashMap::new(), + images: self.images, + last_decode_threads: vec![], + #[cfg(feature = "export_raw")] + last_decode_trace: Vec::new(), + }; + + let options = ipt::Options::default(); + + let input = ipt::InputBuffer::start_process_trace(*intel_pt.target_process_handle, options); + let (_, out_size) = intel_pt.send_device_io_request(&input)?; + debug_assert_eq!(out_size, 0); + + Ok(intel_pt) + } + + #[must_use] + pub fn pid(mut self, pid: u32) -> Self { + self.pid = pid; + self + } + + #[must_use] + pub fn images(mut self, images: &'a [PtImage<'_>]) -> Self { + self.images = images; + self + } +} + +pub(crate) fn availability_in_windows() -> Result<(), String> { + let mut reasons = Vec::new(); + + if let Err(e) = open_ipt_handle() { + reasons.push(e); + } + + if reasons.is_empty() { + Ok(()) + } else { + Err(reasons.join("; ")) + } +} + +/// Number of address filters available on the running CPU +fn nr_addr_filters() -> Result { + let cpuid = CpuId::new(); + cpuid + .get_processor_trace_info() + .ok_or("Failed to read CPU Processor Trace Info") + .map(|pti| pti.configurable_address_ranges()) +} + +fn open_ipt_handle() -> Result, String> { + let ipt_path = w!("\\??\\IPT\0"); + + unsafe { + CreateFileW( + ipt_path, + FILE_GENERIC_READ.0, + FILE_SHARE_READ, + None, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN | FILE_FLAG_NO_BUFFERING, + None, + ) + } + .map(|h| unsafe { Owned::new(h) }) + .map_err(|e| { + format!( + "Failed to open IPT device: {e}; \n\ + Make sure the ipt service is running with `sc start ipt` from an admin shell." + ) + }) +} diff --git a/crates/libafl_intelpt/tests/integration_tests_windows.rs b/crates/libafl_intelpt/tests/integration_tests_windows.rs new file mode 100644 index 0000000000..023be8831c --- /dev/null +++ b/crates/libafl_intelpt/tests/integration_tests_windows.rs @@ -0,0 +1,72 @@ +#![cfg(feature = "std")] +#![cfg(target_os = "windows")] + +extern crate alloc; + +use alloc::slice; +use std::{arch::asm, process}; + +use libafl_intelpt::{IntelPT, availability}; +use log::LevelFilter; +use proc_maps::get_process_maps; +use ptcov::PtImage; +use windows::Win32::System::Threading::GetCurrentThreadId; + +#[test] +fn intel_pt_trace_loop() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + + if let Err(reason) = availability() { + println!("Intel PT is not available, skipping test. Reasons:"); + println!("{reason}"); + return; + } + + let pid = process::id(); + + let maps = get_process_maps(pid).expect("failed to get process maps"); + let images = maps + .iter() + .filter(|map| map.is_exec()) + .map(|pm| { + let data = unsafe { slice::from_raw_parts(pm.start() as *const u8, pm.size()) }; + PtImage::new(data, pm.start() as u64) + }) + .collect::>(); + + let mut pt = IntelPT::builder() + .images(&images) + .build() + .expect("Failed to create IntelPT for worker thread"); + let tid = unsafe { GetCurrentThreadId() }; + pt.set_thread_id(Some(tid)); + pt.enable_tracing().unwrap(); + + let mut count = 0; + unsafe { + asm!( + "2:", + "add {0:r}, 1", + "cmp {0:r}, 255", + "jle 2b", + inout(reg) count, + options(nostack) + ); + } + let _ = count; + + pt.disable_tracing().unwrap(); + + let mut map = vec![0u16; 0x10_00]; + pt.decode_traces_into_map(map.as_mut_ptr(), map.len()) + .unwrap(); + + let assembly_jump_id = map.iter().position(|count| *count >= 254); + assert!( + assembly_jump_id.is_some(), + "Assembly jumps not found in traces" + ); +} diff --git a/fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml b/fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml index 8d26d04ee8..c9c7d4eca3 100644 --- a/fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml +++ b/fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml @@ -15,6 +15,10 @@ tui = ["libafl/tui_monitor"] libafl = { path = "../../../crates/libafl", features = ["intel_pt"] } libafl_bolts = { path = "../../../crates/libafl_bolts" } proc-maps = "0.4.0" +env_logger = "0.11.10" + +[target.'cfg(target_os = "windows")'.dependencies] +windows = "0.62.2" [profile.release] lto = true diff --git a/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs b/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs index 49b0c2d4f0..50917744ef 100644 --- a/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs +++ b/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs @@ -21,6 +21,8 @@ use libafl::{ }; use libafl_bolts::{current_nanos, nonnull_raw_mut, rands::StdRand, tuples::tuple_list}; use proc_maps::get_process_maps; +#[cfg(windows)] +use windows::Win32::System::Threading::GetCurrentThreadId; // Edge coverage map. const MAP_SIZE: usize = 4096; @@ -28,6 +30,8 @@ static mut MAP: [u8; MAP_SIZE] = [0; MAP_SIZE]; static mut MAP_PTR: *mut u8 = &raw mut MAP as _; pub fn main() { + env_logger::init(); + // The function that we want to fuzz let mut harness = |input: &BytesInput| { let buf = input.target_bytes(); @@ -84,12 +88,13 @@ pub fn main() { // Get the memory map of the current process, copy the executable memory that will be // disassembled and used for Intel PT trace decoding - let my_pid = i32::try_from(process::id()).unwrap(); - let process_maps = get_process_maps(my_pid).unwrap(); + let my_pid = process::id(); + #[cfg_attr(windows, expect(clippy::useless_conversion))] + let process_maps = get_process_maps(my_pid.try_into().unwrap()).unwrap(); let images = process_maps .iter() .filter_map(|pm| { - if pm.is_exec() && pm.filename().is_some() && pm.inode != 0 { + if pm.is_exec() { let data = unsafe { slice::from_raw_parts(pm.start() as *const u8, pm.size()) }; Some(PtImage::new(data, pm.start() as u64)) } else { @@ -99,7 +104,11 @@ pub fn main() { .collect::>(); // Pass the executable memory to the code responsible for Intel PT trace decoding - let pt = IntelPT::builder().images(&images).build().unwrap(); + #[cfg_attr(not(windows), expect(unused_mut))] + let mut pt = IntelPT::builder().images(&images).build().unwrap(); + #[cfg(windows)] + pt.set_thread_id(Some(unsafe { GetCurrentThreadId() })); + // Intel PT hook that will handle the setup of Intel PT for each execution and fill the map let pt_hook = unsafe { IntelPTHook::builder()