Implement crash report services

This commit is contained in:
Jackson Coxson
2025-05-09 13:46:00 -06:00
parent 4b29eefc36
commit d938531f96
5 changed files with 281 additions and 0 deletions

View File

@@ -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",

View File

@@ -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<Self, IdeviceError> {
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<Vec<String>, 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<String>) -> Result<Vec<u8>, 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<String>) -> 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))
}
}

View File

@@ -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<u8>),
#[cfg(any(feature = "tss", feature = "tunneld"))]
#[error("http reqwest error")]
Reqwest(#[from] reqwest::Error),

View File

@@ -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"

87
tools/src/crash_logs.rs Normal file
View File

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