binsec 1.0.0

Swiss Army Knife for Binary (In)Security
Documentation
//! Implements a YARA-based interface for deploying rule checks against a binary. While this does not
//! implement a foreign function interface directly with the system-installed YARA library component,
//! this is used instead since the currently available Rust bindings to YARA only support up to 3.11.

use crate::check::FeatureCheck;
use crate::errors::{BinError, BinResult, ErrorKind};

use colored::*;
use serde::{Deserialize, Serialize};

use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

/// A `YaraCollection` is denoted as a single file in a ruleset that stores all of the rules
/// grouped together for the type of analysis being done. Each file that is apart of the
/// collection must have a `Name` and `Description` comment parsed for informational display.
#[derive(Deserialize, Serialize, Debug)]
pub struct YaraCollection {
    /// Defines the name identifying the collection
    name: String,

    /// Represents an anecdotal description regarding the checks
    description: String,

    /// Represents collection of individual rules being run, and their resultant status
    rules: BTreeMap<String, bool>,
}

impl std::string::ToString for YaraCollection {
    fn to_string(&self) -> String {
        let mut output: String = String::new();
        output.push_str(&format!("{}", "Collection Name: ".bold()));
        output.push_str(&self.name);
        output.push_str("\n");

        output.push_str(&format!("{}", "Description: ".bold()));
        output.push_str(&self.description);
        output
    }
}

impl YaraCollection {
    /// Given a path to YARA file, parse it and create a `YaraMatch` to be represented
    fn parse(path: PathBuf) -> BinResult<Self> {
        // open and read file into string
        let contents: String = fs::read_to_string(path)?;
        let lines: Vec<&str> = contents.lines().collect();

        // read first line to determine collection name
        let _split = lines[0].split("// Name: ");
        let split: Vec<&str> = _split.collect();
        let name: String = split[1].to_string();

        // read second line to determine description for collection
        let _split = lines[1].split("// Description: ");
        let split: Vec<&str> = _split.collect();
        let description: String = split[1].to_string();

        // create BTreeMap for storing rules
        let mut rules: BTreeMap<String, bool> = BTreeMap::new();
        let _rules: Vec<String> = lines[2..]
            .iter()
            .filter(|r| r.starts_with("rule"))
            .map(|r| {
                let _split = r.split("rule ");
                let split: Vec<&str> = _split.collect();
                split[1].to_string()
            })
            .collect();

        // populate rules mapping with each
        for rule in _rules.iter() {
            rules.insert(rule.to_string(), false);
        }

        // find all instances of rules regex and parse out name
        Ok(Self {
            name,
            description,
            rules,
        })
    }
}

/// Represents a strongly typed collection of YARA rules, and their statuses when executed against a binary.
/// This is to be what ends up being serialized and returned to the user, or displayed as a table.
#[derive(Deserialize, Serialize, Debug, Default)]
pub struct YaraMatches(Vec<YaraCollection>);

#[typetag::serde]
impl FeatureCheck for YaraMatches {
    /// Re-implementation of `BinTable::parse`, since this requires handling for specific categorized rulesets
    /// and does not have genericized values in the internal mapping.
    fn output(&self) -> String {
        use term_table::row::Row;
        use term_table::table_cell::{Alignment, TableCell};
        use term_table::{Table, TableStyle};

        // new custom table
        let mut table = Table::new();
        table.max_column_width = 60;
        table.style = TableStyle::rounded();

        // create main header
        let main_header: &str = &format!("{}", "Enhanced (YARA) Checks".bold().underline());
        table.add_row(Row::new(vec![TableCell::new_with_alignment(
            main_header,
            2,
            term_table::table_cell::Alignment::Center,
        )]));

        for col in &self.0 {
            // add subheader
            table.add_row(Row::new(vec![TableCell::new_with_alignment(
                &col.to_string(),
                2,
                term_table::table_cell::Alignment::Left,
            )]));

            // add columns with checks, reimplementation of `BinTable` parser
            for (name, feature) in &col.rules {
                // format display based on content
                let feature_cell = match feature {
                    true => TableCell::new_with_alignment("✔️".green(), 1, Alignment::Center),
                    false => TableCell::new_with_alignment("✖️".red(), 1, Alignment::Center),
                };

                table.add_row(Row::new(vec![TableCell::new(name), feature_cell]));
            }
        }
        table.render()
    }
}

