Struct hyperpom::coverage::GlobalCoverage
source · pub struct GlobalCoverage {
pub inner: Arc<RwLock<GlobalCoverageInner>>,
}
Expand description
Global coverage structure shared between threads and updated after each testcase run.
Role of Coverage in the Fuzzer
Coverage-guided fuzzing is used in a lot of modern fuzzers nowadays (e.g. LibFuzzer, HonggFuzz, etc.). The idea is to gather information during the execution of a program to identify which parts have actually been executed. The information gathered can be of different types (addresses, stack frames generated, etc.) and so do gathering methods (hardware mechanisms, hooks added at compile-time / runtime, etc.).
While using coverage is not necessary and can hinder performances, it’s a decent strategy for a generic fuzzer unaware of the input formats expected by the program, since the generated data can be used to identify interesting testcases (e.g. if they cover new paths).
Coverage Implementation
Coverage
Coverage is implemented in this fuzzer by hooking instructions that perform arbitrary or conditional branching, namely:
- all the
b.cond
instructions; cbz
andcbnz
;tbz
andtbnz
;blr
andbr
.
The hooking function used for coverage is GlobalCoverage::hook_coverage
and is placed while
initializing the fuzzed program’s virtual space using GlobalCoverage::add_hooks
.
Note: for more information on hooking, you can refer to Hooks
.
Although, placing these hooks is a little bit complicated on ARM systems: it’s possible to
have data sections, such as literal pools, wove into code sections. If we were to
indiscriminately place hooks on any byte sequences that can be disassembled into the
instructions listed above, we could corrupt one of these data sections, which might result in
unwanted crashes or undefined behaviours. To prevent this from happening, the user is
responsible for defining the ranges where coverage should be applied by implementing
Loader::code_ranges
from the
Loader
trait. This function will return a list of CoverageRange
s
defining the virtual address ranges that are safe to hook.
Once a program is running on a worker, coverage hooks that are hit take the current address
and store it into a Coverage
object. Each worker owns an instance of this structure.
To make fuzzing more efficient, coverage information are shared between all workers using
GlobalCoverage
. At the end of each iteration, a worker take the coverage information that
was generated from the current testcase and compare it to the global one. If new paths have
been found, the testcase is added to the corpus so it can be reused and mutated to reach
increasingly deeper execution paths.
Additionally, since the only information we’re interested in here is whether or not new paths
have been hit, keeping hooks for paths that have already been covered is redundant. We can then
get better performances by removing coverage hooks corresponding to new paths using
Hooks::revert_coverage_hooks
.
Comparison Unrolling
One of the main roadblocks to overcome while fuzzing is handling comparisons with constant
magic values. For example, imagine that we have a program that checks that our input starts
with 0xdeadbeef
before processing it.
if u32::from_le_bytes(input[0..4]) == 0xdeadbeef {
process();
} else {
exit();
}
The fuzzer would have to guess 32 bits in a single iteration, which is very unlikely and would stall the fuzzer until it guesses it correctly.
There are different approaches possible to solve this problem, like hooking comparisons to always pass them or informing the mutator of the comparison value to generate a custom testcase with it. This fuzzer implements comparison unrolling. The idea is to take a comparison on a multi-byte value and split it into multiple single-byte comparisons.
if input[3] == 0xde {
if input[2] == 0xad {
if input[1] == 0xbe {
// [...]
}
}
}
When the fuzzer is initialized, it looks for every comparison instructions it can find and
then place a GlobalCoverage::hook_cmp
hook on them. This function takes the hooked
comparison instruction, disassemble it to retrieve the values being compared and adds a path
for every byte it manages to guess correctly.
One of the issue with this implementation is that it adds new testcases for input values that partially match. Once we have a testcase that pass the comparison, the other intermediate testcases are less likely to produce interesting results and are mostly clogging up the input queue at this point. A possible way to improve the fuzzer in the future would be to add an additional queue for these intermediate testcases that would be flushed when the comparison is no longer an issue.
Fields
inner: Arc<RwLock<GlobalCoverageInner>>
Implementations
sourceimpl GlobalCoverage
impl GlobalCoverage
sourcepub fn new(ranges: Vec<CoverageRange>) -> Self
pub fn new(ranges: Vec<CoverageRange>) -> Self
Instanciantes a new shared global coverage structure using user-provided virtual memory ranges where coverage should be applied.
sourcepub fn add_hooks<LD: Clone, GD: Clone>(
&self,
vma: &VirtMemAllocator,
hooks: &mut Hooks<LD, GD>,
comparison_unrolling: bool
) -> Result<()>
pub fn add_hooks<LD: Clone, GD: Clone>(
&self,
vma: &VirtMemAllocator,
hooks: &mut Hooks<LD, GD>,
comparison_unrolling: bool
) -> Result<()>
Adds coverage hooks to the fuzzed program.
There are two hook types:
- coverage hooks that updates the global coverage map;
- comparison hooks that perform comparison unrolling and update the global coverage map when a new byte is matched.
sourcepub fn update_new_crashes(&mut self, hash: u64) -> bool
pub fn update_new_crashes(&mut self, hash: u64) -> bool
Checks if the crash already exists in the list of known crashes and inserts it if it’s
not the case. Returns true
if the crash is new, false
otherwise.
sourcepub fn update_new_coverage(&self, other: &Coverage) -> Option<u64>
pub fn update_new_coverage(&self, other: &Coverage) -> Option<u64>
Checks if the coverage computed by the current worker adds new path to the global coverage.
If it’s the case, the function adds the new paths covered to the global coverage map and
returns true
to signal that the current testcase yielded interesting results and should
be added to the corpus.
sourcepub fn hook_coverage<LD, GD>(
args: &mut HookArgs<'_, LD, GD>
) -> Result<ExitKind>
pub fn hook_coverage<LD, GD>(
args: &mut HookArgs<'_, LD, GD>
) -> Result<ExitKind>
Handles coverage hooks and adds the current instruction’s address to the local worker coverage.
Trait Implementations
sourceimpl Clone for GlobalCoverage
impl Clone for GlobalCoverage
sourcefn clone(&self) -> GlobalCoverage
fn clone(&self) -> GlobalCoverage
1.0.0 · sourcefn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source
. Read more