diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 6985561..b9739a8 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -37,6 +37,7 @@ sha2 = { version = "0.10", optional = true } [features] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] +debug_proxy = [] heartbeat = [] installation_proxy = [] mounter = ["dep:sha2"] @@ -54,6 +55,7 @@ xpc = [ ] full = [ "core_device_proxy", + "debug_proxy", "heartbeat", "installation_proxy", "mounter", diff --git a/idevice/src/debug_proxy.rs b/idevice/src/debug_proxy.rs new file mode 100644 index 0000000..5ceddb7 --- /dev/null +++ b/idevice/src/debug_proxy.rs @@ -0,0 +1,196 @@ +// Jackson Coxson +// https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packets.html#Packets + +use log::debug; +use std::fmt::Write; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::{IdeviceError, ReadWrite}; + +pub const SERVICE_NAME: &str = "com.apple.internal.dt.remote.debugproxy"; + +pub struct DebugProxyClient { + pub socket: Box, + pub noack_mode: bool, +} + +pub struct DebugserverCommand { + pub name: String, + pub argv: Vec, +} + +impl DebugserverCommand { + pub fn new(name: String, argv: Vec) -> Self { + Self { name, argv } + } +} + +impl DebugProxyClient { + pub fn new(socket: Box) -> Self { + Self { + socket, + noack_mode: false, + } + } + + pub async fn send_command( + &mut self, + command: DebugserverCommand, + ) -> Result, IdeviceError> { + // Hex-encode the arguments + let hex_args = command + .argv + .iter() + .map(|arg| hex_encode(arg.as_bytes())) + .collect::>() + .join(""); + + // Construct the packet data (command + hex-encoded arguments) + let packet_data = format!("{}{}", command.name, hex_args); + + // Calculate the checksum + let checksum = calculate_checksum(&packet_data); + + // Construct the full packet + let packet = format!("${}#{}", packet_data, checksum); + + // Log the packet for debugging + debug!("Sending packet: {}", packet); + + // Send the packet + self.socket.write_all(packet.as_bytes()).await?; + + // Read the response + let response = self.read_response().await?; + Ok(response) + } + + pub async fn read_response(&mut self) -> Result, IdeviceError> { + let mut buffer = Vec::new(); + let mut received_char = [0u8; 1]; + + if !self.noack_mode { + self.socket.read_exact(&mut received_char).await?; + if received_char[0] != b'+' { + debug!("No + ack"); + return Ok(None); + } + } + + self.socket.read_exact(&mut received_char).await?; + if received_char[0] != b'$' { + debug!("No $ response"); + return Ok(None); + } + + loop { + self.socket.read_exact(&mut received_char).await?; + if received_char[0] == b'#' { + break; + } + buffer.push(received_char[0]); + } + + if !self.noack_mode { + self.send_ack().await?; + } + + let response = String::from_utf8(buffer)?; + Ok(Some(response)) + } + + pub async fn send_raw(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> { + self.socket.write_all(bytes).await?; + Ok(()) + } + + pub async fn read(&mut self, len: usize) -> Result { + let mut buf = vec![0; len]; + let r = self.socket.read(&mut buf).await?; + + Ok(String::from_utf8_lossy(&buf[..r]).to_string()) + } + + pub async fn set_argv(&mut self, argv: Vec) -> Result { + if argv.is_empty() { + return Err(IdeviceError::InvalidArgument); + } + + // Calculate the total length of the packet + let mut pkt_len = 0; + for (i, arg) in argv.iter().enumerate() { + let prefix = format!(",{},{},", arg.len() * 2, i); + pkt_len += prefix.len() + arg.len() * 2; + } + + // Allocate and initialize the packet + let mut pkt = vec![0u8; pkt_len + 1]; + let mut pktp = 0; + + for (i, arg) in argv.iter().enumerate() { + let prefix = format!(",{},{},", arg.len() * 2, i); + let prefix_bytes = prefix.as_bytes(); + + // Copy prefix to the packet + pkt[pktp..pktp + prefix_bytes.len()].copy_from_slice(prefix_bytes); + pktp += prefix_bytes.len(); + + // Hex encode the argument + for byte in arg.bytes() { + let hex = format!("{:02X}", byte); + pkt[pktp..pktp + 2].copy_from_slice(hex.as_bytes()); + pktp += 2; + } + } + + // Set the first byte of the packet + pkt[0] = b'A'; + + // Simulate sending the command and receiving a response + self.send_raw(&pkt).await?; + let response = self.read(16).await?; + + Ok(response) + } + + pub async fn send_ack(&mut self) -> Result<(), IdeviceError> { + self.socket.write_all(b"+").await?; + Ok(()) + } + + pub async fn send_noack(&mut self) -> Result<(), IdeviceError> { + self.socket.write_all(b"-").await?; + Ok(()) + } + + pub fn set_ack_mode(&mut self, enabled: bool) { + self.noack_mode = !enabled; + } +} + +fn calculate_checksum(data: &str) -> String { + let checksum = data.bytes().fold(0u8, |acc, byte| acc.wrapping_add(byte)); + format!("{:02x}", checksum) +} + +fn hex_encode(bytes: &[u8]) -> String { + bytes.iter().fold(String::new(), |mut output, b| { + let _ = write!(output, "{b:02X}"); + output + }) +} + +impl From for DebugserverCommand { + fn from(s: String) -> Self { + // Split string into command and arguments + let mut split = s.split_whitespace(); + let command = split.next().unwrap_or("").to_string(); + let arguments: Vec = split.map(|s| s.to_string()).collect(); + Self::new(command, arguments) + } +} +impl From<&str> for DebugserverCommand { + fn from(s: &str) -> DebugserverCommand { + s.to_string().into() + } +} diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 3d13b02..083725c 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -2,6 +2,8 @@ #[cfg(feature = "core_device_proxy")] pub mod core_device_proxy; +#[cfg(feature = "debug_proxy")] +pub mod debug_proxy; #[cfg(feature = "heartbeat")] pub mod heartbeat; #[cfg(feature = "xpc")] @@ -294,6 +296,10 @@ pub enum IdeviceError { #[error("xpc message failed")] Xpc(#[from] xpc::error::XPCError), + #[cfg(feature = "debug_proxy")] + #[error("invalid argument passed")] + InvalidArgument, + #[error("unknown error `{0}` returned from device")] UnknownErrorType(String), } diff --git a/idevice/src/xpc/mod.rs b/idevice/src/xpc/mod.rs index be6e204..82ee865 100644 --- a/idevice/src/xpc/mod.rs +++ b/idevice/src/xpc/mod.rs @@ -227,7 +227,7 @@ impl XPCConnection { return Ok(decoded); } Err(err) => { - log::error!("Error decoding message: {:?}", err); + log::warn!("Error decoding message: {:?}", err); buf.extend_from_slice(&self.inner.read_streamid(stream_id).await?); } } diff --git a/tools/Cargo.toml b/tools/Cargo.toml index b2950b5..43f87b0 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -37,6 +37,9 @@ path = "src/idevice_id.rs" name = "remotexpc" path = "src/remotexpc.rs" +[[bin]] +name = "debug_proxy" +path = "src/debug_proxy.rs" [dependencies] idevice = { path = "../idevice", features = ["full"] } diff --git a/tools/src/debug_proxy.rs b/tools/src/debug_proxy.rs new file mode 100644 index 0000000..bfb14cc --- /dev/null +++ b/tools/src/debug_proxy.rs @@ -0,0 +1,102 @@ +// Jackson Coxson + +use std::{ + io::Write, + net::{IpAddr, SocketAddr}, + str::FromStr, +}; + +use clap::{Arg, Command}; +use idevice::{debug_proxy::DebugProxyClient, tunneld::get_tunneld_devices, xpc::XPCDevice}; +use tokio::net::TcpStream; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("remotexpc") + .about("Get services from RemoteXPC") + .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), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("debug_proxy - connect to the debug proxy and run commands"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("udid"); + + let socket = SocketAddr::new( + IpAddr::from_str("127.0.0.1").unwrap(), + idevice::tunneld::DEFAULT_PORT, + ); + let mut devices = get_tunneld_devices(socket) + .await + .expect("Failed to get tunneld devices"); + + let (_udid, device) = match udid { + Some(u) => ( + u.to_owned(), + devices.remove(u).expect("Device not in tunneld"), + ), + None => devices.into_iter().next().expect("No devices"), + }; + + // Make the connection to RemoteXPC + let client = XPCDevice::new(Box::new( + TcpStream::connect((device.tunnel_address.as_str(), device.tunnel_port)) + .await + .unwrap(), + )) + .await + .unwrap(); + + // Get the debug proxy + let service = client + .services + .get(idevice::debug_proxy::SERVICE_NAME) + .expect("Client did not contain debug proxy service"); + + let stream = TcpStream::connect(SocketAddr::new( + IpAddr::from_str(&device.tunnel_address).unwrap(), + service.port, + )) + .await + .expect("Failed to connect"); + + let mut dp = DebugProxyClient::new(Box::new(stream)); + + println!("Shell connected!"); + loop { + print!("> "); + std::io::stdout().flush().unwrap(); + + let mut buf = String::new(); + std::io::stdin().read_line(&mut buf).unwrap(); + + let buf = buf.trim(); + + if buf == "exit" { + break; + } + + let res = dp.send_command(buf.into()).await.expect("Failed to send"); + if let Some(res) = res { + println!("{res}"); + } + } +}