/// Defines a builder executor that calls yara directly through the command line rather than bindings,
/// and is able to consume rules and executables to match those rules against. The output format
/// that is generated is a `YaraMatches` -typed mapping.
#[derive(Deserialize, Serialize, Default)]
pub struct YaraExecutor {
    pub binpath: PathBuf,
    pub rules: Vec<PathBuf>,
    pub matches: YaraMatches,
}

impl YaraExecutor {
    /// Instantiates a new executor with no rules and executable to match against.
    pub fn new(binpath: PathBuf) -> Self {
        Self {
            binpath,
            ..Self::default()
        }
    }

    /// Add a rule to test against an executable, and parse it for correlation.
    pub fn add_rule(&mut self, rule: PathBuf) -> BinResult<()> {
        // store path to rule for later command reconstruction
        let _rule = rule.clone();
        self.rules.push(_rule);

        // create a new yara match to set for binary
        self.matches.0.push(YaraCollection::parse(rule)?);

        Ok(())
    }

    /// Given an executable path and singular rule from ruleset, build a command to execute
    /// against and test for matches, and execute. Once done, parse out all of the rules within
    /// that collection that passed for the executable passed in.
    fn exec_cmd(&self, exec_name: &str) -> BinResult<Vec<String>> {
        let mut command = Command::new("yara");

        // construct arguments to commands
        command.arg("--no-warnings");
        for rule in &self.rules {
            command.arg(rule);
        }

        // append binary name at end
        command.arg(exec_name);

        // execute command against the binary and error-check
        let _output = command.output().map_err(|e| BinError {
            kind: ErrorKind::RuleEngineError,
            msg: e.to_string(),
        })?;

        // convert output to string, parse and return appropriately
        let output: &[u8] = _output.stdout.as_slice();
        match std::str::from_utf8(&output) {
            Ok(out) => {
                // get all lines as vector
                let _lines: Vec<&str> = out.lines().collect();

                // iterate over each line, parse out rule from
                let lines: Vec<String> = _lines
                    .iter()
                    .map(|l| {
                        // split and recover rule name matched
                        let _split = l.split(exec_name);
                        let split: Vec<&str> = _split.collect();

                        // cleanup and remove any trailing whitespace
                        let mut res = split[0].to_string();
                        res.retain(|c| !c.is_whitespace());
                        res
                    })
                    .collect();

                Ok(lines)
            }
            Err(_e) => Err(BinError {
                kind: ErrorKind::RuleEngineError,
                msg: "cannot parse output from YARA execution".to_string(),
            }),
        }
    }

    /// Executes given a ruleset and a target binary to check against. Stores and mutates results
    /// in the `YaraExecutor.matches` attribute to represent checks done.
    pub fn execute(&mut self) -> BinResult<()> {
        // if empty ruleset, return error
        if self.rules.is_empty() {
            return Err(BinError {
                kind: ErrorKind::RuleEngineError,
                msg: "no rules found to test against binary".to_string(),
            });
        }

        // construct command to run and run against binary
        let bin_name: &str = self.binpath.to_str().unwrap();
        let result: Vec<String> = self.exec_cmd(bin_name)?;

        // parse results, and set respective
        // TODO: very ugly, cleanup
        if !result.is_empty() {
            for res in result.iter() {
                for ymatches in &mut self.matches.0 {
                    if ymatches.rules.contains_key(res) {
                        *ymatches.rules.get_mut(&res.to_string()).unwrap() = true;
                    }
                }
            }
        }
        Ok(())
    }
}