-
Notifications
You must be signed in to change notification settings - Fork 190
feat: install name tool style macho writing #509
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
c580304
09adbca
386deda
9bb1120
7cf34a6
6d706e8
01392b1
b8a22ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| //! A simple install_name_tool clone using goblin's MachOWriter | ||
| //! | ||
| //! This tool supports a subset of install_name_tool operations: | ||
| //! - `-id <name>`: Change the install name of a dylib | ||
| //! - `-change <old> <new>`: Change a dylib dependency | ||
| //! - `-add_rpath <path>`: Add an rpath | ||
| //! - `-delete_rpath <path>`: Delete an rpath | ||
| //! - `-rpath <old> <new>`: Change an rpath | ||
|
|
||
| use goblin::mach::writer::modify_fat_binary; | ||
| use std::env; | ||
| use std::fs; | ||
| use std::process; | ||
|
|
||
| fn print_usage() { | ||
| eprintln!("Usage: install_name_tool [options] <input_file>"); | ||
| eprintln!(); | ||
| eprintln!("Options:"); | ||
| eprintln!(" -id <name> Change the install name (LC_ID_DYLIB)"); | ||
| eprintln!(" -change <old> <new> Change a dylib dependency"); | ||
| eprintln!(" -add_rpath <path> Add an rpath"); | ||
| eprintln!(" -delete_rpath <path> Delete an rpath"); | ||
| eprintln!(" -rpath <old> <new> Change an rpath"); | ||
| eprintln!(" -o <output_file> Write to output file (default: modify in place)"); | ||
| eprintln!(); | ||
| eprintln!("Multiple options can be combined in a single invocation."); | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| enum Operation { | ||
| ChangeId(String), | ||
| ChangeDylib { old: String, new: String }, | ||
| AddRpath(String), | ||
| DeleteRpath(String), | ||
| ChangeRpath { old: String, new: String }, | ||
| } | ||
|
|
||
| fn main() { | ||
| let args: Vec<String> = env::args().collect(); | ||
|
|
||
| if args.len() < 2 { | ||
| print_usage(); | ||
| process::exit(1); | ||
| } | ||
|
|
||
| let mut operations: Vec<Operation> = Vec::new(); | ||
| let mut input_file: Option<String> = None; | ||
| let mut output_file: Option<String> = None; | ||
|
|
||
| let mut i = 1; | ||
| while i < args.len() { | ||
| match args[i].as_str() { | ||
| "-id" => { | ||
| if i + 1 >= args.len() { | ||
| eprintln!("Error: -id requires an argument"); | ||
| process::exit(1); | ||
| } | ||
| operations.push(Operation::ChangeId(args[i + 1].clone())); | ||
| i += 2; | ||
| } | ||
| "-change" => { | ||
| if i + 2 >= args.len() { | ||
| eprintln!("Error: -change requires two arguments"); | ||
| process::exit(1); | ||
| } | ||
| operations.push(Operation::ChangeDylib { | ||
| old: args[i + 1].clone(), | ||
| new: args[i + 2].clone(), | ||
| }); | ||
| i += 3; | ||
| } | ||
| "-add_rpath" => { | ||
| if i + 1 >= args.len() { | ||
| eprintln!("Error: -add_rpath requires an argument"); | ||
| process::exit(1); | ||
| } | ||
| operations.push(Operation::AddRpath(args[i + 1].clone())); | ||
| i += 2; | ||
| } | ||
| "-delete_rpath" => { | ||
| if i + 1 >= args.len() { | ||
| eprintln!("Error: -delete_rpath requires an argument"); | ||
| process::exit(1); | ||
| } | ||
| operations.push(Operation::DeleteRpath(args[i + 1].clone())); | ||
| i += 2; | ||
| } | ||
| "-rpath" => { | ||
| if i + 2 >= args.len() { | ||
| eprintln!("Error: -rpath requires two arguments"); | ||
| process::exit(1); | ||
| } | ||
| operations.push(Operation::ChangeRpath { | ||
| old: args[i + 1].clone(), | ||
| new: args[i + 2].clone(), | ||
| }); | ||
| i += 3; | ||
| } | ||
| "-o" => { | ||
| if i + 1 >= args.len() { | ||
| eprintln!("Error: -o requires an argument"); | ||
| process::exit(1); | ||
| } | ||
| output_file = Some(args[i + 1].clone()); | ||
| i += 2; | ||
| } | ||
| "-h" | "--help" => { | ||
| print_usage(); | ||
| process::exit(0); | ||
| } | ||
| arg if arg.starts_with('-') => { | ||
| eprintln!("Error: Unknown option: {}", arg); | ||
| print_usage(); | ||
| process::exit(1); | ||
| } | ||
| _ => { | ||
| if input_file.is_some() { | ||
| eprintln!("Error: Multiple input files specified"); | ||
| process::exit(1); | ||
| } | ||
| input_file = Some(args[i].clone()); | ||
| i += 1; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let input_file = match input_file { | ||
| Some(f) => f, | ||
| None => { | ||
| eprintln!("Error: No input file specified"); | ||
| print_usage(); | ||
| process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| if operations.is_empty() { | ||
| eprintln!("Error: No operations specified"); | ||
| print_usage(); | ||
| process::exit(1); | ||
| } | ||
|
|
||
| // Read the input file | ||
| let data = match fs::read(&input_file) { | ||
| Ok(d) => d, | ||
| Err(e) => { | ||
| eprintln!("Error reading '{}': {}", input_file, e); | ||
| process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| // Apply operations | ||
| let result = modify_fat_binary(data, |writer| { | ||
| for op in &operations { | ||
| match op { | ||
| Operation::ChangeId(name) => { | ||
| writer.change_id(name)?; | ||
| } | ||
| Operation::ChangeDylib { old, new } => { | ||
| writer.change_dylib(old, new)?; | ||
| } | ||
| Operation::AddRpath(path) => { | ||
| writer.add_rpath(path)?; | ||
| } | ||
| Operation::DeleteRpath(path) => { | ||
| writer.delete_rpath(path)?; | ||
| } | ||
| Operation::ChangeRpath { old, new } => { | ||
| writer.change_rpath(old, new)?; | ||
| } | ||
| } | ||
| } | ||
| Ok(()) | ||
| }); | ||
|
|
||
| let modified = match result { | ||
| Ok(d) => d, | ||
| Err(e) => { | ||
| eprintln!("Error modifying binary: {}", e); | ||
| process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| // Re-sign if the binary was linker-signed (like Apple's install_name_tool does) | ||
| // Only re-sign if the original binary had the linker-signed flag (0x20000) | ||
| // Files with just adhoc (0x2) flag are NOT re-signed by Apple | ||
| #[cfg(feature = "codesign")] | ||
| let modified = { | ||
| use goblin::mach::writer::{adhoc_sign, is_linker_signed}; | ||
| use std::path::Path; | ||
|
|
||
| // Only re-sign if the original binary was linker-signed | ||
| // Check the modified data since that's what we'll sign | ||
| if is_linker_signed(&modified) { | ||
| let output_path = output_file.as_ref().unwrap_or(&input_file); | ||
| let identifier = Path::new(output_path) | ||
| .file_name() | ||
| .and_then(|s| s.to_str()) | ||
| .unwrap_or("a.out"); | ||
|
|
||
| match adhoc_sign(modified.clone(), identifier) { | ||
| Ok(signed) => signed, | ||
| Err(_) => modified, | ||
| } | ||
| } else { | ||
| modified | ||
| } | ||
| }; | ||
|
|
||
| // Write output | ||
| let output_path = output_file.as_ref().unwrap_or(&input_file); | ||
| if let Err(e) = fs::write(output_path, &modified) { | ||
| eprintln!("Error writing '{}': {}", output_path, e); | ||
| process::exit(1); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,9 @@ use scroll::{Pread, Pwrite, SizeWith}; | |
|
|
||
| pub const FAT_MAGIC: u32 = 0xcafe_babe; | ||
| pub const FAT_CIGAM: u32 = 0xbeba_feca; | ||
| /// 64-bit fat magic number (for archives with 64-bit offsets/sizes) | ||
| pub const FAT_MAGIC_64: u32 = 0xcafe_babf; | ||
| pub const FAT_CIGAM_64: u32 = 0xbfba_feca; | ||
|
Comment on lines
+17
to
+18
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these are good changes, though I don't think I've ever seen this magic in the wild, though I haven't poked at macho binaries in a while |
||
|
|
||
| #[repr(C)] | ||
| #[derive(Clone, Copy, Default, Pread, Pwrite, SizeWith)] | ||
|
|
@@ -130,3 +133,101 @@ impl FatArch { | |
| Ok(arch) | ||
| } | ||
| } | ||
|
|
||
| #[repr(C)] | ||
| #[derive(Clone, Copy, Default, Pread, Pwrite, SizeWith)] | ||
| /// 64-bit version of `FatArch` for fat binaries with large offsets/sizes | ||
| /// Uses u64 for offset and size fields | ||
| pub struct FatArch64 { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so in mod.rs we implement a FatArhcIterator, and SingleArchIterator that returns these FatArch64's; since this patch doesn't add those features, I don't see where this would be used by a client? Again I've never seen this struct in the wild before, can you upload a binary that has it?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @m4b thanks for the review. I added a fat64 binary to the assets and a test case. Also happy to remove all of that (could not find a single one on my system to send you, but was able to build one). |
||
| /// What kind of CPU this binary is | ||
| pub cputype: u32, | ||
| pub cpusubtype: u32, | ||
| /// Where in the fat binary it starts (64-bit) | ||
| pub offset: u64, | ||
| /// How big the binary is (64-bit) | ||
| pub size: u64, | ||
| pub align: u32, | ||
| /// Reserved for future use | ||
| pub reserved: u32, | ||
| } | ||
|
|
||
| pub const SIZEOF_FAT_ARCH_64: usize = 32; | ||
|
|
||
| impl fmt::Debug for FatArch64 { | ||
| fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
| fmt.debug_struct("FatArch64") | ||
| .field("cputype", &self.cputype()) | ||
| .field("cpusubtype", &self.cpusubtype()) | ||
| .field("offset", &format_args!("{:#x}", &self.offset)) | ||
| .field("size", &self.size) | ||
| .field("align", &self.align) | ||
| .finish() | ||
| } | ||
| } | ||
|
|
||
| impl FatArch64 { | ||
| /// Get the slice of bytes this header describes from `bytes` | ||
| pub fn slice<'a>(&self, bytes: &'a [u8]) -> &'a [u8] { | ||
| let start = self.offset as usize; | ||
| match start | ||
| .checked_add(self.size as usize) | ||
| .and_then(|end| bytes.get(start..end)) | ||
| { | ||
| Some(slice) => slice, | ||
| None => { | ||
| log::warn!("invalid `FatArch64` offset"); | ||
| &[] | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Returns the cpu type | ||
| pub fn cputype(&self) -> CpuType { | ||
m4b marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self.cputype | ||
| } | ||
|
|
||
| /// Returns the cpu subtype with the capabilities removed | ||
| pub fn cpusubtype(&self) -> CpuSubType { | ||
| self.cpusubtype & !CPU_SUBTYPE_MASK | ||
| } | ||
|
|
||
| /// Returns the capabilities of the CPU | ||
| pub fn cpu_caps(&self) -> u32 { | ||
| (self.cpusubtype & CPU_SUBTYPE_MASK) >> 24 | ||
| } | ||
|
|
||
| /// Whether this fat architecture header describes a 64-bit binary | ||
| pub fn is_64(&self) -> bool { | ||
| (self.cputype & CPU_ARCH_ABI64) == CPU_ARCH_ABI64 | ||
| } | ||
|
|
||
| /// Parse a `FatArch64` header from `bytes` at `offset` | ||
| pub fn parse(bytes: &[u8], offset: usize) -> error::Result<Self> { | ||
| let arch = bytes.pread_with::<FatArch64>(offset, scroll::BE)?; | ||
| Ok(arch) | ||
| } | ||
|
|
||
| /// Convert to a 32-bit FatArch (may lose precision for large values) | ||
| pub fn to_fat_arch(&self) -> FatArch { | ||
| FatArch { | ||
| cputype: self.cputype, | ||
| cpusubtype: self.cpusubtype, | ||
| offset: self.offset as u32, | ||
| size: self.size as u32, | ||
| align: self.align, | ||
| } | ||
| } | ||
m4b marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| impl From<FatArch> for FatArch64 { | ||
| fn from(arch: FatArch) -> Self { | ||
| FatArch64 { | ||
| cputype: arch.cputype, | ||
| cpusubtype: arch.cpusubtype, | ||
| offset: arch.offset as u64, | ||
| size: arch.size as u64, | ||
| align: arch.align, | ||
| reserved: 0, | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -224,7 +224,7 @@ pub struct DylibCommand { | |
| pub dylib: Dylib, | ||
| } | ||
|
|
||
| pub const SIZEOF_DYLIB_COMMAND: usize = 20; | ||
| pub const SIZEOF_DYLIB_COMMAND: usize = 24; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to understand why this changed (and perhaps why a test didn't fail when it did?) |
||
|
|
||
| /// A dynamically linked shared library may be a subframework of an umbrella | ||
| /// framework. If so it will be linked with "-umbrella umbrella_name" where | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.