Add DrCovReader to read DrCov files and DrCov dumper and merge utils (#2680)

* Add DrCov Reader

* Removed libafl_jumper deps

* Fix DrCovWriter, add dump_drcov_addrs

* Taplo

* Move frida from usize to u64

* DrCov usize=>u64

* Better error print

* More u64

* ?

* debug

* clippy

* clippy

* Add Merge option to DrCovReader

* Add drcov_merge tool

* Move folder around

* DrCov

* More assert

* fmt

* Move around

* Fix print

* Add option to read multiple files/full folders
This commit is contained in:
Dominik Maier 2024-11-12 22:18:57 -03:00 committed by GitHub
parent 0ef0684e43
commit 7fada7d985
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 779 additions and 106 deletions

View File

@ -20,9 +20,10 @@ members = [
"libafl_concolic/test/runtime_test",
"utils/build_and_test_fuzzers",
"utils/deexit",
"utils/drcov_utils",
"utils/gramatron/construct_automata",
"utils/libafl_benches",
"utils/libafl_jumper",
"utils/gramatron/construct_automata",
]
default-members = [
"libafl",

View File

@ -162,7 +162,7 @@ impl FridaRuntime for AsanRuntime {
fn init(
&mut self,
gum: &Gum,
_ranges: &RangeMap<usize, (u16, String)>,
_ranges: &RangeMap<u64, (u16, String)>,
module_map: &Rc<ModuleMap>,
) {
self.allocator.init();

View File

@ -124,7 +124,7 @@ impl FridaRuntime for CmpLogRuntime {
fn init(
&mut self,
_gum: &frida_gum::Gum,
_ranges: &RangeMap<usize, (u16, String)>,
_ranges: &RangeMap<u64, (u16, String)>,
_module_map: &Rc<ModuleMap>,
) {
self.generate_instrumentation_blobs();

View File

@ -37,7 +37,7 @@ impl FridaRuntime for CoverageRuntime {
fn init(
&mut self,
_gum: &frida_gum::Gum,
_ranges: &RangeMap<usize, (u16, String)>,
_ranges: &RangeMap<u64, (u16, String)>,
_module_map: &Rc<ModuleMap>,
) {
}

View File

@ -23,7 +23,7 @@ pub struct DrCovRuntime {
/// The basic blocks of this execution
pub drcov_basic_blocks: Vec<DrCovBasicBlock>,
/// The memory ranges of this target
ranges: RangeMap<usize, (u16, String)>,
ranges: RangeMap<u64, (u16, String)>,
coverage_directory: PathBuf,
}
@ -32,7 +32,7 @@ impl FridaRuntime for DrCovRuntime {
fn init(
&mut self,
_gum: &frida_gum::Gum,
ranges: &RangeMap<usize, (u16, String)>,
ranges: &RangeMap<u64, (u16, String)>,
_module_map: &Rc<ModuleMap>,
) {
self.ranges = ranges.clone();
@ -61,8 +61,8 @@ impl FridaRuntime for DrCovRuntime {
let mut coverage_hasher = RandomState::with_seeds(0, 0, 0, 0).build_hasher();
for bb in &self.drcov_basic_blocks {
coverage_hasher.write_usize(bb.start);
coverage_hasher.write_usize(bb.end);
coverage_hasher.write_u64(bb.start);
coverage_hasher.write_u64(bb.end);
}
let coverage_hash = coverage_hasher.finish();

View File

@ -189,10 +189,10 @@ where
let mut ranges = helper.ranges().clone();
for module in frida_gum::Module::obtain(gum).enumerate_modules() {
if module.base_address < Self::new as usize
&& (Self::new as usize) < module.base_address + module.size
&& (Self::new as usize as u64) < module.base_address as u64 + module.size as u64
{
ranges.insert(
module.base_address..(module.base_address + module.size),
module.base_address as u64..(module.base_address as u64 + module.size as u64),
(0xffff, "fuzzer".to_string()),
);
break;
@ -201,11 +201,13 @@ where
log::info!("disable_excludes: {:}", helper.disable_excludes);
if !helper.disable_excludes {
for range in ranges.gaps(&(0..usize::MAX)) {
for range in ranges.gaps(&(0..u64::MAX)) {
log::info!("excluding range: {:x}-{:x}", range.start, range.end);
stalker.exclude(&MemoryRange::new(
NativePointer(range.start as *mut c_void),
range.end - range.start,
usize::try_from(range.end - range.start).unwrap_or_else(|err| {
panic!("Address out of usize range: {range:?} - {err}")
}),
));
}
}

View File

@ -42,7 +42,7 @@ pub trait FridaRuntime: 'static + Debug {
fn init(
&mut self,
gum: &Gum,
ranges: &RangeMap<usize, (u16, String)>,
ranges: &RangeMap<u64, (u16, String)>,
module_map: &Rc<ModuleMap>,
);
/// Deinitialization
@ -61,7 +61,7 @@ pub trait FridaRuntimeTuple: MatchFirstType + Debug {
fn init_all(
&mut self,
gum: &Gum,
ranges: &RangeMap<usize, (u16, String)>,
ranges: &RangeMap<u64, (u16, String)>,
module_map: &Rc<ModuleMap>,
);
@ -79,7 +79,7 @@ impl FridaRuntimeTuple for () {
fn init_all(
&mut self,
_gum: &Gum,
_ranges: &RangeMap<usize, (u16, String)>,
_ranges: &RangeMap<u64, (u16, String)>,
_module_map: &Rc<ModuleMap>,
) {
}
@ -101,7 +101,7 @@ where
fn init_all(
&mut self,
gum: &Gum,
ranges: &RangeMap<usize, (u16, String)>,
ranges: &RangeMap<u64, (u16, String)>,
module_map: &Rc<ModuleMap>,
) {
self.0.init(gum, ranges, module_map);
@ -317,20 +317,23 @@ impl FridaInstrumentationHelperBuilder {
module.range().base_address().0 as usize
);
let range = module.range();
let start = range.base_address().0 as usize;
ranges
.borrow_mut()
.insert(start..(start + range.size()), (i as u16, module.path()));
let start = range.base_address().0 as u64;
ranges.borrow_mut().insert(
start..(start + range.size() as u64),
(i as u16, module.path()),
);
}
for skip in skip_ranges {
match skip {
SkipRange::Absolute(range) => ranges.borrow_mut().remove(range),
SkipRange::Absolute(range) => ranges
.borrow_mut()
.remove(range.start as u64..range.end as u64),
SkipRange::ModuleRelative { name, range } => {
let module_details = ModuleDetails::with_name(name).unwrap();
let lib_start = module_details.range().base_address().0 as usize;
ranges
.borrow_mut()
.remove((lib_start + range.start)..(lib_start + range.end));
let lib_start = module_details.range().base_address().0 as u64;
ranges.borrow_mut().remove(
(lib_start + range.start as u64)..(lib_start + range.end as u64),
);
}
}
}
@ -388,7 +391,7 @@ impl Default for FridaInstrumentationHelperBuilder {
/// An helper that feeds `FridaInProcessExecutor` with edge-coverage instrumentation
pub struct FridaInstrumentationHelper<'a, RT: 'a> {
transformer: Transformer<'a>,
ranges: Rc<RefCell<RangeMap<usize, (u16, String)>>>,
ranges: Rc<RefCell<RangeMap<u64, (u16, String)>>>,
runtimes: Rc<RefCell<RT>>,
stalker_enabled: bool,
pub(crate) disable_excludes: bool,
@ -491,7 +494,7 @@ where
#[allow(clippy::too_many_lines)]
fn build_transformer(
gum: &'a Gum,
ranges: &Rc<RefCell<RangeMap<usize, (u16, String)>>>,
ranges: &Rc<RefCell<RangeMap<u64, (u16, String)>>>,
runtimes: &Rc<RefCell<RT>>,
) -> Transformer<'a> {
let ranges = Rc::clone(ranges);
@ -512,7 +515,7 @@ where
fn transform(
basic_block: StalkerIterator,
output: &StalkerOutput,
ranges: &Rc<RefCell<RangeMap<usize, (u16, String)>>>,
ranges: &Rc<RefCell<RangeMap<u64, (u16, String)>>>,
runtimes_unborrowed: &Rc<RefCell<RT>>,
decoder: InstDecoder,
) {
@ -525,7 +528,7 @@ where
let address = instr.address();
// log::trace!("x - block @ {:x} transformed to {:x}", address, output.writer().pc());
//the ASAN check needs to be done before the hook_rt check due to x86 insns such as call [mem]
if ranges.borrow().contains_key(&(address as usize)) {
if ranges.borrow().contains_key(&address) {
let mut runtimes = (*runtimes_unborrowed).borrow_mut();
if first {
first = false;
@ -634,8 +637,8 @@ where
{
log::trace!("{basic_block_start:#016X}:{basic_block_size:X}");
rt.drcov_basic_blocks.push(DrCovBasicBlock::new(
basic_block_start as usize,
basic_block_start as usize + basic_block_size,
basic_block_start,
basic_block_start + basic_block_size as u64,
));
}
}
@ -697,7 +700,7 @@ where
pub fn init(
&mut self,
gum: &'a Gum,
ranges: &RangeMap<usize, (u16, String)>,
ranges: &RangeMap<u64, (u16, String)>,
module_map: &Rc<ModuleMap>,
) {
(*self.runtimes)
@ -731,12 +734,12 @@ where
/// Ranges
#[must_use]
pub fn ranges(&self) -> Ref<RangeMap<usize, (u16, String)>> {
pub fn ranges(&self) -> Ref<RangeMap<u64, (u16, String)>> {
self.ranges.borrow()
}
/// Mutable ranges
pub fn ranges_mut(&mut self) -> RefMut<RangeMap<usize, (u16, String)>> {
pub fn ranges_mut(&mut self) -> RefMut<RangeMap<u64, (u16, String)>> {
(*self.ranges).borrow_mut()
}
}

View File

@ -40,7 +40,7 @@ libafl_bolts::impl_serdeany!(DrCovMetadata);
#[derive(Debug)]
pub struct DrCovModuleBuilder<F> {
filter: Option<F>,
module_mapping: Option<RangeMap<usize, (u16, String)>>,
module_mapping: Option<RangeMap<u64, (u16, String)>>,
filename: Option<PathBuf>,
full_trace: Option<bool>,
}
@ -68,7 +68,7 @@ where
}
#[must_use]
pub fn module_mapping(self, module_mapping: RangeMap<usize, (u16, String)>) -> Self {
pub fn module_mapping(self, module_mapping: RangeMap<u64, (u16, String)>) -> Self {
Self {
filter: self.filter,
module_mapping: Some(module_mapping),
@ -101,7 +101,7 @@ where
#[derive(Debug)]
pub struct DrCovModule<F> {
filter: F,
module_mapping: Option<RangeMap<usize, (u16, String)>>,
module_mapping: Option<RangeMap<u64, (u16, String)>>,
filename: PathBuf,
full_trace: bool,
drcov_len: usize,
@ -124,7 +124,7 @@ impl<F> DrCovModule<F> {
pub fn new(
filter: F,
filename: PathBuf,
module_mapping: Option<RangeMap<usize, (u16, String)>>,
module_mapping: Option<RangeMap<u64, (u16, String)>>,
full_trace: bool,
) -> Self {
if full_trace {
@ -168,11 +168,12 @@ impl<F> DrCovModule<F> {
continue 'pcs_full;
}
if *idm == *id {
#[allow(clippy::unnecessary_cast)] // for GuestAddr -> u64
match lengths.get(pc) {
Some(block_length) => {
drcov_vec.push(DrCovBasicBlock::new(
*pc as usize,
*pc as usize + *block_length as usize,
*pc as u64,
*pc as u64 + *block_length as u64,
));
}
None => {
@ -215,11 +216,13 @@ impl<F> DrCovModule<F> {
if !module_found {
continue 'pcs;
}
#[allow(clippy::unnecessary_cast)] // for GuestAddr -> u64
match lengths.get(pc) {
Some(block_length) => {
drcov_vec.push(DrCovBasicBlock::new(
*pc as usize,
*pc as usize + *block_length as usize,
*pc as u64,
*pc as u64 + *block_length as u64,
));
}
None => {
@ -282,13 +285,14 @@ where
let qemu = emulator_modules.qemu();
let mut module_mapping: RangeMap<usize, (u16, String)> = RangeMap::new();
let mut module_mapping: RangeMap<u64, (u16, String)> = RangeMap::new();
#[allow(clippy::unnecessary_cast)] // for GuestAddr -> u64
for (i, (r, p)) in qemu
.mappings()
.filter_map(|m| {
m.path()
.map(|p| ((m.start() as usize)..(m.end() as usize), p.to_string()))
.map(|p| ((m.start() as u64)..(m.end() as u64), p.to_string()))
.filter(|(_, p)| !p.is_empty())
})
.enumerate()

View File

@ -1,59 +1,112 @@
//! [`DrCov`](https://dynamorio.org/page_drcov.html) support for `LibAFL` `FRIDA` mode.
//!
//! It's writing basic-block trace files to be read by coverage analysis tools, such as [Lighthouse](https://github.com/gaasedelen/lighthouse),
//! [bncov](https://github.com/ForAllSecure/bncov), [dragondance](https://github.com/0ffffffffh/dragondance), etc.
//! [bncov](https://github.com/ForAllSecure/bncov), [cartographer](https://github.com/nccgroup/Cartographer), etc.
use alloc::{string::String, vec::Vec};
use core::{fmt::Debug, num::ParseIntError, ptr};
use std::{
fs::File,
io::{BufWriter, Write},
path::Path,
io::{BufRead, BufReader, BufWriter, Read, Write},
path::{Path, PathBuf},
};
use hashbrown::HashSet;
use libafl::Error;
use rangemap::RangeMap;
/// A basic block struct
/// This can be used to keep track of new addresses.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DrCovBasicBlock {
/// Start of this basic block
pub start: usize,
pub start: u64,
/// End of this basic block
pub end: usize,
pub end: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// A (Raw) Basic Block List Entry.
/// This is only relevant in combination with a [`DrCovReader`] or a [`DrCovWriter`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(C)]
struct DrCovBasicBlockEntry {
start: u32,
pub struct DrCovBasicBlockEntry {
/// Start of this basic block
pub start: u32,
/// Size of this basic block
size: u16,
/// The id of the `DrCov` module this block is in
mod_id: u16,
}
impl From<&[u8; 8]> for DrCovBasicBlockEntry {
fn from(value: &[u8; 8]) -> Self {
// # Safety
// The value is a valid u8 pointer.
// There's a chance that the value is not aligned to 32 bit, so we use `read_unaligned`.
assert_eq!(
size_of::<DrCovBasicBlockEntry>(),
size_of::<[u8; 8]>(),
"`DrCovBasicBlockEntry` size changed!"
);
unsafe { ptr::read_unaligned(ptr::from_ref(value) as *const DrCovBasicBlockEntry) }
}
}
impl From<DrCovBasicBlockEntry> for [u8; 8] {
fn from(value: DrCovBasicBlockEntry) -> Self {
// # Safety
// The value is a c struct.
// Casting its pointer to bytes should be safe.
// The resulting pointer needs to be less aligned.
assert_eq!(
size_of::<DrCovBasicBlockEntry>(),
size_of::<[u8; 8]>(),
"`DrCovBasicBlockEntry` size changed!"
);
unsafe { std::slice::from_raw_parts(ptr::from_ref(&value).cast::<u8>(), 8) }
.try_into()
.unwrap()
}
}
impl From<&DrCovBasicBlockEntry> for &[u8] {
fn from(value: &DrCovBasicBlockEntry) -> Self {
// # Safety
// The value is a c struct.
// Casting its pointer to bytes should be safe.
unsafe {
std::slice::from_raw_parts(
ptr::from_ref(value).cast::<u8>(),
size_of::<DrCovBasicBlockEntry>(),
)
}
}
}
/// A writer for `DrCov` files
#[derive(Debug)]
pub struct DrCovWriter<'a> {
module_mapping: &'a RangeMap<usize, (u16, String)>,
module_mapping: &'a RangeMap<u64, (u16, String)>,
}
impl DrCovBasicBlock {
/// Create a new [`DrCovBasicBlock`] with the given `start` and `end` addresses.
#[must_use]
pub fn new(start: usize, end: usize) -> Self {
pub fn new(start: u64, end: u64) -> Self {
Self { start, end }
}
/// Create a new [`DrCovBasicBlock`] with a given `start` address and a block size.
#[must_use]
pub fn with_size(start: usize, size: usize) -> Self {
Self::new(start, start + size)
pub fn with_size(start: u64, size: usize) -> Self {
Self::new(start, start + u64::try_from(size).unwrap())
}
}
impl<'a> DrCovWriter<'a> {
/// Create a new [`DrCovWriter`]
#[must_use]
pub fn new(module_mapping: &'a RangeMap<usize, (u16, String)>) -> Self {
pub fn new(module_mapping: &'a RangeMap<u64, (u16, String)>) -> Self {
Self { module_mapping }
}
@ -63,49 +116,496 @@ impl<'a> DrCovWriter<'a> {
P: AsRef<Path>,
{
let mut writer = BufWriter::new(File::create(path)?);
let modules = self.module_entries();
writer.write_all(b"DRCOV VERSION: 2\nDRCOV FLAVOR: libafl\n")?;
writer
.write_all(b"DRCOV VERSION: 2\nDRCOV FLAVOR: libafl\n")
.unwrap();
let modules: Vec<(&std::ops::Range<usize>, &(u16, String))> =
self.module_mapping.iter().collect();
writer
.write_all(format!("Module Table: version 2, count {}\n", modules.len()).as_bytes())
.unwrap();
writer
.write_all(b"Columns: id, base, end, entry, checksum, timestamp, path\n")
.unwrap();
.write_all(format!("Module Table: version 2, count {}\n", modules.len()).as_bytes())?;
writer.write_all(b"Columns: id, base, end, entry, checksum, timestamp, path\n")?;
for module in modules {
let (range, (id, path)) = module;
writer
.write_all(
format!(
"{:03}, 0x{:x}, 0x{:x}, 0x00000000, 0x00000000, 0x00000000, {}\n",
id, range.start, range.end, path
)
.as_bytes(),
)
.unwrap();
writer.write_all(module.to_module_line().as_bytes())?;
writer.write_all(b"\n")?;
}
writer
.write_all(format!("BB Table: {} bbs\n", basic_blocks.len()).as_bytes())
.unwrap();
for block in basic_blocks {
let (range, (id, _)) = self.module_mapping.get_key_value(&block.start).unwrap();
let basic_block = DrCovBasicBlockEntry {
start: (block.start - range.start) as u32,
size: (block.end - block.start) as u16,
mod_id: *id,
};
writer
.write_all(unsafe {
std::slice::from_raw_parts(&raw const (basic_block) as *const u8, 8)
})
.unwrap();
writer.write_all(format!("BB Table: {} bbs\n", basic_blocks.len()).as_bytes())?;
for block in self.basic_block_entries(basic_blocks) {
writer.write_all((&block).into()).unwrap();
}
writer.flush()?;
Ok(())
}
/// Gets a [`Vec`] of all [`DrCovModuleEntry`] elements in this [`DrCovWriter`].
#[must_use]
pub fn module_entries(&self) -> Vec<DrCovModuleEntry> {
self.module_mapping
.iter()
.map(|x| {
let (range, (id, path)) = x;
DrCovModuleEntry {
id: *id,
base: range.start,
end: range.end,
entry: 0,
checksum: 0,
timestamp: 0,
path: PathBuf::from(path),
}
})
.collect()
}
/// Gets a [`Vec`] of all [`DrCovBasicBlockEntry`] elements from a list of [`DrCovBasicBlock`] entries using the modules from this [`DrCovWriter`].
#[must_use]
pub fn basic_block_entries(
&self,
basic_blocks: &[DrCovBasicBlock],
) -> Vec<DrCovBasicBlockEntry> {
let mut ret = Vec::with_capacity(basic_blocks.len());
for block in basic_blocks {
let (range, (id, _)) = self
.module_mapping
.get_key_value(&block.start)
.unwrap_or_else(|| {
panic!(
"Could not read module at addr {:?}. Module list: {:?}.",
block.start, self.module_mapping
)
});
let basic_block = DrCovBasicBlockEntry {
start: (block.start - range.start) as u32,
size: (block.end - block.start) as u16,
mod_id: *id,
};
ret.push(basic_block);
}
ret
}
/// Creates a [`DrCovReader`] module out of this [`DrCovWriter`]
#[must_use]
pub fn to_reader(&self, basic_blocks: &[DrCovBasicBlock]) -> DrCovReader {
let modules = self.module_entries();
let basic_blocks = self.basic_block_entries(basic_blocks);
DrCovReader::from_data(modules, basic_blocks)
}
}
/// An entry in the `DrCov` module list.
#[derive(Debug, Clone)]
pub struct DrCovModuleEntry {
/// The index of this module
pub id: u16,
/// Base of this module
pub base: u64,
/// End address of this module
pub end: u64,
/// Entry (can be zero)
pub entry: usize,
/// Checksum (can be zero)
pub checksum: usize,
/// Timestamp (can be zero)
pub timestamp: usize,
/// The path of this module
pub path: PathBuf,
}
impl DrCovModuleEntry {
/// Gets the module line from this [`DrCovModuleEntry`]
#[must_use]
pub fn to_module_line(&self) -> String {
format!(
"{:03}, 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x}, {:?}",
self.id, self.base, self.end, self.entry, self.checksum, self.timestamp, self.path
)
}
}
/// Read `DrCov` (v2) files created with [`DrCovWriter`] or other tools
pub struct DrCovReader {
/// The modules in this `DrCov` file
pub module_entries: Vec<DrCovModuleEntry>,
/// The list of basic blocks as [`DrCovBasicBlockEntry`].
/// To get the blocks as [`DrCovBasicBlock`], call [`Self::basic_blocks`] instead.
pub basic_block_entries: Vec<DrCovBasicBlockEntry>,
}
impl Debug for DrCovReader {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("DrCovReader")
.field("modules", &self.module_entries)
.field("basic_blocks", &self.basic_block_entries.len())
.finish()
}
}
fn parse_hex_to_usize(str: &str) -> Result<usize, ParseIntError> {
// Cut off the first 0x
usize::from_str_radix(&str[2..], 16)
}
fn parse_hex_to_u64(str: &str) -> Result<u64, ParseIntError> {
// Cut off the first 0x
u64::from_str_radix(&str[2..], 16)
}
impl DrCovReader {
/// Parse a `drcov` file to memory.
pub fn read<P: AsRef<Path> + ?Sized>(file: &P) -> Result<Self, Error> {
let f = File::open(file)?;
let mut reader = BufReader::new(f);
let mut header = String::new();
reader.read_line(&mut header)?;
let drcov_version = "DRCOV VERSION: 2";
if header.to_uppercase().trim() != drcov_version {
return Err(Error::illegal_state(format!(
"No valid header. Expected {drcov_version} but got {header}"
)));
}
header.clear();
reader.read_line(&mut header)?;
let drcov_flavor = "DRCOV FLAVOR:";
if header.to_uppercase().starts_with(drcov_flavor) {
// Ignore flavor line if it's not present.
log::info!("Got drcov flavor {drcov_flavor}");
header.clear();
reader.read_line(&mut header)?;
}
let Some(Ok(module_count)) = header
.split("Module Table: version 2, count ")
.nth(1)
.map(|x| x.trim().parse::<usize>())
else {
return Err(Error::illegal_state(format!(
"Expected module table but got: {header}"
)));
};
header.clear();
reader.read_line(&mut header)?;
if !header.starts_with("Columns: id, base, end, entry, checksum, timestamp, path") {
return Err(Error::illegal_state(format!(
"Module table has unknown or illegal columns: {header}"
)));
}
let mut modules = Vec::with_capacity(module_count);
for _ in 0..module_count {
header.clear();
reader.read_line(&mut header)?;
let err = |x| {
Error::illegal_argument(format!(
"Unexpected module entry while parsing {x} in header: {header}"
))
};
let mut split = header.split(", ");
let Some(Ok(id)) = split.next().map(str::parse) else {
return Err(err("id"));
};
let Some(Ok(base)) = split.next().map(parse_hex_to_u64) else {
return Err(err("base"));
};
let Some(Ok(end)) = split.next().map(parse_hex_to_u64) else {
return Err(err("end"));
};
let Some(Ok(entry)) = split.next().map(parse_hex_to_usize) else {
return Err(err("entry"));
};
let Some(Ok(checksum)) = split.next().map(parse_hex_to_usize) else {
return Err(err("checksum"));
};
let Some(Ok(timestamp)) = split.next().map(parse_hex_to_usize) else {
return Err(err("timestamp"));
};
let Some(path) = split.next().map(|s| PathBuf::from(s.trim())) else {
return Err(err("path"));
};
modules.push(DrCovModuleEntry {
id,
base,
end,
entry,
checksum,
timestamp,
path,
});
}
header.clear();
reader.read_line(&mut header)?;
//"BB Table: {} bbs\n"
if !header.starts_with("BB Table: ") {
return Err(Error::illegal_state(format!(
"Error reading BB Table header. Got: {header}"
)));
}
let mut bb = header.split(' ');
let Some(Ok(bb_count)) = bb.nth(2).map(str::parse) else {
return Err(Error::illegal_state(format!(
"Error parsing BB Table header count. Got: {header}"
)));
};
let mut basic_blocks = Vec::with_capacity(bb_count);
for _ in 0..bb_count {
let mut bb_entry = [0_u8; 8];
reader.read_exact(&mut bb_entry)?;
basic_blocks.push((&bb_entry).into());
}
Ok(DrCovReader {
module_entries: modules,
basic_block_entries: basic_blocks,
})
}
/// Creates a [`DrCovReader`] pre-filled with data.
/// Rather pointless, use [`Self::read`] to actually read a file from disk.
#[must_use]
pub fn from_data(
modules: Vec<DrCovModuleEntry>,
basic_blocks: Vec<DrCovBasicBlockEntry>,
) -> Self {
Self {
module_entries: modules,
basic_block_entries: basic_blocks,
}
}
/// Get a list of traversed [`DrCovBasicBlock`] nodes
#[must_use]
pub fn basic_blocks(&self) -> Vec<DrCovBasicBlock> {
let mut ret = Vec::with_capacity(self.basic_block_entries.len());
for basic_block in &self.basic_block_entries {
let bb_id = basic_block.mod_id;
if let Some(module) = self.module_by_id(bb_id) {
let start = module.base + u64::from(basic_block.start);
let end = start + u64::from(basic_block.size);
ret.push(DrCovBasicBlock::new(start, end));
} else {
log::error!("Skipping basic block outside of any modules: {basic_block:?}");
}
}
ret
}
/// Get the module (range) map. This can be used to create a new [`DrCovWriter`].
#[must_use]
pub fn module_map(&self) -> RangeMap<u64, (u16, String)> {
let mut ret = RangeMap::new();
for module in &self.module_entries {
ret.insert(
module.base..module.end,
(
module.id,
module.path.clone().into_os_string().into_string().unwrap(),
),
);
}
ret
}
/// Writes this data out to disk (again).
pub fn write<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
let ranges = self.module_map();
let mut writer = DrCovWriter::new(&ranges);
writer.write(path, &self.basic_blocks())
}
/// Gets a list of all basic blocks, as absolute addresses, for u64 targets.
/// Useful for example for [`JmpScare`](https://github.com/fgsect/JMPscare) and other analyses.
#[must_use]
pub fn basic_block_addresses_u64(&self) -> Vec<u64> {
self.basic_blocks().iter().map(|x| x.start).collect()
}
/// Gets a list of all basic blocks, as absolute addresses, for u32 targets.
/// Will return an [`Error`] if addresses are larger than 32 bit.
pub fn basic_block_addresses_u32(&self) -> Result<Vec<u32>, Error> {
let blocks = self.basic_blocks();
let mut ret = Vec::with_capacity(blocks.len());
for block in self.basic_blocks() {
ret.push(u32::try_from(block.start)?);
}
Ok(ret)
}
/// Merges the contents of another [`DrCovReader`] instance into this one.
/// Useful to merge multiple coverage files of a fuzzing run into one drcov file.
/// Similar to [drcov-merge](https://github.com/vanhauser-thc/drcov-merge).
///
/// If `unique` is set to 1, each block will end up in the resulting [`DrCovReader`] at most once.
///
/// Will return an `Error` if the individual modules are not mergable.
/// In this case, the module list may already have been changed.
pub fn merge(&mut self, other: &DrCovReader, unique: bool) -> Result<(), Error> {
for module in &other.module_entries {
if let Some(own_module) = self.module_by_id(module.id) {
// Module exists, make sure it's the same.
if own_module.base != module.base || own_module.end != module.end {
return Err(Error::illegal_argument(format!("Module id of file to merge doesn't fit! Own modules: {:#x?}, other modules: {:#x?}", self.module_entries, other.module_entries)));
}
} else {
// We don't know the module. Insert as new module.
self.module_entries.push(module.clone());
}
}
if unique {
self.make_unique();
}
let mut blocks = HashSet::new();
for block in &self.basic_block_entries {
blocks.insert(*block);
}
for block in &other.basic_block_entries {
if !blocks.contains(block) {
blocks.insert(*block);
self.basic_block_entries.push(*block);
}
}
Ok(())
}
/// Remove blocks that exist more than once in the trace, in-place.
pub fn make_unique(&mut self) {
let mut blocks = HashSet::new();
let new_vec = self
.basic_block_entries
.iter()
.filter(|x| {
if blocks.contains(x) {
false
} else {
blocks.insert(*x);
true
}
})
.copied()
.collect();
drop(blocks);
self.basic_block_entries = new_vec;
}
/// Returns the module for a given `id`, or [`None`].
#[must_use]
pub fn module_by_id(&self, id: u16) -> Option<&DrCovModuleEntry> {
self.module_entries.iter().find(|module| module.id == id)
}
}
#[cfg(test)]
mod test {
use std::{
env::temp_dir,
fs,
path::PathBuf,
string::{String, ToString},
};
use rangemap::RangeMap;
use super::{DrCovModuleEntry, DrCovReader, DrCovWriter};
use crate::drcov::{DrCovBasicBlock, DrCovBasicBlockEntry};
#[test]
fn test_write_read_drcov() {
let mut ranges = RangeMap::<u64, (u16, String)>::new();
ranges.insert(0x00..0x4242, (0xffff, "fuzzer".to_string()));
ranges.insert(0x4242..0xFFFF, (0, "Entry0".to_string()));
ranges.insert(0xFFFF..0x424242, (1, "Entry1".to_string()));
let mut writer = DrCovWriter::new(&ranges);
let tmpdir = temp_dir();
let drcov_tmp_file = tmpdir.join("drcov_test.drcov");
writer
.write(
&drcov_tmp_file,
&[
DrCovBasicBlock::new(0x4242, 0x4250),
DrCovBasicBlock::new(0x10, 0x100),
DrCovBasicBlock::new(0x424200, 0x424240),
DrCovBasicBlock::new(0x10, 0x100),
],
)
.unwrap();
let reader = DrCovReader::read(&drcov_tmp_file).unwrap();
assert_eq!(reader.basic_block_entries.len(), 4);
assert_eq!(reader.module_map().len(), 3);
assert_eq!(reader.basic_blocks().len(), 4);
// Let's do one more round :)
reader.write(&drcov_tmp_file).unwrap();
let reader = DrCovReader::read(&drcov_tmp_file).unwrap();
assert_eq!(reader.basic_block_entries.len(), 4);
assert_eq!(reader.module_map().len(), 3);
assert_eq!(reader.basic_blocks().len(), 4);
fs::remove_file(&drcov_tmp_file).unwrap();
}
#[test]
fn test_merge() {
let modules = vec![DrCovModuleEntry {
id: 0,
base: 0,
end: 0x4242,
entry: 0,
checksum: 0,
timestamp: 0,
path: PathBuf::new(),
}];
let basic_blocks1 = vec![DrCovBasicBlockEntry {
mod_id: 0,
start: 0,
size: 42,
}];
let mut basic_blocks2 = basic_blocks1.clone();
basic_blocks2.push(DrCovBasicBlockEntry {
mod_id: 0,
start: 4200,
size: 42,
});
let mut first = DrCovReader::from_data(modules.clone(), basic_blocks1);
let second = DrCovReader::from_data(modules, basic_blocks2);
first.merge(&second, true).unwrap();
assert_eq!(first.basic_block_entries.len(), 2);
}
}

View File

@ -0,0 +1,16 @@
[package]
name = "drcov_utils"
edition = "2021"
version.workspace = true
description = "Utility functions to work with DrCov coverage files"
repository = "https://github.com/AFLplusplus/LibAFL/"
license = "MIT OR Apache-2.0"
categories = ["development-tools"]
keywords = ["fuzzing", "libafl", "drcov"]
[dependencies]
libafl_targets = { path = "../../libafl_targets" }
clap = { workspace = true, features = ["derive", "wrap_help"] }
[lints]
workspace = true

View File

@ -0,0 +1,16 @@
# LibAFL DrCov Utilities
## Dump-DrCov_Addrs
Simple commandline tool to display a list of all basic block addresses in a program.
This information can, for example, be used for further processing such as in [JmpScare](https://github.com/fgsect/JMPscare) or similar.
At the same time this tools shows how easily LibAFL's `DrCov` module can be used to parse coverage files.
Run with `cargo run --release --bin drcov_dump_addrs -- -h`
## DrCov_Merge
A performant clone of [drcov-merge](https://github.com/vanhauser-thc/drcov-merge) using LibAFL's `DrCov` reader.
It can merge multiple DrCov files into a single DrCov file.
Run with `cargo run --release --bin drcov_merge -- -h`

View File

@ -0,0 +1,80 @@
use std::{
fs::{create_dir_all, File},
io::Write,
path::PathBuf,
};
use clap::Parser;
use libafl_targets::drcov::DrCovReader;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
#[allow(clippy::module_name_repetitions)]
#[command(
name = "drcov_dump_addrs",
about,
long_about = "Writes a list of all addresses from a DrCovFile"
)]
pub struct Opt {
#[arg(short, long, help = "DrCov traces to read", required = true)]
pub inputs: Vec<PathBuf>,
#[arg(
short,
long,
help = "Output folder to write address files to. If none is set, this will output all addresses to stdout."
)]
pub out_dir: Option<PathBuf>,
}
fn main() {
let opts = Opt::parse();
if let Some(out_dir) = &opts.out_dir {
if !out_dir.exists() {
if let Err(err) = create_dir_all(out_dir) {
eprint!("Failed to create dir {out_dir:?}: {err:?}");
}
}
assert!(out_dir.is_dir(), "Out_dir {out_dir:?} not a directory!");
}
for input in opts.inputs {
let Ok(drcov) = DrCovReader::read(&input)
.map_err(|err| eprint!("Ignored coverage file {input:?}, reason: {err:?}"))
else {
continue;
};
if let Some(out_dir) = &opts.out_dir {
// Write files to a directory
let out_file = out_dir.join(
input
.file_name()
.expect("File without filename shouldn't exist"),
);
let Ok(mut file) = File::create_new(&out_file).map_err(|err| {
eprintln!("Could not create file {out_file:?} - continuing: {err:?}");
}) else {
continue;
};
println!("Dumping addresses from drcov file {input:?} to {out_file:?}");
for line in drcov.basic_block_addresses_u64() {
file.write_all(format!("{line:#x}\n").as_bytes())
.expect("Could not write to file");
}
} else {
// dump to stdout
println!("# Blocks covered in {input:?}:");
for line in drcov.basic_block_addresses_u64() {
println!("{line:#x}");
}
println!();
}
}
}

View File

@ -0,0 +1,60 @@
use std::path::PathBuf;
use clap::Parser;
use libafl_targets::drcov::DrCovReader;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
#[allow(clippy::module_name_repetitions)]
#[command(
name = "drcov_merge",
about,
long_about = "Merges multiple DrCov coverage files into one"
)]
pub struct Opt {
#[arg(short, long, help = "DrCovFiles to merge", required = true)]
pub inputs: Vec<PathBuf>,
#[arg(short, long, help = "Output DrCov file")]
pub output: PathBuf,
#[arg(
short,
long,
help = "If set, the merged file will contain every block exactly once."
)]
pub unique: bool,
}
fn main() {
let opts = Opt::parse();
assert!(
opts.inputs.len() > 1,
"Need at least two inputs to merge anything."
);
let mut inputs = opts.inputs.iter();
let initial_input = inputs.next().unwrap();
if opts.unique {
println!("Unique block mode");
}
println!("Reading inital drcov file from {initial_input:?}");
let mut main_drcov = DrCovReader::read(initial_input).expect("Failed to read fist input!");
for input in inputs {
if let Ok(current_drcov) = DrCovReader::read(input)
.map_err(|err| eprintln!("Warning: failed to read drcov file at {input:?}: {err:?}"))
{
println!("Merging {input:?}");
if let Err(err) = main_drcov.merge(&current_drcov, opts.unique) {
eprintln!("Warning: failed to merge drcov file at {input:?}: {err:?}");
}
}
}
main_drcov
.write(opts.output)
.expect("Failed to write merged drcov file to output path");
}

View File

@ -14,6 +14,3 @@ std = []
[build-dependencies]
cc = "1"
[dependencies]
hex = { version = "0.4", default-features = false }

View File

@ -5,7 +5,7 @@
use core::ffi::CStr;
#[cfg(not(any(test, feature = "std")))]
use core::panic::PanicInfo;
use core::{arch::asm, ffi::c_void, ops::Shl};
use core::{arch::asm, ffi::c_void};
#[cfg(not(any(test, feature = "std")))]
#[panic_handler]
@ -120,15 +120,9 @@ pub unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> ! {
}
fn decode_hex_and_jmp(hex_string: &str) -> ! {
let mut hex_buf = [0_u8; 8];
let hex_buf = &mut hex_buf[..hex_string.len() / 2];
hex::decode_to_slice(hex_string, hex_buf).unwrap();
let mut addr: u64 = 0;
for val in hex_buf {
addr = addr.shl(8);
addr += u64::from(*val);
}
let Ok(addr) = u64::from_str_radix(hex_string, 16) else {
panic!("Could not parse hex string: {hex_string}");
};
#[cfg(feature = "std")]
println!("Hex: {addr:#x}");