mach_object 0.1.17

Mach-O File Format Parser for Rust
Documentation
#![cfg(all(target_os = "macos", feature = "integration_tests"))]

#[macro_use]
extern crate log;

use std::collections::HashSet;
use std::fs::File;
use std::io::{self, Cursor};
use std::os::unix::fs::PermissionsExt;
use std::path::Path;

use anyhow::Error;
use hexplay::HexViewBuilder;
use memmap::Mmap;
use time::Instant;
use walkdir::{DirEntry, WalkDir};

use mach_object::{LoadCommand, MachCommand, MachError, OFile};

const SYSTEM_PATH: &[&str] = &[
    // binary
    "/bin",
    "/sbin",
    "/usr/bin",
    "/usr/sbin",
    "/usr/libexec",
    "/usr/local/bin",
    "/usr/local/sbin",
    "/usr/local/libexec",
    // library
    "/usr/lib",
    "/usr/local/lib",
    // framework
    "/System/Library/Frameworks",
    "/Library/Frameworks",
    "/usr/local/Frameworks",
    // application
    "/Applications",
    "~/Applications",
];

fn load_mach_file(entry: &DirEntry) -> Result<(), Error> {
    if entry.metadata()?.len() == 0 {
        trace!("skip the empty file, {:?}", entry.path());

        return Ok(());
    }

    match File::open(entry.path()) {
        Ok(file) => {
            let start_time = Instant::now();

            let mmap = unsafe { Mmap::map(&file) }?;
            let payload = mmap.as_ref();

            if payload.len() < 8 {
                trace!("skip too small file, {:?}", entry.path());
            } else if payload.starts_with(b"#") {
                trace!("skip the scripts, {:?}", entry.path());
            } else if payload.starts_with(b"<") {
                trace!("skip the XML/HTML file, {:?}", entry.path());
            } else if payload.starts_with(b"MZ") {
                trace!("skip the PE file, {:?}", entry.path());
            } else if payload.starts_with(b"PK") {
                trace!("skip the compressed file, {:?}", entry.path());
            } else {
                let mut cur = Cursor::new(payload);
                let ofile = match OFile::parse(&mut cur) {
                    Ok(ofile) => ofile,
                    Err(err) => {
                        if let MachError::UnknownMagic(magic) = err {
                            trace!("skip unknown file format: 0x{:08x}, {:?}", magic, entry.path());

                            return Ok(());
                        } else {
                            return Err(err.into());
                        }
                    }
                };

                verify_mach_file(entry.path(), &ofile);

                info!("loaded in {}, {:?}", start_time.elapsed(), entry.path(),);
            }

            Ok(())
        }
        Err(ref err @ io::Error { .. }) if err.kind() == io::ErrorKind::PermissionDenied => {
            trace!("ignore the permission denied, {:?}", entry.path());

            Ok(())
        }
        Err(err) => Err(err.into()),
    }
}

fn verify_mach_file(path: &Path, ofile: &OFile) {
    match ofile {
        OFile::MachFile { ref commands, .. } => {
            for &MachCommand(ref cmd, cmdsize) in commands {
                if let LoadCommand::Command { cmd, ref payload } = cmd {
                    warn!(
                        "unsolved command #{} with {} bytes in {:?}:\n{}",
                        cmd,
                        cmdsize,
                        path,
                        HexViewBuilder::new(payload).finish()
                    );
                }
            }
        }
        OFile::FatFile { ref files, .. } => {
            for (arch, ofile) in files {
                verify_mach_file(&path.join(arch.name().unwrap_or_default()), ofile)
            }
        }
        OFile::ArFile { ref files } => {
            for (header, ofile) in files {
                verify_mach_file(&path.join(&header.ar_name), ofile)
            }
        }
        OFile::SymDef { .. } => {}
    }
}

#[test]
fn test_system_files() {
    let _ = pretty_env_logger::try_init();

    let mut solved = HashSet::new();

    for path in SYSTEM_PATH.iter() {
        trace!("walking directory: {:?}", path);

        for entry in WalkDir::new(path)
            .follow_links(true)
            .into_iter()
            .filter_map(|entry| entry.ok())
            .filter(|entry| !entry.file_type().is_dir())
        {
            if solved.contains(entry.path()) {
                continue;
            }

            let need_parse = if (entry.metadata().unwrap().permissions().mode() & 0o111) != 0 {
                true
            } else {
                match entry.path().extension().and_then(|ext| ext.to_str()) {
                    Some("dylib") | Some("so") | Some("a") | Some("o") => true,
                    _ => false,
                }
            };

            if need_parse {
                load_mach_file(&entry).expect(&format!("load binary file: {:?}", entry));
            }

            solved.insert(entry.path().to_owned());
        }
    }
}