Skip to content

Commit 3eba538

Browse files
committed
Add bootloader example fuzzer
Running in qemu/kvm with intel PT tracing
1 parent df8f6ff commit 3eba538

7 files changed

Lines changed: 374 additions & 0 deletions

File tree

.github/workflows/build_and_test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ jobs:
313313
- full_system/nyx_launcher
314314
- full_system/nyx_libxml2_standalone
315315
- full_system/nyx_libxml2_parallel
316+
- full_system/qemu_intel_pt_bootloader
316317

317318
# Structure-aware
318319
- structure_aware/nautilus_sync

.github/workflows/fuzzer-tester-prepare/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ runs:
4848
uses: browser-actions/setup-chrome@v1
4949
with:
5050
chrome-version: stable
51+
- name: install nasm
52+
if: ${{ inputs.fuzzer-name == 'full_system/qemu_intel_pt_bootloader' }}
53+
shell: bash
54+
run: sudo apt install nasm
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "qemu_intel_pt_bootloader"
3+
version = "0.1.0"
4+
authors = ["Marco Cavenati <cavenatimarco+libafl@gmail.com>"]
5+
edition = "2021"
6+
7+
[dependencies]
8+
libafl = { path = "../../../crates/libafl" }
9+
libafl_bolts = { path = "../../../crates/libafl_bolts" }
10+
libafl_qemu = { path = "../../../crates/libafl_qemu", features = [
11+
"intel_pt",
12+
], default-features = false }
13+
env_logger = "0.11.8"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import "../../../just/libafl.just"
2+
FUZZER_NAME := "qemu_intel_pt_bootloader"
3+
BIOS_DIR := TARGET_DIR / PROFILE_DIR / "qemu-libafl-bridge/build/qemu-bundle/usr/local/share/qemu"
4+
5+
run: build setcap convert_target_image
6+
BIOS_DIR={{BIOS_DIR}} {{TARGET_DIR}}/{{PROFILE_DIR}}/{{FUZZER_NAME}}
7+
sudo umount /mnt/libafl_qemu_tmpfs
8+
sleep 1
9+
sudo rm -r /mnt/libafl_qemu_tmpfs
10+
11+
# Create target directory if it doesn't exist
12+
[private]
13+
target_dir:
14+
@mkdir -p {{TARGET_DIR}}
15+
16+
# Setup RAM disk to store the qcow2 disk
17+
ram_disk:
18+
sudo mkdir -p /mnt/libafl_qemu_tmpfs
19+
sudo mount -o size=128M -t tmpfs none /mnt/libafl_qemu_tmpfs
20+
sudo chown $(id -u):$(id -g) "/mnt/libafl_qemu_tmpfs"
21+
22+
# Build the bootloader
23+
build_target: target_dir
24+
nasm -o {{TARGET_DIR}}/boot.bin ./src/boot.s
25+
26+
build_fuzzer:
27+
cargo build --profile {{PROFILE}}
28+
29+
build: build_fuzzer build_target
30+
31+
# Convert bootloader bin to qcow2 image
32+
convert_target_image: build_target ram_disk
33+
qemu-img convert -O qcow2 {{TARGET_DIR}}/boot.bin /mnt/libafl_qemu_tmpfs/boot.qcow2
34+
35+
# Set capabilities on the binary
36+
setcap: build_fuzzer
37+
sudo setcap cap_ipc_lock,cap_sys_ptrace,cap_sys_admin,cap_syslog=ep {{TARGET_DIR}}/{{PROFILE_DIR}}/{{FUZZER_NAME}}
38+
39+
test: build
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Bootloader Fuzzing in QEMU/KVM with Intel Pt Tracing
2+
3+
A minimalistic example about how to create a LibAFL based fuzzer with Intel
4+
PT tracing using QEMU/KVM to target a bootloader. The target is a nasty x86
5+
bootloader that if detects a specific BIOS version, it hangs forever. The
6+
fuzzer runs until it finds the right input for which the bootloader tries to
7+
hang and then it exits.
8+
9+
During execution the fuzzer prints some statistics to the terminal, like the
10+
number of executions and corpus size (the number of inputs the fuzzer marked
11+
as interesting so far). At the end of the execution, the input causing the
12+
crash is saved to the `crashes/` folder and printed to the terminal.
13+
14+
## How to build from source
15+
16+
You can build from source running `just` to build and then run the fuzzer
17+
(requires `just`, `qemu` and `nasm` installed):
18+
19+
This command requires to run `sudo` to give the fuzzer the necessary
20+
capabilities to use hardware tracing, you may have to enter `root` password.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
[bits 16] ; use 16 bits
2+
[org 0x7c00] ; sets the start address
3+
4+
%macro print_string 1 ; %1: Pointer to the string (null-terminated)
5+
mov si, %1 ; Load the pointer to the string
6+
.print_char:
7+
lodsb ; Load the next byte from [SI] into AL
8+
or al, al ; Check if it's the null terminator
9+
jz .done ; If zero, we are done
10+
mov ah, 0x0E ; BIOS teletype function
11+
int 0x10 ; Call BIOS interrupt
12+
jmp .print_char ; Repeat for the next character
13+
.done:
14+
mov al, 0x0d ; CR
15+
int 0x10
16+
mov al, 0x0a ; LF
17+
int 0x10
18+
%endmacro
19+
20+
start:
21+
mov ah, 0xc0
22+
int 0x15 ; ask for the system configuration parameters
23+
jc fail ; carry must be 0
24+
cmp ah, 0 ; ah must be 0
25+
jne fail
26+
27+
mov ax, [es:bx] ; byte count of the system configuration parameters
28+
cmp ax, 8
29+
jl fail
30+
31+
mov ch, [es:bx + 2] ; Model
32+
mov cl, [es:bx + 3] ; Submodel
33+
mov dh, [es:bx + 4] ; BIOS revision
34+
35+
cmp ch, 'a'
36+
jne fail
37+
cmp cl, 'b'
38+
jne fail
39+
cmp dh, 'c'
40+
jne fail
41+
42+
shutdown:
43+
print_string bye
44+
45+
; sleep a bit to make sure output is printed
46+
xor cx, cx
47+
mov dx, 0xffff
48+
mov ah, 0x86
49+
int 0x15
50+
51+
; actual shutdown
52+
mov ax, 0x1000
53+
mov ax, ss
54+
mov sp, 0xf000
55+
mov ax, 0x5307
56+
mov bx, 0x0001
57+
mov cx, 0x0003
58+
int 0x15
59+
60+
fail:
61+
print_string fail_msg
62+
sleep_forever:
63+
mov cx, 0xffff
64+
mov dx, 0xffff
65+
mov ah, 0x86
66+
int 0x15
67+
jmp sleep_forever
68+
69+
fail_msg db "I don't like your BIOS. :(", 0
70+
bye db "Artificial bug triggered =)", 0
71+
72+
times 510-($-$$) db 0 ; fill the output file with zeroes until 510 bytes are full
73+
74+
dw 0xaa55 ; magic bytes that tell BIOS that this is bootable
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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

Comments
 (0)