From 7853f2d6d01b1724700bc049f8dbff93f291cd94 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 16 Oct 2025 12:10:14 -0600 Subject: [PATCH] Initial implementation of installcoordination_proxy --- idevice/Cargo.toml | 2 + .../src/services/installcoordination_proxy.rs | 105 ++++++++++++++ idevice/src/services/mod.rs | 7 +- tools/Cargo.toml | 5 + tools/src/installcoordination_proxy.rs | 132 ++++++++++++++++++ 5 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 idevice/src/services/installcoordination_proxy.rs create mode 100644 tools/src/installcoordination_proxy.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 7ed18ab..fa758d3 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -87,6 +87,7 @@ installation_proxy = [ "async_zip/deflate", "tokio/fs", ] +installcoordination_proxy = [] springboardservices = [] misagent = [] mobile_image_mounter = ["dep:sha2"] @@ -127,6 +128,7 @@ full = [ "heartbeat", "house_arrest", "installation_proxy", + "installcoordination_proxy", "location_simulation", "misagent", "mobile_image_mounter", diff --git a/idevice/src/services/installcoordination_proxy.rs b/idevice/src/services/installcoordination_proxy.rs new file mode 100644 index 0000000..d2751c4 --- /dev/null +++ b/idevice/src/services/installcoordination_proxy.rs @@ -0,0 +1,105 @@ +// Jackson Coxson + +use crate::{IdeviceError, ReadWrite, RemoteXpcClient, RsdService, obf}; + +impl RsdService for InstallcoordinationProxy> { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.remote.installcoordination_proxy") + } + + async fn from_stream(stream: Box) -> Result { + let mut client = RemoteXpcClient::new(stream).await?; + client.do_handshake().await?; + Ok(Self { inner: client }) + } +} + +pub struct InstallcoordinationProxy { + inner: RemoteXpcClient, +} + +impl InstallcoordinationProxy { + // TODO: implement 2 missing functions + // + // # REVERT STASH + // Revert Stashed App (RequestType: 2) + // This request rolls back an application to a previously "stashed" version, which is typically done after a failed update. + // + // Handler: _handleRevertStashMessage_forRemoteConnection_ + // + // RequestType: 2 + // ProtocolVersion: 1 + // BundleID: The bundle identifier of the app to revert. + // + // Action: The service creates an IXSRemoteReverter object, which calls the IXAppInstallCoordinator to perform the revert. It responds with a success or failure message. + // + // # INSTALL + // This is the most complex request. It tells the service to install a new application. + // Purpose: To stream an application binary from a client and install it on the device. + // + // Handler: _handleInstallBeginMessage_forRemoteConnection_ + // RequestType: 1 + // ProtocolVersion: 1 + // AssetSize: The total size of the app binary. + // AssetStreamFD: A file descriptor from which the service will read the app binary. + // RemoteInstallOptions: A dictionary containing all the app's metadata, like: + // + // { + // BundleID: (String) The application's bundle identifier (e.g., com.example.myapp). + // LocalizedName: (String) The display name of the app. + // InstallMode: (uint64) An enum specifying the installation mode (e.g., full install, update). + // Importance: (uint64) An enum defining the install's priority. 1 for "user" and 2 for "system". + // InstallableType: (uint64) Specifies the type of content being installed (e.g., app, system component). + // StoreMetadata: (Dictionary) A dictionary containing App Store metadata. + // SINFData: (Data) The legacy iTunes Sinf data for DRM. + // ProvisioningProfiles: (Array of Data) An array of provisioning profiles to install alongside the app. + // IconData: (Data, Optional) The raw data for the application's icon. + // IconDataType: (uint64, Optional) An enum specifying the format of the IconData + // } + // + // Action: The service creates an IXSRemoteInstaller object. It reads the app data from the file descriptor and passes it to the system's IXAppInstallCoordinator to handle the installation, placeholder creation, and data management. It sends back progress updates and a final completion message. + + pub async fn uninstall_app(&mut self, bundle_id: &str) -> Result<(), IdeviceError> { + let req = crate::xpc!({ + "RequestVersion": 1u64, + "ProtocolVersion": 1u64, + "RequestType": 3u64, + "BundleID": bundle_id, + }); + + self.inner.send_object(req, true).await?; + let res = self.inner.recv_root().await?; // it responds on the root?? + + match res + .as_dictionary() + .and_then(|x| x.get("Success")) + .and_then(|x| x.as_boolean()) + { + Some(true) => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + pub async fn query_app_path(&mut self, bundle_id: &str) -> Result { + let req = crate::xpc!({ + "RequestVersion": 1u64, + "ProtocolVersion": 1u64, + "RequestType": 4u64, + "BundleID": bundle_id, + }); + + self.inner.send_object(req, true).await?; + let res = self.inner.recv_root().await?; // it responds on the root?? + + match res + .as_dictionary() + .and_then(|x| x.get("InstallPath")) + .and_then(|x| x.as_dictionary()) + .and_then(|x| x.get("com.apple.CFURL.string")) + .and_then(|x| x.as_string()) + { + Some(s) => Ok(s.to_string()), + None => Err(IdeviceError::UnexpectedResponse), + } + } +} diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index f827678..e795578 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -24,6 +24,8 @@ pub mod heartbeat; pub mod house_arrest; #[cfg(feature = "installation_proxy")] pub mod installation_proxy; +#[cfg(feature = "installcoordination_proxy")] +pub mod installcoordination_proxy; pub mod lockdown; #[cfg(feature = "misagent")] pub mod misagent; @@ -45,10 +47,9 @@ pub mod restore_service; pub mod rsd; #[cfg(feature = "screenshotr")] pub mod screenshotr; +#[cfg(feature = "location_simulation")] +pub mod simulate_location; #[cfg(feature = "springboardservices")] pub mod springboardservices; #[cfg(feature = "syslog_relay")] pub mod syslog_relay; - -#[cfg(feature = "location_simulation")] -pub mod simulate_location; diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 43d0d5e..dc876e7 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -138,6 +138,11 @@ path = "src/activation.rs" name = "notifications" path = "src/notifications.rs" + +[[bin]] +name = "installcoordination_proxy" +path = "src/installcoordination_proxy.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/installcoordination_proxy.rs b/tools/src/installcoordination_proxy.rs new file mode 100644 index 0000000..4722aa3 --- /dev/null +++ b/tools/src/installcoordination_proxy.rs @@ -0,0 +1,132 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{ + IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, + installcoordination_proxy::InstallcoordinationProxy, rsd::RsdHandshake, +}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("installationcoordination_proxy") + .about("") + .arg( + Arg::new("host") + .long("host") + .value_name("HOST") + .help("IP address of the device"), + ) + .arg( + Arg::new("pairing_file") + .long("pairing-file") + .value_name("PATH") + .help("Path to the pairing file"), + ) + .arg( + Arg::new("udid") + .value_name("UDID") + .help("UDID of the device (overrides host/pairing file)") + .index(1), + ) + .arg( + Arg::new("tunneld") + .long("tunneld") + .help("Use tunneld") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand( + Command::new("info") + .about("Get info about an app on the device") + .arg( + Arg::new("bundle_id") + .required(true) + .help("The bundle ID to query"), + ), + ) + .subcommand( + Command::new("uninstall") + .about("Get info about an app on the device") + .arg( + Arg::new("bundle_id") + .required(true) + .help("The bundle ID to query"), + ), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("debug_proxy - connect to the debug proxy and run commands"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let pairing_file = matches.get_one::("pairing_file"); + let host = matches.get_one::("host"); + + let provider = + match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + let proxy = CoreDeviceProxy::connect(&*provider) + .await + .expect("no core proxy"); + let rsd_port = proxy.handshake.server_rsd_port; + + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + adapter + .pcap("/Users/jacksoncoxson/Desktop/hmmmm.pcap") + .await + .unwrap(); + + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + + let mut icp = InstallcoordinationProxy::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("no connect"); + + if let Some(matches) = matches.subcommand_matches("info") { + let bundle_id: &String = match matches.get_one("bundle_id") { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + + let res = icp.query_app_path(bundle_id).await.expect("no info"); + println!("Path: {res}"); + } else if let Some(matches) = matches.subcommand_matches("uninstall") { + let bundle_id: &String = match matches.get_one("bundle_id") { + Some(b) => b, + None => { + eprintln!("No bundle ID passed"); + return; + } + }; + + icp.uninstall_app(bundle_id) + .await + .expect("uninstall failed"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } +}