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},
};
const CFSTRING_STRIDE: usize = 32;
const CFSTRING_FLAG_ENCODING_MASK: u32 = 0x07f8;
const CFSTRING_FLAG_ASCII: u32 = 0x07c8;
const CFSTRING_FLAG_UTF16: u32 = 0x07d0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CFStringEncoding {
Ascii,
Utf16Le,
Other(u32),
}
impl CFStringEncoding {
pub fn from_flags(flags: u32) -> Self {
match flags & CFSTRING_FLAG_ENCODING_MASK {
CFSTRING_FLAG_ASCII => Self::Ascii,
CFSTRING_FLAG_UTF16 => Self::Utf16Le,
_ => Self::Other(flags),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CFStringBody<'a> {
Ascii(&'a str),
Utf16(String),
Unresolved,
}
#[derive(Debug, Clone)]
pub struct CFString<'a> {
pub address: u64,
pub flags: u32,
pub encoding: CFStringEncoding,
pub body_address: u64,
pub length: u64,
pub body: CFStringBody<'a>,
}
#[derive(Debug)]
pub struct CFStringRuntime<'a> {
data: &'a [u8],
segments: Vec<(u64, u64, u64, u64)>,
section_body: &'a [u8],
section_vmaddr: u64,
rebases_by_va: HashMap<u64, u64>,
}
impl<'a> CFStringRuntime<'a> {
pub(crate) fn build(bin: &MachoBinary<'a>) -> Option<Self> {
if !bin.header().is_64() {
#[cfg(feature = "tracing")]
tracing::debug!(
"darwinscope::cfstring: 32-bit Mach-O - CFString walker is 64-bit only"
);
return None;
}
let mut section_body: &[u8] = &[];
let mut section_vmaddr: u64 = 0;
let mut found = false;
for sect in bin.sections() {
if sect.sectname() == "__cfstring" {
section_body = sect.body();
section_vmaddr = sect.addr();
found = true;
break;
}
}
if !found {
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: bin.raw(),
segments,
section_body,
section_vmaddr,
rebases_by_va,
})
}
pub fn iter(&self) -> CFStringIter<'a, '_> {
CFStringIter {
rt: self,
cursor: 0,
_phantom: PhantomData,
}
}
}
pub struct CFStringIter<'a, 'p> {
rt: &'p CFStringRuntime<'a>,
cursor: usize,
_phantom: PhantomData<&'a ()>,
}
impl<'a, 'p> Iterator for CFStringIter<'a, 'p> {
type Item = CFString<'a>;
fn next(&mut self) -> Option<Self::Item> {
let body = self.rt.section_body;
let entry_off = self.cursor;
let next_cursor = self.cursor.checked_add(CFSTRING_STRIDE)?;
if next_cursor > body.len() {
return None;
}
self.cursor = next_cursor;
let flags_off = entry_off.checked_add(0x08)?;
let str_off = entry_off.checked_add(0x10)?;
let length_off = entry_off.checked_add(0x18)?;
let flags = read_u32_le_at(body, flags_off).unwrap_or(0);
let raw_str = read_u64_le_at(body, str_off).unwrap_or(0);
let length = read_u64_le_at(body, length_off).unwrap_or(0);
let entry_va = self.rt.section_vmaddr.wrapping_add(entry_off as u64);
let str_slot_va = self.rt.section_vmaddr.wrapping_add(str_off as u64);
let body_address = resolve_pointer(self.rt, str_slot_va, raw_str);
let encoding = CFStringEncoding::from_flags(flags);
let body = decode_body(self.rt, encoding, body_address, length);
Some(CFString {
address: entry_va,
flags,
encoding,
body_address,
length,
body,
})
}
}
fn resolve_pointer(rt: &CFStringRuntime<'_>, slot_va: u64, raw: u64) -> u64 {
if let Some(&target) = rt.rebases_by_va.get(&slot_va) {
return target;
}
strip_signature(raw)
}
fn decode_body<'a>(
rt: &CFStringRuntime<'a>,
encoding: CFStringEncoding,
body_address: u64,
length: u64,
) -> CFStringBody<'a> {
if body_address == 0 {
return CFStringBody::Unresolved;
}
let Some(off_u64) = vm_to_file_offset_in(rt.segments.iter().copied(), body_address) else {
return CFStringBody::Unresolved;
};
let off = off_u64 as usize;
match encoding {
CFStringEncoding::Ascii => {
let Ok(want) = usize::try_from(length) else {
return CFStringBody::Unresolved;
};
let Some(end) = off.checked_add(want) else {
return CFStringBody::Unresolved;
};
let Some(slice) = rt.data.get(off..end) else {
return match read_cstr_at(rt.data, off) {
Some(s) => CFStringBody::Ascii(s),
None => CFStringBody::Unresolved,
};
};
match core::str::from_utf8(slice) {
Ok(s) => CFStringBody::Ascii(s),
Err(_) => CFStringBody::Unresolved,
}
}
CFStringEncoding::Utf16Le => {
let Ok(chars) = usize::try_from(length) else {
return CFStringBody::Unresolved;
};
let Some(byte_len) = chars.checked_mul(2) else {
return CFStringBody::Unresolved;
};
let Some(end) = off.checked_add(byte_len) else {
return CFStringBody::Unresolved;
};
let Some(slice) = rt.data.get(off..end) else {
return CFStringBody::Unresolved;
};
decode_utf16_le(slice)
.map(CFStringBody::Utf16)
.unwrap_or(CFStringBody::Unresolved)
}
CFStringEncoding::Other(_) => CFStringBody::Unresolved,
}
}
fn decode_utf16_le(bytes: &[u8]) -> Option<String> {
if !bytes.len().is_multiple_of(2) {
return None;
}
let mut units: Vec<u16> = Vec::with_capacity(bytes.len() / 2);
let mut i = 0usize;
while i < bytes.len() {
let lo = *bytes.get(i)?;
let hi = *bytes.get(i.checked_add(1)?)?;
units.push(u16::from_le_bytes([lo, hi]));
i = i.checked_add(2)?;
}
String::from_utf16(&units).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encoding_from_flags_ascii_and_utf16() {
assert_eq!(
CFStringEncoding::from_flags(0x07c8),
CFStringEncoding::Ascii
);
assert_eq!(
CFStringEncoding::from_flags(0x07d0),
CFStringEncoding::Utf16Le
);
assert_eq!(
CFStringEncoding::from_flags(0xffff_07c8),
CFStringEncoding::Ascii
);
assert_eq!(
CFStringEncoding::from_flags(0xdead_07d0),
CFStringEncoding::Utf16Le
);
assert!(matches!(
CFStringEncoding::from_flags(0x1234),
CFStringEncoding::Other(0x1234)
));
}
#[test]
fn decode_utf16_le_basic() {
let bytes = [0x48, 0x00, 0x69, 0x00];
assert_eq!(decode_utf16_le(&bytes).as_deref(), Some("Hi"));
}
#[test]
fn decode_utf16_le_supplementary_plane() {
let bytes = [0x3d, 0xd8, 0x00, 0xde];
let s = decode_utf16_le(&bytes).expect("valid surrogate pair decodes");
assert_eq!(s, "\u{1F600}");
}
#[test]
fn decode_utf16_le_rejects_odd_length() {
assert_eq!(decode_utf16_le(&[0x48, 0x00, 0x69]), None);
}
#[test]
fn decode_utf16_le_rejects_lone_surrogate() {
let bytes = [0x3d, 0xd8, 0x00, 0x00];
assert_eq!(decode_utf16_le(&bytes), None);
}
}