#![warn(missing_docs)]
#![warn(clippy::all)]
pub mod arm64;
pub mod converter;
pub mod dyld;
pub mod error;
pub mod macho;
pub mod util;
pub use dyld::{DyldContext, ImageEntry, MappingEntry};
pub use error::{Error, Result};
pub use macho::MachOContext;
use std::collections::{HashSet, VecDeque};
use std::fs;
use std::path::Path;
use std::sync::Arc;
use converter::{
ExtractionContext, MergeContext, fix_header_and_load_commands, fix_merged_pointers,
fix_merged_stubs, fix_objc, fix_stubs, include_shared_regions, inject_dependency_segments,
merge_images, optimize_linkedit, optimize_offsets, process_slide_info, rebuild_merged_linkedit,
resolve_indirect_pointers, update_merged_load_commands, write_macho,
};
pub fn extract_image<P: AsRef<Path>>(
cache: &Arc<DyldContext>,
image_path: &str,
output_path: P,
) -> Result<()> {
extract_image_with_options(cache, image_path, output_path, ExtractionOptions::default())
}
#[derive(Debug, Clone)]
pub struct ExtractionOptions {
pub verbosity: u8,
pub skip_slide_info: bool,
pub skip_linkedit: bool,
pub skip_stubs: bool,
pub skip_objc: bool,
}
impl Default for ExtractionOptions {
fn default() -> Self {
Self {
verbosity: 1,
skip_slide_info: false,
skip_linkedit: false,
skip_stubs: false,
skip_objc: false,
}
}
}
#[derive(Debug, Clone)]
pub struct MergeExtractionOptions {
pub verbosity: u8,
pub max_depth: usize,
}
impl Default for MergeExtractionOptions {
fn default() -> Self {
Self {
verbosity: 1,
max_depth: 1,
}
}
}
pub fn extract_image_with_merged_deps<P: AsRef<Path>>(
cache: &Arc<DyldContext>,
image_path: &str,
output_path: P,
options: MergeExtractionOptions,
) -> Result<()> {
let output_path = output_path.as_ref();
let mut ctx = MergeContext::new(Arc::clone(cache), image_path.to_string(), options.max_depth)
.with_verbosity(options.verbosity);
merge_images(&mut ctx)?;
include_shared_regions(&mut ctx)?;
inject_dependency_segments(&mut ctx)?;
update_merged_load_commands(&mut ctx)?;
fix_merged_stubs(&mut ctx)?;
fix_merged_pointers(&mut ctx)?;
resolve_indirect_pointers(&mut ctx)?;
rebuild_merged_linkedit(&mut ctx)?;
if let Some(parent) = output_path.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent).map_err(|e| Error::FileOpen {
path: parent.to_path_buf(),
source: e,
})?;
}
}
fs::write(output_path, &ctx.data).map_err(|e| Error::FileOpen {
path: output_path.to_path_buf(),
source: e,
})?;
Ok(())
}
pub fn extract_image_with_options<P: AsRef<Path>>(
cache: &Arc<DyldContext>,
image_path: &str,
output_path: P,
options: ExtractionOptions,
) -> Result<()> {
use macho::MachHeader64;
use zerocopy::FromBytes;
let image = cache
.find_image(image_path)
.ok_or_else(|| Error::ImageNotFound {
name: image_path.to_string(),
})?;
let header_data = cache.data_at_addr(image.address, MachHeader64::SIZE)?;
let header = MachHeader64::read_from_prefix(header_data)
.map_err(|_| Error::InvalidMachoMagic(0))?
.0;
let header_and_cmds_size = MachHeader64::SIZE + header.sizeofcmds as usize;
let header_and_cmds = cache.data_at_addr(image.address, header_and_cmds_size)?;
#[derive(Clone, Copy)]
struct SegmentToCopy {
vmaddr: u64,
fileoff: u64,
filesize: u64,
}
let mut segments: Vec<SegmentToCopy> = Vec::with_capacity(8);
let mut max_file_end: u64 = header_and_cmds_size as u64;
let mut offset = MachHeader64::SIZE;
for _ in 0..header.ncmds {
if offset + 8 > header_and_cmds.len() {
break;
}
let cmd = u32::from_le_bytes([
header_and_cmds[offset],
header_and_cmds[offset + 1],
header_and_cmds[offset + 2],
header_and_cmds[offset + 3],
]);
let cmdsize = u32::from_le_bytes([
header_and_cmds[offset + 4],
header_and_cmds[offset + 5],
header_and_cmds[offset + 6],
header_and_cmds[offset + 7],
]);
if cmd == 0x19 && offset + 72 <= header_and_cmds.len() {
let vmaddr = u64::from_le_bytes([
header_and_cmds[offset + 24],
header_and_cmds[offset + 25],
header_and_cmds[offset + 26],
header_and_cmds[offset + 27],
header_and_cmds[offset + 28],
header_and_cmds[offset + 29],
header_and_cmds[offset + 30],
header_and_cmds[offset + 31],
]);
let fileoff = u64::from_le_bytes([
header_and_cmds[offset + 40],
header_and_cmds[offset + 41],
header_and_cmds[offset + 42],
header_and_cmds[offset + 43],
header_and_cmds[offset + 44],
header_and_cmds[offset + 45],
header_and_cmds[offset + 46],
header_and_cmds[offset + 47],
]);
let filesize = u64::from_le_bytes([
header_and_cmds[offset + 48],
header_and_cmds[offset + 49],
header_and_cmds[offset + 50],
header_and_cmds[offset + 51],
header_and_cmds[offset + 52],
header_and_cmds[offset + 53],
header_and_cmds[offset + 54],
header_and_cmds[offset + 55],
]);
if filesize > 0 {
segments.push(SegmentToCopy {
vmaddr,
fileoff,
filesize,
});
}
let seg_end = fileoff + filesize;
if seg_end > max_file_end {
max_file_end = seg_end;
}
}
offset += cmdsize as usize;
}
let mut macho_buffer = vec![0u8; max_file_end as usize];
macho_buffer[..header_and_cmds_size].copy_from_slice(header_and_cmds);
for seg in &segments {
if let Ok(seg_data) = cache.data_at_addr(seg.vmaddr, seg.filesize as usize) {
let dst_start = seg.fileoff as usize;
let dst_end = dst_start + seg.filesize as usize;
if dst_end <= macho_buffer.len() {
macho_buffer[dst_start..dst_end].copy_from_slice(seg_data);
}
}
}
if image.path.contains("CloudKitAuthenticationPlugin") {
let shared_got_addrs: &[(u64, &str)] = &[
(0x1ee842fd8, "___stack_chk_fail"),
(0x1ee8473a0, "__os_log_error_impl"),
(0x1ee8473b0, "__os_log_impl"),
(0x1ee8423c8, "_dispatch_once"),
(0x1ee8391e0, "_objc_alloc_init"),
(0x1ee839218, "_objc_autoreleaseReturnValue"),
(0x1ee839398, "_objc_release"),
(0x1ee839448, "_objc_retain"),
(0x1ee839460, "_objc_retainAutoreleasedReturnValue"),
(0x1ee8475b8, "_os_log_type_enabled"),
];
tracing::info!("DEBUG: Reading shared region GOT entries (what stubs actually use):");
for (addr, name) in shared_got_addrs {
if let Ok(data) = cache.data_at_addr(*addr, 8) {
let val = u64::from_le_bytes(data.try_into().unwrap());
tracing::info!(" 0x{:x} ({}): 0x{:016x}", addr, name, val);
} else {
tracing::warn!(" 0x{:x} ({}): FAILED TO READ", addr, name);
}
}
}
let macho_offset =
cache
.addr_to_offset(image.address)
.ok_or_else(|| Error::AddressNotFound {
addr: image.address,
})?;
let macho = MachOContext::new(&macho_buffer, macho_offset as usize)?;
let mut ctx =
ExtractionContext::new(Arc::clone(cache), macho, image.path.clone(), image.address)
.with_verbosity(options.verbosity);
fix_header_and_load_commands(&mut ctx)?;
if !options.skip_slide_info {
process_slide_info(&mut ctx)?;
}
if !options.skip_linkedit {
optimize_linkedit(&mut ctx)?;
}
if !options.skip_stubs {
fix_stubs(&mut ctx)?;
}
if !options.skip_objc {
fix_objc(&mut ctx)?;
}
let procedures = optimize_offsets(&mut ctx)?;
write_macho(&ctx, &procedures, output_path)?;
Ok(())
}
pub fn list_images(cache: &DyldContext) -> impl Iterator<Item = &ImageEntry> {
cache.iter_images()
}
pub fn find_images<'a>(
cache: &'a DyldContext,
filter: &'a str,
) -> impl Iterator<Item = &'a ImageEntry> {
cache
.iter_images()
.filter(move |img| img.matches_filter(filter))
}
#[derive(Debug, Clone, Default)]
pub struct DependencyExtractionStats {
pub root_images: usize,
pub dependencies: usize,
pub total_extracted: usize,
pub failed: usize,
pub skipped: usize,
}
#[derive(Debug, Clone)]
struct ImageToExtract {
path: String,
depth: usize,
}
fn get_image_dependencies(cache: &DyldContext, image_path: &str) -> Result<Vec<String>> {
use macho::MachHeader64;
use zerocopy::FromBytes;
let image = cache
.find_image(image_path)
.ok_or_else(|| Error::ImageNotFound {
name: image_path.to_string(),
})?;
let header_data = cache.data_at_addr(image.address, MachHeader64::SIZE)?;
let header = MachHeader64::read_from_prefix(header_data)
.map_err(|_| Error::InvalidMachoMagic(0))?
.0;
let header_and_cmds_size = MachHeader64::SIZE + header.sizeofcmds as usize;
let header_and_cmds = cache.data_at_addr(image.address, header_and_cmds_size)?;
let macho_offset =
cache
.addr_to_offset(image.address)
.ok_or_else(|| Error::AddressNotFound {
addr: image.address,
})? as usize;
let macho = MachOContext::new(header_and_cmds, macho_offset)?;
Ok(macho.dependencies())
}
fn build_dependency_graph(
cache: &DyldContext,
root_paths: &[String],
max_depth: Option<usize>,
) -> (Vec<ImageToExtract>, Vec<String>) {
let mut to_extract: Vec<ImageToExtract> = Vec::new();
let mut visited: HashSet<String> = HashSet::new();
let mut not_in_cache: Vec<String> = Vec::new();
let mut queue: VecDeque<ImageToExtract> = VecDeque::new();
for path in root_paths {
if !visited.contains(path) {
visited.insert(path.clone());
queue.push_back(ImageToExtract {
path: path.clone(),
depth: 0,
});
}
}
while let Some(item) = queue.pop_front() {
if let Some(max) = max_depth {
if item.depth > max {
continue;
}
}
if cache.find_image(&item.path).is_none() {
if !not_in_cache.contains(&item.path) {
not_in_cache.push(item.path.clone());
}
continue;
}
to_extract.push(item.clone());
let should_traverse = match max_depth {
Some(max) => item.depth < max,
None => true,
};
if should_traverse {
match get_image_dependencies(cache, &item.path) {
Ok(deps) => {
for dep_path in deps {
let normalized = normalize_dependency_path(&dep_path);
if !visited.contains(&normalized) {
visited.insert(normalized.clone());
queue.push_back(ImageToExtract {
path: normalized,
depth: item.depth + 1,
});
}
}
}
Err(_) => {
}
}
}
}
(to_extract, not_in_cache)
}
fn normalize_dependency_path(path: &str) -> String {
if let Some(rest) = path.strip_prefix("@rpath/") {
if rest.contains('/') {
return format!("/System/Library/{}", rest);
}
if rest.ends_with(".dylib") {
return format!("/usr/lib/{}", rest);
}
let framework_name = rest.trim_end_matches(".dylib");
return format!(
"/System/Library/Frameworks/{}.framework/{}",
framework_name, framework_name
);
}
if let Some(rest) = path.strip_prefix("@executable_path/") {
return format!("/usr/lib/{}", rest);
}
if let Some(rest) = path.strip_prefix("@loader_path/") {
return format!("/usr/lib/{}", rest);
}
path.to_string()
}
pub fn extract_images_with_dependencies<P, F>(
cache: &Arc<DyldContext>,
root_paths: &[String],
output_dir: P,
options: ExtractionOptions,
max_depth: Option<usize>,
mut progress_callback: F,
) -> Result<DependencyExtractionStats>
where
P: AsRef<Path>,
F: FnMut(usize, usize, &str),
{
let output_dir = output_dir.as_ref();
let root_count = root_paths.len();
let (images_to_extract, not_in_cache) = build_dependency_graph(cache, root_paths, max_depth);
let total = images_to_extract.len();
let mut stats = DependencyExtractionStats {
root_images: root_count,
dependencies: total.saturating_sub(root_count),
total_extracted: 0,
failed: 0,
skipped: not_in_cache.len(),
};
fs::create_dir_all(output_dir).map_err(|e| Error::FileOpen {
path: output_dir.to_path_buf(),
source: e,
})?;
let mut extracted: HashSet<String> = HashSet::new();
for (index, item) in images_to_extract.iter().enumerate() {
if extracted.contains(&item.path) {
continue;
}
progress_callback(index + 1, total, &item.path);
let relative_path = item.path.trim_start_matches('/');
let output_path = output_dir.join(relative_path);
if let Some(parent) = output_path.parent() {
if let Err(_) = fs::create_dir_all(parent) {
stats.failed += 1;
continue;
}
}
match extract_image_with_options(cache, &item.path, &output_path, options.clone()) {
Ok(_) => {
stats.total_extracted += 1;
extracted.insert(item.path.clone());
}
Err(_) => {
stats.failed += 1;
}
}
}
Ok(stats)
}