From f7f8dff6cd2fb1f4ebc5cae0e8c51ff8d4bbaa5f Mon Sep 17 00:00:00 2001 From: "Marco C." <46560192+Marcondiro@users.noreply.github.com> Date: Wed, 13 Nov 2024 02:34:46 +0100 Subject: [PATCH] Add Intel PT tracing support (#2471) * WIP: IntelPT qemu systemmode * use perf-event-open-sys instead of bindgen * intelPT Add enable and disable tracing, add test * Use static_assertions crate * Fix volatiles, finish test * Add Intel PT availability check * Use LibAFL errors in Result * Improve filtering * Add KVM pt_mode check * move static_assertions use * Check for perf_event_open support * Add (empty) IntelPT module * Add IntelPTModule POC * partial ideas to implement intel pt * forgot smth * trace decoding draft * add libipt decoder * use cpuid instead of reading /proc/cpuinfo * investigating nondeterministic behaviour * intel_pt module add thread creation hook * Fully identify deps versions Cargo docs: Although it looks like a specific version of the crate, it actually specifies a range of versions and allows SemVer compatible updates * Move mem image to module, output to file for debug * fixup! Use static_assertions crate * Exclude host kernel from traces * Bump libipt-rs * Callback to get memory as an alterantive to image * WIP Add bootloader fuzzer example * Split availability check: add availability_with_qemu * Move IntelPT to observer * Improve test docs * Clippy happy now * Taplo happy now * Add IntelPTObserver boilerplate * Hook instead of Observer * Clippy & Taplo * Add psb_freq setting * Extremely bad and dirty babyfuzzer stealing * Use thread local cell instead of mutex * Try a trace diff based naive feedback * fix perf aux buffer wrap handling * Use f64 for feedback score * Fix clippy for cargo test * Add config format tests * WIP intelpt babyfuzzer with fork * Fix not wrapped tail offset in split buffer * Baby PT with raw traces diff working * Cache nr_filters * Use Lazy_lock for perf_type * Add baby_fuzzer_intel_pt * restore baby fuzzer * baby_fuzzer with block decoder * instruction decoder instead of block * Fix after upstream merge * OwnedRefMut instead of Cow * Read mem directly instead of going through files * Fix cache lifetime and tail update * clippy * Taplo * Compile caps only on linux * clippy * Fail compilation on unsupported OSes * Add baby_fuzzer_intel_pt to CI * Cleanup * Move intel pt + linux check * fix baby pt * rollback forkexecutor * Remove unused dep * Cleanup * Lints * Compute an edge id instead of using only block ip * Binary only intelPT POC * put linux specific code behind target_os=linux * Clippy & Taplo * fix CI * Disable relocation * No unwrap in decode * No expect in decode * Better logging, smaller aux buffer * add IntelPTBuilder * some lints * Add exclude_hv config * Per CPU tracing and inheritance * Parametrize buffer size * Try not to break commandExecutor API pt.1 * Try not to break commandExecutor API pt.2 * Try not to break commandExecutor API pt.3 * fix baby PT * Support on_crash & on_timeout callbacks for libafl_qemu modules (#2620) * support (unsafe) on_crash / on_timeout callbacks for modules * use libc types in bindgen * Move common code to bolts * Cleanup * Revert changes to backtrace_baby_fuzzers/command_executor * Move intel_pt in one file * Use workspace deps * add nr_addr_filter fallback * Cleaning * Improve decode * Clippy * Improve errors and docs * Impl from for libafl::Error * Merge hooks * Docs * Clean command executor * fix baby PT * fix baby PT warnings * decoder fills the map with no vec alloc * WIP command executor intel PT * filter_map() instead of filter().map() * fix docs * fix windows? * Baby lints * Small cleanings * Use personality to disable ASLR at runtime * Fix nix dep * Use prc-maps in babyfuzzer * working ET_DYN elf * Cleanup Cargo.toml * Clean command executor * introduce PtraceCommandConfigurator * Fix clippy & taplo * input via stdin * libipt as workspace dep * Check kernel version * support Arg input location * Reorder stuff * File input * timeout support for PtraceExec * Lints * Move out method not needing self form IntelPT * unimplemented * Lints * Move intel_pt_baby_fuzzer * Move intel_pt_command_executor * Document the need for smp_rmb * Better comment * Readme and Makefile.toml instead of build.rs * Move out from libafl_bolts to libafl_intelpt * Fix hooks * (Almost) fix intel_pt command exec * fix intel_pt command exec debug * Fix baby_fuzzer * &raw over addr_of! * cfg(target_os = "linux") * bolts Cargo.toml leftover * minimum wage README.md * extract join_split_trace from decode * extract decode_block from decode * add 1 to `previous_block_ip` to avoid that all the recursive basic blocks map to 0 * More generic hook * fix windows * Update CI, fmt * No bitbybit * Fix docker? * Fix Apple silicon? * Use old libipt from crates.io --------- Co-authored-by: Romain Malmain Co-authored-by: Dominik Maier --- .github/workflows/build_and_test.yml | 2 + .github/workflows/ubuntu-prepare/action.yml | 2 +- Cargo.toml | 4 + Dockerfile | 5 + .../intel_pt_baby_fuzzer/Cargo.toml | 19 + .../intel_pt_baby_fuzzer/README.md | 15 + .../intel_pt_baby_fuzzer/src/main.rs | 153 +++ .../intel_pt_command_executor/Cargo.toml | 14 + .../intel_pt_command_executor/Makefile.toml | 33 + .../intel_pt_command_executor/README.md | 21 + .../intel_pt_command_executor/src/main.rs | 146 +++ .../src/target_program.rs | 19 + libafl/Cargo.toml | 22 +- libafl/src/executors/command.rs | 246 +++- libafl/src/executors/hooks/intel_pt.rs | 106 ++ libafl/src/executors/hooks/mod.rs | 4 + libafl_bolts/Cargo.toml | 3 +- libafl_bolts/src/llmp.rs | 4 +- libafl_bolts/src/staterestore.rs | 6 +- libafl_intelpt/Cargo.toml | 42 + libafl_intelpt/README.md | 5 + libafl_intelpt/src/lib.rs | 1030 +++++++++++++++++ .../tests/integration_tests_linux.rs | 95 ++ .../run_integration_tests_linux_with_caps.sh | 11 + 24 files changed, 1981 insertions(+), 26 deletions(-) create mode 100644 fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml create mode 100644 fuzzers/binary_only/intel_pt_baby_fuzzer/README.md create mode 100644 fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs create mode 100644 fuzzers/binary_only/intel_pt_command_executor/Cargo.toml create mode 100644 fuzzers/binary_only/intel_pt_command_executor/Makefile.toml create mode 100644 fuzzers/binary_only/intel_pt_command_executor/README.md create mode 100644 fuzzers/binary_only/intel_pt_command_executor/src/main.rs create mode 100644 fuzzers/binary_only/intel_pt_command_executor/src/target_program.rs create mode 100644 libafl/src/executors/hooks/intel_pt.rs create mode 100644 libafl_intelpt/Cargo.toml create mode 100644 libafl_intelpt/README.md create mode 100644 libafl_intelpt/src/lib.rs create mode 100644 libafl_intelpt/tests/integration_tests_linux.rs create mode 100755 libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 140274a1da..dcd9e16dc8 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -258,6 +258,8 @@ jobs: - ./fuzzers/binary_only/frida_windows_gdiplus - ./fuzzers/binary_only/frida_libpng - ./fuzzers/binary_only/fuzzbench_qemu + - ./fuzzers/binary_only/intel_pt_baby_fuzzer + - ./fuzzers/binary_only/intel_pt_command_executor - ./fuzzers/binary_only/tinyinst_simple # Forkserver diff --git a/.github/workflows/ubuntu-prepare/action.yml b/.github/workflows/ubuntu-prepare/action.yml index f8b843f5c1..a1f5e439a7 100644 --- a/.github/workflows/ubuntu-prepare/action.yml +++ b/.github/workflows/ubuntu-prepare/action.yml @@ -5,7 +5,7 @@ runs: steps: - name: Install and cache deps shell: bash - run: sudo apt-get update && sudo apt-get install -y curl lsb-release wget software-properties-common gnupg ninja-build shellcheck pax-utils nasm libsqlite3-dev libc6-dev libgtk-3-dev gcc g++ gcc-arm-none-eabi gcc-arm-linux-gnueabi g++-arm-linux-gnueabi libslirp-dev libz3-dev build-essential + run: sudo apt-get update && sudo apt-get install -y curl lsb-release wget software-properties-common gnupg ninja-build shellcheck pax-utils nasm libsqlite3-dev libc6-dev libgtk-3-dev gcc g++ gcc-arm-none-eabi gcc-arm-linux-gnueabi g++-arm-linux-gnueabi libslirp-dev libz3-dev build-essential cmake - uses: dtolnay/rust-toolchain@stable - name: Add stable clippy shell: bash diff --git a/Cargo.toml b/Cargo.toml index 6f3289d70c..6fff8139a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "libafl_concolic/symcc_libafl", "libafl_derive", "libafl_frida", + "libafl_intelpt", "libafl_libfuzzer", "libafl_nyx", "libafl_targets", @@ -49,6 +50,7 @@ exclude = [ [workspace.package] version = "0.13.2" +license = "MIT OR Apache-2.0" [workspace.dependencies] ahash = { version = "0.8.11", default-features = false } # The hash function already used in hashbrown @@ -60,6 +62,7 @@ cmake = "0.1.51" document-features = "0.2.10" hashbrown = { version = "0.14.5", default-features = false } # A faster hashmap, nostd compatible libc = "0.2.159" # For (*nix) libc +libipt = "0.1.4" log = "0.4.22" meminterval = "0.4.1" mimalloc = { version = "0.1.43", default-features = false } @@ -77,6 +80,7 @@ serde = { version = "1.0.210", default-features = false } # serialization lib serial_test = { version = "3.1.1", default-features = false } serde_json = { version = "1.0.128", default-features = false } serde_yaml = { version = "0.9.34" } # For parsing the injections yaml file +static_assertions = "1.1.0" strum = "0.26.3" strum_macros = "0.26.4" toml = "0.8.19" # For parsing the injections toml file diff --git a/Dockerfile b/Dockerfile index be4000acf2..1807afcc78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,6 +52,9 @@ COPY libafl_frida/Cargo.toml libafl_frida/build.rs libafl_frida/ COPY scripts/dummy.rs libafl_frida/src/lib.rs COPY libafl_frida/src/gettls.c libafl_frida/src/gettls.c +COPY libafl_intelpt/Cargo.toml libafl_intelpt/README.md libafl_intelpt/ +COPY scripts/dummy.rs libafl_intelpt/src/lib.rs + COPY libafl_qemu/Cargo.toml libafl_qemu/build.rs libafl_qemu/build_linux.rs libafl_qemu/ COPY scripts/dummy.rs libafl_qemu/src/lib.rs @@ -144,6 +147,8 @@ COPY libafl_libfuzzer/src libafl_libfuzzer/src COPY libafl_libfuzzer/runtime libafl_libfuzzer/runtime COPY libafl_libfuzzer/build.rs libafl_libfuzzer/build.rs RUN touch libafl_libfuzzer/src/lib.rs +COPY libafl_intelpt/src libafl_intelpt/src +RUN touch libafl_intelpt/src/lib.rs RUN cargo build && cargo build --release # Copy fuzzers over diff --git a/fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml b/fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml new file mode 100644 index 0000000000..e123a22ea4 --- /dev/null +++ b/fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "intel_pt_baby_fuzzer" +version = "0.13.2" +authors = [ + "Andrea Fioraldi ", + "Dominik Maier ", + "Marco Cavenati ", +] +edition = "2021" + +[features] +tui = [] + +[dependencies] +libafl = { path = "../../../libafl/", default-features = false, features = [ + "intel_pt", +] } +libafl_bolts = { path = "../../../libafl_bolts" } +proc-maps = "0.4.0" diff --git a/fuzzers/binary_only/intel_pt_baby_fuzzer/README.md b/fuzzers/binary_only/intel_pt_baby_fuzzer/README.md new file mode 100644 index 0000000000..79fc1ced19 --- /dev/null +++ b/fuzzers/binary_only/intel_pt_baby_fuzzer/README.md @@ -0,0 +1,15 @@ +# Baby fuzzer with Intel PT tracing + +This is a minimalistic example about how to create a libafl based fuzzer with Intel PT tracing. + +It runs on a single core until a crash occurs and then exits. + +The tested program is a simple Rust function without any instrumentation. + +After building this example with `cargo build`, you need to give to the executable the necessary capabilities with +`sudo setcap cap_ipc_lock,cap_sys_ptrace,cap_sys_admin,cap_syslog=ep ./target/debug/intel_pt_baby_fuzzer`. + +You can run this example using `cargo run`, and you can enable the TUI feature by building and running with +`--features tui`. + +This fuzzer is compatible with Linux hosts only having an Intel PT compatible CPU. diff --git a/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs b/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs new file mode 100644 index 0000000000..cc84cfce2f --- /dev/null +++ b/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs @@ -0,0 +1,153 @@ +use std::{hint::black_box, num::NonZero, path::PathBuf, process, time::Duration}; + +#[cfg(feature = "tui")] +use libafl::monitors::tui::TuiMonitor; +#[cfg(not(feature = "tui"))] +use libafl::monitors::SimpleMonitor; +use libafl::{ + corpus::{InMemoryCorpus, OnDiskCorpus}, + events::SimpleEventManager, + executors::{ + hooks::intel_pt::{IntelPTHook, Section}, + inprocess::GenericInProcessExecutor, + ExitKind, + }, + feedbacks::{CrashFeedback, MaxMapFeedback}, + fuzzer::{Fuzzer, StdFuzzer}, + generators::RandPrintablesGenerator, + inputs::{BytesInput, HasTargetBytes}, + mutators::{havoc_mutations::havoc_mutations, scheduled::StdScheduledMutator}, + observers::StdMapObserver, + schedulers::QueueScheduler, + stages::mutational::StdMutationalStage, + state::StdState, +}; +use libafl_bolts::{current_nanos, rands::StdRand, tuples::tuple_list, AsSlice}; +use proc_maps::get_process_maps; + +// Coverage map +const MAP_SIZE: usize = 4096; +static mut MAP: [u8; MAP_SIZE] = [0; MAP_SIZE]; +#[allow(static_mut_refs)] +static mut MAP_PTR: *mut u8 = unsafe { MAP.as_mut_ptr() }; + +pub fn main() { + // The closure that we want to fuzz + let mut harness = |input: &BytesInput| { + let target = input.target_bytes(); + let buf = target.as_slice(); + if !buf.is_empty() && buf[0] == b'a' { + let _do_something = black_box(0); + if buf.len() > 1 && buf[1] == b'b' { + let _do_something = black_box(0); + if buf.len() > 2 && buf[2] == b'c' { + panic!("Artificial bug triggered =)"); + } + } + } + ExitKind::Ok + }; + + // Create an observation channel using the map + let observer = unsafe { StdMapObserver::from_mut_ptr("signals", MAP_PTR, MAP_SIZE) }; + + // Feedback to rate the interestingness of an input + let mut feedback = MaxMapFeedback::new(&observer); + + // A feedback to choose if an input is a solution or not + let mut objective = CrashFeedback::new(); + + // create a State from scratch + let mut state = StdState::new( + // RNG + StdRand::with_seed(current_nanos()), + // Corpus that will be evolved, we keep it in memory for performance + InMemoryCorpus::new(), + // Corpus in which we store solutions (crashes in this example), + // on disk so the user can get them after stopping the fuzzer + OnDiskCorpus::new(PathBuf::from("./crashes")).unwrap(), + // States of the feedbacks. + // The feedbacks can report the data that should persist in the State. + &mut feedback, + // Same for objective feedbacks + &mut objective, + ) + .unwrap(); + + // The Monitor trait define how the fuzzer stats are displayed to the user + #[cfg(not(feature = "tui"))] + let mon = SimpleMonitor::new(|s| println!("{s}")); + #[cfg(feature = "tui")] + let mon = TuiMonitor::builder() + .title("Baby Fuzzer Intel PT") + .enhanced_graphics(false) + .build(); + + // The event manager handle the various events generated during the fuzzing loop + // such as the notification of the addition of a new item to the corpus + let mut mgr = SimpleEventManager::new(mon); + + // A queue policy to get testcases from the corpus + let scheduler = QueueScheduler::new(); + + // A fuzzer with feedbacks and a corpus scheduler + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); + + // Get the memory map of the current process + let my_pid = i32::try_from(process::id()).unwrap(); + let process_maps = get_process_maps(my_pid).unwrap(); + let sections = process_maps + .iter() + .filter_map(|pm| { + if pm.is_exec() && pm.filename().is_some() { + Some(Section { + file_path: pm.filename().unwrap().to_string_lossy().to_string(), + file_offset: pm.offset as u64, + size: pm.size() as u64, + virtual_address: pm.start() as u64, + }) + } else { + None + } + }) + .collect::>(); + + // Intel PT hook that will handle the setup of Intel PT for each execution and fill the map + let pt_hook = unsafe { + IntelPTHook::builder() + .map_ptr(MAP_PTR) + .map_len(MAP_SIZE) + .image(§ions) + } + .build(); + + type PTInProcessExecutor<'a, H, OT, S, T> = + GenericInProcessExecutor, ()), OT, S>; + // Create the executor for an in-process function with just one observer + let mut executor = PTInProcessExecutor::with_timeout_generic( + tuple_list!(pt_hook), + &mut harness, + tuple_list!(observer), + &mut fuzzer, + &mut state, + &mut mgr, + Duration::from_millis(5000), + ) + .expect("Failed to create the Executor"); + + // Generator of printable bytearrays of max size 32 + let mut generator = RandPrintablesGenerator::new(NonZero::new(32).unwrap()); + + // Generate 8 initial inputs + state + .generate_initial_inputs(&mut fuzzer, &mut executor, &mut generator, &mut mgr, 8) + .expect("Failed to generate the initial corpus"); + + // Set up a mutational stage with a basic bytes mutator + let mutator = StdScheduledMutator::new(havoc_mutations()); + let mut stages = tuple_list!(StdMutationalStage::new(mutator)); + + fuzzer + .fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr) + .expect("Error in the fuzzing loop"); +} diff --git a/fuzzers/binary_only/intel_pt_command_executor/Cargo.toml b/fuzzers/binary_only/intel_pt_command_executor/Cargo.toml new file mode 100644 index 0000000000..6aa937c7a9 --- /dev/null +++ b/fuzzers/binary_only/intel_pt_command_executor/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "intel_pt_command_executor" +version = "0.1.0" +authors = ["Marco Cavenati "] +edition = "2021" + +[dependencies] +env_logger = "0.11.5" +libafl = { path = "../../../libafl", default-features = false, features = [ + "intel_pt", +] } +libafl_bolts = { path = "../../../libafl_bolts" } +libafl_intelpt = { path = "../../../libafl_intelpt" } +log = { version = "0.4.22", features = ["release_max_level_info"] } diff --git a/fuzzers/binary_only/intel_pt_command_executor/Makefile.toml b/fuzzers/binary_only/intel_pt_command_executor/Makefile.toml new file mode 100644 index 0000000000..9c2d97e4eb --- /dev/null +++ b/fuzzers/binary_only/intel_pt_command_executor/Makefile.toml @@ -0,0 +1,33 @@ +[env.development] +PROFILE_DIR = "debug" + +[env.release] +PROFILE_DIR = "release" + +[tasks.build_target] +command = "rustc" +args = [ + "src/target_program.rs", + "--out-dir", + "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/${PROFILE_DIR}", + "-O", +] + +[tasks.build_fuzzer] +command = "cargo" +args = ["build", "--profile", "${CARGO_MAKE_CARGO_PROFILE}"] + +[tasks.build] +dependencies = ["build_fuzzer", "build_target"] + +[tasks.setcap] +script = "sudo setcap cap_ipc_lock,cap_sys_ptrace,cap_sys_admin,cap_syslog=ep ${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/${PROFILE_DIR}/${CARGO_MAKE_CRATE_NAME}" +dependencies = ["build_fuzzer"] + +[tasks.run] +command = "cargo" +args = ["run", "--profile", "${CARGO_MAKE_CARGO_PROFILE}"] +dependencies = ["build", "setcap"] + +[tasks.default] +alias = "run" diff --git a/fuzzers/binary_only/intel_pt_command_executor/README.md b/fuzzers/binary_only/intel_pt_command_executor/README.md new file mode 100644 index 0000000000..967733b505 --- /dev/null +++ b/fuzzers/binary_only/intel_pt_command_executor/README.md @@ -0,0 +1,21 @@ +# Linux Binary-Only Fuzzer with Intel PT Tracing + +This fuzzer is designed to target a Linux binary (without requiring source code instrumentation) and leverages Intel +Processor Trace (PT) to compute code coverage. + +## Prerequisites + +- A Linux host with an Intel Processor Trace (PT) compatible CPU +- `cargo-make` installed +- Sudo access to grant necessary capabilities to the fuzzer + +## How to Run the Fuzzer + +To compile and run the fuzzer (and the target program) execute the following command: +```sh +cargo make +``` + +> **Note**: This command may prompt you for your password to assign capabilities required for Intel PT. If you'd prefer +> not to run it with elevated permissions, you can review and execute the commands from `Makefile.toml` +> individually. diff --git a/fuzzers/binary_only/intel_pt_command_executor/src/main.rs b/fuzzers/binary_only/intel_pt_command_executor/src/main.rs new file mode 100644 index 0000000000..e8c977a775 --- /dev/null +++ b/fuzzers/binary_only/intel_pt_command_executor/src/main.rs @@ -0,0 +1,146 @@ +use std::{ + env, ffi::CString, num::NonZero, os::unix::ffi::OsStrExt, path::PathBuf, time::Duration, +}; + +use libafl::{ + corpus::{InMemoryCorpus, OnDiskCorpus}, + events::SimpleEventManager, + executors::{ + command::{CommandConfigurator, PTraceCommandConfigurator}, + hooks::intel_pt::{IntelPTHook, Section}, + }, + feedbacks::{CrashFeedback, MaxMapFeedback}, + fuzzer::{Fuzzer, StdFuzzer}, + generators::RandPrintablesGenerator, + monitors::SimpleMonitor, + mutators::{havoc_mutations::havoc_mutations, scheduled::StdScheduledMutator}, + observers::StdMapObserver, + schedulers::QueueScheduler, + stages::mutational::StdMutationalStage, + state::StdState, +}; +use libafl_bolts::{core_affinity, rands::StdRand, tuples::tuple_list}; +use libafl_intelpt::{IntelPT, PAGE_SIZE}; + +// Coverage map +const MAP_SIZE: usize = 4096; +static mut MAP: [u8; MAP_SIZE] = [0; MAP_SIZE]; +#[allow(static_mut_refs)] +static mut MAP_PTR: *mut u8 = unsafe { MAP.as_mut_ptr() }; + +pub fn main() { + // Let's set the default logging level to `warn` + if env::var("RUST_LOG").is_err() { + env::set_var("RUST_LOG", "warn") + } + // Enable logging + env_logger::init(); + + let target_path = PathBuf::from(env::args().next().unwrap()) + .parent() + .unwrap() + .join("target_program"); + + // We'll run the target on cpu (aka core) 0 + let cpu = core_affinity::get_core_ids().unwrap()[0]; + log::debug!("Using core {} for fuzzing", cpu.0); + + // Create an observation channel using the map + let observer = unsafe { StdMapObserver::from_mut_ptr("signals", MAP_PTR, MAP_SIZE) }; + + // Feedback to rate the interestingness of an input + let mut feedback = MaxMapFeedback::new(&observer); + + // A feedback to choose if an input is a solution or not + let mut objective = CrashFeedback::new(); + + // create a State from scratch + let mut state = StdState::new( + // RNG + StdRand::new(), + // Corpus that will be evolved, we keep it in memory for performance + InMemoryCorpus::new(), + // Corpus in which we store solutions (crashes in this example), + // on disk so the user can get them after stopping the fuzzer + OnDiskCorpus::new(PathBuf::from("./crashes")).unwrap(), + // States of the feedbacks. + // The feedbacks can report the data that should persist in the State. + &mut feedback, + // Same for objective feedbacks + &mut objective, + ) + .unwrap(); + + // The Monitor trait define how the fuzzer stats are displayed to the user + let mon = SimpleMonitor::new(|s| println!("{s}")); + + // The event manager handle the various events generated during the fuzzing loop + // such as the notification of the addition of a new item to the corpus + let mut mgr = SimpleEventManager::new(mon); + + // A queue policy to get testcases from the corpus + let scheduler = QueueScheduler::new(); + + // 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); + + // 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; + + intel_pt + .set_ip_filters(&[code_memory_addresses.clone()]) + .unwrap(); + + let sections = [Section { + file_path: target_path.to_string_lossy().to_string(), + file_offset: 0x13000, + size: (*code_memory_addresses.end() - *code_memory_addresses.start() + 1) as u64, + virtual_address: *code_memory_addresses.start() as u64, + }]; + + let hook = unsafe { IntelPTHook::builder().map_ptr(MAP_PTR).map_len(MAP_SIZE) } + .intel_pt(intel_pt) + .image(§ions) + .build(); + + let target_cstring = CString::from( + target_path + .as_os_str() + .as_bytes() + .iter() + .map(|&b| NonZero::new(b).unwrap()) + .collect::>(), + ); + + let command_configurator = PTraceCommandConfigurator::builder() + .path(target_cstring) + .cpu(cpu) + .timeout(Duration::from_secs(2)) + .build(); + let mut executor = + command_configurator.into_executor_with_hooks(tuple_list!(observer), tuple_list!(hook)); + + // Generator of printable bytearrays of max size 32 + let mut generator = RandPrintablesGenerator::new(NonZero::new(32).unwrap()); + + // Generate 8 initial inputs + state + .generate_initial_inputs(&mut fuzzer, &mut executor, &mut generator, &mut mgr, 8) + .expect("Failed to generate the initial corpus"); + + // Setup a mutational stage with a basic bytes mutator + let mutator = StdScheduledMutator::new(havoc_mutations()); + let mut stages = tuple_list!(StdMutationalStage::new(mutator)); + + fuzzer + .fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr) + .expect("Error in the fuzzing loop"); +} diff --git a/fuzzers/binary_only/intel_pt_command_executor/src/target_program.rs b/fuzzers/binary_only/intel_pt_command_executor/src/target_program.rs new file mode 100644 index 0000000000..f1636bb6dc --- /dev/null +++ b/fuzzers/binary_only/intel_pt_command_executor/src/target_program.rs @@ -0,0 +1,19 @@ +use std::{ + hint::black_box, + io::{stdin, Read}, +}; + +fn main() { + let mut buf = Vec::new(); + stdin().read_to_end(&mut buf).unwrap(); + + if !buf.is_empty() && buf[0] == b'a' { + let _do_something = black_box(0); + if buf.len() > 1 && buf[1] == b'b' { + let _do_something = black_box(0); + if buf.len() > 2 && buf[2] == b'c' { + panic!("Artificial bug triggered =)"); + } + } + } +} diff --git a/libafl/Cargo.toml b/libafl/Cargo.toml index 92f281c124..4ac7f1a17f 100644 --- a/libafl/Cargo.toml +++ b/libafl/Cargo.toml @@ -49,7 +49,7 @@ document-features = ["dep:document-features"] std = [ "serde_json", "serde_json/std", - "nix", + "dep:nix", "serde/std", "bincode", "wait-timeout", @@ -107,6 +107,15 @@ regex = ["std", "dep:regex"] ## Enables deduplication based on `libcasr` for `StacktraceObserver` casr = ["libcasr", "std", "regex"] +## Intel Processor Trace +intel_pt = [ + "std", + "dep:libafl_intelpt", + "dep:libipt", + "dep:nix", + "dep:num_enum", +] + ## Enables features for corpus minimization cmin = ["z3"] @@ -194,12 +203,14 @@ serde_json = { workspace = true, default-features = false, features = [ ] } # clippy-suggested optimised byte counter bytecount = "0.6.8" +static_assertions = { workspace = true } [dependencies] libafl_bolts = { version = "0.13.2", path = "../libafl_bolts", default-features = false, features = [ "alloc", ] } libafl_derive = { version = "0.13.2", path = "../libafl_derive", optional = true } +libafl_intelpt = { path = "../libafl_intelpt", optional = true } rustversion = { workspace = true } tuple_list = { version = "0.1.3" } @@ -220,7 +231,12 @@ typed-builder = { workspace = true, optional = true } # Implement the builder pa serde_json = { workspace = true, optional = true, default-features = false, features = [ "alloc", ] } -nix = { workspace = true, default-features = true, optional = true } +nix = { workspace = true, optional = true, features = [ + "signal", + "ptrace", + "personality", + "fs", +] } regex = { workspace = true, optional = true } uuid = { workspace = true, optional = true, features = ["serde", "v4"] } libm = "0.2.8" @@ -272,6 +288,8 @@ serial_test = { workspace = true, optional = true, default-features = false, fea document-features = { workspace = true, optional = true } # Optional clap = { workspace = true, optional = true } +num_enum = { workspace = true, optional = true } +libipt = { workspace = true, optional = true } [lints] workspace = true diff --git a/libafl/src/executors/command.rs b/libafl/src/executors/command.rs index 6ab23dc18b..7e98568d10 100644 --- a/libafl/src/executors/command.rs +++ b/libafl/src/executors/command.rs @@ -7,28 +7,41 @@ use core::{ }; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; +#[cfg(all(feature = "std", target_os = "linux"))] +use std::{ + ffi::{CStr, CString}, + os::fd::AsRawFd, +}; #[cfg(feature = "std")] -use std::process::Child; use std::{ ffi::{OsStr, OsString}, io::{Read, Write}, path::{Path, PathBuf}, + process::Child, process::{Command, Stdio}, time::Duration, }; +#[cfg(all(feature = "std", target_os = "linux"))] +use libafl_bolts::core_affinity::CoreId; use libafl_bolts::{ fs::{get_unique_std_input_file, InputFile}, tuples::{Handle, MatchName, RefIndexable}, AsSlice, }; +#[cfg(all(feature = "std", target_os = "linux"))] +use libc::STDIN_FILENO; +#[cfg(all(feature = "std", target_os = "linux"))] +use nix::unistd::Pid; +#[cfg(all(feature = "std", target_os = "linux"))] +use typed_builder::TypedBuilder; use super::HasTimeout; #[cfg(all(feature = "std", unix))] use crate::executors::{Executor, ExitKind}; use crate::{ corpus::Corpus, - executors::HasObservers, + executors::{hooks::ExecutorHooksTuple, HasObservers}, inputs::{HasTargetBytes, UsesInput}, observers::{ObserversTuple, StdErrObserver, StdOutObserver}, state::{HasCorpus, HasExecutions, State, UsesState}, @@ -40,7 +53,7 @@ use crate::{inputs::Input, Error}; /// How to deliver input to an external program /// `StdIn`: The target reads from stdin /// `File`: The target reads from the specified [`InputFile`] -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum InputLocation { /// Mutate a commandline argument to deliver an input Arg { @@ -48,6 +61,7 @@ pub enum InputLocation { argnum: usize, }, /// Deliver input via `StdIn` + #[default] StdIn, /// Deliver the input via the specified [`InputFile`] /// You can use specify [`InputFile::create(INPUTFILE_STD)`] to use a default filename. @@ -158,16 +172,116 @@ where } } -/// A `CommandExecutor` is a wrapper around [`std::process::Command`] to execute a target as a child process. +/// Linux specific [`CommandConfigurator`] that leverages `ptrace` +/// +/// This configurator was primarly developed to be used in conjunction with +/// [`crate::executors::hooks::intel_pt::IntelPTHook`] +#[cfg(all(feature = "std", target_os = "linux"))] +#[derive(Debug, Clone, PartialEq, Eq, TypedBuilder)] +pub struct PTraceCommandConfigurator { + #[builder(setter(into))] + path: CString, + #[builder(default)] + args: Vec, + #[builder(default)] + env: Vec, + #[builder(default)] + input_location: InputLocation, + #[builder(default, setter(strip_option))] + cpu: Option, + #[builder(default = 5 * 60, setter(transform = |t: Duration| t.as_secs() as u32))] + timeout: u32, +} + +#[cfg(all(feature = "std", target_os = "linux"))] +impl CommandConfigurator for PTraceCommandConfigurator +where + I: HasTargetBytes, +{ + fn spawn_child(&mut self, input: &I) -> Result { + use nix::{ + sys::{ + personality, ptrace, + signal::{raise, Signal}, + }, + unistd::{alarm, dup2, execve, fork, pipe, write, ForkResult}, + }; + + 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(); + } + + // Disable Address Space Layout Randomization (ASLR) for consistent memory + // addresses between executions + let pers = personality::get().unwrap(); + personality::set(pers | personality::Persona::ADDR_NO_RANDOMIZE).unwrap(); + + match &mut self.input_location { + InputLocation::Arg { argnum } => { + // self.args[argnum] will be overwritten if already present. + assert!( + *argnum <= self.args.len(), + "If you want to fuzz arg {argnum}, you have to specify the other {argnum} (static) args." + ); + let terminated_input = [&input.target_bytes() as &[u8], &[0]].concat(); + let cstring_input = + CString::from(CStr::from_bytes_until_nul(&terminated_input).unwrap()); + if *argnum == self.args.len() { + self.args.push(cstring_input); + } else { + self.args[*argnum] = cstring_input; + } + } + InputLocation::StdIn => { + let (pipe_read, pipe_write) = pipe().unwrap(); + write(pipe_write, &input.target_bytes()).unwrap(); + dup2(pipe_read.as_raw_fd(), STDIN_FILENO).unwrap(); + } + InputLocation::File { out_file } => { + out_file.write_buf(input.target_bytes().as_slice()).unwrap(); + } + } + + // After this STOP, the process is traced with PTrace (no hooks yet) + raise(Signal::SIGSTOP).unwrap(); + + alarm::set(self.timeout); + + // Just before this returns, hooks pre_execs are called + execve(&self.path, &self.args, &self.env).unwrap(); + unreachable!("execve returns only on error and its result is unwrapped"); + } + Err(e) => Err(Error::unknown(format!("Fork failed: {e}"))), + } + } + + fn exec_timeout(&self) -> Duration { + Duration::from_secs(u64::from(self.timeout)) + } + + /// Use [`PTraceCommandConfigurator::builder().timeout`] instead + fn exec_timeout_mut(&mut self) -> &mut Duration { + unimplemented!("Use [`PTraceCommandConfigurator::builder().timeout`] instead") + } +} + +/// A `CommandExecutor` is a wrapper around [`Command`] to execute a target as a child process. /// /// Construct a `CommandExecutor` by implementing [`CommandConfigurator`] for a type of your choice and calling [`CommandConfigurator::into_executor`] on it. /// Instead, you can use [`CommandExecutor::builder()`] to construct a [`CommandExecutor`] backed by a [`StdCommandConfigurator`]. -pub struct CommandExecutor { +pub struct CommandExecutor { /// The wrapped command configurer configurer: T, /// The observers used by this executor observers: OT, + hooks: HT, phantom: PhantomData, + phantom_child: PhantomData, } impl CommandExecutor<(), (), ()> { @@ -179,7 +293,7 @@ impl CommandExecutor<(), (), ()> { /// `arg`, `args`, `env`, and so on. /// /// By default, input is read from stdin, unless you specify a different location using - /// * `arg_input_arg` for input delivered _as_ an command line argument + /// * `arg_input_arg` for input delivered _as_ a command line argument /// * `arg_input_file` for input via a file of a specific name /// * `arg_input_file_std` for a file with default name (at the right location in the arguments) #[must_use] @@ -188,20 +302,22 @@ impl CommandExecutor<(), (), ()> { } } -impl Debug for CommandExecutor +impl Debug for CommandExecutor where T: Debug, OT: Debug, + HT: Debug, { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("CommandExecutor") .field("inner", &self.configurer) .field("observers", &self.observers) + .field("hooks", &self.hooks) .finish() } } -impl CommandExecutor +impl CommandExecutor where T: Debug, OT: Debug, @@ -317,14 +433,94 @@ where } } -impl UsesState for CommandExecutor +#[cfg(all(feature = "std", target_os = "linux"))] +impl Executor for CommandExecutor +where + EM: UsesState, + S: State + HasExecutions + UsesInput, + T: CommandConfigurator + Debug, + OT: Debug + MatchName + ObserversTuple, + Z: UsesState, + HT: ExecutorHooksTuple, +{ + /// Linux specific low level implementation, to directly handle `fork`, `exec` and use linux + /// `ptrace` + /// + /// Hooks' `pre_exec` and observers' `pre_exec_child` are called with the child process stopped + /// just before the `exec` return (after forking). + fn run_target( + &mut self, + _fuzzer: &mut Z, + state: &mut Self::State, + _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))?; + if !matches!(wait_status, Stopped(c, Signal::SIGSTOP) if c == child) { + return Err(Error::unknown("Unexpected state of child process")); + } + + ptrace::setoptions(child, ptrace::Options::PTRACE_O_TRACEEXEC)?; + ptrace::cont(child, None)?; + + let wait_status = waitpid(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")); + } + + self.observers.pre_exec_child_all(state, input)?; + if *state.executions() == 1 { + self.hooks.init_all::(state); + } + self.hooks.pre_exec_all(state, input); + + ptrace::detach(child, None)?; + let res = match waitpid(child, None)? { + Exited(pid, 0) if pid == child => ExitKind::Ok, + Exited(pid, _) if pid == child => ExitKind::Crash, + 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, + s => { + // TODO other cases? + return Err(Error::unsupported( + format!("Target program returned an unexpected state when waiting on it. {s:?} (waiting for pid {child})") + )); + } + }; + + self.hooks.post_exec_all(state, input); + self.observers.post_exec_child_all(state, input, &res)?; + Ok(res) + } +} + +impl UsesState for CommandExecutor where S: State, { type State = S; } -impl HasObservers for CommandExecutor +impl HasObservers for CommandExecutor where S: State, T: Debug, @@ -569,7 +765,7 @@ impl CommandExecutorBuilder { } } -/// A `CommandConfigurator` takes care of creating and spawning a [`std::process::Command`] for the [`CommandExecutor`]. +/// A `CommandConfigurator` takes care of creating and spawning a [`Command`] for the [`CommandExecutor`]. /// # Example #[cfg_attr(all(feature = "std", unix), doc = " ```")] #[cfg_attr(not(all(feature = "std", unix)), doc = " ```ignore")] @@ -614,7 +810,7 @@ impl CommandExecutorBuilder { /// } /// ``` #[cfg(all(feature = "std", any(unix, doc)))] -pub trait CommandConfigurator: Sized { +pub trait CommandConfigurator: Sized { /// Get the stdout fn stdout_observer(&self) -> Option> { None @@ -625,7 +821,7 @@ pub trait CommandConfigurator: Sized { } /// Spawns a new process with the given configuration. - fn spawn_child(&mut self, input: &I) -> Result; + fn spawn_child(&mut self, input: &I) -> Result; /// Provides timeout duration for execution of the child process. fn exec_timeout(&self) -> Duration; @@ -633,14 +829,36 @@ pub trait CommandConfigurator: Sized { fn exec_timeout_mut(&mut self) -> &mut Duration; /// Create an `Executor` from this `CommandConfigurator`. - fn into_executor(self, observers: OT) -> CommandExecutor + fn into_executor(self, observers: OT) -> CommandExecutor + where + OT: MatchName, + { + CommandExecutor { + configurer: self, + observers, + hooks: (), + phantom: PhantomData, + phantom_child: PhantomData, + } + } + + /// Create an `Executor` with hooks from this `CommandConfigurator`. + fn into_executor_with_hooks( + self, + observers: OT, + hooks: HT, + ) -> CommandExecutor where OT: MatchName, + HT: ExecutorHooksTuple, + S: UsesInput, { CommandExecutor { configurer: self, observers, + hooks, phantom: PhantomData, + phantom_child: PhantomData, } } } diff --git a/libafl/src/executors/hooks/intel_pt.rs b/libafl/src/executors/hooks/intel_pt.rs new file mode 100644 index 0000000000..f4acce679e --- /dev/null +++ b/libafl/src/executors/hooks/intel_pt.rs @@ -0,0 +1,106 @@ +use core::fmt::Debug; +use std::{ + ptr::slice_from_raw_parts_mut, + string::{String, ToString}, +}; + +use libafl_intelpt::{error_from_pt_error, IntelPT}; +use libipt::{Asid, Image, SectionCache}; +use num_traits::SaturatingAdd; +use serde::Serialize; +use typed_builder::TypedBuilder; + +use crate::{ + executors::{hooks::ExecutorHook, HasObservers}, + inputs::UsesInput, + Error, +}; + +/// Info of a binary's section that can be used during `Intel PT` traces decoding +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Section { + /// Path of the binary + pub file_path: String, + /// Offset of the section in the file + pub file_offset: u64, + /// Size of the section + pub size: u64, + /// Start virtual address of the section once loaded in memory + pub virtual_address: u64, +} + +/// Hook to enable Intel Processor Trace (PT) tracing +#[derive(TypedBuilder)] +pub struct IntelPTHook { + #[builder(default = IntelPT::builder().build().unwrap())] + intel_pt: IntelPT, + #[builder(setter(transform = |sections: &[Section]| sections_to_image(sections).unwrap()))] + image: (Image<'static>, SectionCache<'static>), + map_ptr: *mut T, + 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, + T: SaturatingAdd + From + Debug, +{ + fn init(&mut self, _state: &mut S) {} + + fn pre_exec(&mut self, _state: &mut S, _input: &S::Input) { + self.intel_pt.enable_tracing().unwrap(); + } + + fn post_exec(&mut self, _state: &mut S, _input: &S::Input) { + self.intel_pt.disable_tracing().unwrap(); + + let slice = unsafe { &mut *slice_from_raw_parts_mut(self.map_ptr, self.map_len) }; + let _ = self + .intel_pt + .decode_traces_into_map(&mut self.image.0, slice) + .inspect_err(|e| log::warn!("Intel PT trace decoding failed: {e}")); + } +} + +// It would be nice to have this as a `TryFrom>`, but Rust's orphan rule doesn't +// like this (and `TryFromIter` is not a thing atm) +fn sections_to_image( + sections: &[Section], +) -> Result<(Image<'static>, SectionCache<'static>), Error> { + let mut image_cache = SectionCache::new(Some("image_cache")).map_err(error_from_pt_error)?; + let mut image = Image::new(Some("image")).map_err(error_from_pt_error)?; + + for s in sections { + let isid = image_cache.add_file(&s.file_path, s.file_offset, s.size, s.virtual_address); + if let Err(e) = isid { + log::warn!( + "Error while caching {} {} - skipped", + s.file_path, + e.to_string() + ); + continue; + } + + if let Err(e) = image.add_cached(&mut image_cache, isid.unwrap(), Asid::default()) { + log::warn!( + "Error while adding cache to image {} {} - skipped", + s.file_path, + e.to_string() + ); + continue; + } + } + + Ok((image, image_cache)) +} diff --git a/libafl/src/executors/hooks/mod.rs b/libafl/src/executors/hooks/mod.rs index 35453192fa..4296c3a16d 100644 --- a/libafl/src/executors/hooks/mod.rs +++ b/libafl/src/executors/hooks/mod.rs @@ -22,6 +22,10 @@ pub mod inprocess; #[cfg(feature = "std")] pub mod timer; +/// Intel Processor Trace (PT) +#[cfg(all(feature = "intel_pt", target_os = "linux"))] +pub mod intel_pt; + /// The hook that runs before and after the executor runs the target pub trait ExecutorHook where diff --git a/libafl_bolts/Cargo.toml b/libafl_bolts/Cargo.toml index e243e765e6..d33639f941 100644 --- a/libafl_bolts/Cargo.toml +++ b/libafl_bolts/Cargo.toml @@ -19,7 +19,6 @@ categories = [ "os", "no-std", ] -rust-version = "1.70.0" [package.metadata.docs.rs] features = ["document-features"] @@ -121,7 +120,7 @@ rustversion = { workspace = true } [dependencies] libafl_derive = { version = "0.13.2", optional = true, path = "../libafl_derive" } -static_assertions = "1.1.0" +static_assertions = { workspace = true } tuple_list = { version = "0.1.3" } hashbrown = { workspace = true, features = [ diff --git a/libafl_bolts/src/llmp.rs b/libafl_bolts/src/llmp.rs index 6d2f9e8725..07abd68aa4 100644 --- a/libafl_bolts/src/llmp.rs +++ b/libafl_bolts/src/llmp.rs @@ -1294,7 +1294,7 @@ where log::debug!( "[{} - {:#x}] Send message with id {}", self.id.0, - self as *const Self as u64, + ptr::from_ref::(self) as u64, mid ); @@ -1710,7 +1710,7 @@ where log::debug!( "[{} - {:#x}] Received message with ID {}...", self.id.0, - self as *const Self as u64, + ptr::from_ref::(self) as u64, (*msg).message_id.0 ); diff --git a/libafl_bolts/src/staterestore.rs b/libafl_bolts/src/staterestore.rs index 4607771379..1542c3b089 100644 --- a/libafl_bolts/src/staterestore.rs +++ b/libafl_bolts/src/staterestore.rs @@ -195,11 +195,7 @@ where let shmem_content = self.content_mut(); unsafe { - ptr::copy_nonoverlapping( - EXITING_MAGIC as *const u8, - shmem_content.buf.as_mut_ptr(), - len, - ); + ptr::copy_nonoverlapping(EXITING_MAGIC.as_ptr(), shmem_content.buf.as_mut_ptr(), len); } shmem_content.buf_len = EXITING_MAGIC.len(); } diff --git a/libafl_intelpt/Cargo.toml b/libafl_intelpt/Cargo.toml new file mode 100644 index 0000000000..6fffabdc40 --- /dev/null +++ b/libafl_intelpt/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "libafl_intelpt" +version.workspace = true +authors = ["Marco Cavenati "] +description = "Intel Processor Trace wrapper for libafl" +repository = "https://github.com/AFLplusplus/LibAFL/" +edition = "2021" +license.workspace = true +readme = "./README.md" +keywords = ["fuzzing", "testing", "security", "intelpt"] +categories = ["development-tools::testing", "no-std"] + +[features] +default = ["std", "libipt"] +std = ["libafl_bolts/std"] + +libipt = ["std", "dep:libipt"] + +[dev-dependencies] +static_assertions = { workspace = true } + +[target.'cfg(target_os = "linux" )'.dev-dependencies] +nix = { workspace = true } +proc-maps = "0.4.0" + +[dependencies] +#arbitrary-int = { version = "1.2.7" } +#bitbybit = { version = "1.3.2" } +libafl_bolts = { path = "../libafl_bolts", default-features = false } +libc = { workspace = true } +libipt = { workspace = true, optional = true } +log = { workspace = true } +num_enum = { workspace = true, default-features = false } +num-traits = { workspace = true, default-features = false } +raw-cpuid = { version = "11.1.0" } + +[target.'cfg(target_os = "linux" )'.dependencies] +caps = { version = "0.5.5" } +perf-event-open-sys = { version = "4.0.0" } + +[lints] +workspace = true diff --git a/libafl_intelpt/README.md b/libafl_intelpt/README.md new file mode 100644 index 0000000000..237d591a8b --- /dev/null +++ b/libafl_intelpt/README.md @@ -0,0 +1,5 @@ +# Intel Processor Trace (PT) low level code + +This module is a wrapper around the IntelPT kernel driver, exposing functionalities specifically crafted for libafl. + +At the moment only linux hosts are supported. diff --git a/libafl_intelpt/src/lib.rs b/libafl_intelpt/src/lib.rs new file mode 100644 index 0000000000..f9616d7e3b --- /dev/null +++ b/libafl_intelpt/src/lib.rs @@ -0,0 +1,1030 @@ +//! Intel Processor Trace (PT) low level code +//! +//! This crate interacts with the linux kernel (specifically with perf) and therefore it only works +//! on linux hosts + +// Just in case this crate will have real no_std support in the future +#![no_std] +#![cfg(target_arch = "x86_64")] +#![cfg(feature = "std")] +#![cfg(feature = "libipt")] + +#[macro_use] +extern crate std; + +use std::{ + borrow::ToOwned, + string::{String, ToString}, + vec::Vec, +}; +#[cfg(target_os = "linux")] +use std::{ + ffi::{CStr, CString}, + fmt::Debug, + format, fs, + ops::RangeInclusive, + os::{ + fd::{AsRawFd, FromRawFd, OwnedFd}, + raw::c_void, + }, + path::Path, + ptr, slice, + sync::LazyLock, +}; + +// #[cfg(target_os = "linux")] +// use arbitrary_int::u4; +// #[cfg(target_os = "linux")] +// use bitbybit::bitfield; +#[cfg(target_os = "linux")] +use caps::{CapSet, Capability}; +#[cfg(target_os = "linux")] +use libafl_bolts::ownedref::OwnedRefMut; +use libafl_bolts::Error; +use libipt::PtError; +#[cfg(target_os = "linux")] +use libipt::{ + block::BlockDecoder, AddrConfig, AddrFilter, AddrFilterBuilder, AddrRange, BlockFlags, + ConfigBuilder, Cpu, Image, PtErrorCode, Status, +}; +#[cfg(target_os = "linux")] +use num_enum::TryFromPrimitive; +#[cfg(target_os = "linux")] +use num_traits::{Euclid, SaturatingAdd}; +#[cfg(target_os = "linux")] +use perf_event_open_sys::{ + bindings::{perf_event_attr, perf_event_mmap_page, PERF_FLAG_FD_CLOEXEC}, + ioctls::{DISABLE, ENABLE, SET_FILTER}, + perf_event_open, +}; +use raw_cpuid::CpuId; + +/// Size of a memory page +pub const PAGE_SIZE: usize = 4096; + +#[cfg(target_os = "linux")] +const PT_EVENT_PATH: &str = "/sys/bus/event_source/devices/intel_pt"; + +#[cfg(target_os = "linux")] +static NR_ADDR_FILTERS: LazyLock> = LazyLock::new(|| { + // This info is available in two different files, use the second path as fail-over + let path = format!("{PT_EVENT_PATH}/nr_addr_filters"); + let path2 = format!("{PT_EVENT_PATH}/caps/num_address_ranges"); + let err = format!("Failed to read Intel PT number of address filters from {path} and {path2}"); + + let s = fs::read_to_string(&path); + if let Ok(s) = s { + let n = s.trim().parse::(); + if let Ok(n) = n { + return Ok(n); + } + } + + let s2 = fs::read_to_string(&path2).map_err(|_| err.clone())?; + s2.trim().parse::().map_err(|_| err) +}); + +#[cfg(target_os = "linux")] +static CURRENT_CPU: LazyLock> = LazyLock::new(|| { + let cpuid = CpuId::new(); + cpuid + .get_feature_info() + .map(|fi| Cpu::intel(fi.family_id().into(), fi.model_id(), fi.stepping_id())) +}); + +#[cfg(target_os = "linux")] +static PERF_EVENT_TYPE: LazyLock> = LazyLock::new(|| { + let path = format!("{PT_EVENT_PATH}/type"); + let s = fs::read_to_string(&path) + .map_err(|_| format!("Failed to read Intel PT perf event type from {path}"))?; + s.trim() + .parse::() + .map_err(|_| format!("Failed to parse Intel PT perf event type in {path}")) +}); + +/// Intel PT mode of operation with KVM +/// +/// Check out +/// for more details +#[cfg(target_os = "linux")] +#[derive(TryFromPrimitive, Debug)] +#[repr(i32)] +pub enum KvmPTMode { + /// trace both host/guest and output to host buffer + System = 0, + /// trace host and guest simultaneously and output to their respective buffer + HostGuest = 1, +} + +/// Intel Processor Trace (PT) +#[cfg(target_os = "linux")] +#[derive(Debug)] +pub struct IntelPT { + fd: OwnedFd, + perf_buffer: *mut c_void, + perf_aux_buffer: *mut c_void, + perf_buffer_size: usize, + perf_aux_buffer_size: usize, + aux_head: *mut u64, + aux_tail: *mut u64, + previous_decode_head: u64, + ip_filters: Vec>, +} + +#[cfg(target_os = "linux")] +impl IntelPT { + /// Create a default builder + /// + /// Checkout [`IntelPTBuilder::default()`] for more details + #[must_use] + pub fn builder() -> IntelPTBuilder { + IntelPTBuilder::default() + } + + /// Set filters based on Instruction Pointer (IP) + /// + /// Only instructions in `filters` ranges will be traced. + pub fn set_ip_filters(&mut self, filters: &[RangeInclusive]) -> Result<(), Error> { + let str_filter = filters + .iter() + .map(|filter| { + let size = filter.end() - filter.start(); + format!("filter {:#016x}/{:#016x} ", filter.start(), size) + }) + .reduce(|acc, s| acc + &s) + .unwrap_or_default(); + + // SAFETY: CString::from_vec_unchecked is safe because no null bytes are added to str_filter + let c_str_filter = unsafe { CString::from_vec_unchecked(str_filter.into_bytes()) }; + match unsafe { SET_FILTER(self.fd.as_raw_fd(), c_str_filter.into_raw()) } { + -1 => { + let availability = match availability() { + Ok(()) => String::new(), + Err(reasons) => format!(" Possible reasons: {reasons}"), + }; + Err(Error::last_os_error(format!( + "Failed to set IP filters.{availability}" + ))) + } + 0 => { + self.ip_filters = filters.to_vec(); + Ok(()) + } + ret => Err(Error::unsupported(format!( + "Failed to set IP filter, ioctl returned unexpected value {ret}" + ))), + } + } + + fn ip_filters_to_addr_filter(&self) -> AddrFilter { + let mut builder = AddrFilterBuilder::new(); + let mut iter = self + .ip_filters + .iter() + .map(|f| AddrRange::new(*f.start() as u64, *f.end() as u64, AddrConfig::FILTER)); + if let Some(f) = iter.next() { + builder.addr0(f); + if let Some(f) = iter.next() { + builder.addr1(f); + if let Some(f) = iter.next() { + builder.addr2(f); + if let Some(f) = iter.next() { + builder.addr3(f); + } + } + } + } + builder.finish() + } + + /// Start tracing + /// + /// Be aware that the tracing is not started on [`IntelPT`] construction. + pub fn enable_tracing(&mut self) -> Result<(), Error> { + match unsafe { ENABLE(self.fd.as_raw_fd(), 0) } { + -1 => { + let availability = match availability() { + Ok(()) => String::new(), + Err(reasons) => format!(" Possible reasons: {reasons}"), + }; + Err(Error::last_os_error(format!( + "Failed to enable tracing.{availability}" + ))) + } + 0 => Ok(()), + ret => Err(Error::unsupported(format!( + "Failed to enable tracing, ioctl returned unexpected value {ret}" + ))), + } + } + + /// Stop tracing + /// + /// This doesn't drop [`IntelPT`], the configuration will be preserved. + pub fn disable_tracing(&mut self) -> Result<(), Error> { + match unsafe { DISABLE(self.fd.as_raw_fd(), 0) } { + -1 => Err(Error::last_os_error("Failed to disable tracing")), + 0 => Ok(()), + ret => Err(Error::unsupported(format!( + "Failed to disable tracing, ioctl returned unexpected value {ret}" + ))), + } + } + + // // let read_mem = |buf: &mut [u8], addr: u64| { + // // let src = addr as *const u8; + // // let dst = buf.as_mut_ptr(); + // // let size = buf.len(); + // // unsafe { + // // ptr::copy_nonoverlapping(src, dst, size); + // // } + // // }; + // #[allow(clippy::cast_possible_wrap)] + // fn decode_with_callback( + // &mut self, + // read_memory: F, + // copy_buffer: Option<&mut Vec>, + // ) -> Result, Error> { + // self.decode( + // Some(|buff: &mut [u8], addr: u64, _: Asid| { + // debug_assert!(i32::try_from(buff.len()).is_ok()); + // read_memory(buff, addr); + // buff.len() as i32 + // }), + // None, + // copy_buffer, + // ) + // } + + /// Fill the coverage map by decoding the PT traces + /// + /// This function consumes the traces. + pub fn decode_traces_into_map( + &mut self, + image: &mut Image, + map: &mut [T], + ) -> Result<(), Error> + where + T: SaturatingAdd + From + Debug, + { + let head = unsafe { self.aux_head.read_volatile() }; + let tail = unsafe { self.aux_tail.read_volatile() }; + if head < tail { + return Err(Error::unknown( + "Intel PT: aux buffer head is behind aux tail.", + )); + }; + if self.previous_decode_head < tail { + return Err(Error::unknown( + "Intel PT: aux previous head is behind aux tail.", + )); + }; + let len = (head - tail) as usize; + if len >= self.perf_aux_buffer_size { + log::warn!( + "The fuzzer run filled the entire PT buffer. Consider increasing the aux buffer \ + size or refining the IP filters." + ); + } + let skip = self.previous_decode_head - tail; + + let head_wrap = wrap_aux_pointer(head, self.perf_aux_buffer_size); + let tail_wrap = wrap_aux_pointer(tail, self.perf_aux_buffer_size); + + // after reading the data_head value, user space should issue an rmb() + // https://manpages.debian.org/bookworm/manpages-dev/perf_event_open.2.en.html#data_head + smp_rmb(); + + let mut data = if head_wrap >= tail_wrap { + unsafe { + let ptr = self.perf_aux_buffer.add(tail_wrap as usize) as *mut u8; + OwnedRefMut::Ref(slice::from_raw_parts_mut(ptr, len)) + } + } else { + // Head pointer wrapped, the trace is split + unsafe { self.join_split_trace(head_wrap, tail_wrap) } + }; + + let mut config = ConfigBuilder::new(data.as_mut()).map_err(error_from_pt_error)?; + config.filter(self.ip_filters_to_addr_filter()); + if let Some(cpu) = &*CURRENT_CPU { + config.cpu(*cpu); + } + let flags = BlockFlags::END_ON_CALL.union(BlockFlags::END_ON_JUMP); + config.flags(flags); + let mut decoder = BlockDecoder::new(&config.finish()).map_err(error_from_pt_error)?; + decoder + .set_image(Some(image)) + .map_err(error_from_pt_error)?; + + let mut previous_block_ip = 0; + let mut status; + 'sync: loop { + match decoder.sync_forward() { + Ok(s) => { + status = s; + Self::decode_blocks( + &mut decoder, + &mut status, + &mut previous_block_ip, + skip, + map, + )?; + } + Err(e) => { + if e.code() != PtErrorCode::Eos { + log::trace!("PT error in sync forward {e:?}"); + } + break 'sync; + } + }; + } + + // Advance the trace pointer up to the latest sync point, otherwise next execution's trace + // might not contain a PSB packet. + decoder.sync_backward().map_err(error_from_pt_error)?; + let offset = decoder.sync_offset().map_err(error_from_pt_error)?; + unsafe { self.aux_tail.write_volatile(tail + offset) }; + self.previous_decode_head = head; + Ok(()) + } + + #[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; + 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(), + ) + } + + #[inline] + fn decode_blocks( + decoder: &mut BlockDecoder<()>, + status: &mut Status, + previous_block_ip: &mut u64, + skip: u64, + map: &mut [T], + ) -> Result<(), Error> + where + T: SaturatingAdd + From + Debug, + { + 'block: loop { + while status.event_pending() { + match decoder.event() { + Ok((_, s)) => { + *status = s; + } + Err(e) => { + log::trace!("PT error in event {e:?}"); + break 'block; + } + }; + } + + match decoder.next() { + Ok((b, s)) => { + *status = s; + let offset = decoder.offset().map_err(error_from_pt_error)?; + + if !b.speculative() && skip < offset { + // add 1 to `previous_block_ip` to avoid that all the recursive basic blocks map to 0 + let id = hash_me(*previous_block_ip + 1) ^ 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()) }; + *map_loc = (*map_loc).saturating_add(&1u8.into()); + + *previous_block_ip = b.ip(); + } + } + Err(e) => { + if e.code() != PtErrorCode::Eos { + log::trace!("PT error in block next {e:?}"); + } + } + } + if status.eos() { + break 'block; + } + } + Ok(()) + } +} + +#[cfg(target_os = "linux")] +impl Drop for IntelPT { + fn drop(&mut self) { + unsafe { + let ret = libc::munmap(self.perf_aux_buffer, self.perf_aux_buffer_size); + assert_eq!(ret, 0, "Intel PT: Failed to unmap perf aux buffer"); + let ret = libc::munmap(self.perf_buffer, self.perf_buffer_size); + assert_eq!(ret, 0, "Intel PT: Failed to unmap perf buffer"); + } + } +} + +/// Builder for [`IntelPT`] +#[cfg(target_os = "linux")] +#[derive(Debug, Clone, PartialEq)] +pub struct IntelPTBuilder { + pid: Option, + cpu: i32, + exclude_kernel: bool, + exclude_hv: bool, + inherit: bool, + perf_buffer_size: usize, + perf_aux_buffer_size: usize, +} + +#[cfg(target_os = "linux")] +impl Default for IntelPTBuilder { + /// Create a default builder for [`IntelPT`] + /// + /// The default configuration corresponds to: + /// ```rust + /// use libafl_intelpt::{IntelPTBuilder, PAGE_SIZE}; + /// let builder = unsafe { std::mem::zeroed::() } + /// .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(); + /// assert_eq!(builder, IntelPTBuilder::default()); + /// ``` + fn default() -> Self { + Self { + pid: None, + cpu: -1, + exclude_kernel: true, + exclude_hv: true, + inherit: false, + perf_buffer_size: 128 * PAGE_SIZE + PAGE_SIZE, + perf_aux_buffer_size: 2 * 1024 * 1024, + } + } +} + +#[cfg(target_os = "linux")] +impl IntelPTBuilder { + /// Build the [`IntelPT`] struct + pub fn build(&self) -> Result { + self.check_config(); + let mut perf_event_attr = new_perf_event_attr_intel_pt()?; + perf_event_attr.set_exclude_kernel(self.exclude_kernel.into()); + perf_event_attr.set_exclude_hv(self.exclude_hv.into()); + perf_event_attr.set_inherit(self.inherit.into()); + + // SAFETY: perf_event_attr is properly initialized + let fd = match unsafe { + perf_event_open( + ptr::from_mut(&mut perf_event_attr), + self.pid.unwrap_or(0), + self.cpu, + -1, + PERF_FLAG_FD_CLOEXEC.into(), + ) + } { + -1 => { + let availability = match availability() { + Ok(()) => String::new(), + Err(reasons) => format!(" Possible reasons: {reasons}"), + }; + return Err(Error::last_os_error(format!( + "Failed to open Intel PT perf event.{availability}" + ))); + } + fd => { + // SAFETY: On success, perf_event_open() returns a new file descriptor. + // On error, -1 is returned, and it is checked above + unsafe { OwnedFd::from_raw_fd(fd) } + } + }; + + let perf_buffer = setup_perf_buffer(&fd, self.perf_buffer_size)?; + + // the first perf_buff page is a metadata page + let buff_metadata = perf_buffer.cast::(); + let aux_offset = unsafe { &raw mut (*buff_metadata).aux_offset }; + let aux_size = unsafe { &raw mut (*buff_metadata).aux_size }; + let data_offset = unsafe { &raw mut (*buff_metadata).data_offset }; + let data_size = unsafe { &raw mut (*buff_metadata).data_size }; + + unsafe { + aux_offset.write_volatile(next_page_aligned_addr( + data_offset.read_volatile() + data_size.read_volatile(), + )); + aux_size.write_volatile(self.perf_aux_buffer_size as u64); + } + + let perf_aux_buffer = unsafe { + setup_perf_aux_buffer(&fd, aux_size.read_volatile(), aux_offset.read_volatile())? + }; + + 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 { + fd, + perf_buffer, + perf_aux_buffer, + perf_buffer_size: self.perf_buffer_size, + perf_aux_buffer_size: self.perf_aux_buffer_size, + aux_head, + aux_tail, + previous_decode_head: 0, + ip_filters, + }) + } + + /// Warn if the configuration is not recommended + #[inline] + fn check_config(&self) { + if self.inherit && self.cpu == -1 { + log::warn!( + "IntelPT set up on all CPUs with process inheritance enabled. This configuration \ + is not recommended and might not work as expected" + ); + } + } + + #[must_use] + /// Set the process to be traced via its `PID`. Set to `None` to trace the current process. + pub fn pid(mut self, pid: Option) -> Self { + self.pid = pid; + self + } + + #[must_use] + /// Set the CPU to be traced + /// + /// # Panics + /// + /// The function will panic if `cpu` is greater than `i32::MAX` + pub fn cpu(mut self, cpu: usize) -> Self { + self.cpu = cpu.try_into().unwrap(); + self + } + + #[must_use] + /// Trace all the CPUs + pub fn all_cpus(mut self) -> Self { + self.cpu = -1; + self + } + + #[must_use] + /// Do not trace kernel code + pub fn exclude_kernel(mut self, exclude_kernel: bool) -> Self { + self.exclude_kernel = exclude_kernel; + self + } + + #[must_use] + /// Do not trace Hypervisor code + pub fn exclude_hv(mut self, exclude_hv: bool) -> Self { + self.exclude_hv = exclude_hv; + self + } + + #[must_use] + /// Child processes are traced + pub fn inherit(mut self, inherit: bool) -> Self { + self.inherit = inherit; + self + } + + /// Set the size of the perf buffer + pub fn perf_buffer_size(mut self, perf_buffer_size: usize) -> Result { + let err = Err(Error::illegal_argument( + "IntelPT perf_buffer_size should be 1+2^n pages", + )); + if perf_buffer_size < PAGE_SIZE { + return err; + } + let (q, r) = (perf_buffer_size - PAGE_SIZE).div_rem_euclid(&PAGE_SIZE); + if !q.is_power_of_two() || r != 0 { + return err; + } + + self.perf_buffer_size = perf_buffer_size; + Ok(self) + } + + /// Set the size of the perf aux buffer (actual PT traces buffer) + pub fn perf_aux_buffer_size(mut self, perf_aux_buffer_size: usize) -> Result { + // todo:replace with is_multiple_of once stable + if perf_aux_buffer_size % PAGE_SIZE != 0 { + return Err(Error::illegal_argument( + "IntelPT perf_aux_buffer must be page aligned", + )); + } + if !perf_aux_buffer_size.is_power_of_two() { + return Err(Error::illegal_argument( + "IntelPT perf_aux_buffer must be a power of two", + )); + } + + self.perf_aux_buffer_size = perf_aux_buffer_size; + Ok(self) + } +} + +// /// Perf event config for `IntelPT` +// /// +// /// (This is almost mapped to `IA32_RTIT_CTL MSR` by perf) +// #[cfg(target_os = "linux")] +// #[bitfield(u64, default = 0)] +// struct PtConfig { +// /// Disable call return address compression. AKA DisRETC in Intel SDM. +// #[bit(11, rw)] +// noretcomp: bool, +// /// Indicates the frequency of PSB packets. AKA PSBFreq in Intel SDM. +// #[bits(24..=27, rw)] +// psb_period: u4, +// } + +/// Number of address filters available on the running CPU +#[cfg(target_os = "linux")] +pub fn nr_addr_filters() -> Result { + NR_ADDR_FILTERS.clone() +} + +/// Check if Intel PT is available on the current system. +/// +/// Returns `Ok(())` if Intel PT is available and has the features used by `LibAFL`, otherwise +/// returns an `Err` containing a description of the reasons. +/// +/// If you use this with QEMU check out [`Self::availability_in_qemu()`] instead. +/// +/// Due to the numerous factors that can affect `IntelPT` availability, this function was +/// developed on a best-effort basis. +/// The outcome of these checks does not fully guarantee whether `IntelPT` will function or not. +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()); + } + } else { + reasons.push("Failed to read CPU vendor".to_owned()); + } + + if let Some(ef) = cpuid.get_extended_feature_info() { + if !ef.has_processor_trace() { + reasons.push("Intel PT is not supported by the CPU".to_owned()); + } + } else { + reasons.push("Failed to read CPU Extended Features".to_owned()); + } + + #[cfg(target_os = "linux")] + if let Err(r) = availability_in_linux() { + reasons.push(r); + } + #[cfg(not(target_os = "linux"))] + reasons.push("Only linux hosts are supported at the moment".to_owned()); + + if reasons.is_empty() { + Ok(()) + } else { + Err(reasons.join("; ")) + } +} + +/// Check if Intel PT is available on the current system and can be used in combination with +/// QEMU. +/// +/// If you don't use this with QEMU check out [`IntelPT::availability()`] instead. +pub fn availability_in_qemu_kvm() -> Result<(), String> { + let mut reasons = match availability() { + Err(s) => vec![s], + Ok(()) => Vec::new(), + }; + + #[cfg(target_os = "linux")] + { + let kvm_pt_mode_path = "/sys/module/kvm_intel/parameters/pt_mode"; + if let Ok(s) = fs::read_to_string(kvm_pt_mode_path) { + match s.trim().parse::().map(TryInto::try_into) { + Ok(Ok(KvmPTMode::System)) => (), + Ok(Ok(KvmPTMode::HostGuest)) => reasons.push(format!( + "KVM Intel PT mode must be set to {:?} `{}` to be used with libafl_qemu", + KvmPTMode::System, + KvmPTMode::System as i32 + )), + _ => reasons.push(format!( + "Failed to parse KVM Intel PT mode in {kvm_pt_mode_path}" + )), + } + }; + } + #[cfg(not(target_os = "linux"))] + reasons.push("Only linux hosts are supported at the moment".to_owned()); + + if reasons.is_empty() { + Ok(()) + } else { + Err(reasons.join("; ")) + } +} + +/// Convert [`PtError`] into [`Error`] +#[inline] +#[must_use] +pub fn error_from_pt_error(err: PtError) -> Error { + Error::unknown(err.to_string()) +} + +#[cfg(target_os = "linux")] +fn availability_in_linux() -> Result<(), String> { + let mut reasons = Vec::new(); + match linux_version() { + // https://docs.rs/perf-event-open-sys/4.0.0/perf_event_open_sys/#kernel-versions + Ok(ver) if ver >= (5, 19, 4) => {} + Ok((major, minor, patch)) => reasons.push(format!( + "Kernel version {major}.{minor}.{patch} is older than 5.19.4 and might not work." + )), + Err(()) => reasons.push("Failed to retrieve kernel version".to_owned()), + } + + if let Err(e) = &*PERF_EVENT_TYPE { + reasons.push(e.clone()); + } + + if let Err(e) = &*NR_ADDR_FILTERS { + reasons.push(e.clone()); + } + + // official way of knowing if perf_event_open() support is enabled + // https://man7.org/linux/man-pages/man2/perf_event_open.2.html + let perf_event_support_path = "/proc/sys/kernel/perf_event_paranoid"; + if !Path::new(perf_event_support_path).exists() { + reasons.push(format!( + "perf_event_open() support is not enabled: {perf_event_support_path} not found" + )); + } + + // TODO check also the value of perf_event_paranoid, check which values are required by pt + // https://www.kernel.org/doc/Documentation/sysctl/kernel.txt + // also, looks like it is distribution dependent + // https://askubuntu.com/questions/1400874/what-does-perf-paranoia-level-four-do + // CAP_SYS_ADMIN might make this check useless + + match caps::read(None, CapSet::Permitted) { + Ok(current_capabilities) => { + let required_caps = [ + Capability::CAP_IPC_LOCK, + Capability::CAP_SYS_PTRACE, + Capability::CAP_SYS_ADMIN, // TODO: CAP_PERFMON doesn't look to be enough!? + Capability::CAP_SYSLOG, + ]; + + for rc in required_caps { + if !current_capabilities.contains(&rc) { + reasons.push(format!("Required capability {rc} missing")); + } + } + } + Err(e) => reasons.push(format!("Failed to read linux capabilities: {e}")), + }; + + if reasons.is_empty() { + Ok(()) + } else { + Err(reasons.join("; ")) + } +} + +#[cfg(target_os = "linux")] +fn new_perf_event_attr_intel_pt() -> Result { + let type_ = match &*PERF_EVENT_TYPE { + Ok(t) => Ok(*t), + Err(e) => Err(Error::unsupported(e.clone())), + }?; + // let config = PtConfig::builder() + // .with_noretcomp(true) + // .with_psb_period(u4::new(0)) + // .build() + // .raw_value; + let config = 0x08_00; // noretcomp + + let mut attr = perf_event_attr { + size: size_of::() as u32, + type_, + config, + ..Default::default() + }; + + // Do not enable tracing as soon as the perf_event_open syscall is issued + attr.set_disabled(true.into()); + + Ok(attr) +} + +#[cfg(target_os = "linux")] +fn setup_perf_buffer(fd: &OwnedFd, perf_buffer_size: usize) -> Result<*mut c_void, Error> { + match unsafe { + libc::mmap( + ptr::null_mut(), + perf_buffer_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd.as_raw_fd(), + 0, + ) + } { + libc::MAP_FAILED => Err(Error::last_os_error("IntelPT: Failed to mmap perf buffer")), + mmap_addr => Ok(mmap_addr), + } +} + +#[cfg(target_os = "linux")] +fn setup_perf_aux_buffer(fd: &OwnedFd, size: u64, offset: u64) -> Result<*mut c_void, Error> { + match unsafe { + libc::mmap( + ptr::null_mut(), + size as usize, + // PROT_WRITE sets PT to stop when the buffer is full + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd.as_raw_fd(), + i64::try_from(offset)?, + ) + } { + libc::MAP_FAILED => Err(Error::last_os_error( + "IntelPT: Failed to mmap perf aux buffer", + )), + mmap_addr => Ok(mmap_addr), + } +} + +#[cfg(target_os = "linux")] +fn linux_version() -> Result<(usize, usize, usize), ()> { + let mut uname_data = libc::utsname { + sysname: [0; 65], + nodename: [0; 65], + release: [0; 65], + version: [0; 65], + machine: [0; 65], + domainname: [0; 65], + }; + + if unsafe { libc::uname(&mut uname_data) } != 0 { + return Err(()); + } + + let release = unsafe { CStr::from_ptr(uname_data.release.as_ptr()) }; + let mut parts = release + .to_bytes() + .split(|&c| c == b'.' || c == b'-') + .take(3) + .map(|s| String::from_utf8_lossy(s).parse::()); + if let (Some(Ok(major)), Some(Ok(minor)), Some(Ok(patch))) = + (parts.next(), parts.next(), parts.next()) + { + Ok((major, minor, patch)) + } else { + Err(()) + } +} + +#[cfg(target_os = "linux")] +#[inline] +const fn next_page_aligned_addr(address: u64) -> u64 { + (address + PAGE_SIZE as u64 - 1) & !(PAGE_SIZE as u64 - 1) +} + +// copy pasted from libafl_qemu/src/modules/edges.rs +// adapted from https://xorshift.di.unimi.it/splitmix64.c +#[cfg(target_os = "linux")] +#[inline] +#[must_use] +const fn hash_me(mut x: u64) -> u64 { + x = (x ^ (x.overflowing_shr(30).0)) + .overflowing_mul(0xbf58476d1ce4e5b9) + .0; + x = (x ^ (x.overflowing_shr(27).0)) + .overflowing_mul(0x94d049bb133111eb) + .0; + x ^ (x.overflowing_shr(31).0) +} + +#[cfg(target_os = "linux")] +#[inline] +fn smp_rmb() { + // SAFETY: just a memory barrier + unsafe { + core::arch::asm!("lfence", options(nostack, preserves_flags)); + } +} + +#[cfg(target_os = "linux")] +#[inline] +const fn wrap_aux_pointer(ptr: u64, perf_aux_buffer_size: usize) -> u64 { + ptr & (perf_aux_buffer_size as u64 - 1) +} + +#[cfg(test)] +mod test { + // #[cfg(target_os = "linux")] + // use arbitrary_int::Number; + use static_assertions::assert_eq_size; + + use super::*; + + // Only 64-bit systems are supported, ensure we can use usize and u64 interchangeably + assert_eq_size!(usize, u64); + + /// Quick way to check if your machine is compatible with Intl PT's features used by libafl + /// + /// Simply run `cargo test intel_pt_check_availability -- --show-output` + #[test] + fn intel_pt_check_availability() { + print!("Intel PT availability:\t\t\t"); + match availability() { + Ok(()) => println!("✔"), + Err(e) => println!("❌\tReasons: {e}"), + } + + print!("Intel PT availability in QEMU/KVM:\t"); + match availability_in_qemu_kvm() { + Ok(()) => println!("✔"), + Err(e) => println!("❌\tReasons: {e}"), + } + } + + #[test] + #[cfg(target_os = "linux")] + fn intel_pt_builder_default_values_are_valid() { + let default = IntelPT::builder(); + IntelPT::builder() + .perf_buffer_size(default.perf_buffer_size) + .unwrap(); + IntelPT::builder() + .perf_aux_buffer_size(default.perf_aux_buffer_size) + .unwrap(); + } + + // #[test] + // #[cfg(target_os = "linux")] + // fn intel_pt_pt_config_noretcomp_format() { + // let ptconfig_noretcomp = PtConfig::DEFAULT.with_noretcomp(true).raw_value; + // let path = format!("{PT_EVENT_PATH}/format/noretcomp"); + // let s = fs::read_to_string(&path).expect("Failed to read Intel PT config noretcomp format"); + // assert!( + // s.starts_with("config:"), + // "Unexpected Intel PT config noretcomp format" + // ); + // let bit = s["config:".len()..] + // .trim() + // .parse::() + // .expect("Failed to parse Intel PT config noretcomp format"); + // assert_eq!( + // ptconfig_noretcomp, + // 0b1 << bit, + // "Unexpected Intel PT config noretcomp format" + // ); + // } + // + // #[test] + // #[cfg(target_os = "linux")] + // fn intel_pt_pt_config_psb_period_format() { + // let ptconfig_psb_period = PtConfig::DEFAULT.with_psb_period(u4::MAX).raw_value; + // let path = format!("{PT_EVENT_PATH}/format/psb_period"); + // let s = + // fs::read_to_string(&path).expect("Failed to read Intel PT config psb_period format"); + // assert!( + // s.starts_with("config:"), + // "Unexpected Intel PT config psb_period format" + // ); + // let from = s["config:".len().."config:".len() + 2] + // .parse::() + // .expect("Failed to parse Intel PT config psb_period format"); + // let to = s["config:".len() + 3..] + // .trim() + // .parse::() + // .expect("Failed to parse Intel PT config psb_period format"); + // let mut format = 0; + // for bit in from..=to { + // format |= 0b1 << bit; + // } + // assert_eq!( + // ptconfig_psb_period, format, + // "Unexpected Intel PT config psb_period format" + // ); + // } +} diff --git a/libafl_intelpt/tests/integration_tests_linux.rs b/libafl_intelpt/tests/integration_tests_linux.rs new file mode 100644 index 0000000000..ebd6f7c109 --- /dev/null +++ b/libafl_intelpt/tests/integration_tests_linux.rs @@ -0,0 +1,95 @@ +#![cfg(feature = "std")] +#![cfg(feature = "libipt")] +#![cfg(target_os = "linux")] + +use std::{arch::asm, process}; + +use libafl_intelpt::{availability, IntelPT}; +use libipt::Image; +use nix::{ + sys::{ + signal::{kill, raise, Signal}, + wait::{waitpid, WaitPidFlag}, + }, + unistd::{fork, ForkResult}, +}; +use proc_maps::get_process_maps; + +/// To run this test ensure that the executable has the required capabilities. +/// This can be achieved with the script `./run_integration_tests_linux_with_caps.sh` +#[test] +fn intel_pt_trace_fork() { + if let Err(reason) = availability() { + // Mark as `skipped` once this will be possible https://github.com/rust-lang/rust/issues/68007 + println!("Intel PT is not available, skipping test. Reasons:"); + println!("{reason}"); + return; + } + + let pid = match unsafe { fork() } { + Ok(ForkResult::Parent { child }) => child, + Ok(ForkResult::Child) => { + raise(Signal::SIGSTOP).expect("Failed to stop the process"); + // This will generate a sequence of tnt packets containing 255 taken branches + unsafe { + let mut count = 0; + asm!( + "2:", + "add {0:r}, 1", + "cmp {0:r}, 255", + "jle 2b", + inout(reg) count, + options(nostack) + ); + let _ = count; + } + process::exit(0); + } + Err(e) => panic!("Fork failed {e}"), + }; + + let pt_builder = IntelPT::builder().pid(Some(pid.as_raw())); + let mut pt = pt_builder.build().expect("Failed to create IntelPT"); + pt.enable_tracing().expect("Failed to enable tracing"); + + waitpid(pid, Some(WaitPidFlag::WUNTRACED)).expect("Failed to wait for the child process"); + let maps = get_process_maps(pid.into()).unwrap(); + kill(pid, Signal::SIGCONT).expect("Failed to continue the process"); + + waitpid(pid, None).expect("Failed to wait for the child process"); + pt.disable_tracing().expect("Failed to disable tracing"); + + let mut image = Image::new(Some("test_trace_pid")).unwrap(); + for map in maps { + if map.is_exec() && map.filename().is_some() { + match image.add_file( + map.filename().unwrap().to_str().unwrap(), + map.offset as u64, + map.size() as u64, + None, + map.start() as u64, + ) { + Err(e) => println!( + "Error adding mapping for {:?}: {:?}, skipping", + map.filename().unwrap(), + e + ), + Ok(()) => println!( + "mapping for {:?} added successfully {:#x} - {:#x}", + map.filename().unwrap(), + map.start(), + map.start() + map.size() + ), + } + } + } + + let mut map = vec![0u16; 0x10_00]; + pt.decode_traces_into_map(&mut image, &mut map).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/libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh b/libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh new file mode 100755 index 0000000000..00b001650f --- /dev/null +++ b/libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +cargo test intel_pt_trace_fork --no-run + +for test_bin in ../target/debug/deps/integration_tests_linux-*; do + if file "$test_bin" | grep -q "ELF"; then + sudo setcap cap_ipc_lock,cap_sys_ptrace,cap_sys_admin,cap_syslog=ep "$test_bin" + fi +done + +cargo test intel_pt_trace_fork