Initial implementation of installcoordination_proxy

This commit is contained in:
Jackson Coxson
2025-10-16 12:10:14 -06:00
parent 526773d8ca
commit 7853f2d6d0
5 changed files with 248 additions and 3 deletions

View File

@@ -87,6 +87,7 @@ installation_proxy = [
"async_zip/deflate", "async_zip/deflate",
"tokio/fs", "tokio/fs",
] ]
installcoordination_proxy = []
springboardservices = [] springboardservices = []
misagent = [] misagent = []
mobile_image_mounter = ["dep:sha2"] mobile_image_mounter = ["dep:sha2"]
@@ -127,6 +128,7 @@ full = [
"heartbeat", "heartbeat",
"house_arrest", "house_arrest",
"installation_proxy", "installation_proxy",
"installcoordination_proxy",
"location_simulation", "location_simulation",
"misagent", "misagent",
"mobile_image_mounter", "mobile_image_mounter",

View File

@@ -0,0 +1,105 @@
// Jackson Coxson
use crate::{IdeviceError, ReadWrite, RemoteXpcClient, RsdService, obf};
impl RsdService for InstallcoordinationProxy<Box<dyn ReadWrite>> {
fn rsd_service_name() -> std::borrow::Cow<'static, str> {
obf!("com.apple.remote.installcoordination_proxy")
}
async fn from_stream(stream: Box<dyn ReadWrite>) -> Result<Self, IdeviceError> {
let mut client = RemoteXpcClient::new(stream).await?;
client.do_handshake().await?;
Ok(Self { inner: client })
}
}
pub struct InstallcoordinationProxy<R: ReadWrite> {
inner: RemoteXpcClient<R>,
}
impl<R: ReadWrite> InstallcoordinationProxy<R> {
// 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<String, IdeviceError> {
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),
}
}
}

View File

@@ -24,6 +24,8 @@ pub mod heartbeat;
pub mod house_arrest; pub mod house_arrest;
#[cfg(feature = "installation_proxy")] #[cfg(feature = "installation_proxy")]
pub mod installation_proxy; pub mod installation_proxy;
#[cfg(feature = "installcoordination_proxy")]
pub mod installcoordination_proxy;
pub mod lockdown; pub mod lockdown;
#[cfg(feature = "misagent")] #[cfg(feature = "misagent")]
pub mod misagent; pub mod misagent;
@@ -45,10 +47,9 @@ pub mod restore_service;
pub mod rsd; pub mod rsd;
#[cfg(feature = "screenshotr")] #[cfg(feature = "screenshotr")]
pub mod screenshotr; pub mod screenshotr;
#[cfg(feature = "location_simulation")]
pub mod simulate_location;
#[cfg(feature = "springboardservices")] #[cfg(feature = "springboardservices")]
pub mod springboardservices; pub mod springboardservices;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]
pub mod syslog_relay; pub mod syslog_relay;
#[cfg(feature = "location_simulation")]
pub mod simulate_location;

View File

@@ -138,6 +138,11 @@ path = "src/activation.rs"
name = "notifications" name = "notifications"
path = "src/notifications.rs" path = "src/notifications.rs"
[[bin]]
name = "installcoordination_proxy"
path = "src/installcoordination_proxy.rs"
[dependencies] [dependencies]
idevice = { path = "../idevice", features = ["full"], default-features = false } idevice = { path = "../idevice", features = ["full"], default-features = false }
tokio = { version = "1.43", features = ["full"] } tokio = { version = "1.43", features = ["full"] }

View File

@@ -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::<String>("udid");
let pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("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");
}
}