Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions apps/plumesign/src/commands/macho.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ pub struct MachArgs {
/// Add a dylib dependency (e.g., @rpath/MyLib.dylib)
#[arg(long, value_name = "DYLIB_PATH")]
pub add_dylib: Option<String>,
/// List all LC_RPATH search paths
#[arg(long)]
pub list_rpaths: bool,
/// Add an LC_RPATH search path
#[arg(long, value_name = "RPATH")]
pub add_rpath: Option<String>,
/// Replace an existing dylib dependency
#[arg(long, value_names = &["OLD", "NEW"], num_args = 2)]
pub replace_dylib: Option<Vec<String>>,
Expand All @@ -32,6 +38,11 @@ pub async fn execute(args: MachArgs) -> Result<()> {
return Ok(());
}

if let Some(rpath) = &args.add_rpath {
macho.add_rpath(rpath)?;
return Ok(());
}

if let Some(replace_paths) = &args.replace_dylib {
if replace_paths.len() == 2 {
macho.replace_dylib(&replace_paths[0], &replace_paths[1])?;
Expand All @@ -53,6 +64,13 @@ pub async fn execute(args: MachArgs) -> Result<()> {
return Ok(());
}

if args.list_rpaths {
for path in macho.rpaths()? {
println!("{path}");
}
return Ok(());
}

if let Some(sdk_version) = &args.sdk_version {
macho.replace_sdk_version(sdk_version)?;
return Ok(());
Expand Down
29 changes: 27 additions & 2 deletions apps/plumesign/src/commands/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use anyhow::Result;
use clap::Args;

use plume_core::{CertificateIdentity, MobileProvision};
use plume_utils::{Bundle, Package, Signer, SignerMode, SignerOptions};
use plume_utils::{
Bundle, Package, Signer, SignerMode, SignerOptions, TweakInjectFolder, TweakInjectPath,
TweakInjection, TweakLoader,
};

use crate::{
commands::{
Expand Down Expand Up @@ -38,9 +41,22 @@ pub struct SignArgs {
/// Custom bundle version to set
#[arg(long = "custom-version", value_name = "VERSION")]
pub version: Option<String>,
/// Perform ad-hoc signing (no certificate required)
/// Tweak files to apply before signing (.deb, .dylib, .framework, .bundle, .appex)
#[arg(long, short, num_args = 1..)]
pub tweaks: Option<Vec<PathBuf>>,
/// Loader/runtime to bundle when applying tweaks
#[arg(long = "tweak-loader", value_name = "LOADER", default_value_t = TweakLoader::ElleKit)]
pub tweak_loader: TweakLoader,
/// Load-path prefix for user tweak dylibs/frameworks; supplying this opts into custom injection-path mode
#[arg(long = "tweak-inject-path", alias = "inject-path", value_name = "PATH")]
pub tweak_inject_path: Option<TweakInjectPath>,
/// Load-path folder for user tweak dylibs/frameworks; supplying this opts into custom injection-path mode
#[arg(
long = "tweak-inject-folder",
alias = "inject-folder",
value_name = "FOLDER"
)]
pub tweak_inject_folder: Option<TweakInjectFolder>,
/// Register device and install after signing
#[arg(long)]
pub register_and_install: bool,
Expand All @@ -63,11 +79,20 @@ pub async fn execute(args: SignArgs) -> Result<()> {
));
}

let tweak_injection = match (args.tweak_inject_path, args.tweak_inject_folder) {
(None, None) => TweakInjection::Legacy,
(path, folder) => {
TweakInjection::custom(path.unwrap_or_default(), folder.unwrap_or_default())
}
};

let mut options = SignerOptions {
custom_identifier: args.bundle_identifier,
custom_name: args.name,
custom_version: args.version,
tweaks: args.tweaks,
tweak_loader: args.tweak_loader,
tweak_injection,
..Default::default()
};

Expand Down
142 changes: 134 additions & 8 deletions crates/plume_core/src/utils/macho.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use goblin::mach::{
cputype::CPU_TYPE_ARM64,
load_command::{
CommandVariant, LC_LAZY_LOAD_DYLIB, LC_LOAD_DYLIB, LC_LOAD_UPWARD_DYLIB,
LC_LOAD_WEAK_DYLIB, LC_REEXPORT_DYLIB,
LC_LOAD_WEAK_DYLIB, LC_REEXPORT_DYLIB, LC_RPATH,
},
};
use plist::{Dictionary, Value};
Expand Down Expand Up @@ -82,6 +82,23 @@ impl MachO {
Ok(())
}

pub fn rpaths(&self) -> Result<Vec<String>, Error> {
let mut out = Vec::new();
for macho in self.macho_file.iter_macho() {
out.extend(macho.rpath_load_paths()?);
}
Ok(out)
}

pub fn add_rpath(&mut self, path: &str) -> Result<(), Error> {
let machos = self.macho_file.iter_macho_mut();
for macho in machos {
macho.add_rpath_load_path(path)?;
}
self.write_changes()?;
Ok(())
}

pub fn replace_dylib(&mut self, old_path: &str, new_path: &str) -> Result<(), Error> {
let machos = self.macho_file.iter_macho_mut();
for macho in machos {
Expand Down Expand Up @@ -114,7 +131,9 @@ impl MachO {
pub trait MachOExt {
fn embedded_entitlements(&self) -> Result<Option<Dictionary>, Error>;
fn dylib_load_paths(&self) -> Result<Vec<String>, Error>;
fn rpath_load_paths(&self) -> Result<Vec<String>, Error>;
fn add_dylib_load_path(&mut self, path: &str) -> Result<(), Error>;
fn add_rpath_load_path(&mut self, path: &str) -> Result<(), Error>;
fn remove_dylib_load_path(&mut self, path: &str) -> Result<(), Error>;
fn replace_dylib_load_path(&mut self, old_path: &str, new_path: &str) -> Result<(), Error>;
fn replace_sdk_version(&mut self, new_version: &str) -> Result<(), Error>;
Expand Down Expand Up @@ -163,6 +182,20 @@ impl<'a> MachOExt for MachOBinary<'a> {
Ok(paths)
}

fn rpath_load_paths(&self) -> Result<Vec<String>, Error> {
let mut paths = Vec::new();

for load_cmd in &self.macho.load_commands {
if load_cmd.command.cmd() == LC_RPATH {
if let Some(path) = manually_parse_lc_str(self.data, load_cmd.offset, 8) {
paths.push(path);
}
}
}

Ok(paths)
}

// these require rewriting the Mach-O
fn add_dylib_load_path(&mut self, path: &str) -> Result<(), Error> {
let macho = &self.macho;
Expand Down Expand Up @@ -284,6 +317,91 @@ impl<'a> MachOExt for MachOBinary<'a> {
Ok(())
}

fn add_rpath_load_path(&mut self, path: &str) -> Result<(), Error> {
let macho = &self.macho;

let read_u32_le = |data: &[u8], offset: usize| -> u32 {
u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
};

let rpath_exists = macho.load_commands.iter().any(|load_cmd| {
load_cmd.command.cmd() == LC_RPATH
&& manually_parse_lc_str(self.data, load_cmd.offset, 8)
.map_or(false, |existing| existing == path)
});

if rpath_exists {
log::warn!("RPath already exists in binary: {}", path);
return Ok(());
}

let is_64 = matches!(macho.header.cputype, CPU_TYPE_ARM64);
let current_sizeofcmds = read_u32_le(&self.data, 20);
let current_ncmds = read_u32_le(&self.data, 16);

let mut data = self.data.to_vec();

let header_size = if is_64 { 32 } else { 28 };
let rpath_len = path.len();
let padding = (8 - ((rpath_len + 1) % 8)) % 8;
let command_size = 12 + rpath_len + 1 + padding;

let load_commands_offset = header_size;
let load_commands_end = load_commands_offset + current_sizeofcmds as usize;

let min_fileoff = macho
.load_commands
.iter()
.filter_map(|load_cmd| match &load_cmd.command {
CommandVariant::Segment64(seg) if seg.filesize > 0 && seg.fileoff > 0 => {
Some(seg.fileoff)
}
CommandVariant::Segment32(seg) if seg.filesize > 0 && seg.fileoff > 0 => {
Some(seg.fileoff as u64)
}
_ => None,
})
.min()
.unwrap_or(u64::MAX);

let data_start = if min_fileoff < u64::MAX {
min_fileoff as usize
} else {
data.len()
};

let available_space = data_start.saturating_sub(load_commands_end);
if command_size > available_space {
return Err(Error::Parse);
}

let insert_offset = load_commands_end;
let mut new_command = Vec::new();
new_command.extend_from_slice(&(LC_RPATH as u32).to_le_bytes());
new_command.extend_from_slice(&(command_size as u32).to_le_bytes());
new_command.extend_from_slice(&12u32.to_le_bytes());
new_command.extend_from_slice(path.as_bytes());
new_command.push(0);
new_command.extend(vec![0u8; padding]);

data[insert_offset..insert_offset + command_size].copy_from_slice(&new_command);

let new_sizeofcmds = current_sizeofcmds + command_size as u32;
let new_ncmds = current_ncmds + 1;

data[20..24].copy_from_slice(&new_sizeofcmds.to_le_bytes());
data[16..20].copy_from_slice(&new_ncmds.to_le_bytes());

self.data = Box::leak(data.into_boxed_slice());

Ok(())
}

fn remove_dylib_load_path(&mut self, path: &str) -> Result<(), Error> {
let macho = &self.macho;
let mut data = self.data.to_vec();
Expand Down Expand Up @@ -490,18 +608,26 @@ fn extract_dylib_path(
.map(|s| s.to_string())
}

// TODO: our custom ones need manual parsing?
fn manually_parse_dylib(file_data: &[u8], load_cmd_offset: usize) -> Option<String> {
if load_cmd_offset + 12 > file_data.len() {
fn manually_parse_lc_str(
file_data: &[u8],
load_cmd_offset: usize,
offset_field: usize,
) -> Option<String> {
if load_cmd_offset + offset_field + 4 > file_data.len() {
return None;
}

let name_offset_field = u32::from_le_bytes([
file_data[load_cmd_offset + 8],
file_data[load_cmd_offset + 9],
file_data[load_cmd_offset + 10],
file_data[load_cmd_offset + 11],
file_data[load_cmd_offset + offset_field],
file_data[load_cmd_offset + offset_field + 1],
file_data[load_cmd_offset + offset_field + 2],
file_data[load_cmd_offset + offset_field + 3],
]);

extract_dylib_path(file_data, load_cmd_offset, name_offset_field)
}

// TODO: our custom ones need manual parsing?
fn manually_parse_dylib(file_data: &[u8], load_cmd_offset: usize) -> Option<String> {
manually_parse_lc_str(file_data, load_cmd_offset, 8)
}
4 changes: 4 additions & 0 deletions crates/plume_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pub use options::{
SignerInstallMode, // Installation mode
SignerMode, // Signing mode
SignerOptions, // Main
TweakInjectFolder,
TweakInjectPath,
TweakInjection,
TweakLoader,
};
pub use package::Package; // Package helper
pub use signer::Signer; // Signer
Expand Down
Loading