From d938531f96622ae62c85d5f858dad9fa76529ee2 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Fri, 9 May 2025 13:46:00 -0600 Subject: [PATCH] Implement crash report services --- idevice/Cargo.toml | 2 + idevice/src/crashreportcopymobile.rs | 182 +++++++++++++++++++++++++++ idevice/src/lib.rs | 6 + tools/Cargo.toml | 4 + tools/src/crash_logs.rs | 87 +++++++++++++ 5 files changed, 281 insertions(+) create mode 100644 idevice/src/crashreportcopymobile.rs create mode 100644 tools/src/crash_logs.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 8073738..5cb3136 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -57,6 +57,7 @@ bytes = "1.10.1" afc = ["dep:chrono"] amfi = [] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] +crashreportcopymobile = ["afc"] debug_proxy = [] dvt = ["dep:byteorder", "dep:ns-keyed-archive"] heartbeat = ["tokio/macros", "tokio/time"] @@ -82,6 +83,7 @@ full = [ "afc", "amfi", "core_device_proxy", + "crashreportcopymobile", "debug_proxy", "dvt", "heartbeat", diff --git a/idevice/src/crashreportcopymobile.rs b/idevice/src/crashreportcopymobile.rs new file mode 100644 index 0000000..329b41a --- /dev/null +++ b/idevice/src/crashreportcopymobile.rs @@ -0,0 +1,182 @@ +//! iOS Crash Logs +//! +//! Provides functionality for managing crash logs on a connected iOS device. +//! +//! This module enables clients to list, pull, and remove crash logs via the +//! `CrashReportCopyMobile` service using the AFC protocol. It also includes a +//! function to trigger a flush of crash logs from system storage into the +//! crash reports directory by connecting to the `com.apple.crashreportmover` service. + +use log::{debug, warn}; + +use crate::{afc::AfcClient, lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; + +/// Client for managing crash logs on an iOS device. +/// +/// This client wraps access to the `com.apple.crashreportcopymobile` service, +/// which exposes crash logs through the Apple File Conduit (AFC). +pub struct CrashReportCopyMobileClient { + /// The underlying AFC client connected to the crash logs directory. + pub afc_client: AfcClient, +} + +impl IdeviceService for CrashReportCopyMobileClient { + /// Returns the name of the CrashReportCopyMobile service. + fn service_name() -> &'static str { + "com.apple.crashreportcopymobile" + } + + /// Connects to the CrashReportCopyMobile service on the device. + /// + /// # Arguments + /// * `provider` - The provider used to access the device and pairing info. + /// + /// # Returns + /// A connected `CrashReportCopyMobileClient`. + /// + /// # Errors + /// Returns `IdeviceError` if the connection fails at any stage. + /// + /// # Process + /// 1. Connects to the lockdownd service. + /// 2. Starts a lockdown session. + /// 3. Requests the CrashReportCopyMobile service. + /// 4. Establishes a connection to the service. + /// 5. Performs SSL handshake if required. + async fn connect( + provider: &dyn crate::provider::IdeviceProvider, + ) -> Result { + let mut lockdown = LockdownClient::connect(provider).await?; + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + + let (port, ssl) = lockdown.start_service(Self::service_name()).await?; + + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?) + .await?; + } + + Ok(Self { + afc_client: AfcClient::new(idevice), + }) + } +} + +impl CrashReportCopyMobileClient { + /// Creates a new client from an existing AFC-capable device connection. + /// + /// # Arguments + /// * `idevice` - A pre-established connection to the device. + pub fn new(idevice: Idevice) -> Self { + Self { + afc_client: AfcClient::new(idevice), + } + } + + /// Lists crash report files in the root of the crash logs directory. + /// + /// # Returns + /// A list of filenames. + /// + /// # Errors + /// Returns `IdeviceError` if listing the directory fails. + pub async fn ls(&mut self) -> Result, IdeviceError> { + let mut res = self.afc_client.list_dir("/").await?; + if res.len() > 2 { + if &res[0] == "." { + res.swap_remove(0); + } + if &res[1] == ".." { + res.swap_remove(1); + } + } + + Ok(res) + } + + /// Retrieves the contents of a specified crash log file. + /// + /// # Arguments + /// * `log` - Name of the log file to retrieve. + /// + /// # Returns + /// A byte vector containing the file contents. + /// + /// # Errors + /// Returns `IdeviceError` if the file cannot be opened or read. + pub async fn pull(&mut self, log: impl Into) -> Result, IdeviceError> { + let log = log.into(); + let mut f = self + .afc_client + .open(format!("/{log}"), crate::afc::opcode::AfcFopenMode::RdOnly) + .await?; + + f.read().await + } + + /// Removes a specified crash log file from the device. + /// + /// # Arguments + /// * `log` - Name of the log file to remove. + /// + /// # Errors + /// Returns `IdeviceError` if the file could not be deleted. + pub async fn remove(&mut self, log: impl Into) -> Result<(), IdeviceError> { + let log = log.into(); + self.afc_client.remove(format!("/{log}")).await + } + + /// Consumes this client and returns the inner AFC client. + pub fn to_afc_client(self) -> AfcClient { + self.afc_client + } +} + +const EXPECTED_FLUSH: [u8; 4] = [0x70, 0x69, 0x6E, 0x67]; // 'ping' + +/// Triggers a flush of crash logs from system storage. +/// +/// This connects to the `com.apple.crashreportmover` service, +/// which moves crash logs into the AFC-accessible directory. +/// +/// # Arguments +/// * `provider` - The device provider used for connection and pairing info. +/// +/// # Returns +/// `Ok(())` if the service responds with a valid flush indicator. +/// `Err(IdeviceError)` if the service responds with unexpected data +/// or the connection fails. +pub async fn flush_reports( + provider: &dyn crate::provider::IdeviceProvider, +) -> Result<(), IdeviceError> { + let mut lockdown = LockdownClient::connect(provider).await?; + lockdown + .start_session(&provider.get_pairing_file().await?) + .await?; + + let (port, ssl) = lockdown.start_service("com.apple.crashreportmover").await?; + + let mut idevice = provider.connect(port).await?; + if ssl { + idevice + .start_session(&provider.get_pairing_file().await?) + .await?; + } + + let res = idevice.read_raw(4).await?; + debug!( + "Flush reports response: {:?}", + String::from_utf8_lossy(&res) + ); + + if res[..4] == EXPECTED_FLUSH { + Ok(()) + } else { + warn!("crashreportmover sent wrong bytes: {res:02X?}"); + Err(IdeviceError::CrashReportMoverBadResponse(res)) + } +} diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 16d9e1b..481572c 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -9,6 +9,8 @@ pub mod amfi; mod ca; #[cfg(feature = "core_device_proxy")] pub mod core_device_proxy; +#[cfg(feature = "crashreportcopymobile")] +pub mod crashreportcopymobile; #[cfg(feature = "debug_proxy")] pub mod debug_proxy; #[cfg(feature = "dvt")] @@ -451,6 +453,10 @@ pub enum IdeviceError { #[error("missing file attribute")] AfcMissingAttribute, + #[cfg(feature = "crashreportcopymobile")] + #[error("crash report mover sent the wrong response")] + CrashReportMoverBadResponse(Vec), + #[cfg(any(feature = "tss", feature = "tunneld"))] #[error("http reqwest error")] Reqwest(#[from] reqwest::Error), diff --git a/tools/Cargo.toml b/tools/Cargo.toml index ee1f817..b854807 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -61,6 +61,10 @@ path = "src/location_simulation.rs" name = "afc" path = "src/afc.rs" +[[bin]] +name = "crash_logs" +path = "src/crash_logs.rs" + [[bin]] name = "amfi" path = "src/amfi.rs" diff --git a/tools/src/crash_logs.rs b/tools/src/crash_logs.rs new file mode 100644 index 0000000..8c545bc --- /dev/null +++ b/tools/src/crash_logs.rs @@ -0,0 +1,87 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{ + crashreportcopymobile::{flush_reports, CrashReportCopyMobileClient}, + IdeviceService, +}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("crash_logs") + .about("Manage crash logs") + .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("list").about("Lists the items in the directory")) + .subcommand(Command::new("flush").about("Flushes reports to the directory")) + .subcommand( + Command::new("pull") + .about("Pulls a log") + .arg(Arg::new("path").required(true).index(1)) + .arg(Arg::new("save").required(true).index(2)), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("crash_logs - manage crash logs on the device"); + 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, "afc-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + let mut crash_client = CrashReportCopyMobileClient::connect(&*provider) + .await + .expect("Unable to connect to misagent"); + + if let Some(_matches) = matches.subcommand_matches("list") { + let res = crash_client.ls().await.expect("Failed to read dir"); + println!("{res:#?}"); + } else if matches.subcommand_matches("flush").is_some() { + flush_reports(&*provider).await.expect("Failed to flush"); + } else if let Some(matches) = matches.subcommand_matches("pull") { + let path = matches.get_one::("path").expect("No path passed"); + let save = matches.get_one::("save").expect("No path passed"); + + let res = crash_client.pull(path).await.expect("Failed to pull log"); + tokio::fs::write(save, res) + .await + .expect("Failed to write to file"); + } else { + eprintln!("Invalid usage, pass -h for help"); + } +}