use crate::{
Result,
security::{
options::status::{HasSecurityStatus, YesNoUnknownStatus},
parser::BinaryParser,
},
};
use goblin::mach::{Mach, MachO};
const MH_NOUNDEFS: u32 = 0x0000_0001;
const MH_TWOLEVEL: u32 = 0x0000_0080;
const MH_PIE: u32 = 0x0020_0000;
const MH_NO_HEAP_EXECUTION: u32 = 0x0100_0000;
const VM_PROT_EXECUTE: u32 = 0x04;
const CSMAGIC_EMBEDDED_SIGNATURE: u32 = 0xfade_0cc0;
const CSMAGIC_CODEDIRECTORY: u32 = 0xfade_0c02;
const CSMAGIC_EMBEDDED_ENTITLEMENTS: u32 = 0xfade_7171;
const CS_RUNTIME: u32 = 0x0001_0000;
pub(crate) fn analyze_binary(
parser: &BinaryParser,
_options: &crate::BinarySecurityCheckOptions,
) -> Result<Vec<Box<dyn HasSecurityStatus>>> {
let bytes = parser.bytes();
let mach_obj = match parser.object() {
goblin::Object::Mach(m) => m,
_ => {
return Ok(vec![
Box::new(YesNoUnknownStatus::unknown("MACHO-PARSE")) as Box<dyn HasSecurityStatus>
]);
}
};
match mach_obj {
Mach::Binary(m) => run_checks(m, bytes, 0),
Mach::Fat(fat) => {
for (i, arch_res) in fat.iter_arches().enumerate() {
let arch = match arch_res {
Ok(a) => a,
Err(_) => continue,
};
if let Ok(goblin::mach::SingleArch::MachO(m)) = fat.get(i) {
return run_checks(&m, bytes, arch.offset as usize);
}
}
Ok(vec![
Box::new(YesNoUnknownStatus::unknown("MACHO-PARSE")) as Box<dyn HasSecurityStatus>
])
}
}
}
fn run_checks(
slice: &MachO,
raw: &[u8],
slice_offset: usize,
) -> Result<Vec<Box<dyn HasSecurityStatus>>> {
let flags = slice.header.flags;
let pie = YesNoUnknownStatus::new("PIE", flags & MH_PIE != 0);
let nx_flag = flags & MH_NO_HEAP_EXECUTION != 0;
let any_data_exec = slice.segments.iter().any(|seg| {
let name = std::str::from_utf8(&seg.segname)
.unwrap_or("")
.trim_end_matches('\0');
name.starts_with("__DATA") && (seg.initprot & VM_PROT_EXECUTE) != 0
});
let dep = YesNoUnknownStatus::new("DATA-EXEC-PREVENT", nx_flag || !any_data_exec);
let canary = {
let mut found = false;
for sym in slice.symbols().flatten() {
let (name, _nlist) = sym;
if name == "___stack_chk_guard" || name == "___stack_chk_fail" {
found = true;
break;
}
}
YesNoUnknownStatus::new("STACK-CANARY", found)
};
let has_restrict = slice.segments.iter().any(|seg| {
let name = std::str::from_utf8(&seg.segname)
.unwrap_or("")
.trim_end_matches('\0');
name == "__RESTRICT"
});
let restrict = YesNoUnknownStatus::new("RESTRICT", has_restrict);
let cs_loc = slice.load_commands.iter().find_map(|lc| match &lc.command {
goblin::mach::load_command::CommandVariant::CodeSignature(cs) if cs.datasize > 0 => {
Some((cs.dataoff as usize, cs.datasize as usize))
}
_ => None,
});
let code_sig = YesNoUnknownStatus::new("CODE-SIGNATURE", cs_loc.is_some());
let two_level = YesNoUnknownStatus::new("TWO-LEVEL-NAMESPACE", flags & MH_TWOLEVEL != 0);
let no_undef = YesNoUnknownStatus::new("NO-UNDEF-SYMS", flags & MH_NOUNDEFS != 0);
let (hardened, allow_jit) = match cs_loc {
None => (
YesNoUnknownStatus::new("HARDENED-RUNTIME", false),
YesNoUnknownStatus::new("ALLOW-JIT", false),
),
Some((dataoff, datasize)) => {
let blob_start = slice_offset.saturating_add(dataoff);
let blob_end = blob_start.saturating_add(datasize);
if blob_end > raw.len() {
(
YesNoUnknownStatus::unknown("HARDENED-RUNTIME"),
YesNoUnknownStatus::unknown("ALLOW-JIT"),
)
} else {
let blob = &raw[blob_start..blob_end];
let walker = SuperBlob::parse(blob);
let hr = walker
.as_ref()
.and_then(|sb| sb.code_directory_flags())
.map(|cd_flags| cd_flags & CS_RUNTIME != 0);
let aj = walker
.as_ref()
.and_then(|sb| sb.entitlements_plist())
.map(plist_says_allow_jit);
(
match hr {
Some(b) => YesNoUnknownStatus::new("HARDENED-RUNTIME", b),
None => YesNoUnknownStatus::unknown("HARDENED-RUNTIME"),
},
match aj {
Some(b) => YesNoUnknownStatus::new("ALLOW-JIT", b),
None => YesNoUnknownStatus::new("ALLOW-JIT", false),
},
)
}
}
};
Ok(vec![
Box::new(pie) as Box<dyn HasSecurityStatus>,
Box::new(dep) as Box<dyn HasSecurityStatus>,
Box::new(canary) as Box<dyn HasSecurityStatus>,
Box::new(restrict) as Box<dyn HasSecurityStatus>,
Box::new(code_sig) as Box<dyn HasSecurityStatus>,
Box::new(two_level) as Box<dyn HasSecurityStatus>,
Box::new(no_undef) as Box<dyn HasSecurityStatus>,
Box::new(hardened) as Box<dyn HasSecurityStatus>,
Box::new(allow_jit) as Box<dyn HasSecurityStatus>,
])
}
struct SuperBlob<'a> {
raw: &'a [u8],
index: Vec<(u32, u32)>, }
impl<'a> SuperBlob<'a> {
fn parse(raw: &'a [u8]) -> Option<Self> {
if raw.len() < 12 {
return None;
}
let magic = u32::from_be_bytes([raw[0], raw[1], raw[2], raw[3]]);
if magic != CSMAGIC_EMBEDDED_SIGNATURE {
return None;
}
let length = u32::from_be_bytes([raw[4], raw[5], raw[6], raw[7]]) as usize;
if length > raw.len() {
return None;
}
let count = u32::from_be_bytes([raw[8], raw[9], raw[10], raw[11]]) as usize;
if count > 4096 {
return None;
}
let index_start: usize = 12;
let index_end = index_start.checked_add(count.checked_mul(8)?)?;
if index_end > raw.len() {
return None;
}
let mut index = Vec::with_capacity(count);
for i in 0..count {
let off = index_start + i * 8;
let typ = u32::from_be_bytes([raw[off], raw[off + 1], raw[off + 2], raw[off + 3]]);
let blob_off =
u32::from_be_bytes([raw[off + 4], raw[off + 5], raw[off + 6], raw[off + 7]]);
index.push((typ, blob_off));
}
Some(SuperBlob { raw, index })
}
fn find_blob(&self, magic: u32) -> Option<&'a [u8]> {
for &(_typ, off) in &self.index {
let off_usz = off as usize;
if off_usz + 8 > self.raw.len() {
continue;
}
let sub_magic = u32::from_be_bytes([
self.raw[off_usz],
self.raw[off_usz + 1],
self.raw[off_usz + 2],
self.raw[off_usz + 3],
]);
if sub_magic != magic {
continue;
}
let sub_len = u32::from_be_bytes([
self.raw[off_usz + 4],
self.raw[off_usz + 5],
self.raw[off_usz + 6],
self.raw[off_usz + 7],
]) as usize;
let end = off_usz.checked_add(sub_len)?;
if end > self.raw.len() {
return None;
}
return Some(&self.raw[off_usz..end]);
}
None
}
fn code_directory_flags(&self) -> Option<u32> {
let cd = self.find_blob(CSMAGIC_CODEDIRECTORY)?;
if cd.len() < 0x10 {
return None;
}
Some(u32::from_be_bytes([cd[0x0c], cd[0x0d], cd[0x0e], cd[0x0f]]))
}
fn entitlements_plist(&self) -> Option<&'a [u8]> {
let blob = self.find_blob(CSMAGIC_EMBEDDED_ENTITLEMENTS)?;
if blob.len() <= 8 {
return None;
}
Some(&blob[8..])
}
}
fn plist_says_allow_jit(plist_bytes: &[u8]) -> bool {
use plist::Value;
let cursor = std::io::Cursor::new(plist_bytes);
let Ok(value) = Value::from_reader_xml(cursor) else {
let cursor = std::io::Cursor::new(plist_bytes);
return matches!(
Value::from_reader(cursor)
.ok()
.as_ref()
.and_then(|v| v.as_dictionary())
.and_then(|d| d.get("com.apple.security.cs.allow-jit"))
.and_then(|v| v.as_boolean()),
Some(true)
);
};
matches!(
value
.as_dictionary()
.and_then(|d| d.get("com.apple.security.cs.allow-jit"))
.and_then(|v| v.as_boolean()),
Some(true)
)
}