Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ jobs:
- full_system/nyx_launcher
- full_system/nyx_libxml2_standalone
- full_system/nyx_libxml2_parallel
- full_system/qemu_intel_pt_bootloader

# Structure-aware
- structure_aware/nautilus_sync
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/fuzzer-tester-prepare/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ runs:
uses: browser-actions/setup-chrome@v1
with:
chrome-version: stable
- name: install nasm
if: ${{ inputs.fuzzer-name == 'full_system/qemu_intel_pt_bootloader' }}
shell: bash
run: sudo apt install nasm
13 changes: 13 additions & 0 deletions fuzzers/full_system/qemu_intel_pt_bootloader/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "qemu_intel_pt_bootloader"
version = "0.1.0"
authors = ["Marco Cavenati <cavenatimarco+libafl@gmail.com>"]
edition = "2021"

[dependencies]
libafl = { path = "../../../crates/libafl" }
libafl_bolts = { path = "../../../crates/libafl_bolts" }
libafl_qemu = { path = "../../../crates/libafl_qemu", features = [
"intel_pt",
], default-features = false }
env_logger = "0.11.8"
39 changes: 39 additions & 0 deletions fuzzers/full_system/qemu_intel_pt_bootloader/Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import "../../../just/libafl.just"
FUZZER_NAME := "qemu_intel_pt_bootloader"
BIOS_DIR := TARGET_DIR / PROFILE_DIR / "qemu-libafl-bridge/build/qemu-bundle/usr/local/share/qemu"

run: build setcap convert_target_image
BIOS_DIR={{BIOS_DIR}} {{TARGET_DIR}}/{{PROFILE_DIR}}/{{FUZZER_NAME}}
sudo umount /mnt/libafl_qemu_tmpfs
sleep 1
sudo rm -r /mnt/libafl_qemu_tmpfs

# Create target directory if it doesn't exist
[private]
target_dir:
@mkdir -p {{TARGET_DIR}}

# Setup RAM disk to store the qcow2 disk
ram_disk:
sudo mkdir -p /mnt/libafl_qemu_tmpfs
sudo mount -o size=128M -t tmpfs none /mnt/libafl_qemu_tmpfs
sudo chown $(id -u):$(id -g) "/mnt/libafl_qemu_tmpfs"

# Build the bootloader
build_target: target_dir
nasm -o {{TARGET_DIR}}/boot.bin ./src/boot.s

build_fuzzer:
cargo build --profile {{PROFILE}}

build: build_fuzzer build_target

# Convert bootloader bin to qcow2 image
convert_target_image: build_target ram_disk
qemu-img convert -O qcow2 {{TARGET_DIR}}/boot.bin /mnt/libafl_qemu_tmpfs/boot.qcow2

# Set capabilities on the binary
setcap: build_fuzzer
sudo setcap cap_ipc_lock,cap_sys_ptrace,cap_sys_admin,cap_syslog=ep {{TARGET_DIR}}/{{PROFILE_DIR}}/{{FUZZER_NAME}}

test: build
20 changes: 20 additions & 0 deletions fuzzers/full_system/qemu_intel_pt_bootloader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Bootloader Fuzzing in QEMU/KVM with Intel Pt Tracing

A minimalistic example about how to create a LibAFL based fuzzer with Intel
PT tracing using QEMU/KVM to target a bootloader. The target is a nasty x86
bootloader that if detects a specific BIOS version, it hangs forever. The
fuzzer runs until it finds the right input for which the bootloader tries to
hang and then it exits.

During execution the fuzzer prints some statistics to the terminal, like the
number of executions and corpus size (the number of inputs the fuzzer marked
as interesting so far). At the end of the execution, the input causing the
crash is saved to the `crashes/` folder and printed to the terminal.

## How to build from source

You can build from source running `just` to build and then run the fuzzer
(requires `just`, `qemu` and `nasm` installed):

This command requires to run `sudo` to give the fuzzer the necessary
capabilities to use hardware tracing, you may have to enter `root` password.
74 changes: 74 additions & 0 deletions fuzzers/full_system/qemu_intel_pt_bootloader/src/boot.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
[bits 16] ; use 16 bits
[org 0x7c00] ; sets the start address

%macro print_string 1 ; %1: Pointer to the string (null-terminated)
mov si, %1 ; Load the pointer to the string
.print_char:
lodsb ; Load the next byte from [SI] into AL
or al, al ; Check if it's the null terminator
jz .done ; If zero, we are done
mov ah, 0x0E ; BIOS teletype function
int 0x10 ; Call BIOS interrupt
jmp .print_char ; Repeat for the next character
.done:
mov al, 0x0d ; CR
int 0x10
mov al, 0x0a ; LF
int 0x10
%endmacro

