mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +01:00
Implement pcapd
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
260
idevice/src/services/pcapd.rs
Normal file
260
idevice/src/services/pcapd.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
97
tools/src/pcapd.rs
Normal 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:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user