diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index a69ede4..454fac0 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -85,6 +85,7 @@ mobile_image_mounter = ["dep:sha2"] mobilebackup2 = [] location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] +pcapd = [] obfuscate = ["dep:obfstr"] restore_service = [] rsd = ["xpc"] @@ -120,6 +121,7 @@ full = [ "mobile_image_mounter", "mobilebackup2", "pair", + "pcapd", "restore_service", "rsd", "springboardservices", diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 7637998..b93e396 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -373,6 +373,22 @@ impl Idevice { /// # Errors /// Returns `IdeviceError` if reading, parsing fails, or device reports an error async fn read_plist(&mut self) -> Result { + let res = self.read_plist_value().await?; + let res: plist::Dictionary = plist::from_value(&res)?; + debug!("Received plist: {}", pretty_print_dictionary(&res)); + + if let Some(e) = res.get("Error") { + let e: String = plist::from_value(e)?; + if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { + return Err(e); + } else { + return Err(IdeviceError::UnknownErrorType(e)); + } + } + Ok(res) + } + + async fn read_plist_value(&mut self) -> Result { if let Some(socket) = &mut self.socket { debug!("Reading response size"); let mut buf = [0u8; 4]; @@ -380,17 +396,7 @@ impl Idevice { let len = u32::from_be_bytes(buf); let mut buf = vec![0; len as usize]; socket.read_exact(&mut buf).await?; - let res: plist::Dictionary = plist::from_bytes(&buf)?; - debug!("Received plist: {}", pretty_print_dictionary(&res)); - - if let Some(e) = res.get("Error") { - let e: String = plist::from_value(e)?; - if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) { - return Err(e); - } else { - return Err(IdeviceError::UnknownErrorType(e)); - } - } + let res: plist::Value = plist::from_bytes(&buf)?; Ok(res) } else { Err(IdeviceError::NoEstablishedConnection) @@ -689,6 +695,8 @@ pub enum IdeviceError { UnsupportedWatchKey = -63, #[error("malformed command")] MalformedCommand = -64, + #[error("integer overflow")] + IntegerOverflow = -65, } impl IdeviceError { @@ -839,6 +847,7 @@ impl IdeviceError { IdeviceError::FfiBufferTooSmall(_, _) => -62, IdeviceError::UnsupportedWatchKey => -63, IdeviceError::MalformedCommand => -64, + IdeviceError::IntegerOverflow => -65, } } } diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index 6b14605..852f2ab 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -33,6 +33,8 @@ pub mod mobile_image_mounter; pub mod mobilebackup2; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; +#[cfg(feature = "pcapd")] +pub mod pcapd; #[cfg(feature = "restore_service")] pub mod restore_service; #[cfg(feature = "rsd")] diff --git a/idevice/src/services/pcapd.rs b/idevice/src/services/pcapd.rs new file mode 100644 index 0000000..e4a1359 --- /dev/null +++ b/idevice/src/services/pcapd.rs @@ -0,0 +1,260 @@ +//! Abstraction for pcapd + +use plist::Value; +use tokio::io::AsyncWrite; +use tokio::io::AsyncWriteExt; + +use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf}; + +const ETHERNET_HEADER: &[u8] = &[ + 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0x08, 0x00, +]; + +/// Client for interacting with the pcapd service on the device. +pub struct PcapdClient { + /// The underlying device connection with established service + pub idevice: Idevice, +} + +impl IdeviceService for PcapdClient { + fn service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.pcapd") + } + + async fn from_stream(idevice: Idevice) -> Result { + Ok(Self::new(idevice)) + } +} + +impl RsdService for PcapdClient { + fn rsd_service_name() -> std::borrow::Cow<'static, str> { + obf!("com.apple.pcapd.shim.remote") + } + + async fn from_stream(stream: Box) -> Result { + Ok(Self::new(Idevice::new(stream, "".to_string()))) + } +} + +/// A Rust representation of the iOS pcapd device packet header and data. +#[derive(Debug, Clone)] +pub struct DevicePacket { + pub header_length: u32, + pub header_version: u8, + pub packet_length: u32, + pub interface_type: u8, + pub unit: u16, + pub io: u8, + pub protocol_family: u32, + pub frame_pre_length: u32, + pub frame_post_length: u32, + pub interface_name: String, + pub pid: u32, + pub comm: String, + pub svc: u32, + pub epid: u32, + pub ecomm: String, + pub seconds: u32, + pub microseconds: u32, + pub data: Vec, +} + +impl PcapdClient { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + pub async fn next_packet(&mut self) -> Result { + let packet = self.idevice.read_plist_value().await?; + let packet = match packet { + Value::Data(p) => p, + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + }; + let mut packet = DevicePacket::from_vec(&packet)?; + packet.normalize_data(); + Ok(packet) + } +} + +impl DevicePacket { + /// Normalizes the packet data by adding a fake Ethernet header if necessary. + /// This is required for tools like Wireshark to correctly dissect raw IP packets. + pub fn normalize_data(&mut self) { + if self.frame_pre_length == 0 { + // Prepend the fake ethernet header for raw IP packets. + let mut new_data = ETHERNET_HEADER.to_vec(); + new_data.append(&mut self.data); + self.data = new_data; + } else if self.interface_name.starts_with("pdp_ip") { + // For cellular interfaces, skip the first 4 bytes of the original data + // before prepending the header. + if self.data.len() >= 4 { + let mut new_data = ETHERNET_HEADER.to_vec(); + new_data.extend_from_slice(&self.data[4..]); + self.data = new_data; + } + } + } + + /// Parses a byte vector into a DevicePacket. + /// + /// This is the primary method for creating a struct from the raw data + /// received from the device. + /// + /// # Arguments + /// * `bytes` - A `Vec` containing the raw bytes of a single packet frame. + /// + /// # Returns + /// A `Result` containing the parsed `DevicePacket` + pub fn from_vec(bytes: &[u8]) -> Result { + let mut r = ByteReader::new(bytes); + + // --- Parse Header --- + let header_length = r.read_u32_be()?; + let header_version = r.read_u8()?; + let packet_length = r.read_u32_be()?; + let interface_type = r.read_u8()?; + let unit = r.read_u16_be()?; + let io = r.read_u8()?; + let protocol_family = r.read_u32_be()?; + let frame_pre_length = r.read_u32_be()?; + let frame_post_length = r.read_u32_be()?; + let interface_name = r.read_cstr(16)?; + let pid = r.read_u32_le()?; // Little Endian + let comm = r.read_cstr(17)?; + let svc = r.read_u32_be()?; + let epid = r.read_u32_le()?; // Little Endian + let ecomm = r.read_cstr(17)?; + let seconds = r.read_u32_be()?; + let microseconds = r.read_u32_be()?; + + // --- Extract Packet Data --- + // The data starts at an absolute offset defined by `header_length`. + let data_start = header_length as usize; + let data_end = data_start.saturating_add(packet_length as usize); + + if data_end > bytes.len() { + return Err(IdeviceError::NotEnoughBytes(bytes.len(), data_end)); + } + let data = bytes[data_start..data_end].to_vec(); + + Ok(DevicePacket { + header_length, + header_version, + packet_length, + interface_type, + unit, + io, + protocol_family, + frame_pre_length, + frame_post_length, + interface_name, + pid, + comm, + svc, + epid, + ecomm, + seconds, + microseconds, + data, + }) + } +} + +/// A helper struct to safely read from a byte slice. +struct ByteReader<'a> { + slice: &'a [u8], + cursor: usize, +} + +impl<'a> ByteReader<'a> { + fn new(slice: &'a [u8]) -> Self { + Self { slice, cursor: 0 } + } + + /// Reads an exact number of bytes and advances the cursor. + fn read_exact(&mut self, len: usize) -> Result<&'a [u8], IdeviceError> { + let end = self + .cursor + .checked_add(len) + .ok_or(IdeviceError::IntegerOverflow)?; + if end > self.slice.len() { + return Err(IdeviceError::NotEnoughBytes(len, self.slice.len())); + } + let result = &self.slice[self.cursor..end]; + self.cursor = end; + Ok(result) + } + + fn read_u8(&mut self) -> Result { + self.read_exact(1).map(|s| s[0]) + } + + fn read_u16_be(&mut self) -> Result { + self.read_exact(2) + .map(|s| u16::from_be_bytes(s.try_into().unwrap())) + } + + fn read_u32_be(&mut self) -> Result { + self.read_exact(4) + .map(|s| u32::from_be_bytes(s.try_into().unwrap())) + } + + fn read_u32_le(&mut self) -> Result { + self.read_exact(4) + .map(|s| u32::from_le_bytes(s.try_into().unwrap())) + } + + /// Reads a fixed-size, null-padded C-style string. + fn read_cstr(&mut self, len: usize) -> Result { + let buffer = self.read_exact(len)?; + let end = buffer.iter().position(|&b| b == 0).unwrap_or(len); + String::from_utf8(buffer[..end].to_vec()).map_err(IdeviceError::Utf8) + } +} + +/// A writer for creating `.pcap` files from DevicePackets without external dependencies. +pub struct PcapFileWriter { + writer: W, +} + +impl PcapFileWriter { + /// Creates a new writer and asynchronously writes the pcap global header. + pub async fn new(mut writer: W) -> Result { + // Correct pcap global header for LINKTYPE_ETHERNET. + // We use big-endian format, as is traditional. + let header = [ + 0xa1, 0xb2, 0xc3, 0xd4, // magic number (big-endian) + 0x00, 0x02, // version_major + 0x00, 0x04, // version_minor + 0x00, 0x00, 0x00, 0x00, // thiszone (GMT) + 0x00, 0x00, 0x00, 0x00, // sigfigs (accuracy) + 0x00, 0x04, 0x00, 0x00, // snaplen (max packet size, 262144) + 0x00, 0x00, 0x00, 0x01, // network (LINKTYPE_ETHERNET) + ]; + writer.write_all(&header).await?; + Ok(Self { writer }) + } + + /// Asynchronously writes a single DevicePacket to the pcap file. + pub async fn write_packet(&mut self, packet: &DevicePacket) -> Result<(), std::io::Error> { + let mut record_header = [0u8; 16]; + + // Use the packet's own timestamp for accuracy. + record_header[0..4].copy_from_slice(&packet.seconds.to_be_bytes()); + record_header[4..8].copy_from_slice(&packet.microseconds.to_be_bytes()); + + // incl_len and orig_len + let len_bytes = (packet.data.len() as u32).to_be_bytes(); + record_header[8..12].copy_from_slice(&len_bytes); + record_header[12..16].copy_from_slice(&len_bytes); + + // Write the record header and packet data sequentially. + self.writer.write_all(&record_header).await?; + self.writer.write_all(&packet.data).await?; + + Ok(()) + } +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 717838a..4943428 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -113,6 +113,10 @@ path = "src/diagnosticsservice.rs" name = "bt_packet_logger" path = "src/bt_packet_logger.rs" +[[bin]] +name = "pcapd" +path = "src/pcapd.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/pcapd.rs b/tools/src/pcapd.rs new file mode 100644 index 0000000..682520a --- /dev/null +++ b/tools/src/pcapd.rs @@ -0,0 +1,97 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{ + IdeviceService, + pcapd::{PcapFileWriter, PcapdClient}, +}; + +mod common; +mod pcap; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("pcapd") + .about("Capture IP packets") + .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("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .arg( + Arg::new("out") + .long("out") + .value_name("PCAP") + .help("Write PCAP to this file (use '-' for stdout)"), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("bt_packet_logger - capture bluetooth packets"); + 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 out = matches.get_one::("out").map(String::to_owned); + + let provider = match common::get_provider(udid, host, pairing_file, "pcapd-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + + let mut logger_client = PcapdClient::connect(&*provider) + .await + .expect("Failed to connect to pcapd"); + + logger_client.next_packet().await.unwrap(); + + // Open output (default to stdout if --out omitted) + let mut out_writer = match out.as_deref() { + Some(path) => Some( + PcapFileWriter::new(tokio::fs::File::create(path).await.expect("open pcap")) + .await + .expect("write header"), + ), + _ => None, + }; + + println!("Starting packet stream"); + loop { + let packet = logger_client + .next_packet() + .await + .expect("failed to read next packet"); + if let Some(writer) = &mut out_writer { + writer.write_packet(&packet).await.expect("write packet"); + } else { + println!("{packet:?}"); + } + } +}