Implement basic debug proxy support

This commit is contained in:
Jackson Coxson
2025-02-26 22:42:48 -07:00
parent b10815bb0b
commit ffb5d92726
6 changed files with 310 additions and 1 deletions

View File

@@ -37,6 +37,7 @@ sha2 = { version = "0.10", optional = true }
[features] [features]
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
debug_proxy = []
heartbeat = [] heartbeat = []
installation_proxy = [] installation_proxy = []
mounter = ["dep:sha2"] mounter = ["dep:sha2"]
@@ -54,6 +55,7 @@ xpc = [
] ]
full = [ full = [
"core_device_proxy", "core_device_proxy",
"debug_proxy",
"heartbeat", "heartbeat",
"installation_proxy", "installation_proxy",
"mounter", "mounter",

196
idevice/src/debug_proxy.rs Normal file
View File

@@ -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<dyn ReadWrite>,
pub noack_mode: bool,
}
pub struct DebugserverCommand {
pub name: String,
pub argv: Vec<String>,
}
impl DebugserverCommand {
pub fn new(name: String, argv: Vec<String>) -> Self {
Self { name, argv }
}
}
impl DebugProxyClient {
pub fn new(socket: Box<dyn ReadWrite>) -> Self {
Self {
socket,
noack_mode: false,
}
}
pub async fn send_command(
&mut self,
command: DebugserverCommand,
) -> Result<Option<String>, IdeviceError> {
// Hex-encode the arguments
let hex_args = command
.argv
.iter()
.map(|arg| hex_encode(arg.as_bytes()))
.collect::<Vec<String>>()
.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<Option<String>, 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<String, IdeviceError> {
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<String>) -> Result<String, IdeviceError> {
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<String> 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<String> = 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()
}
}

View File

@@ -2,6 +2,8 @@
#[cfg(feature = "core_device_proxy")] #[cfg(feature = "core_device_proxy")]
pub mod core_device_proxy; pub mod core_device_proxy;
#[cfg(feature = "debug_proxy")]
pub mod debug_proxy;
#[cfg(feature = "heartbeat")] #[cfg(feature = "heartbeat")]
pub mod heartbeat; pub mod heartbeat;
#[cfg(feature = "xpc")] #[cfg(feature = "xpc")]
@@ -294,6 +296,10 @@ pub enum IdeviceError {
#[error("xpc message failed")] #[error("xpc message failed")]
Xpc(#[from] xpc::error::XPCError), Xpc(#[from] xpc::error::XPCError),
#[cfg(feature = "debug_proxy")]
#[error("invalid argument passed")]
InvalidArgument,
#[error("unknown error `{0}` returned from device")] #[error("unknown error `{0}` returned from device")]
UnknownErrorType(String), UnknownErrorType(String),
} }

View File

@@ -227,7 +227,7 @@ impl XPCConnection {
return Ok(decoded); return Ok(decoded);
} }
Err(err) => { 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?); buf.extend_from_slice(&self.inner.read_streamid(stream_id).await?);
} }
} }

View File

@@ -37,6 +37,9 @@ path = "src/idevice_id.rs"
name = "remotexpc" name = "remotexpc"
path = "src/remotexpc.rs" path = "src/remotexpc.rs"
[[bin]]
name = "debug_proxy"
path = "src/debug_proxy.rs"
[dependencies] [dependencies]
idevice = { path = "../idevice", features = ["full"] } idevice = { path = "../idevice", features = ["full"] }

102
tools/src/debug_proxy.rs Normal file
View File

@@ -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::<String>("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}");
}
}
}