ndaal-binsec 3.2.5

Binary (in)security scanner for ELF/PE/Mach-O with native, strictly-validated SARIF 2.1.0 and Markdown output (ndaal fork of binsec)
Documentation
//! ### ELF-Specific Compilation Checks:
//!
//! * Static compilation
//! * Linker Path
//! * Minimum glibc Version
//!
//! ### ELF-Specific Exploit Mitigations:
//!
//! * NX (Non-eXecutable bit) stack
//! * Stack Canaries
//! * FORTIFY_SOURCE
//! * Position-Independent Executable / ASLR
//! * Full/Partial RELRO

use goblin::elf::dynamic::{tag_to_str, Dyn};
use goblin::elf::{header, program_header, Elf};
use regex::Regex;
use serde_json::json;
use std::collections::HashSet;
use std::fmt::{self, Display};

use crate::check::{Analyze, GenericMap};
use crate::BinResult;

use super::UniversalCompilationProperties;

const GLIBC: &str = "GLIBC_2.";

enum LinuxCompiler {
    Gcc(Option<String>),
    Clang(Option<String>),
    Unknown,
}

impl Display for LinuxCompiler {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Gcc(Some(ver)) => write!(f, "GCC version {}", ver),
            Self::Gcc(None) => write!(f, "GCC version <unknown>"),
            Self::Clang(Some(ver)) => write!(f, "Clang/LLVM version {}", ver),
            Self::Clang(None) => write!(f, "Clang/LLVM version <unknown>"),
            _ => write!(f, "<unknown>"),
        }
    }
}

impl LinuxCompiler {
    /// Given data from `.comment`, deduce a compiler + version triplet.
    /// TODO(alan): need more binary artifacts to test and make this better.
    fn parse(compiler_string: &str) -> LinuxCompiler {
        // parse for unique version triplets in the string
        let Ok(ver_triplet_re) = Regex::new(r"\b\d+\.\d+(\.\d+)?\b") else {
            return LinuxCompiler::Unknown;
        };
        let ver_set: Vec<&str> = ver_triplet_re
            .find_iter(compiler_string)
            .map(|mat| mat.as_str())
            .collect();

        let unique_vers: HashSet<&str> = ver_set.into_iter().collect();
        let versions: Vec<&str> = unique_vers.into_iter().collect();
        let min_ver = versions.first().map(|s| s.to_string());

        // parse for the actual compiler
        if compiler_string.contains("clang") {
            LinuxCompiler::Clang(min_ver)
        } else if compiler_string.contains("GCC:") {
            LinuxCompiler::Gcc(min_ver)
        } else {
            LinuxCompiler::Unknown
        }
    }
}

#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub enum Relro {
    Partial,
    Full,
    None,
}

impl UniversalCompilationProperties for Elf<'_> {
    // common: shared object (pie exec or .so) or non-pie executable
    fn binary_type(&self) -> &str {
        header::et_to_str(self.header.e_type)
    }

    fn is_stripped(&self) -> bool {
        self.syms.is_empty()
    }

    fn compiler_runtime(&self, bytes: &[u8]) -> Option<String> {
        // most simple: `.comment` section annotating the compiler version
        let mut compilation_string: Option<&str> = None;
        for section in self.section_headers.clone().into_iter() {
            if let Some(sym) = self.shdr_strtab.get_at(section.sh_name) {
                if sym == ".comment" {
                    let comment_section = &bytes[section.sh_offset as usize
                        ..(section.sh_offset + section.sh_size) as usize];
                    if let Ok(comment_str) = std::str::from_utf8(comment_section) {
                        compilation_string = Some(comment_str);
                    }
                }
            }
        }

        if let Some(comp_string) = compilation_string {
            let comp_value = LinuxCompiler::parse(comp_string);
            return Some(comp_value.to_string());
        }
        None
    }
}

trait ElfCompilationProperties {
    fn is_statically_compiled(&self) -> bool;
    fn linker_path(&self) -> Option<&str>;
    fn libc(&self) -> f64;
}

impl ElfCompilationProperties for Elf<'_> {
    // elf static executable: check if PT_INTERP segment exists
    fn is_statically_compiled(&self) -> bool {
        self.program_headers
            .iter()
            .any(|ph| program_header::pt_to_str(ph.p_type) == "PT_INTERP")
    }

    fn linker_path(&self) -> Option<&str> {
        self.interpreter
    }

    // TODO(alan): match on other stdlib runtimes, right now only glibc support
    fn libc(&self) -> f64 {
        let mut glibcs: Vec<f64> = vec![];
        let Ok(dynsyms) = self.dynstrtab.to_vec() else {
            return f64::INFINITY;
        };
        for sym in dynsyms {
            if let Some(ver_str) = sym.strip_prefix(GLIBC) {
                if let Ok(version) = ver_str.parse::<f64>() {
                    glibcs.push(version);
                }
            }
        }
        if !glibcs.is_empty() {
            glibcs.iter().fold(f64::INFINITY, |a, &b| a.min(b))
        } else {
            f64::INFINITY
        }
    }
}

