From 732940581e3dd12343007ff7caf0ca0c1755bb11 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 5 Feb 2025 00:41:10 -0700 Subject: [PATCH] Implement TSS support --- idevice/Cargo.toml | 6 +- idevice/src/lib.rs | 10 + idevice/src/lockdownd.rs | 5 +- idevice/src/mounter.rs | 377 +++++++++++++++++++++++++++++- idevice/src/tss.rs | 140 +++++++++++ idevice/src/usbmuxd/raw_packet.rs | 9 +- idevice/src/util.rs | 9 + 7 files changed, 544 insertions(+), 12 deletions(-) create mode 100644 idevice/src/tss.rs create mode 100644 idevice/src/util.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 3ba80bc..7d93c10 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -22,7 +22,7 @@ log = { version = "0.4" } env_logger = { version = "0.11" } indexmap = { version = "2.7", features = ["serde"], optional = true } -uuid = { version = "1.12", features = ["serde"], optional = true } +uuid = { version = "1.12", features = ["serde", "v4"], optional = true } async-recursion = { version = "1.1", optional = true } base64 = { version = "0.22", optional = true } @@ -30,6 +30,8 @@ serde_json = { version = "1", optional = true } json = { version = "0.12", optional = true } byteorder = { version = "1.5", optional = true } +reqwest = { version = "0.12", features = ["json"], optional = true } + [features] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] @@ -38,6 +40,7 @@ installation_proxy = [] mounter = [] usbmuxd = [] tcp = ["tokio/net"] +tss = ["dep:uuid", "dep:reqwest"] xpc = [ "tokio/full", "dep:indexmap", @@ -53,6 +56,7 @@ full = [ "usbmuxd", "xpc", "tcp", + "tss", ] # Why: https://github.com/rust-lang/cargo/issues/1197 diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index b991e4c..6ba1da4 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -13,8 +13,11 @@ pub mod lockdownd; pub mod mounter; pub mod pairing_file; pub mod provider; +#[cfg(feature = "tss")] +pub mod tss; #[cfg(feature = "usbmuxd")] pub mod usbmuxd; +mod util; #[cfg(feature = "xpc")] pub mod xpc; @@ -216,6 +219,13 @@ pub enum IdeviceError { #[error("usb bad version")] UsbBadVersion, + #[error("bad build manifest")] + BadBuildManifest, + + #[cfg(feature = "tss")] + #[error("http reqwest error")] + Reqwest(#[from] reqwest::Error), + #[error("unknown error `{0}` returned from device")] UnknownErrorType(String), } diff --git a/idevice/src/lockdownd.rs b/idevice/src/lockdownd.rs index 1b8a184..df91bce 100644 --- a/idevice/src/lockdownd.rs +++ b/idevice/src/lockdownd.rs @@ -2,6 +2,7 @@ // Abstractions for lockdownd use log::error; +use plist::Value; use serde::{Deserialize, Serialize}; use crate::{pairing_file, Idevice, IdeviceError, IdeviceService}; @@ -37,7 +38,7 @@ impl LockdowndClient { pub fn new(idevice: Idevice) -> Self { Self { idevice } } - pub async fn get_value(&mut self, value: impl Into) -> Result { + pub async fn get_value(&mut self, value: impl Into) -> Result { let req = LockdowndRequest { label: self.idevice.label.clone(), key: Some(value.into()), @@ -47,7 +48,7 @@ impl LockdowndClient { self.idevice.send_plist(message).await?; let message: plist::Dictionary = self.idevice.read_plist().await?; match message.get("Value") { - Some(m) => Ok(plist::from_value(m)?), + Some(m) => Ok(m.to_owned()), None => Err(IdeviceError::UnexpectedResponse), } } diff --git a/idevice/src/mounter.rs b/idevice/src/mounter.rs index ab3bc56..1bc30c7 100644 --- a/idevice/src/mounter.rs +++ b/idevice/src/mounter.rs @@ -1,6 +1,9 @@ // Jackson Coxson -use crate::{lockdownd::LockdowndClient, Idevice, IdeviceError, IdeviceService}; +use crate::{ + lockdownd::LockdowndClient, tss::TSSRequest, util::hashmap_to_dictionary, Idevice, + IdeviceError, IdeviceService, +}; pub struct ImageMounter { idevice: Idevice, @@ -51,6 +54,26 @@ impl ImageMounter { } } + /// Looks up an image and returns the signature + pub async fn lookup_image( + &mut self, + image_type: impl Into, + ) -> Result, IdeviceError> { + let image_type = image_type.into(); + let mut req = plist::Dictionary::new(); + req.insert("Command".into(), "LookupImage".into()); + req.insert("ImageType".into(), image_type.into()); + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + + let res = self.idevice.read_plist().await?; + match res.get("ImageSignature") { + Some(plist::Value::Data(signature)) => Ok(signature.clone()), + _ => Err(IdeviceError::NotFound), + } + } + pub async fn upload_image( &mut self, image_type: impl Into, @@ -129,6 +152,28 @@ impl ImageMounter { Ok(()) } + /// Unmounts an image at a specified path. + /// Use ``/Developer`` for pre-iOS 17 developer images. + /// Use ``/System/Developer`` for personalized images. + pub async fn unmount_image( + &mut self, + mount_path: impl Into, + ) -> Result<(), IdeviceError> { + let mount_path = mount_path.into(); + let mut req = plist::Dictionary::new(); + req.insert("Command".into(), "UnmountImage".into()); + req.insert("MountPath".into(), mount_path.into()); + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + + let res = self.idevice.read_plist().await?; + match res.get("Status") { + Some(plist::Value::String(s)) if s.as_str() == "Complete" => Ok(()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + /// Queries the personalization manifest from the device. /// On failure, the socket must be closed and reestablished. pub async fn query_personalization_manifest( @@ -153,4 +198,334 @@ impl ImageMounter { _ => Err(IdeviceError::NotFound), } } + + pub async fn query_developer_mode_status(&mut self) -> Result { + let mut req = plist::Dictionary::new(); + req.insert("Command".into(), "QueryDeveloperModeStatus".into()); + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + + let res = self.idevice.read_plist().await?; + match res.get("DeveloperModeStatus") { + Some(plist::Value::Boolean(status)) => Ok(*status), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + pub async fn query_nonce( + &mut self, + personalized_image_type: Option, + ) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Command".into(), "QueryNonce".into()); + if let Some(image_type) = personalized_image_type { + req.insert("PersonalizedImageType".into(), image_type.into()); + } + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + + let res = self.idevice.read_plist().await?; + match res.get("PersonalizationNonce") { + Some(plist::Value::Data(nonce)) => Ok(nonce.clone()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + pub async fn query_personalization_identifiers( + &mut self, + image_type: Option, + ) -> Result { + let mut req = plist::Dictionary::new(); + req.insert("Command".into(), "QueryPersonalizationIdentifiers".into()); + if let Some(image_type) = image_type { + req.insert("PersonalizedImageType".into(), image_type.into()); + } + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + + let res = self.idevice.read_plist().await?; + match res.get("PersonalizationIdentifiers") { + Some(plist::Value::Dictionary(identifiers)) => Ok(identifiers.clone()), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + pub async fn roll_personalization_nonce(&mut self) -> Result<(), IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Command".into(), "RollPersonalizationNonce".into()); + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + + Ok(()) + } + + pub async fn roll_cryptex_nonce(&mut self) -> Result<(), IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Command".into(), "RollCryptexNonce".into()); + self.idevice + .send_plist(plist::Value::Dictionary(req)) + .await?; + + Ok(()) + } + + pub async fn mount_developer( + &mut self, + image: &[u8], + signature: Vec, + ) -> Result<(), IdeviceError> { + self.upload_image("Developer", &image, signature.clone()) + .await?; + self.mount_image( + "Developer", + signature, + Vec::new(), + plist::Value::Dictionary(plist::Dictionary::new()), + ) + .await?; + + Ok(()) + } + + pub async fn mount_personalized( + &mut self, + image: Vec, + trust_cache: Vec, + build_manifest: &[u8], + info_plist: Option, + unique_chip_id: u64, + ) -> Result<(), IdeviceError> { + // Try to fetch personalization manifest + let manifest = match self + .query_personalization_manifest("DeveloperDiskImage", image.clone()) // TODO: + .await + { + Ok(manifest) => manifest, + Err(IdeviceError::NotFound) => { + // Get manifest from TSS + let manifest_dict: plist::Dictionary = plist::from_bytes(build_manifest)?; + self.get_manifest_from_tss(&manifest_dict, unique_chip_id) + .await? + } + Err(e) => return Err(e), + }; + + self.upload_image("Personalized", &image, manifest.clone()) + .await?; + + let mut extras = plist::Dictionary::new(); + if let Some(info) = info_plist { + extras.insert("ImageInfoPlist".into(), info); + } + extras.insert( + "ImageTrustCache".into(), + plist::Value::Data(trust_cache.clone()), + ); + + self.mount_image( + "Personalized", + manifest, + trust_cache, + plist::Value::Dictionary(extras), + ) + .await?; + + Ok(()) + } + + #[cfg(feature = "tss")] + pub async fn get_manifest_from_tss( + &mut self, + build_manifest: &plist::Dictionary, + unique_chip_id: u64, + ) -> Result, IdeviceError> { + use log::{debug, warn}; + + let mut request = TSSRequest::new(); + + let personalization_identifiers = self.query_personalization_identifiers(None).await?; + for (key, val) in &personalization_identifiers { + if key.starts_with("Ap,") { + request.insert(key, val.clone()); + } + } + + let board_id = match personalization_identifiers.get("BoardId") { + Some(plist::Value::Integer(b)) => match b.as_unsigned() { + Some(b) => b, + None => return Err(IdeviceError::UnexpectedResponse), + }, + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + }; + let chip_id = match personalization_identifiers.get("ChipID") { + Some(plist::Value::Integer(b)) => match b.as_unsigned() { + Some(b) => b, + None => return Err(IdeviceError::UnexpectedResponse), + }, + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + }; + + request.insert("@ApImg4Ticket", true); + request.insert("@BBTicket", true); + request.insert("ApBoardID", board_id); + request.insert("ApChipID", chip_id); + request.insert("ApECID", unique_chip_id); + request.insert( + "ApNonce", + plist::Value::Data( + self.query_nonce(Some("DeveloperDiskImage".to_string())) + .await?, + ), + ); + request.insert("ApProductionMode", true); + request.insert("ApSecurityDomain", 1); + request.insert("ApSecurityMode", true); + request.insert("SepNonce", plist::Value::Data(vec![0; 20])); + request.insert("UID_MODE", false); + + let identities = match build_manifest.get("BuildIdentities") { + Some(plist::Value::Array(i)) => i, + _ => { + return Err(IdeviceError::BadBuildManifest); + } + }; + let mut build_identity = None; + for id in identities { + let id = match id { + plist::Value::Dictionary(id) => id, + _ => { + debug!("build identity wasn't a dictionary"); + continue; + } + }; + + let ap_board_id = match id.get("ApBoardID") { + Some(plist::Value::String(a)) => a, + _ => { + debug!("Build identity contained no ApBoardID"); + continue; + } + }; + let ap_board_id = match u64::from_str_radix(ap_board_id.trim_start_matches("0x"), 16) { + Ok(a) => a, + Err(_) => { + debug!("Could not parse {ap_board_id} as usize"); + continue; + } + }; + if ap_board_id != board_id { + continue; + } + let ap_chip_id = match id.get("ApChipID") { + Some(plist::Value::String(a)) => a, + _ => { + debug!("Build identity contained no ApChipID"); + continue; + } + }; + let ap_chip_id = match u64::from_str_radix(ap_chip_id.trim_start_matches("0x"), 16) { + Ok(a) => a, + Err(_) => { + debug!("Could not parse {ap_board_id} as usize"); + continue; + } + }; + if ap_chip_id != chip_id { + continue; + } + build_identity = Some(id.to_owned()); + break; + } + + let build_identity = match build_identity { + Some(b) => b, + None => { + return Err(IdeviceError::BadBuildManifest); + } + }; + + let manifest = match build_identity.get("Manifest") { + Some(plist::Value::Dictionary(m)) => m, + _ => { + return Err(IdeviceError::BadBuildManifest); + } + }; + + let mut parameters = plist::Dictionary::new(); + parameters.insert("ApProductionMode".into(), true.into()); + parameters.insert("ApSecurityDomain".into(), 1.into()); + parameters.insert("ApSecurityMode".into(), true.into()); + parameters.insert("ApSupportsImg4".into(), true.into()); + + for (key, manifest_item) in manifest { + let manifest_item = match manifest_item { + plist::Value::Dictionary(m) => m, + _ => { + debug!("Manifest item wasn't a dictionary"); + continue; + } + }; + let info = match manifest_item.get("Info") { + Some(plist::Value::Dictionary(i)) => i, + _ => { + debug!("Manifest item didn't contain info"); + continue; + } + }; + + match info.get("Trusted") { + Some(plist::Value::Boolean(t)) => { + if !t { + debug!("Info item isn't trusted"); + continue; + } + } + _ => { + debug!("Info didn't contain trusted bool"); + continue; + } + } + + let mut tss_entry = manifest_item.clone(); + tss_entry.remove("Info"); + + if let Some(plist::Value::Dictionary(l)) = manifest.get("LoadableTrustCache") { + if let Some(plist::Value::Dictionary(i)) = l.get("Info") { + if let Some(plist::Value::Array(rules)) = i.get("RestoreRequestRules") { + crate::tss::apply_restore_request_rules(&mut tss_entry, ¶meters, rules); + } + } + } + + if manifest_item.get("Digest").is_none() { + tss_entry.insert("Digest".into(), plist::Value::Data(vec![])); + } + + request.insert(key, tss_entry); + } + let res = request.send().await?; + let mut res = match res { + plist::Value::Dictionary(r) => r, + _ => { + warn!("Apple returned a non-dictionary plist"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + match res.remove("ApImg4Ticket") { + Some(plist::Value::Data(d)) => Ok(d), + _ => { + warn!("TSS response didn't contain ApImg4Ticket data"); + Err(IdeviceError::UnexpectedResponse) + } + } + } } diff --git a/idevice/src/tss.rs b/idevice/src/tss.rs new file mode 100644 index 0000000..e166b51 --- /dev/null +++ b/idevice/src/tss.rs @@ -0,0 +1,140 @@ +// Jackson Coxson +// Thanks pymobiledevice3 + +use log::{debug, warn}; +use plist::Value; + +use crate::{util::plist_to_bytes, IdeviceError}; + +const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2"; +const TSS_CONTROLLER_ACTION_URL: &str = "http://gs.apple.com/TSS/controller?action=2"; + +pub struct TSSRequest { + inner: plist::Dictionary, +} + +impl TSSRequest { + pub fn new() -> Self { + let mut inner = plist::Dictionary::new(); + inner.insert("@HostPlatformInfo".into(), "mac".into()); + inner.insert("@VersionInfo".into(), TSS_CLIENT_VERSION_STRING.into()); + inner.insert( + "@UUID".into(), + uuid::Uuid::new_v4().to_string().to_uppercase().into(), + ); + Self { + inner: Default::default(), + } + } + + pub fn insert(&mut self, key: impl Into, val: impl Into) { + let key = key.into(); + let val = val.into(); + self.inner.insert(key, val); + } + + pub async fn send(&self) -> Result { + let client = reqwest::Client::new(); + + let res = client + .post(TSS_CONTROLLER_ACTION_URL) + .header("Cache-Control", "no-cache") + .header("Content-type", "text/xml; charset=\"utf-8\"") + .header("User-Agent", "InetURL/1.0") + .header("Expect", "") + .body(plist_to_bytes(&self.inner)) + .send() + .await? + .text() + .await?; + + debug!("Apple responeded with {res}"); + let res = res.trim_start_matches("MESSAGE="); + if !res.starts_with("SUCCESS") { + return Err(IdeviceError::UnexpectedResponse); + } + let res = res.split("REQUEST_STRING=").collect::>(); + if res.len() < 2 { + return Err(IdeviceError::UnexpectedResponse); + } + Ok(plist::from_bytes(res[1].as_bytes())?) + } +} + +impl Default for TSSRequest { + fn default() -> Self { + Self::new() + } +} + +pub fn apply_restore_request_rules( + input: &mut plist::Dictionary, + parameters: &plist::Dictionary, + rules: &Vec, +) { + for rule in rules { + if let plist::Value::Dictionary(rule) = rule { + let mut conditions_fulfulled = true; + let conditions = match rule.get("Conditions") { + Some(plist::Value::Dictionary(c)) => c, + _ => { + warn!("Conditions doesn't exist or wasn't a dictionary!!"); + continue; + } + }; + + for (key, value) in conditions { + let value2 = match key.as_str() { + "ApRawProductionMode" => parameters.get("ApProductionMode"), + "ApCurrentProductionMode" => parameters.get("ApProductionMode"), + "ApRawSecurityMode" => parameters.get("ApSecurityMode"), + "ApRequiresImage4" => parameters.get("ApSupportsImg4"), + "ApDemotionPolicyOverride" => parameters.get("DemotionPolicy"), + "ApInRomDFU" => parameters.get("ApInRomDFU"), + _ => { + warn!("Unhandled key {key}"); + None + } + }; + + conditions_fulfulled = match value2 { + Some(value2) => value == value2, + None => false, + }; + + if !conditions_fulfulled { + break; + } + } + + if !conditions_fulfulled { + continue; + } + + let actions = match rule.get("Actions") { + Some(plist::Value::Dictionary(a)) => a, + _ => { + warn!("Actions doesn't exist or wasn't a dictionary!!"); + continue; + } + }; + + for (key, value) in actions { + if let Some(i) = value.as_unsigned_integer() { + if i == 255 { + continue; + } + } + if let Some(i) = value.as_signed_integer() { + if i == 255 { + continue; + } + } + + input.insert(key.to_owned(), value.to_owned()); + } + } else { + warn!("Rule wasn't a dictionary"); + } + } +} diff --git a/idevice/src/usbmuxd/raw_packet.rs b/idevice/src/usbmuxd/raw_packet.rs index 4feb392..10794d1 100644 --- a/idevice/src/usbmuxd/raw_packet.rs +++ b/idevice/src/usbmuxd/raw_packet.rs @@ -1,5 +1,6 @@ // Jackson Coxson +use crate::util::plist_to_bytes; use log::warn; #[derive(Debug)] @@ -11,14 +12,6 @@ pub struct RawPacket { pub plist: plist::Dictionary, } -fn plist_to_bytes(p: &plist::Dictionary) -> Vec { - let buf = Vec::new(); - let mut writer = std::io::BufWriter::new(buf); - plist::to_writer_xml(&mut writer, &p).unwrap(); - - writer.into_inner().unwrap() -} - impl RawPacket { pub fn new(plist: plist::Dictionary, version: u32, message: u32, tag: u32) -> RawPacket { let plist_bytes = plist_to_bytes(&plist); diff --git a/idevice/src/util.rs b/idevice/src/util.rs new file mode 100644 index 0000000..620aeb0 --- /dev/null +++ b/idevice/src/util.rs @@ -0,0 +1,9 @@ +// Jackson Coxson + +pub fn plist_to_bytes(p: &plist::Dictionary) -> Vec { + let buf = Vec::new(); + let mut writer = std::io::BufWriter::new(buf); + plist::to_writer_xml(&mut writer, &p).unwrap(); + + writer.into_inner().unwrap() +}