|
| 1 | +//! A black box fuzzer targeting a simple bootloader using QEMU/KVM with intel PT tracing |
| 2 | +
|
| 3 | +use core::time::Duration; |
| 4 | +use std::{ |
| 5 | + env, |
| 6 | + fs::File, |
| 7 | + io::Read, |
| 8 | + num::NonZero, |
| 9 | + ops::RangeInclusive, |
| 10 | + path::{Path, PathBuf}, |
| 11 | +}; |
| 12 | + |
| 13 | +use libafl::{ |
| 14 | + corpus::{Corpus, InMemoryCorpus, OnDiskCorpus}, |
| 15 | + events::{ProgressReporter, SimpleEventManager}, |
| 16 | + executors::ExitKind, |
| 17 | + feedback_or, feedback_or_fast, |
| 18 | + feedbacks::{CrashFeedback, MaxMapFeedback, TimeFeedback}, |
| 19 | + fuzzer::{Fuzzer, StdFuzzer}, |
| 20 | + generators::RandPrintablesGenerator, |
| 21 | + inputs::{BytesInput, HasTargetBytes}, |
| 22 | + monitors::SimpleMonitor, |
| 23 | + mutators::{havoc_mutations::havoc_mutations, scheduled::HavocScheduledMutator}, |
| 24 | + observers::{StdMapObserver, TimeObserver}, |
| 25 | + schedulers::QueueScheduler, |
| 26 | + stages::StdMutationalStage, |
| 27 | + state::{HasSolutions, StdState}, |
| 28 | +}; |
| 29 | +use libafl_bolts::{current_nanos, rands::StdRand, tuples::tuple_list, AsSlice}; |
| 30 | +use libafl_qemu::{ |
| 31 | + config, |
| 32 | + config::{Accelerator, DriveCache, KvmProperties, QemuConfig}, |
| 33 | + executor::QemuExecutor, |
| 34 | + modules::intel_pt::{IntelPTModule, PtImage}, |
| 35 | + Emulator, EmulatorBuilder, GuestAddr, QemuExitReason, QemuShutdownCause, |
| 36 | +}; |
| 37 | + |
| 38 | +// Edge coverage map |
| 39 | +const MAP_SIZE: usize = 256; |
| 40 | +static mut MAP: [u8; MAP_SIZE] = [0; MAP_SIZE]; |
| 41 | +static mut MAP_PTR: *mut u8 = &raw mut MAP as _; |
| 42 | + |
| 43 | +// Bootloader code section and sleep fn address retrieved with `ndisasm target/boot.bin` |
| 44 | +const BOOTLOADER_CODE: RangeInclusive<u64> = 0x7c00..=0x7c80; |
| 45 | +// This address is the fuzzer goal |
| 46 | +const BOOTLOADER_SLEEP_FN_ADDR: GuestAddr = 0x7c60; |
| 47 | + |
| 48 | +fn main() { |
| 49 | + // Initialize the logger (use the environment variable RUST_LOG=trace for maximum logging) |
| 50 | + env_logger::init(); |
| 51 | + |
| 52 | + // Hardcoded parameters |
| 53 | + let timeout = Duration::from_secs(5); |
| 54 | + let objective_dir = PathBuf::from("./crashes"); |
| 55 | + |
| 56 | + let mon = SimpleMonitor::new(|s| println!("{s}")); |
| 57 | + |
| 58 | + // The event manager handle the various events generated during the fuzzing loop |
| 59 | + // such as the notification of the addition of a new item to the corpus |
| 60 | + let mut mgr = SimpleEventManager::new(mon); |
| 61 | + |
| 62 | + // directory containing the bootloader binary |
| 63 | + let target_dir = env::var("TARGET_DIR").unwrap_or("target".to_string()); |
| 64 | + // bios directory |
| 65 | + let bios_dir = env::var("BIOS_DIR").unwrap_or(format!( |
| 66 | + "{target_dir}/debug/qemu-libafl-bridge/build/qemu-bundle/usr/local/share/qemu/" |
| 67 | + )); |
| 68 | + |
| 69 | + // Configure QEMU |
| 70 | + let qemu_config = QemuConfig::builder() |
| 71 | + .no_graphic(true) |
| 72 | + .monitor(config::Monitor::Null) |
| 73 | + .serial(config::Serial::Null) |
| 74 | + .cpu("host") |
| 75 | + .ram_size(config::RamSize::MB(2)) |
| 76 | + .drives([config::Drive::builder() |
| 77 | + .format(config::DiskImageFileFormat::Qcow2) |
| 78 | + .file("/mnt/libafl_qemu_tmpfs/boot.qcow2") |
| 79 | + .cache(DriveCache::None) |
| 80 | + .build()]) |
| 81 | + .accelerator(Accelerator::Kvm(KvmProperties::default())) |
| 82 | + .default_devices(false) |
| 83 | + .bios(bios_dir) |
| 84 | + .start_cpu(false) |
| 85 | + .build(); |
| 86 | + |
| 87 | + let mut bootloader_file = File::open(Path::new(&target_dir).join("boot.bin")).unwrap(); |
| 88 | + let mut bootloader_content = |
| 89 | + vec![0; (BOOTLOADER_CODE.end() - BOOTLOADER_CODE.start()) as usize]; |
| 90 | + bootloader_file.read_exact(&mut bootloader_content).unwrap(); |
| 91 | + let image = PtImage::new(bootloader_content, *BOOTLOADER_CODE.start()); |
| 92 | + |
| 93 | + let intel_pt_builder = IntelPTModule::default_pt_builder() |
| 94 | + .ip_filters(vec![BOOTLOADER_CODE]) |
| 95 | + .images(vec![image]); |
| 96 | + let intel_pt_module = IntelPTModule::builder() |
| 97 | + .map_ptr(unsafe { MAP_PTR }) |
| 98 | + .map_len(MAP_SIZE) |
| 99 | + .intel_pt_builder(intel_pt_builder) |
| 100 | + .build(); |
| 101 | + |
| 102 | + let emulator = EmulatorBuilder::empty() |
| 103 | + .qemu_parameters(qemu_config) |
| 104 | + .modules(tuple_list!(intel_pt_module)) |
| 105 | + .build() |
| 106 | + .unwrap(); |
| 107 | + let qemu = emulator.qemu(); |
| 108 | + qemu.set_hw_breakpoint(*BOOTLOADER_CODE.start() as GuestAddr) |
| 109 | + .unwrap(); |
| 110 | + |
| 111 | + // Run the VM until it enters the bootloader |
| 112 | + unsafe { |
| 113 | + match qemu.run() { |
| 114 | + Ok(QemuExitReason::Breakpoint(ba)) if ba == *BOOTLOADER_CODE.start() => {} |
| 115 | + _ => panic!("Pre-harness Unexpected QEMU exit."), |
| 116 | + } |
| 117 | + } |
| 118 | + qemu.remove_hw_breakpoint(*BOOTLOADER_CODE.start() as GuestAddr) |
| 119 | + .unwrap(); |
| 120 | + |
| 121 | + // Set a breakpoint at the target address |
| 122 | + qemu.set_hw_breakpoint(BOOTLOADER_SLEEP_FN_ADDR).unwrap(); |
| 123 | + |
| 124 | + qemu.save_snapshot("bootloader_start", true); |
| 125 | + |
| 126 | + let mut harness = |emulator: &mut Emulator<_, _, _, _, _, _, _>, |
| 127 | + _: &mut StdState<_, _, _, _>, |
| 128 | + input: &BytesInput| unsafe { |
| 129 | + let mut fixed_len_input = input.target_bytes().as_slice().to_vec(); |
| 130 | + fixed_len_input.resize(3, 0); |
| 131 | + |
| 132 | + qemu.load_snapshot("bootloader_start", true); |
| 133 | + qemu.write_phys_mem(0xfe6f7, &fixed_len_input); |
| 134 | + |
| 135 | + let intel_pt_module = emulator.modules_mut().get_mut::<IntelPTModule>().unwrap(); |
| 136 | + intel_pt_module.enable_tracing(); |
| 137 | + |
| 138 | + match emulator.qemu().run() { |
| 139 | + Ok(QemuExitReason::End(QemuShutdownCause::GuestShutdown)) => { |
| 140 | + println!( |
| 141 | + "crashing input: {}", |
| 142 | + String::from_utf8_lossy(&fixed_len_input) |
| 143 | + ); |
| 144 | + ExitKind::Crash |
| 145 | + } |
| 146 | + Ok(QemuExitReason::Breakpoint(_)) => ExitKind::Ok, |
| 147 | + e => panic!("Harness Unexpected QEMU exit. {e:?}"), |
| 148 | + } |
| 149 | + }; |
| 150 | + |
| 151 | + // Create an observation channel using the map |
| 152 | + let observer = unsafe { StdMapObserver::from_mut_ptr("signals", MAP_PTR, MAP_SIZE) }; |
| 153 | + |
| 154 | + // Create an observation channel to keep track of the execution time |
| 155 | + let time_observer = TimeObserver::new("time"); |
| 156 | + |
| 157 | + // Feedback to rate the interestingness of an input |
| 158 | + // This one is composed by two Feedbacks in OR |
| 159 | + let mut feedback = feedback_or!( |
| 160 | + // New maximization map feedback linked to the edges observer and the feedback state |
| 161 | + MaxMapFeedback::new(&observer), |
| 162 | + // Time feedback, this one does not need a feedback state |
| 163 | + TimeFeedback::new(&time_observer) |
| 164 | + ); |
| 165 | + |
| 166 | + // A feedback to choose if an input is a solution or not |
| 167 | + let mut objective = feedback_or_fast!(CrashFeedback::new()); |
| 168 | + |
| 169 | + // If not restarting, create a State from scratch |
| 170 | + let mut state = StdState::new( |
| 171 | + // RNG |
| 172 | + StdRand::with_seed(current_nanos()), |
| 173 | + // Corpus that will be evolved, we keep it in memory for performance |
| 174 | + InMemoryCorpus::new(), |
| 175 | + // Corpus in which we store solutions (crashes in this example), |
| 176 | + // on disk so the user can get them after stopping the fuzzer |
| 177 | + OnDiskCorpus::new(objective_dir.clone()).unwrap(), |
| 178 | + // States of the feedbacks. |
| 179 | + // The feedbacks can report the data that should persist in the State. |
| 180 | + &mut feedback, |
| 181 | + // Same for objective feedbacks |
| 182 | + &mut objective, |
| 183 | + ) |
| 184 | + .unwrap(); |
| 185 | + |
| 186 | + // A queue policy to get testcases from the corpus |
| 187 | + let scheduler = QueueScheduler::new(); |
| 188 | + |
| 189 | + // A fuzzer with feedbacks and a corpus scheduler |
| 190 | + let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); |
| 191 | + |
| 192 | + // Create a QEMU in-process executor |
| 193 | + let mut executor = QemuExecutor::new( |
| 194 | + emulator, |
| 195 | + &mut harness, |
| 196 | + tuple_list!(observer, time_observer), |
| 197 | + &mut fuzzer, |
| 198 | + &mut state, |
| 199 | + &mut mgr, |
| 200 | + timeout, |
| 201 | + ) |
| 202 | + .expect("Failed to create QemuExecutor"); |
| 203 | + |
| 204 | + // Generator of printable bytearrays of max size 3 |
| 205 | + let mut generator = RandPrintablesGenerator::new(NonZero::new(3).unwrap()); |
| 206 | + |
| 207 | + state |
| 208 | + .generate_initial_inputs(&mut fuzzer, &mut executor, &mut generator, &mut mgr, 4) |
| 209 | + .expect("Failed to generate the initial corpus"); |
| 210 | + |
| 211 | + // Setup an havoc mutator with a mutational stage |
| 212 | + let mutator = HavocScheduledMutator::new(havoc_mutations()); |
| 213 | + let mut stages = tuple_list!(StdMutationalStage::new(mutator)); |
| 214 | + |
| 215 | + while state.solutions().is_empty() { |
| 216 | + mgr.maybe_report_progress(&mut state, Duration::from_secs(5)) |
| 217 | + .unwrap(); |
| 218 | + |
| 219 | + fuzzer |
| 220 | + .fuzz_one(&mut stages, &mut executor, &mut state, &mut mgr) |
| 221 | + .expect("Error in the fuzzing loop"); |
| 222 | + } |
| 223 | +} |
0 commit comments