use std::cell::RefCell;
use crate::modules::macho::parser::{N_EXT, N_STAB, N_TYPE};
use crate::modules::prelude::*;
use crate::modules::protos::macho::*;
use bstr::BString;
use itertools::Itertools;
use md5::{Digest, Md5};
mod parser;
#[cfg(test)]
mod tests;
thread_local!(
static DYLIB_MD5_CACHE: RefCell<Option<String>> =
const { RefCell::new(None) };
static ENTITLEMENT_MD5_CACHE: RefCell<Option<String>> =
const { RefCell::new(None) };
static EXPORT_MD5_CACHE: RefCell<Option<String>> =
const { RefCell::new(None) };
static IMPORT_MD5_CACHE: RefCell<Option<String>> =
const { RefCell::new(None) };
static SYM_MD5_CACHE: RefCell<Option<String>> =
const { RefCell::new(None) };
);
#[module_export(name = "file_index_for_arch")]
fn file_index_type(ctx: &mut ScanContext, type_arg: i64) -> Option<i64> {
let macho = ctx.module_output::<Macho>()?;
let nfat = macho.nfat_arch?;
for i in 0..nfat as usize {
if let Some(arch) = macho.fat_arch.get(i)
&& let Some(cputype) = arch.cputype
&& cputype as i64 == type_arg {
return Some(i as i64);
}
}
None
}
#[module_export(name = "file_index_for_arch")]
fn file_index_subtype(
ctx: &mut ScanContext,
type_arg: i64,
subtype_arg: i64,
) -> Option<i64> {
let macho = ctx.module_output::<Macho>()?;
let nfat = macho.nfat_arch?;
for i in 0..nfat as usize {
if let Some(arch) = macho.fat_arch.get(i)
&& let (Some(cputype), Some(cpusubtype)) =
(arch.cputype, arch.cpusubtype)
&& cputype as i64 == type_arg
&& cpusubtype as i64 == subtype_arg
{
return Some(i as i64);
}
}
None
}
#[module_export(name = "entry_point_for_arch")]
fn ep_for_arch_type(ctx: &mut ScanContext, type_arg: i64) -> Option<i64> {
let macho = ctx.module_output::<Macho>()?;
let nfat = macho.nfat_arch?;
for i in 0..nfat as usize {
if let Some(arch) = macho.fat_arch.get(i)
&& let Some(cputype) = arch.cputype
&& cputype as i64 == type_arg {
let file_offset = arch.offset?;
let entry_point = macho.file.get(i)?.entry_point?;
return file_offset
.checked_add(entry_point)
.map(|sum| sum as i64);
}
}
None
}
#[module_export(name = "entry_point_for_arch")]
fn ep_for_arch_subtype(
ctx: &mut ScanContext,
type_arg: i64,
subtype_arg: i64,
) -> Option<i64> {
let macho = ctx.module_output::<Macho>()?;
let nfat = macho.nfat_arch?;
for i in 0..nfat as usize {
if let Some(arch) = macho.fat_arch.get(i)
&& let (Some(cputype), Some(cpusubtype)) =
(arch.cputype, arch.cpusubtype)
&& cputype as i64 == type_arg
&& cpusubtype as i64 == subtype_arg
{
let file_offset = arch.offset?;
let entry_point = macho.file.get(i)?.entry_point?;
return file_offset
.checked_add(entry_point)
.map(|sum| sum as i64);
}
}
None
}
#[module_export]
fn has_entitlement(
ctx: &ScanContext,
entitlement: RuntimeString,
) -> Option<bool> {
let macho = ctx.module_output::<Macho>()?;
let expected = entitlement.as_bstr(ctx);
for entitlement in macho.entitlements.iter() {
if expected.eq_ignore_ascii_case(entitlement.as_bytes()) {
return Some(true);
}
}
for file in macho.file.iter() {
for entitlement in file.entitlements.iter() {
if expected.eq_ignore_ascii_case(entitlement.as_bytes()) {
return Some(true);
}
}
}
Some(false)
}
#[module_export]
fn has_dylib(ctx: &ScanContext, dylib_name: RuntimeString) -> Option<bool> {
let macho = ctx.module_output::<Macho>()?;
let expected_name = dylib_name.as_bstr(ctx);
for dylib in macho.dylibs.iter() {
if dylib.name.as_ref().is_some_and(|name| {
expected_name.eq_ignore_ascii_case(name.as_bytes())
}) {
return Some(true);
}
}
for file in macho.file.iter() {
for dylib in file.dylibs.iter() {
if dylib.name.as_ref().is_some_and(|name| {
expected_name.eq_ignore_ascii_case(name.as_bytes())
}) {
return Some(true);
}
}
}
Some(false)
}
#[module_export]
fn has_rpath(ctx: &ScanContext, rpath: RuntimeString) -> Option<bool> {
let macho = ctx.module_output::<Macho>()?;
let expected_rpath = rpath.as_bstr(ctx);
for rp in macho.rpaths.iter() {
if expected_rpath.eq_ignore_ascii_case(rp.as_bytes()) {
return Some(true);
}
}
for file in macho.file.iter() {
for rp in file.rpaths.iter() {
if expected_rpath.eq_ignore_ascii_case(rp.as_bytes()) {
return Some(true);
}
}
}
Some(false)
}
#[module_export]
fn has_import(ctx: &ScanContext, import: RuntimeString) -> Option<bool> {
let macho = ctx.module_output::<Macho>()?;
let expected_import = import.as_bstr(ctx);
for im in macho.imports.iter() {
if expected_import.eq_ignore_ascii_case(im.as_bytes()) {
return Some(true);
}
}
for file in macho.file.iter() {
for im in file.imports.iter() {
if expected_import.eq_ignore_ascii_case(im.as_bytes()) {
return Some(true);
}
}
}
Some(false)
}
#[module_export]
fn has_export(ctx: &ScanContext, export: RuntimeString) -> Option<bool> {
let macho = ctx.module_output::<Macho>()?;
let expected_export = export.as_bstr(ctx);
for ex in macho.exports.iter() {
if expected_export.eq_ignore_ascii_case(ex.as_bytes()) {
return Some(true);
}
}
for file in macho.file.iter() {
for ex in file.exports.iter() {
if expected_export.eq_ignore_ascii_case(ex.as_bytes()) {
return Some(true);
}
}
}
Some(false)
}
#[module_export]
fn dylib_hash(ctx: &mut ScanContext) -> Option<Lowercase<FixedLenString<32>>> {
let cached = DYLIB_MD5_CACHE.with(
|cache| -> Option<Lowercase<FixedLenString<32>>> {
cache.borrow().as_deref().map(|s| {
Lowercase::<FixedLenString<32>>::from_slice(ctx, s.as_bytes())
})
},
);
if cached.is_some() {
return cached;
}
let macho = ctx.module_output::<Macho>()?;
let mut dylibs_to_hash = &macho.dylibs;
if dylibs_to_hash.is_empty() && !macho.file.is_empty() {
dylibs_to_hash = &macho.file[0].dylibs;
}
if dylibs_to_hash.is_empty() {
return None;
}
let mut md5_hash = Md5::new();
let dylibs_to_hash = dylibs_to_hash
.iter()
.filter_map(|dylib| {
dylib
.name
.as_ref()
.map(|name| BString::new(name.trim().to_lowercase()))
})
.unique()
.sorted()
.join(",");
md5_hash.update(dylibs_to_hash.as_bytes());
let digest = format!("{:x}", md5_hash.finalize());
DYLIB_MD5_CACHE.with(|cache| {
*cache.borrow_mut() = Some(digest.clone());
});
Some(Lowercase::<FixedLenString<32>>::new(digest))
}
#[module_export]
fn entitlement_hash(
ctx: &mut ScanContext,
) -> Option<Lowercase<FixedLenString<32>>> {
let cached = ENTITLEMENT_MD5_CACHE.with(
|cache| -> Option<Lowercase<FixedLenString<32>>> {
cache.borrow().as_deref().map(|s| {
Lowercase::<FixedLenString<32>>::from_slice(ctx, s.as_bytes())
})
},
);
if cached.is_some() {
return cached;
}
let macho = ctx.module_output::<Macho>()?;
let mut entitlements_to_hash = &macho.entitlements;
if entitlements_to_hash.is_empty() && !macho.file.is_empty() {
entitlements_to_hash = &macho.file[0].entitlements;
}
if entitlements_to_hash.is_empty() {
return None;
}
let mut md5_hash = Md5::new();
let entitlements_str: String = entitlements_to_hash
.iter()
.map(|e| e.trim().to_lowercase())
.unique()
.sorted()
.join(",");
md5_hash.update(entitlements_str.as_bytes());
let digest = format!("{:x}", md5_hash.finalize());
ENTITLEMENT_MD5_CACHE.with(|cache| {
*cache.borrow_mut() = Some(digest.clone());
});
Some(Lowercase::<FixedLenString<32>>::new(digest))
}
#[module_export]
fn export_hash(
ctx: &mut ScanContext,
) -> Option<Lowercase<FixedLenString<32>>> {
let cached = EXPORT_MD5_CACHE.with(
|cache| -> Option<Lowercase<FixedLenString<32>>> {
cache.borrow().as_deref().map(|s| {
Lowercase::<FixedLenString<32>>::from_slice(ctx, s.as_bytes())
})
},
);
if cached.is_some() {
return cached;
}
let macho = ctx.module_output::<Macho>()?;
let mut exports_to_hash = &macho.exports;
if exports_to_hash.is_empty() && !macho.file.is_empty() {
exports_to_hash = &macho.file[0].exports;
}
if exports_to_hash.is_empty() {
return None;
}
let mut md5_hash = Md5::new();
let exports_str: String = exports_to_hash
.iter()
.map(|e| e.trim().to_lowercase())
.unique()
.sorted()
.join(",");
md5_hash.update(exports_str.as_bytes());
let digest = format!("{:x}", md5_hash.finalize());
EXPORT_MD5_CACHE.with(|cache| {
*cache.borrow_mut() = Some(digest.clone());
});
Some(Lowercase::<FixedLenString<32>>::new(digest))
}
#[module_export]
fn import_hash(
ctx: &mut ScanContext,
) -> Option<Lowercase<FixedLenString<32>>> {
let cached = IMPORT_MD5_CACHE.with(
|cache| -> Option<Lowercase<FixedLenString<32>>> {
cache.borrow().as_deref().map(|s| {
Lowercase::<FixedLenString<32>>::from_slice(ctx, s.as_bytes())
})
},
);
if cached.is_some() {
return cached;
}
let macho = ctx.module_output::<Macho>()?;
let mut imports_to_hash = &macho.imports;
if imports_to_hash.is_empty() && !macho.file.is_empty() {
imports_to_hash = &macho.file[0].imports;
}
if imports_to_hash.is_empty() {
return None;
}
let mut md5_hash = Md5::new();
let imports_str: String = imports_to_hash
.iter()
.map(|e| e.trim().to_lowercase())
.unique()
.sorted()
.join(",");
md5_hash.update(imports_str.as_bytes());
let digest = format!("{:x}", md5_hash.finalize());
IMPORT_MD5_CACHE.with(|cache| {
*cache.borrow_mut() = Some(digest.clone());
});
Some(Lowercase::<FixedLenString<32>>::new(digest))
}
#[module_export]
fn symhash(ctx: &mut ScanContext) -> Option<Lowercase<FixedLenString<32>>> {
let cached =
SYM_MD5_CACHE.with(|cache| -> Option<Lowercase<FixedLenString<32>>> {
cache.borrow().as_deref().map(|s| {
Lowercase::<FixedLenString<32>>::from_slice(ctx, s.as_bytes())
})
});
if cached.is_some() {
return cached;
}
let macho = ctx.module_output::<Macho>()?;
let mut symtab_to_hash = &macho.symtab.entries;
let mut nlists = &macho.symtab.nlists;
if symtab_to_hash.is_empty() && !macho.file.is_empty() {
symtab_to_hash = &macho.file[0].symtab.entries;
nlists = &macho.file[0].symtab.nlists
}
if symtab_to_hash.is_empty() {
return None;
}
let mut md5_hash = Md5::new();
let symtab_hash_entries = nlists
.iter()
.enumerate()
.filter_map(|(idx, nlist)| {
let n_type = nlist.n_type();
if (n_type & N_STAB as u32 == 0)
&& (n_type & N_EXT as u32) == N_EXT as u32
&& (n_type & N_TYPE as u32) == 0
{
symtab_to_hash.get(idx)
} else {
None
}
})
.map(|s| BString::new(s.trim().to_vec()))
.unique()
.sorted()
.join(",");
md5_hash.update(symtab_hash_entries);
let digest = format!("{:x}", md5_hash.finalize());
SYM_MD5_CACHE.with(|cache| {
*cache.borrow_mut() = Some(digest.clone());
});
Some(Lowercase::<FixedLenString<32>>::new(digest))
}
#[module_main]
fn main(data: &[u8], _meta: Option<&[u8]>) -> Result<Macho, ModuleError> {
DYLIB_MD5_CACHE.with(|cache| *cache.borrow_mut() = None);
ENTITLEMENT_MD5_CACHE.with(|cache| *cache.borrow_mut() = None);
EXPORT_MD5_CACHE.with(|cache| *cache.borrow_mut() = None);
IMPORT_MD5_CACHE.with(|cache| *cache.borrow_mut() = None);
SYM_MD5_CACHE.with(|cache| *cache.borrow_mut() = None);
match parser::MachO::parse(data) {
Ok(macho) => Ok(macho.into()),
Err(_) => Ok(Macho::new()),
}
}