Introduction
Fugue is a binary analysis framework in the spirit of B2R2 and BAP, with a focus on providing reusable components to rapidly prototype new binary analysis tools and techniques.
Fugue is built around a core collection of crates, i.e., fugue-core. These crates provide a number of fundamental capabilities:
-
Data structures and types:
- Architecture definitions (
fugue-arch
). - Bit vectors (
fugue-bv
). - Floating point numbers (
fugue-fp
). - Endian-aware conversion to and from various primitive types
(
fugue-bytes
).
- Architecture definitions (
-
Program representations and abstractions:
- A knowledge database to represent program binaries that can be populated
using third-party tools (
fugue-db
). - Disassembly and lifting to intermediate representations (
fugue-ir
).
- A knowledge database to represent program binaries that can be populated
using third-party tools (
Integration with third-party tools
Fugue interfaces with external tools to populate its database representation (i.e., fugue-db). Currently, Fugue has plugins for the following:
- Ghidra via fugue-ghidra.
- IDA Pro via fugue-idapro.
- Radare/Rizin via fugue-radare.
Supporting new tools
To add support for a new tool, two steps are required:
- Implement suitable interfacing glue for the tool. In the most basic case, this will require serialising the tool's program representation to a file using the flatbuffers schema definition found in fugue-db/schema.
- Implement the
fugue::db::backend::Backend
trait to abstract the interface to the tool.
The template below provides a starting point:
#![allow(unused)] fn main() { use fugue::db::backend::{Backend, Imported}; use fugue::db::Error as ExportError; use url::Url; use thiserror::Error; #[derive(Debug, Error)] pub enum Error { #[error("MyNewTool is not available as a backend")] NotAvailable, // ... } impl From<Error> for ExportError { fn from(e: Error) -> Self { ExportError::importer_error("fugue-my-new-tool", e) } } pub struct MyNewTool; impl Backend for MyNewTool { type Error = Error; fn name(&self) -> &'static str { "fugue-my-new-tool" } fn is_available(&self) -> bool { // ... } fn is_preferred_for(&self, path: &Url) -> Option<bool> { // .... } fn import(&self, program: &Url) -> Result<Imported, Self::Error> { // ... } } }
The return value of Backend::import
can either point to a path to read the
serialised flatbuffers representation from (Imported::File
), or a vector
of bytes (i.e., Vec<u8>
) containing the representation
(Imported::Bytes
).
Micro-execution with Fugue
Fugue supports analysing programs using a number of different paradigms. Micro-execution is a static/dynamic approach that enables a program to be executed from any address. Since program state will often be unknown or missing under such conditions, Fugue provides a means to intercept invalid accesses and recover execution by providing concrete values for missing memory.
Fugue's executor enables different execution events to be hooked, these include:
- Conditional branches
- Memory reads and writes
- Register reads and writes
- Function calls
- Instruction fetches
Hooks attached to these events can manipulate and read the program's internal state, and redirect its execution.
fuguex-core
The fuguex-core family of crates provide the basis of implementing custom
executors based on fugue-core. The fuguex-concrete
crate provides a full
micro-execution-based executor.
Its basic usage requires the two steps:
- Loading a program representation from a database.
- Configuring an initial program state.
The code below demonstrates how to obtain an initial program database for
analysis and build an initial state (via ConcreteContext
):
use either::Either; use fugue::bytes::LE; use fugue::ir::{AddressValue, LanguageDB}; use fuguex::concrete::interpreter::ConcreteContext; use fuguex::loader::{LoaderMapping, MappedDatabase}; use fuguex::machine::{Bound, Machine}; fn main() -> Result<(), Box<dyn std::error::Error>> { let language_db = LanguageDB::from_directory_with("./processors", true)?; let database = if let Either::Left(db) = MappedDatabase::from_path("./path/to/sample", &language_db)? .pcode_state_with::<LE, _>("gcc") { db } else { panic!("invalid calling convention `gcc`") }; let mut ctx = ConcreteContext::<LE, String, 64>::from_loader(database); let mut machine = Machine::new(ctx);
The machine can be executed using the following snippet:
#![allow(unused)] fn main() { let space = machine.interpreter().interpreter_space(); let start = AddressValue::new(space.clone(), 0x1000); let stop = AddressValue::new(space.clone(), 0x1200); machine.step_until(start, Bound::address(stop)); }
The code above creates two address references into the default interpreter
address space and then steps the machine forwards from start
until stop
is reached. For each event kind observed when stepping, the machine will
call-back to its registered hooks.