start:
mov ah, 0xc0
int 0x15 ; ask for the system configuration parameters
jc fail ; carry must be 0
cmp ah, 0 ; ah must be 0
jne fail

mov ax, [es:bx] ; byte count of the system configuration parameters
cmp ax, 8
jl fail

mov ch, [es:bx + 2] ; Model
mov cl, [es:bx + 3] ; Submodel
mov dh, [es:bx + 4] ; BIOS revision

cmp ch, 'a'
jne fail
cmp cl, 'b'
jne fail
cmp dh, 'c'
jne fail

shutdown:
print_string bye

; sleep a bit to make sure output is printed
xor cx, cx
mov dx, 0xffff
mov ah, 0x86
int 0x15

; actual shutdown
mov ax, 0x1000
mov ax, ss
mov sp, 0xf000
mov ax, 0x5307
mov bx, 0x0001
mov cx, 0x0003
int 0x15

fail:
print_string fail_msg
sleep_forever:
mov cx, 0xffff
mov dx, 0xffff
mov ah, 0x86
int 0x15
jmp sleep_forever

fail_msg db "I don't like your BIOS. :(", 0
bye db "Artificial bug triggered =)", 0

times 510-($-$$) db 0 ; fill the output file with zeroes until 510 bytes are full

dw 0xaa55 ; magic bytes that tell BIOS that this is bootable
227 changes: 227 additions & 0 deletions fuzzers/full_system/qemu_intel_pt_bootloader/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
//! A black box fuzzer targeting a simple bootloader using QEMU/KVM with intel PT tracing

use core::time::Duration;
use std::{
env,
fs::File,
io::Read,
num::NonZero,
ops::RangeInclusive,
path::{Path, PathBuf},
};

use libafl::{
corpus::{Corpus, InMemoryCorpus, OnDiskCorpus},
events::{ProgressReporter, SimpleEventManager},
executors::ExitKind,
feedback_or, feedback_or_fast,
feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback},
fuzzer::{Fuzzer, StdFuzzer},
generators::RandPrintablesGenerator,
inputs::{BytesInput, HasTargetBytes},
monitors::SimpleMonitor,
mutators::{havoc_mutations::havoc_mutations, scheduled::HavocScheduledMutator},
observers::{StdMapObserver, TimeObserver},
schedulers::QueueScheduler,
stages::StdMutationalStage,
state::{HasSolutions, StdState},
};
use libafl_bolts::{current_nanos, rands::StdRand, tuples::tuple_list, AsSlice};
use libafl_qemu::{
config,
config::{Accelerator, DriveCache, KvmProperties, QemuConfig},
executor::QemuExecutor,
modules::intel_pt::{IntelPTModule, PtImage},
Emulator, EmulatorBuilder, GuestAddr, QemuExitReason, QemuShutdownCause,
};

// Edge coverage map
const MAP_SIZE: usize = 256;
static mut MAP: [u8; MAP_SIZE] = [0; MAP_SIZE];
static mut MAP_PTR: *mut u8 = &raw mut MAP as _;

// Bootloader code section and sleep fn address retrieved with `ndisasm target/boot.bin`
const BOOTLOADER_CODE: RangeInclusive<u64> = 0x7c00..=0x7c80;
// This address is the fuzzer goal
const BOOTLOADER_SLEEP_FN_ADDR: GuestAddr = 0x7c60;

fn main() {
// Initialize the logger (use the environment variable RUST_LOG=trace for maximum logging)
env_logger::init();

// Hardcoded parameters
let timeout = Duration::from_secs(5);
let objective_dir = PathBuf::from("./crashes");

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);

// directory containing the bootloader binary
let target_dir = env::var("TARGET_DIR").unwrap_or("target".to_string());
// bios directory
let bios_dir = env::var("BIOS_DIR").unwrap_or(format!(
"{target_dir}/debug/qemu-libafl-bridge/build/qemu-bundle/usr/local/share/qemu/"
));

// Configure QEMU
let qemu_config = QemuConfig::builder()
.no_graphic(true)
.monitor(config::Monitor::Null)
.serial(config::Serial::Null)
.cpu("host")
.ram_size(config::RamSize::MB(2))
.drives([config::Drive::builder()
.format(config::DiskImageFileFormat::Qcow2)
.file("/mnt/libafl_qemu_tmpfs/boot.qcow2")
.cache(DriveCache::None)
.build()])
.accelerator(Accelerator::Kvm(KvmProperties::default()))
.default_devices(false)
.bios(bios_dir)
.start_cpu(false)
.build();

