From e4646ddffd1f279bb45379ca17c2da2b40cf7b1c Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 18 Jul 2025 19:35:55 -0600 Subject: [PATCH] Implement restore_service Remove pcap dump --- idevice/Cargo.toml | 12 +- idevice/src/services/mod.rs | 2 + idevice/src/services/restore_service.rs | 232 ++++++++++++++++++++++++ tools/Cargo.toml | 4 + tools/src/restore_service.rs | 125 +++++++++++++ 5 files changed, 370 insertions(+), 5 deletions(-) create mode 100644 idevice/src/services/restore_service.rs create mode 100644 tools/src/restore_service.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 5bdb66c..767f7b7 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -71,6 +71,7 @@ mobile_image_mounter = ["dep:sha2"] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] obfuscate = ["dep:obfstr"] +restore_service = [] rsd = ["xpc"] syslog_relay = ["dep:bytes"] tcp = ["tokio/net"] @@ -89,19 +90,20 @@ full = [ "heartbeat", "house_arrest", "installation_proxy", + "location_simulation", "misagent", "mobile_image_mounter", "pair", - "usbmuxd", - "xpc", - "location_simulation", + "restore_service", "rsd", + "springboardservices", + "syslog_relay", "tcp", "tunnel_tcp_stack", "tss", "tunneld", - "springboardservices", - "syslog_relay", + "usbmuxd", + "xpc", ] [package.metadata.docs.rs] diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index 1c035f4..fdb5397 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -23,6 +23,8 @@ pub mod misagent; pub mod mobile_image_mounter; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; +#[cfg(feature = "restore_service")] +pub mod restore_service; #[cfg(feature = "rsd")] pub mod rsd; #[cfg(feature = "springboardservices")] diff --git a/idevice/src/services/restore_service.rs b/idevice/src/services/restore_service.rs new file mode 100644 index 0000000..4fa7809 --- /dev/null +++ b/idevice/src/services/restore_service.rs @@ -0,0 +1,232 @@ +//! Restore Service + +use log::warn; +use plist::Dictionary; + +use crate::{obf, IdeviceError, ReadWrite, RemoteXpcClient, RsdService}; + +/// Client for interacting with the Restore Service +pub struct RestoreServiceClient { + /// The underlying device connection with established Restore Service service + pub stream: RemoteXpcClient, +} + +impl RsdService for RestoreServiceClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.RestoreRemoteServices.restoreserviced") + } + + async fn from_stream(stream: R) -> Result { + Self::new(stream).await + } + + type Stream = R; +} + +impl RestoreServiceClient { + /// Creates a new Restore Service client a socket connection, + /// and connects to the RemoteXPC service. + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub async fn new(stream: R) -> Result { + let mut stream = RemoteXpcClient::new(stream).await?; + stream.do_handshake().await?; + Ok(Self { stream }) + } + + /// Enter recovery + pub async fn enter_recovery(&mut self) -> Result<(), IdeviceError> { + let mut req = Dictionary::new(); + req.insert("command".into(), "recovery".into()); + + self.stream + .send_object(plist::Value::Dictionary(req), true) + .await?; + + let res = self.stream.recv().await?; + let mut res = match res { + plist::Value::Dictionary(d) => d, + _ => { + warn!("Did not receive dictionary response from XPC"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + match res.remove("result") { + Some(plist::Value::String(r)) => { + if r == "success" { + Ok(()) + } else { + warn!("Failed to enter recovery"); + Err(IdeviceError::UnexpectedResponse) + } + } + _ => { + warn!("XPC dictionary did not contain result"); + Err(IdeviceError::UnexpectedResponse) + } + } + } + + /// Reboot + pub async fn reboot(&mut self) -> Result<(), IdeviceError> { + let mut req = Dictionary::new(); + req.insert("command".into(), "reboot".into()); + + self.stream + .send_object(plist::Value::Dictionary(req), true) + .await?; + + let res = self.stream.recv().await?; + let mut res = match res { + plist::Value::Dictionary(d) => d, + _ => { + warn!("Did not receive dictionary response from XPC"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + match res.remove("result") { + Some(plist::Value::String(r)) => { + if r == "success" { + Ok(()) + } else { + warn!("Failed to enter recovery"); + Err(IdeviceError::UnexpectedResponse) + } + } + _ => { + warn!("XPC dictionary did not contain result"); + Err(IdeviceError::UnexpectedResponse) + } + } + } + + /// Get preflightinfo + pub async fn get_preflightinfo(&mut self) -> Result { + let mut req = Dictionary::new(); + req.insert("command".into(), "getpreflightinfo".into()); + + self.stream + .send_object(plist::Value::Dictionary(req), true) + .await?; + + let res = self.stream.recv().await?; + let mut res = match res { + plist::Value::Dictionary(d) => d, + _ => { + warn!("Did not receive dictionary response from XPC"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let res = match res.remove("preflightinfo") { + Some(plist::Value::Dictionary(i)) => i, + _ => { + warn!("XPC dictionary did not contain preflight info"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + Ok(res) + } + + /// Get nonces + /// Doesn't seem to work + pub async fn get_nonces(&mut self) -> Result { + let mut req = Dictionary::new(); + req.insert("command".into(), "getnonces".into()); + + self.stream + .send_object(plist::Value::Dictionary(req), true) + .await?; + + let res = self.stream.recv().await?; + let mut res = match res { + plist::Value::Dictionary(d) => d, + _ => { + warn!("Did not receive dictionary response from XPC"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let res = match res.remove("nonces") { + Some(plist::Value::Dictionary(i)) => i, + _ => { + warn!("XPC dictionary did not contain nonces"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + Ok(res) + } + + /// Get app parameters + /// Doesn't seem to work + pub async fn get_app_parameters(&mut self) -> Result { + let mut req = Dictionary::new(); + req.insert("command".into(), "getappparameters".into()); + + self.stream + .send_object(plist::Value::Dictionary(req), true) + .await?; + + let res = self.stream.recv().await?; + let mut res = match res { + plist::Value::Dictionary(d) => d, + _ => { + warn!("Did not receive dictionary response from XPC"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let res = match res.remove("appparameters") { + Some(plist::Value::Dictionary(i)) => i, + _ => { + warn!("XPC dictionary did not contain parameters"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + Ok(res) + } + + /// Restores the language + /// Doesn't seem to work + pub async fn restore_lang(&mut self, language: impl Into) -> Result<(), IdeviceError> { + let language = language.into(); + + let mut req = Dictionary::new(); + req.insert("command".into(), "restorelang".into()); + req.insert("argument".into(), language.into()); + + self.stream + .send_object(plist::Value::Dictionary(req), true) + .await?; + + let res = self.stream.recv().await?; + let mut res = match res { + plist::Value::Dictionary(d) => d, + _ => { + warn!("Did not receive dictionary response from XPC"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + match res.remove("result") { + Some(plist::Value::String(r)) => { + if r == "success" { + Ok(()) + } else { + warn!("Failed to restore language"); + Err(IdeviceError::UnexpectedResponse) + } + } + _ => { + warn!("XPC dictionary did not contain result"); + Err(IdeviceError::UnexpectedResponse) + } + } + } +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index ec26a97..3fef0ca 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -85,6 +85,10 @@ path = "src/os_trace_relay.rs" name = "lockdown" path = "src/lockdown.rs" +[[bin]] +name = "restore_service" +path = "src/restore_service.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"] } tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } diff --git a/tools/src/restore_service.rs b/tools/src/restore_service.rs new file mode 100644 index 0000000..b1d1e85 --- /dev/null +++ b/tools/src/restore_service.rs @@ -0,0 +1,125 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{ + core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, + restore_service::RestoreServiceClient, rsd::RsdHandshake, tcp::stream::AdapterStream, + IdeviceService, RsdService, +}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("restore_service") + .about("Interact with the Restore Service service") + .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)"), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .subcommand(Command::new("delay").about("Delay recovery image")) + .subcommand(Command::new("recovery").about("Enter recovery mode")) + .subcommand(Command::new("reboot").about("Reboots the device")) + .subcommand(Command::new("preflightinfo").about("Gets the preflight info")) + .subcommand(Command::new("nonces").about("Gets the nonces")) + .subcommand(Command::new("app_parameters").about("Gets the app parameters")) + .subcommand( + Command::new("restore_lang") + .about("Restores the language") + .arg(Arg::new("language").required(true).index(1)), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = + match common::get_provider(udid, host, pairing_file, "restore_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 mut adapter = proxy.create_software_tunnel().expect("no software tunnel"); + + let stream = AdapterStream::connect(&mut adapter, rsd_port) + .await + .expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + println!("{:?}", handshake.services); + + let mut restore_client = RestoreServiceClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("Unable to connect to service"); + + if matches.subcommand_matches("recovery").is_some() { + restore_client + .enter_recovery() + .await + .expect("command failed"); + } else if matches.subcommand_matches("reboot").is_some() { + restore_client.reboot().await.expect("command failed"); + } else if matches.subcommand_matches("preflightinfo").is_some() { + let info = restore_client + .get_preflightinfo() + .await + .expect("command failed"); + pretty_print_dictionary(&info); + } else if matches.subcommand_matches("nonces").is_some() { + let nonces = restore_client.get_nonces().await.expect("command failed"); + pretty_print_dictionary(&nonces); + } else if matches.subcommand_matches("app_parameters").is_some() { + let params = restore_client + .get_app_parameters() + .await + .expect("command failed"); + pretty_print_dictionary(¶ms); + } else if let Some(matches) = matches.subcommand_matches("restore_lang") { + let lang = matches + .get_one::("language") + .expect("No language passed"); + restore_client + .restore_lang(lang) + .await + .expect("failed to restore lang"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } +}