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).
  • 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).

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:

Supporting new tools

To add support for a new tool, two steps are required:

  1. 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.
  2. 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:

  1. Loading a program representation from a database.
  2. 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.