Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ description = "An impish, cross-platform, ELF, Mach-o, and PE binary parsing and
[dependencies]
plain = "0.2.3"

[dependencies.sha2]
version = "0.10"
optional = true

[dependencies.log]
version = "0.4"
default-features = false
Expand All @@ -47,6 +51,7 @@ default = [
]
std = ["alloc", "scroll/std"]
alloc = ["scroll/derive", "log"]
codesign = ["std", "sha2"]
endian_fd = ["alloc"]
elf32 = []
elf64 = []
Expand Down
215 changes: 215 additions & 0 deletions examples/install_name_tool.rs
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);
}
}
101 changes: 101 additions & 0 deletions src/mach/fat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The 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)]
Expand Down Expand Up @@ -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 {
Copy link
Owner

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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 {
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,
}
}
}

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,
}
}
}
2 changes: 1 addition & 1 deletion src/mach/load_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ pub struct DylibCommand {
pub dylib: Dylib,
}

pub const SIZEOF_DYLIB_COMMAND: usize = 20;
pub const SIZEOF_DYLIB_COMMAND: usize = 24;
Copy link
Owner

Choose a reason for hiding this comment

The 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
Expand Down
1 change: 1 addition & 0 deletions src/mach/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod load_command;
pub mod relocation;
pub mod segment;
pub mod symbols;
pub mod writer;

pub use self::constants::cputype;

Expand Down
Loading
Loading