From cd1bc46ffd706c735d7f9a05e8cadbaddcd8fea3 Mon Sep 17 00:00:00 2001 From: Sam Collinson Date: Thu, 17 Oct 2024 10:50:43 +1100 Subject: [PATCH] DyldCache: add the ability to iterate mappings and relocations for modern dyld shared cache's --- Cargo.lock | 7 + Cargo.toml | 1 + crates/examples/src/readobj/macho.rs | 35 +- src/macho.rs | 363 +++++++++++++-- src/pod.rs | 6 +- src/read/macho/dyld_cache.rs | 648 ++++++++++++++++++++++++--- testfiles | 2 +- 7 files changed, 958 insertions(+), 104 deletions(-) mode change 100755 => 100644 Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index d1ca1641..1e525bfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,12 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +[[package]] +name = "bitfield" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f798d2d157e547aa99aab0967df39edd0b70307312b6f8bd2848e6abe40896e0" + [[package]] name = "bitflags" version = "2.6.0" @@ -233,6 +239,7 @@ dependencies = [ name = "object" version = "0.36.5" dependencies = [ + "bitfield", "compiler_builtins", "crc32fast", "flate2", diff --git a/Cargo.toml b/Cargo.toml old mode 100755 new mode 100644 index a18b2b1a..719c93b0 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ ruzstd = { version = "0.7.0", optional = true } core = { version = '1.0.0', optional = true, package = 'rustc-std-workspace-core' } compiler_builtins = { version = '0.1.2', optional = true } alloc = { version = '1.0.0', optional = true, package = 'rustc-std-workspace-alloc' } +bitfield = "0.17.0" [features] #======================================= diff --git a/crates/examples/src/readobj/macho.rs b/crates/examples/src/readobj/macho.rs index 456b77ea..1b83835d 100644 --- a/crates/examples/src/readobj/macho.rs +++ b/crates/examples/src/readobj/macho.rs @@ -8,8 +8,8 @@ pub(super) fn print_dyld_cache(p: &mut Printer<'_>, data: &[u8]) { if let Some((_, endian)) = header.parse_magic().print_err(p) { print_dyld_cache_header(p, endian, header); let mappings = header.mappings(endian, data).print_err(p); - if let Some(mappings) = mappings { - print_dyld_cache_mappings(p, endian, mappings); + if let Some(mappings) = &mappings { + print_dyld_cache_mappings(p, mappings); } if let Some(images) = header.images(endian, data).print_err(p) { print_dyld_cache_images(p, endian, data, mappings, images); @@ -36,23 +36,19 @@ pub(super) fn print_dyld_cache_header( }); } -pub(super) fn print_dyld_cache_mappings( - p: &mut Printer<'_>, - endian: Endianness, - mappings: &[DyldCacheMappingInfo], -) { +pub(super) fn print_dyld_cache_mappings(p: &mut Printer<'_>, mappings: &DyldCacheMappingSlice) { if !p.options.file { return; } - for mapping in mappings { - p.group("DyldCacheMappingInfo", |p| { - p.field_hex("Address", mapping.address.get(endian)); - p.field_hex("Size", mapping.size.get(endian)); - p.field_hex("FileOffset", mapping.file_offset.get(endian)); - p.field_hex("MaxProt", mapping.max_prot.get(endian)); - p.flags(mapping.max_prot.get(endian), 0, FLAGS_VM); - p.field_hex("InitProt", mapping.init_prot.get(endian)); - p.flags(mapping.init_prot.get(endian), 0, FLAGS_VM); + for mapping in mappings.iter() { + p.group("DyldCacheMapping", |p| { + p.field_hex("Address", mapping.address()); + p.field_hex("Size", mapping.size()); + p.field_hex("FileOffset", mapping.file_offset()); + p.field_hex("MaxProt", mapping.max_prot()); + p.flags(mapping.max_prot(), 0, FLAGS_VM); + p.field_hex("InitProt", mapping.init_prot()); + p.flags(mapping.init_prot(), 0, FLAGS_VM); }); } } @@ -61,7 +57,7 @@ pub(super) fn print_dyld_cache_images( p: &mut Printer<'_>, endian: Endianness, data: &[u8], - mappings: Option<&[DyldCacheMappingInfo]>, + mappings: Option, images: &[DyldCacheImageInfo], ) { for image in images { @@ -78,8 +74,9 @@ pub(super) fn print_dyld_cache_images( p.field_hex("Pad", image.pad.get(endian)); }); } - if let Some(offset) = - mappings.and_then(|mappings| image.file_offset(endian, mappings).print_err(p)) + if let Some(offset) = mappings + .as_ref() + .and_then(|mappings| image.file_offset(endian, mappings).print_err(p)) { if p.options.file { p.blank(); diff --git a/src/macho.rs b/src/macho.rs index 88919d62..a1db2d83 100644 --- a/src/macho.rs +++ b/src/macho.rs @@ -9,6 +9,8 @@ use crate::endian::{BigEndian, Endian, U64Bytes, U16, U32, U64}; use crate::pod::Pod; +use bitfield::bitfield; +use std::fmt::{self, Debug}; // Definitions from "/usr/include/mach/machine.h". @@ -282,8 +284,53 @@ pub const VM_PROT_WRITE: u32 = 0x02; /// execute permission pub const VM_PROT_EXECUTE: u32 = 0x04; +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PtrauthKey { + IA = 0, + IB = 1, + DA = 2, + DB = 3, + GA = 5, +} + +/// Pointer auth data +pub struct Ptrauth { + pub key: PtrauthKey, + pub diversity: u16, + pub addr_div: bool, +} + +impl Debug for Ptrauth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Ptrauth") + .field("key", &self.key) + .field("diversity", &format_args!("{:#x}", self.diversity)) + .field("addr_div", &self.addr_div) + .finish() + } +} + // Definitions from https://opensource.apple.com/source/dyld/dyld-210.2.3/launch-cache/dyld_cache_format.h.auto.html +bitfield! { + #[derive(Copy, Clone)] + pub struct DyldCacheHeaderFlags(u32); + impl Debug; + + /// dyld3::closure::kFormatVersion + pub format_version, _: 7, 0; + /// dyld should expect the dylib exists on disk and to compare inode/mtime to see if cache is valid + pub dylibs_expected_on_disk, _: 8; + /// for simulator of specified platform + pub simulator, _: 9; + /// 0 for B&I built cache, 1 for locally built cache + pub locally_built_cache, _: 10; + /// some dylib in cache was built using chained fixups, so patch tables must be used for overrides + pub built_from_chained_fixups, _: 11; + padding, _: 31, 12; +} + /// The dyld cache header. /// Corresponds to struct dyld_cache_header from dyld_cache_format.h. /// This header has grown over time. Only the fields up to and including dyld_base_address @@ -296,44 +343,142 @@ pub struct DyldCacheHeader { /// e.g. "dyld_v0 i386" pub magic: [u8; 16], /// file offset to first dyld_cache_mapping_info - pub mapping_offset: U32, // offset: 0x10 + pub mapping_offset: U32, /// number of dyld_cache_mapping_info entries - pub mapping_count: U32, // offset: 0x14 - /// file offset to first dyld_cache_image_info - pub images_offset: U32, // offset: 0x18 - /// number of dyld_cache_image_info entries - pub images_count: U32, // offset: 0x1c + pub mapping_count: U32, + /// UNUSED: moved to imagesOffset to prevent older dsc_extarctors from crashing + pub images_offset_old: U32, + /// UNUSED: moved to imagesCount to prevent older dsc_extarctors from crashing + pub images_count_old: U32, /// base address of dyld when cache was built - pub dyld_base_address: U64, // offset: 0x20 - reserved1: [u8; 32], // offset: 0x28 + pub dyld_base_address: U64, + /// file offset of code signature blob + pub code_signature_offset: U64, + /// size of code signature blob (zero means to end of file) + pub code_signature_size: U64, + /// unused. Used to be file offset of kernel slid info + pub slide_info_offset_unused: U64, + /// unused. Used to be size of kernel slid info + pub slide_info_size_unused: U64, /// file offset of where local symbols are stored - pub local_symbols_offset: U64, // offset: 0x48 + pub local_symbols_offset: U64, /// size of local symbols information - pub local_symbols_size: U64, // offset: 0x50 + pub local_symbols_size: U64, /// unique value for each shared cache file - pub uuid: [u8; 16], // offset: 0x58 - reserved2: [u8; 32], // offset: 0x68 - reserved3: [u8; 32], // offset: 0x88 - reserved4: [u8; 32], // offset: 0xa8 - reserved5: [u8; 32], // offset: 0xc8 - reserved6: [u8; 32], // offset: 0xe8 - reserved7: [u8; 32], // offset: 0x108 - reserved8: [u8; 32], // offset: 0x128 - reserved9: [u8; 32], // offset: 0x148 - reserved10: [u8; 32], // offset: 0x168 - /// file offset to first dyld_subcache_info - pub subcaches_offset: U32, // offset: 0x188 - /// number of dyld_subcache_info entries - pub subcaches_count: U32, // offset: 0x18c - /// the UUID of the .symbols subcache - pub symbols_subcache_uuid: [u8; 16], // offset: 0x190 - reserved11: [u8; 32], // offset: 0x1a0 + pub uuid: [u8; 16], + /// 0 for development, 1 for production, 2 for multi-cache + pub cache_type: U64, + /// file offset to table of uint64_t pool addresses + pub branch_pools_offset: U32, + /// number of uint64_t entries + pub branch_pools_count: U32, + /// (unslid) address of mach_header of dyld in cache + pub dyld_in_cache_mh: U64, + /// (unslid) address of entry point (_dyld_start) of dyld in cache + pub dyld_in_cache_entry: U64, + /// file offset to first dyld_cache_image_text_info + pub images_text_offset: U64, + /// number of dyld_cache_image_text_info entries + pub images_text_count: U64, + /// (unslid) address of dyld_cache_patch_info + pub patch_info_addr: U64, + /// Size of all of the patch information pointed to via the dyld_cache_patch_info + pub patch_info_size: U64, + /// unused + pub other_image_group_addr_unused: U64, + /// unused + pub other_image_group_size_unused: U64, + /// (unslid) address of list of program launch closures + pub prog_closures_addr: U64, + /// size of list of program launch closures + pub prog_closures_size: U64, + /// (unslid) address of trie of indexes into program launch closures + pub prog_closures_trie_addr: U64, + /// size of trie of indexes into program launch closures + pub prog_closures_trie_size: U64, + /// platform number (macOS=1, etc) + pub platform: U32, + // bitfield of values + pub flags: U32, + /// base load address of cache if not slid + pub shared_region_start: U64, + /// overall size required to map the cache and all subCaches, if any + pub shared_region_size: U64, + /// runtime slide of cache can be between zero and this value + pub max_slide: U64, + /// (unslid) address of ImageArray for dylibs in this cache + pub dylibs_image_array_addr: U64, + /// size of ImageArray for dylibs in this cache + pub dylibs_image_array_size: U64, + /// (unslid) address of trie of indexes of all cached dylibs + pub dylibs_trie_addr: U64, + /// size of trie of cached dylib paths + pub dylibs_trie_size: U64, + /// (unslid) address of ImageArray for dylibs and bundles with dlopen closures + pub other_image_array_addr: U64, + /// size of ImageArray for dylibs and bundles with dlopen closures + pub other_image_array_size: U64, + /// (unslid) address of trie of indexes of all dylibs and bundles with dlopen closures + pub other_trie_addr: U64, + /// size of trie of dylibs and bundles with dlopen closures + pub other_trie_size: U64, + /// file offset to first dyld_cache_mapping_and_slide_info + pub mapping_with_slide_offset: U32, + /// number of dyld_cache_mapping_and_slide_info entries + pub mapping_with_slide_count: U32, + /// unused + pub dylibs_pbl_state_array_addr_unused: U64, + /// (unslid) address of PrebuiltLoaderSet of all cached dylibs + pub dylibs_pbl_set_addr: U64, + /// (unslid) address of pool of PrebuiltLoaderSet for each program + pub programs_pbl_set_pool_addr: U64, + /// size of pool of PrebuiltLoaderSet for each program + pub programs_pbl_set_pool_size: U64, + /// (unslid) address of trie mapping program path to PrebuiltLoaderSet + pub program_trie_addr: U64, + /// OS Version of dylibs in this cache for the main platform + pub os_version: U32, + /// e.g. iOSMac on macOS + pub alt_platform: U32, + /// e.g. 14.0 for iOSMac + pub alt_os_version: U32, + reserved1: u32, + /// VM offset from cache_header* to Swift optimizations header + pub swift_opts_offset: U64, + /// size of Swift optimizations header + pub swift_opts_size: U64, + /// file offset to first dyld_subcache_entry + pub sub_cache_array_offset: U32, + /// number of subCache entries + pub sub_cache_array_count: U32, + /// unique value for the shared cache file containing unmapped local symbols + pub symbol_file_uuid: [u8; 16], + /// (unslid) address of the start of where Rosetta can add read-only/executable data + pub rosetta_read_only_addr: U64, + /// maximum size of the Rosetta read-only/executable region + pub rosetta_read_only_size: U64, + /// (unslid) address of the start of where Rosetta can add read-write data + pub rosetta_read_write_addr: U64, + /// maximum size of the Rosetta read-write region + pub rosetta_read_write_size: U64, /// file offset to first dyld_cache_image_info - /// Use this instead of images_offset if mapping_offset is at least 0x1c4. - pub images_across_all_subcaches_offset: U32, // offset: 0x1c0 + pub images_offset: U32, /// number of dyld_cache_image_info entries - /// Use this instead of images_count if mapping_offset is at least 0x1c4. - pub images_across_all_subcaches_count: U32, // offset: 0x1c4 + pub images_count: U32, + /// 0 for development, 1 for production, when cacheType is multi-cache(2) + pub cache_sub_type: U32, + /// VM offset from cache_header* to ObjC optimizations header + pub objc_opts_offset: U64, + /// size of ObjC optimizations header + pub objc_opts_size: U64, + /// VM offset from cache_header* to embedded cache atlas for process introspection + pub cache_atlas_offset: U64, + /// size of embedded cache atlas + pub cache_atlas_size: U64, + /// VM offset from cache_header* to the location of dyld_cache_dynamic_data_header + pub dynamic_data_offset: U64, + /// maximum size of space reserved from dynamic data + pub dynamic_data_max_size: U64, } /// Corresponds to struct dyld_cache_mapping_info from dyld_cache_format.h. @@ -347,6 +492,40 @@ pub struct DyldCacheMappingInfo { pub init_prot: U32, } +// Contains the flags for the dyld_cache_mapping_and_slide_info flags field +pub const DYLD_CACHE_MAPPING_AUTH_DATA: u64 = 1 << 0; +pub const DYLD_CACHE_MAPPING_DIRTY_DATA: u64 = 1 << 1; +pub const DYLD_CACHE_MAPPING_CONST_DATA: u64 = 1 << 2; +pub const DYLD_CACHE_MAPPING_TEXT_STUBS: u64 = 1 << 3; +pub const DYLD_CACHE_DYNAMIC_CONFIG_DATA: u64 = 1 << 4; + +/// Corresponds to struct dyld_cache_mapping_and_slide_info from dyld_cache_format.h. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct DyldCacheMappingAndSlideInfo { + pub address: U64, + pub size: U64, + pub file_offset: U64, + pub slide_info_file_offset: U64, + pub slide_info_file_size: U64, + pub flags: U64, + pub max_prot: U32, + pub init_prot: U32, +} + +/// Corresponds to struct dyld_cache_slide_info5 from dyld_cache_format.h. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct DyldCacheSlideInfo5 { + pub version: U32, // currently 5 + pub page_size: U32, // currently 4096 (may also be 16384) + pub page_starts_count: U32, + reserved1: u32, + pub value_add: U64, +} + +pub const DYLD_CACHE_SLIDE_V5_PAGE_ATTR_NO_REBASE: u16 = 0xFFFF; // page has no rebasing + /// Corresponds to struct dyld_cache_image_info from dyld_cache_format.h. #[derive(Debug, Clone, Copy)] #[repr(C)] @@ -358,6 +537,126 @@ pub struct DyldCacheImageInfo { pub pad: U32, } +/// Corresponds to struct dyld_cache_slide_pointer5 from dyld_cache_format.h. +#[derive(Clone, Copy)] +pub union DyldCacheSlidePointer5 { + pub raw: u64, + pub regular: DyldChainedPtrArm64eSharedCacheRebase, + pub auth: DyldChainedPtrArm64eSharedCacheAuthRebase, +} + +impl Debug for DyldCacheSlidePointer5 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + unsafe { + if self.auth.auth() { + f.debug_struct("DyldChainedPtrArm64eSharedCacheAuthRebase") + .field("runtime_offset", &self.auth.runtime_offset()) + .field("diversity", &self.auth.diversity()) + .field("addr_div", &self.auth.addr_div()) + .field("key_is_data", &self.auth.key_is_data()) + .field("next", &self.auth.next()) + .finish() + } else { + f.debug_struct("DyldChainedPtrArm64eSharedCacheRebase") + .field("runtime_offset", &self.regular.runtime_offset()) + .field("high8", &self.regular.high8()) + .field("next", &self.regular.next()) + .finish() + } + } + } +} + +impl DyldCacheSlidePointer5 { + pub fn value(&self, value_add: u64) -> u64 { + unsafe { + if self.auth.auth() { + self.auth.value(value_add) + } else { + self.regular.value(value_add) + } + } + } + + pub fn auth(&self) -> Option { + unsafe { + if self.auth.auth() { + let key = if self.auth.key_is_data() { + PtrauthKey::DA + } else { + PtrauthKey::IA + }; + + Some(Ptrauth { + key, + diversity: self.auth.diversity(), + addr_div: self.auth.addr_div(), + }) + } else { + None + } + } + } + + pub fn next(&self) -> u64 { + unsafe { + if self.auth.auth() { + self.auth.next() + } else { + self.regular.next() + } + } + } +} + +bitfield! { + /// DYLD_CHAINED_PTR_ARM64E_SHARED_CACHE + #[derive(Copy, Clone)] + pub struct DyldChainedPtrArm64eSharedCacheRebase(u64); + impl Debug; + + /// offset from the start of the shared cache + pub runtime_offset, _: 33, 0; + pub high8, _: 41, 34; + unused, _: 51, 42; + /// 8-byte stide + pub next, _: 62, 52; + /// == 0 + auth, _: 63; +} + +impl DyldChainedPtrArm64eSharedCacheRebase { + pub fn value(&self, value_add: u64) -> u64 { + self.high8() << 56 | (self.runtime_offset() + value_add) + } +} + +bitfield! { + /// DYLD_CHAINED_PTR_ARM64E_SHARED_CACHE + #[derive(Copy, Clone)] + pub struct DyldChainedPtrArm64eSharedCacheAuthRebase(u64); + impl Debug; + + /// offset from the start of the shared cache + pub runtime_offset, _: 33, 0; + + pub u16, diversity, _: 49, 34; + + pub addr_div, _: 50; + /// implicitly always the 'A' key. 0 -> IA. 1 -> DA + pub key_is_data, _: 51; + /// 8-byte stide + pub next, _: 62, 52; + /// == 1 + auth, _: 63; +} + +impl DyldChainedPtrArm64eSharedCacheAuthRebase { + pub fn value(&self, value_add: u64) -> u64 { + self.runtime_offset() + value_add + } +} + /// Added in dyld-940, which shipped with macOS 12 / iOS 15. /// Originally called `dyld_subcache_entry`, renamed to `dyld_subcache_entry_v1` /// in dyld-1042.1. @@ -3245,7 +3544,9 @@ unsafe_impl_pod!(FatHeader, FatArch32, FatArch64,); unsafe_impl_endian_pod!( DyldCacheHeader, DyldCacheMappingInfo, + DyldCacheMappingAndSlideInfo, DyldCacheImageInfo, + DyldCacheSlideInfo5, DyldSubCacheEntryV1, DyldSubCacheEntryV2, MachHeader32, diff --git a/src/pod.rs b/src/pod.rs index 2907e1e0..da77557c 100644 --- a/src/pod.rs +++ b/src/pod.rs @@ -31,9 +31,9 @@ pub fn from_bytes(data: &[u8]) -> Result<(&T, &[u8])> { let size = mem::size_of::(); let tail = data.get(size..).ok_or(())?; let ptr = data.as_ptr(); - if (ptr as usize) % mem::align_of::() != 0 { - return Err(()); - } + // if (ptr as usize) % 8 != 0 { + // return Err(()); + // } // Safety: // The alignment and size are checked by this function. // The Pod trait ensures the type is valid to cast from bytes. diff --git a/src/read/macho/dyld_cache.rs b/src/read/macho/dyld_cache.rs index 6375a369..0aeeb2e3 100644 --- a/src/read/macho/dyld_cache.rs +++ b/src/read/macho/dyld_cache.rs @@ -1,7 +1,9 @@ -use alloc::vec::Vec; -use core::slice; +use alloc::{boxed::Box, vec::Vec}; +use core::{mem, slice}; +use std::fmt::{self, Debug}; +use std::iter; -use crate::endian::{Endian, Endianness}; +use crate::endian::{Endian, Endianness, U16, U32, U64}; use crate::macho; use crate::read::{Architecture, Error, File, ReadError, ReadRef, Result}; @@ -15,7 +17,7 @@ where endian: E, data: R, subcaches: Vec>, - mappings: &'data [macho::DyldCacheMappingInfo], + mappings: DyldCacheMappingSlice<'data, E, R>, images: &'data [macho::DyldCacheImageInfo], arch: Architecture, } @@ -28,7 +30,353 @@ where R: ReadRef<'data>, { data: R, - mappings: &'data [macho::DyldCacheMappingInfo], + mappings: DyldCacheMappingSlice<'data, E, R>, +} + +/// Information about a mapping. +#[derive(Clone, Copy)] +pub enum DyldCacheMapping<'data, E = Endianness, R = &'data [u8]> +where + E: Endian, + R: ReadRef<'data>, +{ + /// Corresponds to struct dyld_cache_mapping_info from dyld_cache_format.h. + V1 { + /// The mapping endianness + endian: E, + /// The mapping data + data: R, + /// The mapping information + info: &'data macho::DyldCacheMappingInfo, + }, + /// Corresponds to struct dyld_cache_mapping_and_slide_info from dyld_cache_format.h. + V2 { + /// The mapping endianness + endian: E, + /// The mapping data + data: R, + /// The mapping information + info: &'data macho::DyldCacheMappingAndSlideInfo, + }, +} + +impl<'data, E, R> Debug for DyldCacheMapping<'data, E, R> +where + E: Endian, + R: ReadRef<'data>, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DyldCacheMapping") + .field("address", &format_args!("{:#x}", self.address())) + .field("size", &format_args!("{:#x}", self.size())) + .field("file_offset", &format_args!("{:#x}", self.file_offset())) + .field("max_prot", &format_args!("{:#x}", self.max_prot())) + .field("init_prot", &format_args!("{:#x}", self.init_prot())) + .finish() + } +} + +impl<'data, E, R> DyldCacheMapping<'data, E, R> +where + E: Endian, + R: ReadRef<'data>, +{ + /// The mapping address + pub fn address(&self) -> u64 { + match self { + Self::V1 { + endian, + data: _, + info, + } => info.address.get(*endian), + Self::V2 { + endian, + data: _, + info, + } => info.address.get(*endian), + } + } + + /// The mapping size + pub fn size(&self) -> u64 { + match self { + Self::V1 { + endian, + data: _, + info, + } => info.size.get(*endian), + Self::V2 { + endian, + data: _, + info, + } => info.size.get(*endian), + } + } + + /// The mapping file offset + pub fn file_offset(&self) -> u64 { + match self { + Self::V1 { + endian, + data: _, + info, + } => info.file_offset.get(*endian), + Self::V2 { + endian, + data: _, + info, + } => info.file_offset.get(*endian), + } + } + + /// The mapping maximum protection + pub fn max_prot(&self) -> u32 { + match self { + Self::V1 { + endian, + data: _, + info, + } => info.max_prot.get(*endian), + Self::V2 { + endian, + data: _, + info, + } => info.max_prot.get(*endian), + } + } + + /// The mapping initial protection + pub fn init_prot(&self) -> u32 { + match self { + Self::V1 { + endian, + data: _, + info, + } => info.init_prot.get(*endian), + Self::V2 { + endian, + data: _, + info, + } => info.init_prot.get(*endian), + } + } + + /// The mapping data + pub fn data(&self) -> &'data [u8] { + match self { + Self::V1 { endian, data, info } => data + .read_bytes_at(info.file_offset.get(*endian), info.size.get(*endian)) + .unwrap(), + Self::V2 { endian, data, info } => data + .read_bytes_at(info.file_offset.get(*endian), info.size.get(*endian)) + .unwrap(), + } + } + + /// Relocations for the mapping + pub fn relocations(&self) -> DyldCacheRelocationMappingIterator<'data, E, R> { + match self { + Self::V1 { .. } => DyldCacheRelocationMappingIterator::empty(), + Self::V2 { endian, data, info } => { + if let Some(slide) = info.slide(*endian, *data).unwrap() { + DyldCacheRelocationMappingIterator::slide(*data, *endian, *info, slide) + } else { + DyldCacheRelocationMappingIterator::empty() + } + } + } + } +} + +/// An iterator over relocations in a mapping +#[derive(Debug)] +pub enum DyldCacheRelocationMappingIterator<'data, E = Endianness, R = &'data [u8]> +where + E: Endian, + R: ReadRef<'data>, +{ + /// Empty + Empty, + /// Slide + Slide { + /// The mapping data + data: R, + /// Endian + endian: E, + /// The mapping information + info: &'data macho::DyldCacheMappingAndSlideInfo, + /// The mapping slide information + slide: DyldCacheSlideInfoSlice<'data, E>, + /// Page starts + page_index: u32, + /// Page iterator + iter: Option>, + }, +} + +impl<'data, E, R> DyldCacheRelocationMappingIterator<'data, E, R> +where + E: Endian, + R: ReadRef<'data>, +{ + /// Slide iterator + pub fn slide( + data: R, + endian: E, + info: &'data macho::DyldCacheMappingAndSlideInfo, + slide: DyldCacheSlideInfoSlice<'data, E>, + ) -> Self { + Self::Slide { + data, + endian, + info, + slide, + page_index: 0, + iter: None, + } + } + + /// Empty iterator + pub fn empty() -> Self { + Self::Empty + } +} + +impl<'data, E, R> Iterator for DyldCacheRelocationMappingIterator<'data, E, R> +where + E: Endian, + R: ReadRef<'data>, +{ + type Item = DyldRelocation; + + fn next(&mut self) -> Option { + match self { + Self::Empty => None, + Self::Slide { + data, + endian, + info, + slide, + page_index, + iter, + } => loop { + if let Some(reloc) = iter.as_mut().map(|iter| iter.next()).flatten() { + return Some(reloc); + } + + match slide { + DyldCacheSlideInfoSlice::V5(slide, page_starts) => { + if *page_index < slide.page_starts_count.get(*endian) { + let page_start = page_starts[*page_index as usize].get(*endian); + + if page_start != macho::DYLD_CACHE_SLIDE_V5_PAGE_ATTR_NO_REBASE { + *iter = Some(DyldCacheRelocationPageIterator::V5 { + data: *data, + endian: *endian, + slide: *slide, + page_offset: Some( + info.file_offset.get(*endian) + + (*page_index as u64 + * slide.page_size.get(*endian) as u64) + + page_start as u64, + ), + }); + } else { + *iter = None; + } + + *page_index += 1; + } else { + return None; + } + } + } + }, + } + } +} + +/// A versioned iterator over relocations in a page +#[derive(Debug)] +pub enum DyldCacheRelocationPageIterator<'data, E = Endianness, R = &'data [u8]> +where + E: Endian, + R: ReadRef<'data>, +{ + /// Corresponds to struct dyld_cache_slide_info5 from dyld_cache_format.h. + V5 { + /// The mapping data + data: R, + /// Endian + endian: E, + /// The mapping slide information + slide: &'data macho::DyldCacheSlideInfo5, + /// The current offset into the page + page_offset: Option, + }, +} + +impl<'data, E, R> Iterator for DyldCacheRelocationPageIterator<'data, E, R> +where + E: Endian, + R: ReadRef<'data>, +{ + type Item = DyldRelocation; + + fn next(&mut self) -> Option { + match self { + Self::V5 { + data, + endian, + slide, + page_offset, + } => { + if let Some(offset) = *page_offset { + let pointer = macho::DyldCacheSlidePointer5 { + raw: data.read_at::>(offset).unwrap().get(*endian), + }; + + let next = pointer.next(); + if next == 0 { + *page_offset = None; + } else { + *page_offset = Some(offset + (next * 8)); + } + + let value_add = slide.value_add.get(*endian); + let value = pointer.value(value_add); + let auth = pointer.auth(); + Some(DyldRelocation { + offset, + value, + auth, + }) + } else { + None + } + } + } + } +} + +/// A cache mapping relocation. +pub struct DyldRelocation { + /// The offset of the relocation within the mapping + pub offset: u64, + /// The relocation value + pub value: u64, + /// The value auth context + pub auth: Option, +} + +impl Debug for DyldRelocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DyldRelocation") + .field("offset", &format_args!("{:#x}", self.offset)) + .field("value", &format_args!("{:#x}", self.value)) + .field("auth", &self.auth) + .finish() + } } /// A slice of structs describing each subcache. The struct gained @@ -43,6 +391,143 @@ pub enum DyldSubCacheSlice<'data, E: Endian> { V2(&'data [macho::DyldSubCacheEntryV2]), } +/// An enum of arrays containing dyld cache mappings +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum DyldCacheMappingSlice<'data, E = Endianness, R = &'data [u8]> +where + E: Endian, + R: ReadRef<'data>, +{ + /// Corresponds to an array of struct dyld_cache_mapping_info + V1 { + /// The mapping endianness + endian: E, + /// The mapping data + data: R, + /// The slice of mapping info + info: &'data [macho::DyldCacheMappingInfo], + }, + /// Corresponds to an array of struct dyld_cache_mapping_and_slide_info + V2 { + /// The mapping endianness + endian: E, + /// The mapping data + data: R, + /// The slice of mapping info + info: &'data [macho::DyldCacheMappingAndSlideInfo], + }, +} + +impl<'data, E, R> DyldCacheMappingSlice<'data, E, R> +where + E: Endian, + R: ReadRef<'data>, +{ + /// Return a slice iterator + pub fn iter(self) -> DyldCacheMappingIterator<'data, E, R> { + match self { + Self::V1 { endian, data, info } => DyldCacheMappingIterator::V1 { + endian, + data, + iter: info.iter(), + }, + Self::V2 { endian, data, info } => DyldCacheMappingIterator::V2 { + endian, + data, + iter: info.iter(), + }, + } + } + + /// Find the file offset of the image by looking up its address in the mappings. + pub fn address_to_file_offset(&self, address: u64) -> Option { + for mapping in self.iter() { + let mapping_address = mapping.address(); + if address >= mapping_address && address < mapping_address.wrapping_add(mapping.size()) + { + return Some(address - mapping_address + mapping.file_offset()); + } + } + None + } +} + +/// An iterator over all the mappings in a dyld shared cache. +#[derive(Debug)] +pub enum DyldCacheMappingIterator<'data, E = Endianness, R = &'data [u8]> +where + E: Endian, + R: ReadRef<'data>, +{ + /// Corresponds to struct dyld_cache_mapping_info from dyld_cache_format.h. + V1 { + /// The mapping endianness + endian: E, + /// The mapping data + data: R, + /// The mapping info iterator + iter: slice::Iter<'data, macho::DyldCacheMappingInfo>, + }, + /// Corresponds to struct dyld_cache_mapping_and_slide_info from dyld_cache_format.h. + V2 { + /// The mapping endianness + endian: E, + /// The mapping data + data: R, + /// The mapping info iterator + iter: slice::Iter<'data, macho::DyldCacheMappingAndSlideInfo>, + }, +} + +impl<'data, E, R> Iterator for DyldCacheMappingIterator<'data, E, R> +where + E: Endian, + R: ReadRef<'data>, +{ + type Item = DyldCacheMapping<'data, E, R>; + + fn next(&mut self) -> Option { + match self { + Self::V1 { endian, data, iter } => { + let info = iter.next()?; + Some(DyldCacheMapping::V1 { + endian: *endian, + data: *data, + info, + }) + } + Self::V2 { endian, data, iter } => { + let info = iter.next()?; + Some(DyldCacheMapping::V2 { + endian: *endian, + data: *data, + info, + }) + } + } + } +} + +/// An enum of arrays containing dyld cache mappings +#[derive(Clone, Copy)] +#[non_exhaustive] +pub enum DyldCacheSlideInfoSlice<'data, E: Endian> { + /// Corresponds to struct dyld_cache_slide_info5 from dyld_cache_format.h. + V5(&'data macho::DyldCacheSlideInfo5, &'data [U16]), +} + +impl Debug for DyldCacheSlideInfoSlice<'_, E> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::V5(info, _) => f + .debug_struct("DyldCacheSlideInfoSlice::V5") + .field("info", info) + .finish(), + } + } +} + // This is the offset of the end of the images_across_all_subcaches_count field. const MIN_HEADER_SIZE_SUBCACHES_V1: u32 = 0x1c8; @@ -96,11 +581,11 @@ where }; let uuids = v1.iter().map(|e| &e.uuid).chain(v2.iter().map(|e| &e.uuid)); for (&data, uuid) in subcache_data.iter().zip(uuids) { - let sc_header = macho::DyldCacheHeader::::parse(data)?; - if &sc_header.uuid != uuid { + let header = macho::DyldCacheHeader::::parse(data)?; + if &header.uuid != uuid { return Err(Error("Unexpected SubCache UUID")); } - let mappings = sc_header.mappings(endian, data)?; + let mappings = header.mappings(endian, data)?; subcaches.push(DyldSubCache { data, mappings }); } } @@ -109,11 +594,11 @@ where // Other than the UUID verification, the symbols SubCache is currently unused. let _symbols_subcache = match symbols_subcache_data_and_uuid { Some((data, uuid)) => { - let sc_header = macho::DyldCacheHeader::::parse(data)?; - if sc_header.uuid != uuid { + let header = macho::DyldCacheHeader::::parse(data)?; + if header.uuid != uuid { return Err(Error("Unexpected .symbols SubCache UUID")); } - let mappings = sc_header.mappings(endian, data)?; + let mappings = header.mappings(endian, data)?; Some(DyldSubCache { data, mappings }) } None => None, @@ -158,16 +643,45 @@ where } } + /// Return all the mappings in this cache. + pub fn mappings<'cache>( + &'cache self, + ) -> Box> + 'cache> { + let mut mappings: Box>> = + Box::new(self.mappings.iter()); + + for subcache in &self.subcaches { + mappings = Box::new(mappings.chain(subcache.mappings.iter())); + } + + mappings + } + + /// Return all the relocations in this cache. + pub fn relocations<'cache>(&'cache self) -> Box + 'cache> { + let mut relocations: Box> = Box::new(iter::empty()); + + for mapping in self.mappings.iter() { + relocations = Box::new(relocations.chain(mapping.relocations())); + } + + for subcache in &self.subcaches { + for mapping in subcache.mappings.iter() { + relocations = Box::new(relocations.chain(mapping.relocations())); + } + } + + relocations + } + /// Find the address in a mapping and return the cache or subcache data it was found in, /// together with the translated file offset. pub fn data_and_offset_for_address(&self, address: u64) -> Option<(R, u64)> { - if let Some(file_offset) = address_to_file_offset(address, self.endian, self.mappings) { + if let Some(file_offset) = self.mappings.address_to_file_offset(address) { return Some((self.data, file_offset)); } for subcache in &self.subcaches { - if let Some(file_offset) = - address_to_file_offset(address, self.endian, subcache.mappings) - { + if let Some(file_offset) = subcache.mappings.address_to_file_offset(address) { return Some((subcache.data, file_offset)); } } @@ -241,6 +755,44 @@ where } } +impl macho::DyldCacheMappingAndSlideInfo { + /// Return the (optional) array of slide information structs + pub fn slide<'data, R: ReadRef<'data>>( + &self, + endian: E, + data: R, + ) -> Result>> { + match self.slide_info_file_size.get(endian) { + 0 => Ok(None), + _ => { + let slide_info_file_offset = self.slide_info_file_offset.get(endian); + let version = data + .read_at::>(slide_info_file_offset) + .read_error("Invalid slide info file offset size or alignment")? + .get(endian); + match version { + 5 => { + let slide = data + .read_at::>(slide_info_file_offset) + .read_error("Invalid dyld cache header size or alignment")?; + let page_starts_offset = slide_info_file_offset + .checked_add(mem::size_of::>() as u64) + .unwrap(); + let page_starts = data + .read_slice_at::>( + page_starts_offset, + slide.page_starts_count.get(endian) as usize, + ) + .read_error("Invalid dyld cache header size or alignment")?; + Ok(Some(DyldCacheSlideInfoSlice::V5(slide, page_starts))) + } + _ => todo!("handle other dyld_cache_slide_info versions"), + } + } + } + } +} + impl macho::DyldCacheHeader { /// Read the dyld cache header. pub fn parse<'data, R: ReadRef<'data>>(data: R) -> Result<&'data Self> { @@ -274,12 +826,24 @@ impl macho::DyldCacheHeader { &self, endian: E, data: R, - ) -> Result<&'data [macho::DyldCacheMappingInfo]> { - data.read_slice_at::>( - self.mapping_offset.get(endian).into(), - self.mapping_count.get(endian) as usize, - ) - .read_error("Invalid dyld cache mapping size or alignment") + ) -> Result> { + if self.mapping_with_slide_offset.get(endian) != 0 { + let info = data + .read_slice_at::>( + self.mapping_with_slide_offset.get(endian).into(), + self.mapping_with_slide_count.get(endian) as usize, + ) + .read_error("Invalid dyld cache mapping size or alignment")?; + Ok(DyldCacheMappingSlice::V2 { endian, data, info }) + } else { + let info = data + .read_slice_at::>( + self.mapping_offset.get(endian).into(), + self.mapping_count.get(endian) as usize, + ) + .read_error("Invalid dyld cache mapping size or alignment")?; + Ok(DyldCacheMappingSlice::V1 { endian, data, info }) + } } /// Return the information about subcaches, if present. @@ -294,16 +858,16 @@ impl macho::DyldCacheHeader { if header_size >= MIN_HEADER_SIZE_SUBCACHES_V2 { let subcaches = data .read_slice_at::>( - self.subcaches_offset.get(endian).into(), - self.subcaches_count.get(endian) as usize, + self.sub_cache_array_offset.get(endian).into(), + self.sub_cache_array_count.get(endian) as usize, ) .read_error("Invalid dyld subcaches size or alignment")?; Ok(Some(DyldSubCacheSlice::V2(subcaches))) } else if header_size >= MIN_HEADER_SIZE_SUBCACHES_V1 { let subcaches = data .read_slice_at::>( - self.subcaches_offset.get(endian).into(), - self.subcaches_count.get(endian) as usize, + self.sub_cache_array_offset.get(endian).into(), + self.sub_cache_array_count.get(endian) as usize, ) .read_error("Invalid dyld subcaches size or alignment")?; Ok(Some(DyldSubCacheSlice::V1(subcaches))) @@ -315,7 +879,7 @@ impl macho::DyldCacheHeader { /// Return the UUID for the .symbols subcache, if present. pub fn symbols_subcache_uuid(&self, endian: E) -> Option<[u8; 16]> { if self.mapping_offset.get(endian) >= MIN_HEADER_SIZE_SUBCACHES_V1 { - let uuid = self.symbols_subcache_uuid; + let uuid = self.symbol_file_uuid; if uuid != [0; 16] { return Some(uuid); } @@ -331,14 +895,14 @@ impl macho::DyldCacheHeader { ) -> Result<&'data [macho::DyldCacheImageInfo]> { if self.mapping_offset.get(endian) >= MIN_HEADER_SIZE_SUBCACHES_V1 { data.read_slice_at::>( - self.images_across_all_subcaches_offset.get(endian).into(), - self.images_across_all_subcaches_count.get(endian) as usize, + self.images_offset.get(endian).into(), + self.images_count.get(endian) as usize, ) .read_error("Invalid dyld cache image size or alignment") } else { data.read_slice_at::>( - self.images_offset.get(endian).into(), - self.images_count.get(endian) as usize, + self.images_offset_old.get(endian).into(), + self.images_count_old.get(endian) as usize, ) .read_error("Invalid dyld cache image size or alignment") } @@ -355,30 +919,14 @@ impl macho::DyldCacheImageInfo { } /// Find the file offset of the image by looking up its address in the mappings. - pub fn file_offset( + pub fn file_offset<'data, R: ReadRef<'data>>( &self, endian: E, - mappings: &[macho::DyldCacheMappingInfo], + mappings: &DyldCacheMappingSlice<'data, E, R>, ) -> Result { let address = self.address.get(endian); - address_to_file_offset(address, endian, mappings) + mappings + .address_to_file_offset(address) .read_error("Invalid dyld cache image address") } } - -/// Find the file offset of the image by looking up its address in the mappings. -pub fn address_to_file_offset( - address: u64, - endian: E, - mappings: &[macho::DyldCacheMappingInfo], -) -> Option { - for mapping in mappings { - let mapping_address = mapping.address.get(endian); - if address >= mapping_address - && address < mapping_address.wrapping_add(mapping.size.get(endian)) - { - return Some(address - mapping_address + mapping.file_offset.get(endian)); - } - } - None -} diff --git a/testfiles b/testfiles index e0c21cf7..7651ef6c 160000 --- a/testfiles +++ b/testfiles @@ -1 +1 @@ -Subproject commit e0c21cf79ee7bfefc2fd021d6514ce57c72b1cef +Subproject commit 7651ef6c5b5de609ddf0867eb781bec51a1631b9