Implement pcapd

This commit is contained in:
Jackson Coxson
2025-09-04 08:47:06 -06:00
parent d9bcecb634
commit ddd2f84dd1
6 changed files with 385 additions and 11 deletions

View File

@@ -85,6 +85,7 @@ mobile_image_mounter = ["dep:sha2"]
mobilebackup2 = [] mobilebackup2 = []
location_simulation = [] location_simulation = []
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
pcapd = []
obfuscate = ["dep:obfstr"] obfuscate = ["dep:obfstr"]
restore_service = [] restore_service = []
rsd = ["xpc"] rsd = ["xpc"]
@@ -120,6 +121,7 @@ full = [
"mobile_image_mounter", "mobile_image_mounter",
"mobilebackup2", "mobilebackup2",
"pair", "pair",
"pcapd",
"restore_service", "restore_service",
"rsd", "rsd",
"springboardservices", "springboardservices",

View File

@@ -373,14 +373,8 @@ impl Idevice {
/// # Errors /// # Errors
/// Returns `IdeviceError` if reading, parsing fails, or device reports an error /// Returns `IdeviceError` if reading, parsing fails, or device reports an error
async fn read_plist(&mut self) -> Result<plist::Dictionary, IdeviceError> { async fn read_plist(&mut self) -> Result<plist::Dictionary, IdeviceError> {
if let Some(socket) = &mut self.socket { let res = self.read_plist_value().await?;
debug!("Reading response size"); let res: plist::Dictionary = plist::from_value(&res)?;
let mut buf = [0u8; 4];
socket.read_exact(&mut buf).await?;
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)); debug!("Received plist: {}", pretty_print_dictionary(&res));
if let Some(e) = res.get("Error") { if let Some(e) = res.get("Error") {
@@ -392,6 +386,18 @@ impl Idevice {
} }
} }
Ok(res) Ok(res)
}
async fn read_plist_value(&mut self) -> Result<plist::Value, IdeviceError> {
if let Some(socket) = &mut self.socket {
debug!("Reading response size");
let mut buf = [0u8; 4];
socket.read_exact(&mut buf).await?;
let len = u32::from_be_bytes(buf);
let mut buf = vec![0; len as usize];
socket.read_exact(&mut buf).await?;
let res: plist::Value = plist::from_bytes(&buf)?;
Ok(res)
} else { } else {
Err(IdeviceError::NoEstablishedConnection) Err(IdeviceError::NoEstablishedConnection)
} }
@@ -689,6 +695,8 @@ pub enum IdeviceError {
UnsupportedWatchKey = -63, UnsupportedWatchKey = -63,
#[error("malformed command")] #[error("malformed command")]
MalformedCommand = -64, MalformedCommand = -64,
#[error("integer overflow")]
IntegerOverflow = -65,
} }
impl IdeviceError { impl IdeviceError {
@@ -839,6 +847,7 @@ impl IdeviceError {
IdeviceError::FfiBufferTooSmall(_, _) => -62, IdeviceError::FfiBufferTooSmall(_, _) => -62,
IdeviceError::UnsupportedWatchKey => -63, IdeviceError::UnsupportedWatchKey => -63,
IdeviceError::MalformedCommand => -64, IdeviceError::MalformedCommand => -64,
IdeviceError::IntegerOverflow => -65,
} }
} }
} }

View File

@@ -33,6 +33,8 @@ pub mod mobile_image_mounter;
pub mod mobilebackup2; pub mod mobilebackup2;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]
pub mod os_trace_relay; pub mod os_trace_relay;
#[cfg(feature = "pcapd")]
pub mod pcapd;
#[cfg(feature = "restore_service")] #[cfg(feature = "restore_service")]
pub mod restore_service; pub mod restore_service;
#[cfg(feature = "rsd")] #[cfg(feature = "rsd")]

View File

@@ -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<Self, crate::IdeviceError> {
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<dyn crate::ReadWrite>) -> Result<Self, crate::IdeviceError> {
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<u8>,
}
impl PcapdClient {
pub fn new(idevice: Idevice) -> Self {
Self { idevice }
}
pub async fn next_packet(&mut self) -> Result<DevicePacket, IdeviceError> {
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<u8>` containing the raw bytes of a single packet frame.
///
/// # Returns
/// A `Result` containing the parsed `DevicePacket`
pub fn from_vec(bytes: &[u8]) -> Result<Self, IdeviceError> {
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<u8, IdeviceError> {
self.read_exact(1).map(|s| s[0])
}
fn read_u16_be(&mut self) -> Result<u16, IdeviceError> {
self.read_exact(2)
.map(|s| u16::from_be_bytes(s.try_into().unwrap()))
}
fn read_u32_be(&mut self) -> Result<u32, IdeviceError> {
self.read_exact(4)
.map(|s| u32::from_be_bytes(s.try_into().unwrap()))
}
fn read_u32_le(&mut self) -> Result<u32, IdeviceError> {
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<String, IdeviceError> {
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<W: AsyncWrite + Unpin> {
writer: W,
}
impl<W: AsyncWrite + Unpin> PcapFileWriter<W> {
/// Creates a new writer and asynchronously writes the pcap global header.
pub async fn new(mut writer: W) -> Result<Self, std::io::Error> {
// 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(())
}
}

View File

@@ -113,6 +113,10 @@ path = "src/diagnosticsservice.rs"
name = "bt_packet_logger" name = "bt_packet_logger"
path = "src/bt_packet_logger.rs" path = "src/bt_packet_logger.rs"
[[bin]]
name = "pcapd"
path = "src/pcapd.rs"
[dependencies] [dependencies]
idevice = { path = "../idevice", features = ["full"], default-features = false } idevice = { path = "../idevice", features = ["full"], default-features = false }
tokio = { version = "1.43", features = ["full"] } tokio = { version = "1.43", features = ["full"] }

97
tools/src/pcapd.rs Normal file
View File

@@ -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::<String>("udid");
let host = matches.get_one::<String>("host");
let pairing_file = matches.get_one::<String>("pairing_file");
let out = matches.get_one::<String>("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:?}");
}
}
}