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<PtError> 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 <romain.malmain@pm.me> Co-authored-by: Dominik Maier <domenukk@gmail.com>
This commit is contained in:
parent
5eff9c03d3
commit
f7f8dff6cd
2
.github/workflows/build_and_test.yml
vendored
2
.github/workflows/build_and_test.yml
vendored
@ -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
|
||||
|
2
.github/workflows/ubuntu-prepare/action.yml
vendored
2
.github/workflows/ubuntu-prepare/action.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
19
fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml
Normal file
19
fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "intel_pt_baby_fuzzer"
|
||||
version = "0.13.2"
|
||||
authors = [
|
||||
"Andrea Fioraldi <andreafioraldi@gmail.com>",
|
||||
"Dominik Maier <domenukk@gmail.com>",
|
||||
"Marco Cavenati <cavenatimarco@gmail.com>",
|
||||
]
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
tui = []
|
||||
|
||||
[dependencies]
|
||||
libafl = { path = "../../../libafl/", default-features = false, features = [
|
||||
"intel_pt",
|
||||
] }
|
||||
libafl_bolts = { path = "../../../libafl_bolts" }
|
||||
proc-maps = "0.4.0"
|
15
fuzzers/binary_only/intel_pt_baby_fuzzer/README.md
Normal file
15
fuzzers/binary_only/intel_pt_baby_fuzzer/README.md
Normal file
@ -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.
|
153
fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs
Normal file
153
fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs
Normal file
@ -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::<Vec<_>>();
|
||||
|
||||
// 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<H, &'a mut H, (IntelPTHook<T>, ()), 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");
|
||||
}
|
14
fuzzers/binary_only/intel_pt_command_executor/Cargo.toml
Normal file
14
fuzzers/binary_only/intel_pt_command_executor/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "intel_pt_command_executor"
|
||||
version = "0.1.0"
|
||||
authors = ["Marco Cavenati <cavenatimarco@gmail.com>"]
|
||||
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"] }
|
33
fuzzers/binary_only/intel_pt_command_executor/Makefile.toml
Normal file
33
fuzzers/binary_only/intel_pt_command_executor/Makefile.toml
Normal file
@ -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"
|
21
fuzzers/binary_only/intel_pt_command_executor/README.md
Normal file
21
fuzzers/binary_only/intel_pt_command_executor/README.md
Normal file
@ -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.
|
146
fuzzers/binary_only/intel_pt_command_executor/src/main.rs
Normal file
146
fuzzers/binary_only/intel_pt_command_executor/src/main.rs
Normal file
@ -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::<Vec<_>>(),
|
||||
);
|
||||
|
||||
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");
|
||||
}
|
@ -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 =)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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<CString>,
|
||||
#[builder(default)]
|
||||
env: Vec<CString>,
|
||||
#[builder(default)]
|
||||
input_location: InputLocation,
|
||||
#[builder(default, setter(strip_option))]
|
||||
cpu: Option<CoreId>,
|
||||
#[builder(default = 5 * 60, setter(transform = |t: Duration| t.as_secs() as u32))]
|
||||
timeout: u32,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "std", target_os = "linux"))]
|
||||
impl<I> CommandConfigurator<I, Pid> for PTraceCommandConfigurator
|
||||
where
|
||||
I: HasTargetBytes,
|
||||
{
|
||||
fn spawn_child(&mut self, input: &I) -> Result<Pid, Error> {
|
||||
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<OT, S, T> {
|
||||
pub struct CommandExecutor<OT, S, T, HT = (), C = Child> {
|
||||
/// The wrapped command configurer
|
||||
configurer: T,
|
||||
/// The observers used by this executor
|
||||
observers: OT,
|
||||
hooks: HT,
|
||||
phantom: PhantomData<S>,
|
||||
phantom_child: PhantomData<C>,
|
||||
}
|
||||
|
||||
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<OT, S, T> Debug for CommandExecutor<OT, S, T>
|
||||
impl<OT, S, T, HT, C> Debug for CommandExecutor<OT, S, T, HT, C>
|
||||
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<OT, S, T> CommandExecutor<OT, S, T>
|
||||
impl<OT, S, T, HT, C> CommandExecutor<OT, S, T, HT, C>
|
||||
where
|
||||
T: Debug,
|
||||
OT: Debug,
|
||||
@ -317,14 +433,94 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<OT, S, T> UsesState for CommandExecutor<OT, S, T>
|
||||
#[cfg(all(feature = "std", target_os = "linux"))]
|
||||
impl<EM, OT, S, T, Z, HT> Executor<EM, Z> for CommandExecutor<OT, S, T, HT, Pid>
|
||||
where
|
||||
EM: UsesState<State = S>,
|
||||
S: State + HasExecutions + UsesInput,
|
||||
T: CommandConfigurator<S::Input, Pid> + Debug,
|
||||
OT: Debug + MatchName + ObserversTuple<S::Input, S>,
|
||||
Z: UsesState<State = S>,
|
||||
HT: ExecutorHooksTuple<S>,
|
||||
{
|
||||
/// 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<ExitKind, Error> {
|
||||
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::<Self>(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<OT, S, T, HT, C> UsesState for CommandExecutor<OT, S, T, HT, C>
|
||||
where
|
||||
S: State,
|
||||
{
|
||||
type State = S;
|
||||
}
|
||||
|
||||
impl<OT, S, T> HasObservers for CommandExecutor<OT, S, T>
|
||||
impl<OT, S, T, HT, C> HasObservers for CommandExecutor<OT, S, T, HT, C>
|
||||
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<I>: Sized {
|
||||
pub trait CommandConfigurator<I, C = Child>: Sized {
|
||||
/// Get the stdout
|
||||
fn stdout_observer(&self) -> Option<Handle<StdOutObserver>> {
|
||||
None
|
||||
@ -625,7 +821,7 @@ pub trait CommandConfigurator<I>: Sized {
|
||||
}
|
||||
|
||||
/// Spawns a new process with the given configuration.
|
||||
fn spawn_child(&mut self, input: &I) -> Result<Child, Error>;
|
||||
fn spawn_child(&mut self, input: &I) -> Result<C, Error>;
|
||||
|
||||
/// Provides timeout duration for execution of the child process.
|
||||
fn exec_timeout(&self) -> Duration;
|
||||
@ -633,14 +829,36 @@ pub trait CommandConfigurator<I>: Sized {
|
||||
fn exec_timeout_mut(&mut self) -> &mut Duration;
|
||||
|
||||
/// Create an `Executor` from this `CommandConfigurator`.
|
||||
fn into_executor<OT, S>(self, observers: OT) -> CommandExecutor<OT, S, Self>
|
||||
fn into_executor<OT, S>(self, observers: OT) -> CommandExecutor<OT, S, Self, (), C>
|
||||
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<OT, S, HT>(
|
||||
self,
|
||||
observers: OT,
|
||||
hooks: HT,
|
||||
) -> CommandExecutor<OT, S, Self, HT, C>
|
||||
where
|
||||
OT: MatchName,
|
||||
HT: ExecutorHooksTuple<S>,
|
||||
S: UsesInput<Input = I>,
|
||||
{
|
||||
CommandExecutor {
|
||||
configurer: self,
|
||||
observers,
|
||||
hooks,
|
||||
phantom: PhantomData,
|
||||
phantom_child: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
106
libafl/src/executors/hooks/intel_pt.rs
Normal file
106
libafl/src/executors/hooks/intel_pt.rs
Normal file
@ -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<T> {
|
||||
#[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<T> Debug for IntelPTHook<T> {
|
||||
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<S, T> ExecutorHook<S> for IntelPTHook<T>
|
||||
where
|
||||
S: UsesInput + Serialize,
|
||||
T: SaturatingAdd + From<u8> + Debug,
|
||||
{
|
||||
fn init<E: HasObservers>(&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<IntoIter<Section>>`, 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))
|
||||
}
|
@ -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<S>
|
||||
where
|
||||
|
@ -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 = [
|
||||
|
@ -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>(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>(self) as u64,
|
||||
(*msg).message_id.0
|
||||
);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
42
libafl_intelpt/Cargo.toml
Normal file
42
libafl_intelpt/Cargo.toml
Normal file
@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "libafl_intelpt"
|
||||
version.workspace = true
|
||||
authors = ["Marco Cavenati <cavenatimarco@gmail.com>"]
|
||||
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
|
5
libafl_intelpt/README.md
Normal file
5
libafl_intelpt/README.md
Normal file
@ -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.
|
1030
libafl_intelpt/src/lib.rs
Normal file
1030
libafl_intelpt/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
95
libafl_intelpt/tests/integration_tests_linux.rs
Normal file
95
libafl_intelpt/tests/integration_tests_linux.rs
Normal file
@ -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"
|
||||
);
|
||||
}
|
11
libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh
Executable file
11
libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh
Executable file
@ -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
|
Loading…
x
Reference in New Issue
Block a user