use core::marker::PhantomData;
use std::collections::HashMap;
use crate::{
binary::MachoBinary,
ptrauth::strip_signature,
util::{read_cstr_at, read_u32_le_at, read_u64_le_at, vm_to_file_offset_in},
};
pub const NSCONCRETE_GLOBAL_BLOCK: &str = "_NSConcreteGlobalBlock";
pub const NSCONCRETE_STACK_BLOCK: &str = "_NSConcreteStackBlock";
pub const BLOCK_HAS_COPY_DISPOSE: u32 = 1 << 25;
pub const BLOCK_HAS_CTOR: u32 = 1 << 26;
pub const BLOCK_IS_GLOBAL: u32 = 1 << 28;
pub const BLOCK_HAS_STRET: u32 = 1 << 29;
pub const BLOCK_HAS_SIGNATURE: u32 = 1 << 30;
const BLOCK_LAYOUT_SIZE: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockIsa {
Global,
Stack,
}
#[derive(Debug, Clone, Copy)]
pub struct BlockReference<'a> {
pub kind: BlockIsa,
pub slot_address: u64,
pub dylib: &'a str,
}
#[derive(Debug, Clone)]
pub struct BlockLiteral<'a> {
pub address: u64,
pub isa: BlockIsa,
pub flags: u32,
pub reserved: u32,
pub invoke: u64,
pub descriptor_address: u64,
pub descriptor: Option<BlockDescriptor<'a>>,
}
impl BlockLiteral<'_> {
pub fn is_global(&self) -> bool {
self.flags & BLOCK_IS_GLOBAL != 0
}
pub fn has_copy_dispose(&self) -> bool {
self.flags & BLOCK_HAS_COPY_DISPOSE != 0
}
pub fn has_ctor(&self) -> bool {
self.flags & BLOCK_HAS_CTOR != 0
}
pub fn has_stret(&self) -> bool {
self.flags & BLOCK_HAS_STRET != 0
}
pub fn has_signature(&self) -> bool {
self.flags & BLOCK_HAS_SIGNATURE != 0
}
}
#[derive(Debug, Clone)]
pub struct BlockDescriptor<'a> {
pub address: u64,
pub reserved: u64,
pub size: u64,
pub copy_helper: Option<u64>,
pub dispose_helper: Option<u64>,
pub signature: Option<&'a str>,
pub layout_string: Option<&'a str>,
}
#[derive(Debug)]
pub struct BlockRuntime<'a> {
data: &'a [u8],
segments: Vec<(u64, u64, u64, u64)>,
rebases_by_va: HashMap<u64, u64>,
global_sites: Vec<BlockReference<'a>>,
stack_sites: Vec<BlockReference<'a>>,
}
impl<'a> BlockRuntime<'a> {
pub(crate) fn build(bin: &MachoBinary<'a>) -> Option<Self> {
if !bin.header().is_64() {
#[cfg(feature = "tracing")]
tracing::debug!("darwinscope::block: 32-bit Mach-O - Block walker is 64-bit only");
return None;
}
let data = bin.raw();
let mut global_sites: Vec<BlockReference<'a>> = Vec::new();
let mut stack_sites: Vec<BlockReference<'a>> = Vec::new();
for imp in bin.imports() {
let Some(name) = reborrow_into_data(data, imp.name) else {
continue;
};
let Some(dylib) = reborrow_into_data(data, imp.dylib) else {
continue;
};
let kind = match name {
NSCONCRETE_GLOBAL_BLOCK => BlockIsa::Global,
NSCONCRETE_STACK_BLOCK => BlockIsa::Stack,
_ => continue,
};
let site = BlockReference {
kind,
slot_address: imp.address,
dylib,
};
match kind {
BlockIsa::Global => global_sites.push(site),
BlockIsa::Stack => stack_sites.push(site),
}
}
if global_sites.is_empty() && stack_sites.is_empty() {
return None;
}
let mut segments: Vec<(u64, u64, u64, u64)> = Vec::new();
for s in bin.segments() {
segments.push((s.vmaddr(), s.vmsize(), s.fileoff(), s.filesize()));
}
let mut rebases_by_va: HashMap<u64, u64> = HashMap::new();
for r in bin.chained_rebases() {
rebases_by_va.insert(r.vm_address(), r.target_vmaddr());
}
Some(Self {
data,
segments,
rebases_by_va,
global_sites,
stack_sites,
})
}
pub fn has_global_blocks(&self) -> bool {
!self.global_sites.is_empty()
}
pub fn has_stack_blocks(&self) -> bool {
!self.stack_sites.is_empty()
}
pub fn references(&self) -> ReferenceIter<'a, '_> {
ReferenceIter {
rt: self,
phase: 0,
cursor: 0,
_phantom: PhantomData,
}
}
pub fn literals(&self) -> LiteralIter<'a, '_> {
LiteralIter {
rt: self,
cursor: 0,
_phantom: PhantomData,
}
}
fn resolve_pointer(&self, slot_va: u64, raw: u64) -> u64 {
if let Some(&target) = self.rebases_by_va.get(&slot_va) {
return target;
}
strip_signature(raw)
}
fn read_u32(&self, vmaddr: u64) -> Option<u32> {
let off = vm_to_file_offset_in(self.segments.iter().copied(), vmaddr)? as usize;
read_u32_le_at(self.data, off)
}
fn read_u64(&self, vmaddr: u64) -> Option<u64> {
let off = vm_to_file_offset_in(self.segments.iter().copied(), vmaddr)? as usize;
read_u64_le_at(self.data, off)
}
fn read_cstr(&self, vmaddr: u64) -> Option<&'a str> {
if vmaddr == 0 {
return None;
}
let off = vm_to_file_offset_in(self.segments.iter().copied(), vmaddr)? as usize;
read_cstr_at(self.data, off)
}
fn decode_literal(&self, address: u64) -> Option<BlockLiteral<'a>> {
let flags = self.read_u32(address.wrapping_add(0x08))?;
let reserved = self.read_u32(address.wrapping_add(0x0c))?;
let invoke_slot = address.wrapping_add(0x10);
let invoke_raw = self.read_u64(invoke_slot)?;
let invoke = self.resolve_pointer(invoke_slot, invoke_raw);
let desc_slot = address.wrapping_add(0x18);
let desc_raw = self.read_u64(desc_slot)?;
let descriptor_address = self.resolve_pointer(desc_slot, desc_raw);
let descriptor = self.decode_descriptor(descriptor_address, flags);
Some(BlockLiteral {
address,
isa: BlockIsa::Global,
flags,
reserved,
invoke,
descriptor_address,
descriptor,
})
}
fn decode_descriptor(&self, address: u64, flags: u32) -> Option<BlockDescriptor<'a>> {
if address == 0 {
return None;
}
let reserved = self.read_u64(address)?;
let size = self.read_u64(address.wrapping_add(0x08))?;
if size < BLOCK_LAYOUT_SIZE as u64 {
return None;
}
let mut cursor: u64 = 0x10;
let mut copy_helper = None;
let mut dispose_helper = None;
if flags & BLOCK_HAS_COPY_DISPOSE != 0 {
let copy_slot = address.wrapping_add(cursor);
let copy_raw = self.read_u64(copy_slot)?;
copy_helper = Some(self.resolve_pointer(copy_slot, copy_raw));
cursor = cursor.checked_add(8)?;
let dispose_slot = address.wrapping_add(cursor);
let dispose_raw = self.read_u64(dispose_slot)?;
dispose_helper = Some(self.resolve_pointer(dispose_slot, dispose_raw));
cursor = cursor.checked_add(8)?;
}
let mut signature = None;
let mut layout_string = None;
if flags & BLOCK_HAS_SIGNATURE != 0 {
let sig_slot = address.wrapping_add(cursor);
let sig_raw = self.read_u64(sig_slot)?;
let sig_va = self.resolve_pointer(sig_slot, sig_raw);
signature = self.read_cstr(sig_va);
cursor = cursor.checked_add(8)?;
let layout_slot = address.wrapping_add(cursor);
let layout_raw = self.read_u64(layout_slot)?;
let layout_va = self.resolve_pointer(layout_slot, layout_raw);
layout_string = self.read_cstr(layout_va);
}
Some(BlockDescriptor {
address,
reserved,
size,
copy_helper,
dispose_helper,
signature,
layout_string,
})
}
}
pub struct ReferenceIter<'a, 'p> {
rt: &'p BlockRuntime<'a>,
phase: u8,
cursor: usize,
_phantom: PhantomData<&'a ()>,
}
impl<'a, 'p> Iterator for ReferenceIter<'a, 'p> {
type Item = BlockReference<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.phase {
0 => {
if let Some(r) = self.rt.global_sites.get(self.cursor) {
self.cursor = self.cursor.checked_add(1)?;
return Some(*r);
}
self.phase = 1;
self.cursor = 0;
}
1 => {
if let Some(r) = self.rt.stack_sites.get(self.cursor) {
self.cursor = self.cursor.checked_add(1)?;
return Some(*r);
}
return None;
}
_ => return None,
}
}
}
}
pub struct LiteralIter<'a, 'p> {
rt: &'p BlockRuntime<'a>,
cursor: usize,
_phantom: PhantomData<&'a ()>,
}
impl<'a, 'p> Iterator for LiteralIter<'a, 'p> {
type Item = BlockLiteral<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let site = self.rt.global_sites.get(self.cursor)?;
self.cursor = self.cursor.checked_add(1)?;
if let Some(lit) = self.rt.decode_literal(site.slot_address) {
return Some(lit);
}
}
}
}
fn reborrow_into_data<'a>(data: &'a [u8], s: &str) -> Option<&'a str> {
let s_ptr = s.as_ptr() as usize;
let s_len = s.len();
let data_start = data.as_ptr() as usize;
let data_end = data_start.checked_add(data.len())?;
if s_ptr < data_start {
return None;
}
let s_end = s_ptr.checked_add(s_len)?;
if s_end > data_end {
return None;
}
let off = s_ptr.checked_sub(data_start)?;
let end = off.checked_add(s_len)?;
let bytes = data.get(off..end)?;
core::str::from_utf8(bytes).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flag_accessors_decode_combined_bits() {
let lit = BlockLiteral {
address: 0,
isa: BlockIsa::Global,
flags: BLOCK_IS_GLOBAL
| BLOCK_HAS_COPY_DISPOSE
| BLOCK_HAS_CTOR
| BLOCK_HAS_STRET
| BLOCK_HAS_SIGNATURE,
reserved: 0,
invoke: 0,
descriptor_address: 0,
descriptor: None,
};
assert!(lit.is_global());
assert!(lit.has_copy_dispose());
assert!(lit.has_ctor());
assert!(lit.has_stret());
assert!(lit.has_signature());
}
#[test]
fn flag_accessors_zero_flags_all_false() {
let lit = BlockLiteral {
address: 0,
isa: BlockIsa::Global,
flags: 0,
reserved: 0,
invoke: 0,
descriptor_address: 0,
descriptor: None,
};
assert!(!lit.is_global());
assert!(!lit.has_copy_dispose());
assert!(!lit.has_ctor());
assert!(!lit.has_stret());
assert!(!lit.has_signature());
}
#[test]
fn decode_literal_full_descriptor_3() {
const VMBASE: u64 = 0x1_0000_0000;
let mut buf = vec![0u8; 0x200];
let flags = BLOCK_HAS_COPY_DISPOSE | BLOCK_IS_GLOBAL | BLOCK_HAS_SIGNATURE;
buf[0x08..0x0c].copy_from_slice(&flags.to_le_bytes());
buf[0x10..0x18].copy_from_slice(&0x1_0000_0100u64.to_le_bytes());
buf[0x18..0x20].copy_from_slice(&0x1_0000_0080u64.to_le_bytes());
buf[0x88..0x90].copy_from_slice(&0x40u64.to_le_bytes());
buf[0x90..0x98].copy_from_slice(&0x1_0000_0200u64.to_le_bytes());
buf[0x98..0xa0].copy_from_slice(&0x1_0000_0210u64.to_le_bytes());
buf[0xa0..0xa8].copy_from_slice(&0x1_0000_00c0u64.to_le_bytes());
buf[0xa8..0xb0].copy_from_slice(&0x1_0000_00e0u64.to_le_bytes());
let sig = b"v@?@\"NSString\"\0";
buf[0xc0..0xc0 + sig.len()].copy_from_slice(sig);
buf[0xe0] = 0;
let segments = vec![(VMBASE, buf.len() as u64, 0u64, buf.len() as u64)];
let rt = BlockRuntime {
data: &buf,
segments,
rebases_by_va: HashMap::new(),
global_sites: vec![BlockReference {
kind: BlockIsa::Global,
slot_address: VMBASE,
dylib: "/usr/lib/libobjc.A.dylib",
}],
stack_sites: Vec::new(),
};
let lit = rt.decode_literal(VMBASE).expect("literal decodes");
assert_eq!(lit.address, VMBASE);
assert_eq!(lit.flags, flags);
assert!(lit.is_global());
assert!(lit.has_copy_dispose());
assert!(lit.has_signature());
assert!(!lit.has_stret());
assert_eq!(lit.invoke, 0x1_0000_0100);
assert_eq!(lit.descriptor_address, 0x1_0000_0080);
let desc = lit.descriptor.expect("descriptor decodes");
assert_eq!(desc.address, 0x1_0000_0080);
assert_eq!(desc.reserved, 0);
assert_eq!(desc.size, 0x40);
assert_eq!(desc.copy_helper, Some(0x1_0000_0200));
assert_eq!(desc.dispose_helper, Some(0x1_0000_0210));
assert_eq!(desc.signature, Some("v@?@\"NSString\""));
assert_eq!(desc.layout_string, Some(""));
let lits: Vec<_> = rt.literals().collect();
assert_eq!(lits.len(), 1);
assert_eq!(lits[0].invoke, 0x1_0000_0100);
let refs: Vec<_> = rt.references().collect();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].kind, BlockIsa::Global);
assert_eq!(refs[0].slot_address, VMBASE);
}
#[test]
fn decode_descriptor_rejects_size_below_block_layout() {
const VMBASE: u64 = 0x1_0000_0000;
let mut buf = vec![0u8; 0x40];
buf[0x08..0x10].copy_from_slice(&16u64.to_le_bytes());
let rt = BlockRuntime {
data: &buf,
segments: vec![(VMBASE, buf.len() as u64, 0u64, buf.len() as u64)],
rebases_by_va: HashMap::new(),
global_sites: Vec::new(),
stack_sites: Vec::new(),
};
assert!(rt.decode_descriptor(VMBASE, 0).is_none());
}
#[test]
fn flag_constants_match_block_private_h() {
assert_eq!(BLOCK_HAS_COPY_DISPOSE, 0x0200_0000);
assert_eq!(BLOCK_HAS_CTOR, 0x0400_0000);
assert_eq!(BLOCK_IS_GLOBAL, 0x1000_0000);
assert_eq!(BLOCK_HAS_STRET, 0x2000_0000);
assert_eq!(BLOCK_HAS_SIGNATURE, 0x4000_0000);
}
}