pub trait ElfMitigations {
    fn executable_stack(&self) -> bool;
    fn stack_canary(&self) -> bool;
    fn fortify_source(&self) -> bool;
    fn position_independent(&self) -> bool;
    fn relro(&self) -> Relro;
}

impl ElfMitigations for Elf<'_> {
    fn executable_stack(&self) -> bool {
        self.program_headers
            .iter()
            .any(|ph| program_header::pt_to_str(ph.p_type) == "PT_GNU_STACK" && ph.p_flags == 6)
    }

    fn stack_canary(&self) -> bool {
        self.syms
            .iter()
            .filter_map(|sym| self.strtab.get_at(sym.st_name))
            .any(|symstr| symstr == "__stack_chk_fail" || symstr == "__stack_chk_guard")
    }

    fn fortify_source(&self) -> bool {
        // TODO: list fortified symbols
        self.syms
            .iter()
            .filter_map(|sym| self.strtab.get_at(sym.st_name))
            .any(|symstr| symstr.ends_with("_chk"))
    }

    fn position_independent(&self) -> bool {
        matches!(self.header.e_type, 3)
    }

    fn relro(&self) -> Relro {
        if !self
            .program_headers
            .iter()
            .any(|ph| program_header::pt_to_str(ph.p_type) == "PT_GNU_RELRO")
        {
            return Relro::None;
        }

        // check for full/partial RELRO support by checking dynamic section for DT_BIND_NOW flag.
        // DT_BIND_NOW takes precedence over lazy binding and processes relocations before actual execution.
        if let Some(segs) = &self.dynamic {
            let dyn_seg: Option<Dyn> = segs
                .dyns
                .iter()
                .find(|tag| tag_to_str(tag.d_tag) == "DT_BIND_NOW")
                .cloned();

            if dyn_seg.is_some() {
                return Relro::Full;
            } else {
                return Relro::Partial;
            }
        }
        Relro::None
    }
}

impl Analyze for Elf<'_> {
    fn compilation(&self, bytes: &[u8]) -> BinResult<GenericMap> {
        let mut comp_map: GenericMap = GenericMap::new();
        comp_map.insert("Binary Type".to_string(), json!(self.binary_type()));
        comp_map.insert("Stripped Executable".to_string(), json!(self.is_stripped()));
        comp_map.insert(
            "Statically Compiled".to_string(),
            json!(self.is_statically_compiled()),
        );

        if let Some(comp) = self.compiler_runtime(bytes) {
            comp_map.insert("Compiler Runtime".to_string(), json!(comp));
        }

        // path to linker if dynamic linking enabled
        if let Some(linker) = self.linker_path() {
            comp_map.insert("Linker Path".to_string(), json!(linker));
        }

        if self.libc() != f64::INFINITY {
            comp_map.insert(
                "Minimum Libc Version".to_string(),
                json!(format!("2.{:?}", self.libc())),
            );
        }
        Ok(comp_map)
    }

    fn mitigations(&self) -> GenericMap {
        let mut mitigate_map: GenericMap = GenericMap::new();
        mitigate_map.insert(
            "Executable Stack (NX Bit)".to_string(),
            json!(self.executable_stack()),
        );
        mitigate_map.insert(
            "Read-Only Relocatable (RELRO)".to_string(),
            json!(self.relro()),
        );
        mitigate_map.insert(
            "Position Independent Executable (PIE)".to_string(),
            json!(self.position_independent()),
        );
        mitigate_map.insert("Stack Canary".to_string(), json!(self.stack_canary()));
        mitigate_map.insert("FORTIFY_SOURCE".to_string(), json!(self.fortify_source()));
        mitigate_map
    }

    fn instrumentation(&self) -> GenericMap {
        let mut instr_map: GenericMap = GenericMap::new();
        for sym in self.syms.iter() {
            if let Some(symbol) = self.strtab.get_at(sym.st_name) {
                // /__ubsan\w+\d+/
                if symbol.starts_with("__ubsan") {
                    instr_map.insert(
                        "Undefined Behavior Sanitizer (UBSAN)".to_string(),
                        json!(true),
                    );

                // /_ZN\w+__asan\w+\d+/
                } else if symbol.starts_with("__asan") {
                    instr_map.insert("Address Sanitizer (ASAN)".to_string(), json!(true));

                // /__afl\w+\d+/
                } else if symbol.starts_with("__afl") {
                    instr_map.insert("AFL Instrumentation".to_string(), json!(true));

                // /__llvm\w+\d+/
                } else if symbol.starts_with("__llvm") {
                    instr_map.insert("LLVM Code Coverage".to_string(), json!(true));
                }
            }
        }
        instr_map
    }
}