let mut bootloader_file = File::open(Path::new(&target_dir).join("boot.bin")).unwrap();
let mut bootloader_content =
vec![0; (BOOTLOADER_CODE.end() - BOOTLOADER_CODE.start()) as usize];
bootloader_file.read_exact(&mut bootloader_content).unwrap();
let bootloader_content: &'static [u8] = Box::leak(bootloader_content.into_boxed_slice());
let images: &'static [PtImage] = Box::leak(Box::new([PtImage::new(
bootloader_content,
*BOOTLOADER_CODE.start(),
)]));

let intel_pt_builder = IntelPTModule::default_pt_builder()
.ip_filters(vec![BOOTLOADER_CODE])
.images(images);
let intel_pt_module = IntelPTModule::builder()
.map_ptr(unsafe { MAP_PTR })
.map_len(MAP_SIZE)
.intel_pt_builder(intel_pt_builder)
.build();

let emulator = EmulatorBuilder::empty()
.qemu_parameters(qemu_config)
.modules(tuple_list!(intel_pt_module))
.build()
.unwrap();
let qemu = emulator.qemu();
qemu.set_hw_breakpoint(*BOOTLOADER_CODE.start() as GuestAddr)
.unwrap();

// Run the VM until it enters the bootloader
unsafe {
match qemu.run() {
Ok(QemuExitReason::Breakpoint(ba)) if ba as u64 == *BOOTLOADER_CODE.start() => {}
_ => panic!("Pre-harness Unexpected QEMU exit."),
}
}
qemu.remove_hw_breakpoint(*BOOTLOADER_CODE.start() as GuestAddr)
.unwrap();

// Set a breakpoint at the target address
qemu.set_hw_breakpoint(BOOTLOADER_SLEEP_FN_ADDR).unwrap();

qemu.save_snapshot("bootloader_start", true);

let mut harness = |emulator: &mut Emulator<_, _, _, _, _, _, _>,
_: &mut StdState<_, _, _, _>,
input: &BytesInput| unsafe {
let mut fixed_len_input = input.target_bytes().as_slice().to_vec();
fixed_len_input.resize(3, 0);

qemu.load_snapshot("bootloader_start", true);
qemu.write_phys_mem(0xfe6f7, &fixed_len_input);

let intel_pt_module = emulator.modules_mut().get_mut::<IntelPTModule>().unwrap();
intel_pt_module.enable_tracing();

match emulator.qemu().run() {
Ok(QemuExitReason::End(QemuShutdownCause::GuestShutdown)) => {
println!(
"crashing input: {}",
String::from_utf8_lossy(&fixed_len_input)
);
ExitKind::Crash
}
Ok(QemuExitReason::Breakpoint(_)) => ExitKind::Ok,
e => panic!("Harness Unexpected QEMU exit. {e:?}"),
}
};

// Create an observation channel using the map
let observer = unsafe { StdMapObserver::from_mut_ptr("signals", MAP_PTR, MAP_SIZE) };

// Create an observation channel to keep track of the execution time
let time_observer = TimeObserver::new("time");

// Feedback to rate the interestingness of an input
// This one is composed by two Feedbacks in OR
let mut feedback = feedback_or!(
// New maximization map feedback linked to the edges observer and the feedback state
MaxMapFeedback::new(&observer),
// Time feedback, this one does not need a feedback state
TimeFeedback::new(&time_observer)
);

// A feedback to choose if an input is a solution or not
let mut objective = feedback_or_fast!(CrashFeedback::new());

// If not restarting, 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(objective_dir.clone()).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();

// 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);

// Create a QEMU in-process executor
let mut executor = QemuExecutor::new(
emulator,
&mut harness,
tuple_list!(observer, time_observer),
&mut fuzzer,
&mut state,
&mut mgr,
timeout,
)
.expect("Failed to create QemuExecutor");

// Generator of printable bytearrays of max size 3
let mut generator = RandPrintablesGenerator::new(NonZero::new(3).unwrap());

state
.generate_initial_inputs(&mut fuzzer, &mut executor, &mut generator, &mut mgr, 4)
.expect("Failed to generate the initial corpus");

// Setup an havoc mutator with a mutational stage
let mutator = HavocScheduledMutator::new(havoc_mutations());
let mut stages = tuple_list!(StdMutationalStage::new(mutator));

while state.solutions().is_empty() {
mgr.maybe_report_progress(&mut state, Duration::from_secs(5))
.unwrap();

fuzzer
.fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr)
.expect("Error in the fuzzing loop");
}
}
Loading