diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 767f7b7..4549865 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -58,6 +58,7 @@ bytes = "1.10.1" [features] afc = ["dep:chrono"] amfi = [] +core_device = ["xpc", "dep:uuid"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] crashreportcopymobile = ["afc"] debug_proxy = [] @@ -83,6 +84,7 @@ xpc = ["dep:indexmap", "dep:uuid"] full = [ "afc", "amfi", + "core_device", "core_device_proxy", "crashreportcopymobile", "debug_proxy", diff --git a/idevice/src/services/core_device/app_service.rs b/idevice/src/services/core_device/app_service.rs new file mode 100644 index 0000000..e36aa66 --- /dev/null +++ b/idevice/src/services/core_device/app_service.rs @@ -0,0 +1,99 @@ +// Jackson Coxson + +use log::warn; +use serde::Deserialize; + +use crate::{obf, IdeviceError, ReadWrite, RsdService}; + +use super::CoreDeviceServiceClient; + +impl RsdService for AppServiceClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.coredevice.appservice") + } + + async fn from_stream(stream: R) -> Result { + Ok(Self { + inner: CoreDeviceServiceClient::new(stream).await?, + }) + } + + type Stream = R; +} + +pub struct AppServiceClient { + inner: CoreDeviceServiceClient, +} + +#[derive(Deserialize, Debug)] +pub struct AppListEntry { + #[serde(rename = "isRemovable")] + pub is_removable: bool, + pub name: String, + #[serde(rename = "isFirstParty")] + pub is_first_party: bool, + pub path: String, + #[serde(rename = "bundleIdentifier")] + pub bundle_identifier: String, + #[serde(rename = "isDeveloperApp")] + pub is_developer_app: bool, + #[serde(rename = "bundleVersion")] + pub bundle_version: Option, + #[serde(rename = "isInternal")] + pub is_internal: bool, + #[serde(rename = "isHidden")] + pub is_hidden: bool, + #[serde(rename = "isAppClip")] + pub is_app_clip: bool, + pub version: Option, +} + +impl AppServiceClient { + pub async fn new(stream: R) -> Result { + Ok(Self { + inner: CoreDeviceServiceClient::new(stream).await?, + }) + } + + pub async fn list_apps( + &mut self, + app_clips: bool, + removable_apps: bool, + hidden_apps: bool, + internal_apps: bool, + default_apps: bool, + ) -> Result, IdeviceError> { + let mut options = plist::Dictionary::new(); + options.insert("includeAppClips".into(), app_clips.into()); + options.insert("includeRemovableApps".into(), removable_apps.into()); + options.insert("includeHiddenApps".into(), hidden_apps.into()); + options.insert("includeInternalApps".into(), internal_apps.into()); + options.insert("includeDefaultApps".into(), default_apps.into()); + let res = self + .inner + .invoke("com.apple.coredevice.feature.listapps", Some(options)) + .await?; + + let res = match res.as_array() { + Some(a) => a, + None => { + warn!("CoreDevice result was not an array"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let mut desd = Vec::new(); + for r in res { + let r: AppListEntry = match plist::from_value(r) { + Ok(r) => r, + Err(e) => { + warn!("Failed to parse app entry: {e:?}"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + desd.push(r); + } + + Ok(desd) + } +} diff --git a/idevice/src/services/core_device/mod.rs b/idevice/src/services/core_device/mod.rs new file mode 100644 index 0000000..ae5af0c --- /dev/null +++ b/idevice/src/services/core_device/mod.rs @@ -0,0 +1,101 @@ +// Jackson Coxson +// Ported from pymobiledevice3 + +use log::warn; + +use crate::{ + xpc::{self, XPCObject}, + IdeviceError, ReadWrite, RemoteXpcClient, +}; + +mod app_service; +pub use app_service::{AppListEntry, AppServiceClient}; + +const CORE_SERVICE_VERSION: &str = "443.18"; + +pub struct CoreDeviceServiceClient { + inner: RemoteXpcClient, +} + +impl CoreDeviceServiceClient { + pub async fn new(inner: R) -> Result { + let mut client = RemoteXpcClient::new(inner).await?; + client.do_handshake().await?; + Ok(Self { inner: client }) + } + + pub async fn invoke( + &mut self, + feature: impl Into, + input: Option, + ) -> Result { + let feature = feature.into(); + let input = input.unwrap_or_default(); + + let mut req = xpc::Dictionary::new(); + req.insert( + "CoreDevice.CoreDeviceDDIProtocolVersion".into(), + XPCObject::Int64(0), + ); + req.insert("CoreDevice.action".into(), xpc::Dictionary::new().into()); + req.insert( + "CoreDevice.coreDeviceVersion".into(), + create_xpc_version_from_string(CORE_SERVICE_VERSION).into(), + ); + req.insert( + "CoreDevice.deviceIdentifier".into(), + XPCObject::String(uuid::Uuid::new_v4().to_string()), + ); + req.insert( + "CoreDevice.featureIdentifier".into(), + XPCObject::String(feature), + ); + req.insert( + "CoreDevice.input".into(), + plist::Value::Dictionary(input).into(), + ); + req.insert( + "CoreDevice.invocationIdentifier".into(), + XPCObject::String(uuid::Uuid::new_v4().to_string()), + ); + + self.inner.send_object(req, true).await?; + let res = self.inner.recv().await?; + let mut res = match res { + plist::Value::Dictionary(d) => d, + _ => { + warn!("XPC response was not a dictionary"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let res = match res.remove("CoreDevice.output") { + Some(r) => r, + None => { + warn!("XPC response did not have an output"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + Ok(res) + } +} + +fn create_xpc_version_from_string(version: impl Into) -> xpc::Dictionary { + let version: String = version.into(); + let mut collected_version = Vec::new(); + version.split('.').for_each(|x| { + if let Ok(x) = x.parse() { + collected_version.push(XPCObject::UInt64(x)); + } + }); + + let mut res = xpc::Dictionary::new(); + res.insert( + "originalComponentsCount".into(), + XPCObject::Int64(collected_version.len() as i64), + ); + res.insert("components".into(), XPCObject::Array(collected_version)); + res.insert("stringValue".into(), XPCObject::String(version)); + res +} diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index fdb5397..dea53bf 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -2,6 +2,8 @@ pub mod afc; #[cfg(feature = "amfi")] pub mod amfi; +#[cfg(feature = "core_device")] +pub mod core_device; #[cfg(feature = "core_device_proxy")] pub mod core_device_proxy; #[cfg(feature = "crashreportcopymobile")] diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 3fef0ca..8526f46 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -81,6 +81,10 @@ path = "src/syslog_relay.rs" name = "os_trace_relay" path = "src/os_trace_relay.rs" +[[bin]] +name = "app_service" +path = "src/app_service.rs" + [[bin]] name = "lockdown" path = "src/lockdown.rs" diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs new file mode 100644 index 0000000..28f11d0 --- /dev/null +++ b/tools/src/app_service.rs @@ -0,0 +1,103 @@ +// Jackson Coxson + +use std::io::Write; + +use clap::{Arg, Command}; +use idevice::{ + core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy, + debug_proxy::DebugProxyClient, rsd::RsdHandshake, tcp::stream::AdapterStream, IdeviceService, + RsdService, +}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("remotexpc") + .about("Get services from RemoteXPC") + .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("list").about("Lists the images mounted on the device")) + .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, "debug-proxy-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"); + adapter + .pcap("/Users/jacksoncoxson/Desktop/rs_xpc.pcap") + .await + .unwrap(); + + 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 asc = AppServiceClient::connect_rsd(&mut adapter, &mut handshake) + .await + .expect("no connect"); + + if matches.subcommand_matches("list").is_some() { + let apps = asc + .list_apps(true, true, true, true, true) + .await + .expect("Failed to get apps"); + println!("{apps:#?}"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } +}