Framework for Automating Fuzzable Target Discovery with Static Analysis
Vulnerability researchers conducting security assessments on software will often harness the capabilities of coverage-guided fuzzing through powerful tools like AFL++ and libFuzzer. This is important as it automates the bughunting process and reveals exploitable conditions in targets quickly. However, when encountering large and complex codebases or closed-source binaries, researchers have to painstakingly dedicate time to manually audit and reverse engineer them to identify functions where fuzzing-based exploration can be useful.
Fuzzable is a framework that integrates both with C/C++ source code and binaries to assist vulnerability researchers in identifying function targets that are viable for fuzzing. This is done by applying several static analysis-based heuristics to pinpoint risky behaviors in the software and the functions that executes them. Researchers can then utilize the framework to generate basic harness templates, which can then be used to hunt for vulnerabilities, or to be integrated as part of a continuous fuzzing pipeline, such as Google's oss-fuzz project.
In addition to running as a standalone tool, Fuzzable is also integrated as a plugin for the Binary Ninja disassembler, with support for other disassembly backends being developed.
Check out the original blog post detailing the tool here, which highlights the technical specifications of the static analysis heuristics and how this tool came about. This tool is also featured at Black Hat Arsenal USA 2022.
Some binary targets may require some sanitizing (ie. signature matching, or identifying functions from inlining), and therefore fuzzable primarily uses Binary Ninja as a disassembly backend because of it's ability to effectively solve these problems. Therefore, it can be utilized both as a standalone tool and plugin.
Since Binary Ninja isn't accessible to all and there may be a demand to utilize for security assessments and potentially scaling up in the cloud, an angr fallback backend is also supported. I anticipate to incorporate other disassemblers down the road as well (priority: Ghidra).
If you have Binary Ninja Commercial, be sure to install the API for standalone headless usage:
$ python3 /Applications/Binary\ Ninja.app/Contents/Resources/scripts/install_api.py
Install with pip
:
$ pip install fuzzable
We use poetry for dependency management and building. To do a manual build, clone the repository with the third-party modules:
$ git clone --recursive https://github.com/ex0dus-0x/fuzzable
To install manually:
$ cd fuzzable/
# without poetry
$ pip install .
# with poetry
$ poetry install
# with poetry for a development virtualenv
$ poetry shell
You can now analyze binaries and/or source code with the tool!
# analyzing a single shared object library binary
$ fuzzable analyze examples/binaries/libbasic.so
# analyzing a single C source file
$ fuzzable analyze examples/source/libbasic.c
# analyzing a workspace with multiple C/C++ files and headers
$ fuzzable analyze examples/source/source_bundle/
fuzzable can be easily installed through the Binary Ninja plugin marketplace by going to Binary Ninja > Manage Plugins
and searching for it. Here is an example of the fuzzable plugin running,
accuracy identifying targets for fuzzing and further vulnerability assessment:
fuzzable comes with various options to help better tune your analysis. More will be supported in future plans and any feature requests made.
To determine fuzzability, fuzzable utilize several heuristics to determine which targets are the most viable to target for dynamic analysis. These heuristics are all weighted differently using the scikit-criteria library, which utilizes multi-criteria decision analysis to determine the best candidates. These metrics and are there weights can be seen here:
Heuristic | Description | Weight |
---|---|---|
Fuzz Friendly Name | Symbol name implies behavior that ingests file/buffer input | 0.3 |
Risky Sinks | Arguments that flow into risky calls (ie memcpy) | 0.3 |
Natural Loops | Number of loops detected with the dominance frontier | 0.05 |
Cyclomatic Complexity | Complexity of function target based on edges + nodes | 0.05 |
Coverage Depth | Number of callees the target traverses into | 0.3 |
As mentioned, check out the technical blog post for a more in-depth look into why and how these metrics are utilized.
Many metrics were largely inspired by Vincenzo Iozzo's original work in 0-knowledge fuzzing.
Every targets you want to analyze is diverse, and fuzzable will not be able to account for every edge case behavior in the program target. Thus, it may be important during analysis to tune these weights appropriately to see if different results make more sense for your use case. To tune these weights in the CLI, simply specify the --score-weights
argument:
$ fuzzable analyze <TARGET> --score-weights=0.2,0.2,0.2,0.2,0.2
By default, fuzzable will filter out function targets based on the following criteria:
static
and aren't exposed through headers.To see calls that got filtered out by fuzzable, set the --list_ignored
flag:
$ fuzzable analyze --list-ignored <TARGET>
In Binary Ninja, you can turn this setting in Settings > Fuzzable > List Ignored Calls
.
In the case that fuzzable falsely filters out important calls that should be analyzed, it is recommended to use --include-*
arguments
to include them during the run:
# include ALL non top-level calls that were filtered out
$ fuzzable analyze --include-nontop <TARGET>
# include specific symbols that were filtered out
$ fuzzable analyze --include-sym <SYM> <TARGET>
In Binary Ninja, this is supported through Settings > Fuzzable > Include non-top level calls
and Symbols to Exclude
.
Now that you have found your ideal candidates to fuzz, fuzzable will also help you generate fuzzing harnesses that are (almost) ready to instrument and compile for use with either a file-based fuzzer (ie. AFL++, Honggfuzz) or in-memory fuzzer (libFuzzer). To do so in the CLI:
# generate harness from a candidate
$ fuzzable create-harness target --symbol-name=some_unsafe_call
# make minimal and necessary modifications to the harness
$ vim target_some_unsafe_call_harness.cpp
# example compilation for AFL-QEMU, which is specified in the comments of the generated harness
$ clang target_some_unsafe_call_harness.cpp -no-pie -o target_some_unsafe_call_harness -ldl
# create your base seeds, ideally should be more well-formed for input
$ mkdir in/
$ echo "seed" >> in/seed
# start black box fuzzing
$ afl-fuzz -Q -m none -i in/ -o out/ -- ./target_some_unsafe_call_harness
If this target is a source codebase, the generic source template will be used.
If the target is a binary, the generic black-box template will be used, which ideally can be used with a fuzzing emulation mode like AFL-QEMU. A copy of the binary will also be created as a shared object if the symbol isn't exported directly to be dlopen
ed using LIEF.
At the moment, this feature is quite rudimentary, as it simply will create a standalone C++ harness populated with the appropriate parameters, and will not auto-generate code that is needed for any runtime behaviors (ie. instantiating and freeing structures). However, the templates created for fuzzable should get still get you running quickly. Here are some ambitious features I would like to implement down the road:
fuzzable supports generating reports in various formats. The current ones that are supported are JSON, CSV and Markdown. This can be useful if you are utilizing this as part of automation where you would like to ingest the output in a serializable format.
In the CLI, simply pass the --export
argument with a filename with the appropriate extension:
$ fuzzable analyze --export=report.json <TARGET>
In Binary Ninja, go to Plugins > Fuzzable > Export Fuzzability Report > ...
and select the format you want to
export to and the path you want to write it to.
This tool will be continuously developed, and any help from external mantainers are appreciated!
Fuzzable is licensed under the MIT License.