ndaal-binsec 3.2.4

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
//! ### Mach-O Specific Compilation Checks:
//!
//! * TODO
//!
//! ### Exploit Mitigations:
//!
//! * NX (Non-eXecutable bit) stack
//! * NX (Non-eXecutable bit) heap
//! * Position-Independent Executable
//! * Stack Canaries
//! * Restricted segment
//! * __PAGEZERO segment
use goblin::mach::{header, MachO};
use serde_json::json;

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

use super::UniversalCompilationProperties;

const MH_PIE: u32 = 0x200000;
const MH_ALLOW_STACK_EXECUTION: u32 = 0x20000;
const MH_NO_HEAP_EXECUTION: u32 = 0x1000000;

impl UniversalCompilationProperties for MachO<'_> {
    fn binary_type(&self) -> &str {
        header::filetype_to_str(self.header.filetype)
    }

    fn is_stripped(&self) -> bool {
        self.symbols.is_none()
    }

    // TODO: Mach-O compiler-runtime detection is not implemented yet.
    fn compiler_runtime(&self, _bytes: &[u8]) -> Option<String> {
        None
    }
}

trait MachOMitigations {
    fn executable_stack(&self) -> bool;
    fn executable_heap(&self) -> bool;
    fn position_independent(&self) -> bool;
    fn stack_canary(&self) -> bool;
    fn restricted_segment(&self) -> bool;
    fn pagezero_segment(&self) -> bool;
}

impl MachOMitigations for MachO<'_> {
    fn executable_stack(&self) -> bool {
        matches!(self.header.flags & MH_ALLOW_STACK_EXECUTION, 0)
    }

    fn executable_heap(&self) -> bool {
        matches!(self.header.flags & MH_NO_HEAP_EXECUTION, 0)
    }

    fn position_independent(&self) -> bool {
        matches!(self.header.flags & MH_PIE, 0)
    }

    // Check for stack canary by finding canary functions in imports
    fn stack_canary(&self) -> bool {
        match self.imports() {
            Ok(imports) => imports
                .iter()
                .any(|x| x.name == "__stack_chk_fail" || x.name == "__stack_chk_guard"),
            Err(_) => false,
        }
    }

    // Check for __RESTRICT section for stopping dylib injection
    fn restricted_segment(&self) -> bool {
        self.segments
            .iter()
            .filter_map(|s| s.name().ok())
            .any(|s| s.to_lowercase() == "__restrict")
    }

    fn pagezero_segment(&self) -> bool {
        self.segments
            .iter()
            .filter_map(|s| s.name().ok())
            .any(|s| s.to_lowercase() == "__PAGEZERO")
    }
}

impl Analyze for MachO<'_> {
    fn compilation(&self, _bytes: &[u8]) -> BinResult<GenericMap> {
        let mut comp_map = GenericMap::new();
        comp_map.insert("Binary Type".to_string(), json!(self.binary_type()));
        comp_map.insert("Debug Stripped".to_string(), json!(self.is_stripped()));
        Ok(comp_map)
    }

    fn mitigations(&self) -> GenericMap {
        let mut mitigate_map: GenericMap = GenericMap::new();
        mitigate_map.insert(
            "Non-executable Stack".to_string(),
            json!(self.executable_stack()),
        );
        mitigate_map.insert(
            "Non-executable Heap".to_string(),
            json!(self.executable_heap()),
        );
        mitigate_map.insert(
            "Position Independent Executable / ASLR".to_string(),
            json!(self.position_independent()),
        );
        mitigate_map.insert("Stack Canary".to_string(), json!(self.stack_canary()));
        mitigate_map.insert(
            "__RESTRICT segment".to_string(),
            json!(self.restricted_segment()),
        );
        mitigate_map.insert(
            "__PAGEZERO segment".to_string(),
            json!(self.pagezero_segment()),
        );

        mitigate_map
    }

    fn instrumentation(&self) -> GenericMap {
        let mut instr_map: GenericMap = GenericMap::new();
        let asan: bool = match self.imports() {
            Ok(imports) => imports.iter().any(|x| x.name.starts_with("__asan")),
            Err(_) => false,
        };

        if asan {
            instr_map.insert("Address Sanitizer (ASAN)".to_string(), json!(true));
        }

        instr_map
    }
}