Skip to content

Commit

Permalink
Add Intel PT tracing support (AFLplusplus#2471)
Browse files Browse the repository at this point in the history
* 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 (AFLplusplus#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 <[email protected]>
Co-authored-by: Dominik Maier <[email protected]>
  • Loading branch information
3 people authored Nov 13, 2024
1 parent 5eff9c0 commit f7f8dff
Show file tree
Hide file tree
Showing 24 changed files with 1,981 additions and 26 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ubuntu-prepare/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"libafl_concolic/symcc_libafl",
"libafl_derive",
"libafl_frida",
"libafl_intelpt",
"libafl_libfuzzer",
"libafl_nyx",
"libafl_targets",
Expand Down Expand Up @@ -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
Expand All @@ -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 }
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions fuzzers/binary_only/intel_pt_baby_fuzzer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "intel_pt_baby_fuzzer"
version = "0.13.2"
authors = [
"Andrea Fioraldi <[email protected]>",
"Dominik Maier <[email protected]>",
"Marco Cavenati <[email protected]>",
]
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 changes: 15 additions & 0 deletions fuzzers/binary_only/intel_pt_baby_fuzzer/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 153 additions & 0 deletions fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs
Original file line number Diff line number Diff line change
@@ -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(&sections)
}
.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 changes: 14 additions & 0 deletions fuzzers/binary_only/intel_pt_command_executor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "intel_pt_command_executor"
version = "0.1.0"
authors = ["Marco Cavenati <[email protected]>"]
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 changes: 33 additions & 0 deletions fuzzers/binary_only/intel_pt_command_executor/Makefile.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions fuzzers/binary_only/intel_pt_command_executor/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit f7f8dff

Please sign in to comment.